| RFC-0195:文本 API 中的位置和范围 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 规定使用 Unicode 标量值作为文本编辑 API 中位置和范围的基本单位。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2022-08-26 |
| 审核日期(年-月-日) | 2022-10-25 |
摘要
我们建议 fuchsia.input.text API 应使用 Unicode 标量值作为文本编辑位置和范围(例如插入符号和选择)的原子单位。
设计初衷
fuchsia.input.text 命名空间将提供用于文本编辑和撰写的 FIDL 协议,从而实现文本字段、输入法编辑器 (IME)、复制和粘贴、自动更正及相关功能的跨运行时实现。这些 API 将包含多种用于检索、选择和修改文本范围的方法。作为这些方法设计的基本组成部分,API 必须标准化一种对 Unicode 字符串进行索引的方式。这是必需的,目的是确保,例如,当在 Flutter 中实现的屏幕键盘指示 Chromium WebView 中的文本框“删除插入符号之前的三个字符”时,键盘和浏览器在“三个字符”的含义和插入符号当前位置上达成一致。
Fuchsia 的树内运行时目前尚未就字符串操作的基本单位达成共识(请参阅现有技术和参考资料)。我们审核的几个其他非 Fuchsia 平台的标准 SDK 在此问题上也不一致,并且还受到旧版设计选择的影响,在许多情况下,这些选择早于现代 Unicode 标准。
由于国际化文本编辑是面向用户的现代操作系统的一项关键功能,并且没有 Fuchsia 可以采用的统一标准,因此 Fuchsia 有机会通过为其跨运行时 API 选择符合人体工程学且不受旧版设计阻碍的单一标准来改进现状。
此外,由于 Fuchsia 的文本编辑 API 将作为多个独立运行时(这些运行时不一定彼此了解)之间的互操作机制,因此它必须提供一个定义明确的接口,该接口可实际用于一致地实现,而无需标准化任何一个运行时的实现细节。
利益相关方
Facilitator: abarth@google.com
审核者:
Fuchsia HCI:neelsa@google.com、fmil@google.com
安全性:pesk@google.com
隐私权:enoharemaien@google.com
Chromium:wez@google.com
Flutter:jmccandless@google.com、gspencer@google.com
咨询对象:quiche@google.com
社会化
在作为 RFC CL 发布之前,此设计已在 Fuchsia HCI 团队和部分审核者之间以 Google 文档的形式进行过交流。
设计
本文档中的关键字“必须”“不得”“必需”“会”“不会”“应”“不应”“建议”“可以”和“可选”应按照 IETF RFC 2119 中的描述进行解释。
背景
如需了解更详细的概览,请参阅 FIDL API 可读性评分标准 > 字符串编码。
FIDL 字符串是表示 UTF-8 编码的 Unicode 文本的字节序列。
字符串可以划分为多种单位。
Unicode 标量值:在 Unicode 中,文本的基本原子是“Unicode 标量值”,它是
[0x0, 0xD7FF], [0xE000, 0x10FFFF]范围内的整数,可以映射到“抽象字符”。Unicode 标量值是 Unicode 码位的子集,是
[0x0, 0x10FFFF]范围内的整数。从 Unicode 标量值中排除的码位[0xD800, 0xDFFF]称为代理码位。它们是为 UTF-16 编码的实现细节保留的,不能用于表示任何已分配的字符。Byte:将字符串划分为字节的输出取决于编码。 例如,FIDL 字符串使用的 UTF-8 编码是一种变长编码,每个标量值由 1 到 4 个字节的序列表示。(例如,
k为 1 字节,ك为 2 字节,ከ为 3 字节,𐤊为 4 字节。)UTF-8 标准规定了如何解析字节序列以及如何确定新标量值的起始位置。由于 UTF‑8 是一种变长编码,因此无法在恒定时间内确定 UTF‑8 字符串中的标量值数量,也无法跳转到第 n 个标量值。字形簇:某些 Unicode 标量值的组合在以图形方式呈现时会合并为单个用户感知到的“字符”,在技术上称为“字形簇”。例如,带变音符号的字母 (
á̡)、带有性别和肤色选择器的面部表情符号 (💂🏽♀️),以及组合成旗帜表情符号 (🇦🇺) 的双字母国家/地区代码。将标量值合并为字形簇的规则取决于具体情境,并且依赖于从 Unicode 字符数据库读取的属性;因此,这些规则可能会随着 Unicode 标准的发布而发生变化。
虽然与 FIDL 及其 UTF‑8 字符串没有直接关系,但许多使用 UTF‑16 编码的旧版运行时都提供额外的划分选项:
- UTF‑16 代码单元:在 UTF‑16 编码中,每个 Unicode 标量值都由一个或两个 2 字节序列(称为 UTF‑16“代码单元”)进行编码。UTF‑16 标准规定了如何根据代码单元的各个位来确定它是单代码单元标量值,还是双代码单元代理对的一部分。
设计
在 fuchsia.input.text 中,对于表示一个或多个字符串索引的任何方法形参或返回值,基本单位应为单个 Unicode 标量值。
例如,在以下假设的方法中,Range 是根据字符串或文本字段开头的 Unicode 标量值位置定义的。
protocol ReadableTextField {
/// Retrieves part of the contents of the text field.
GetText(struct {
range Range;
}) -> (struct {
// Note that FIDL string field sizes are specified in bytes
// https://fuchsia.dev/fuchsia-src/reference/fidl/language/language#strings
contents string:MAX_STRING_SIZE;
}) error TextFieldError;
};
type Range = struct {
/// The index of the first scalar value in the range.
start uint32;
/// The index _after_ the last scalar value in the range.
end uint32;
};
如果某个文本字段包含字符串 abcd😀ef🇦🇺gh,则请求范围 [2, 8) 会返回子字符串 cd😀ef🇦。(请注意,字形簇 🇦🇺 会拆分为 🇦、🇺。)
实现
在内部,实现者可以使用所选编程语言或库中最受支持或最方便的任何 Unicode 字符串编码和索引。
不过,fuchsia.input.text 中协议的所有实现
- 必须使用协议中指定的 Unicode 标量值索引正确解读文本位置和范围。
- 必须以 Unicode 标量值索引的形式将其文本编辑命令发送给其他
fuchsia.input.text实现。
供参考:
在 Rust 中,Unicode 标量值是单个
char,并且可以使用String::chars()迭代String或&str中的标量值。在 Dart 中,这是一个
rune。可以使用String.runes属性迭代字符串的 Unicode 标量值。截至 C++ 17,用于处理 Unicode 文本的标准库实用程序尚不完善,因此建议改用
icu::UnicodeString和icu::StringCharacterIterator。例如,可以使用setIndex32(n)检索字符串中的第 n 个标量值。
性能
对于使用可变长度编码(例如 UTF-8 [Rust] 或 UTF-16 [Dart])来表示字符串的运行时,按 Unicode 标量值访问字符串位置或长度是一项线性时间操作。(对于 UTF-32 和类似的固定长度编码,这只会是常数时间操作,但这些编码空间效率低下,并不常用。)
对于经常访问字符串长度并预期存在长字符串的用例,最好缓存长度值或以其他方式预处理字符串,以实现分摊的常数时间。
工效学设计
Unicode 标量值在文本编辑和撰写方面具有以下优势:
- 这种精细度可防止将 UTF-8 编码的字符拆分为无效的字节序列。
- 如有必要,此功能允许在字形集群内部进行编辑。例如,在输入“á”(“a”后跟“◌́”)后,系统允许使用退格键删除重音符号,但不允许删除基本字母。
如需与其他选项进行比较,请参阅缺点、替代方案和未知因素。
向后兼容性
此 RFC 涉及新的文本编辑 API,这些 API 正在从头开始实现,作为 Fuchsia 平台的一部分。除了在 FIDL API 的文本位置表示法与任何给定语言运行时中首选的表示法之间进行转换的固有任务之外,我们预计不会出现向后兼容性问题。
安全注意事项
通过整个 Unicode 标量值(而非字节或 UTF-16 代码单元)来处理文本,可降低字符串被错误截断的可能性。
按 Unicode 标量值进行原子化可能会导致字形簇被拆分,这有时是可取的(请参阅人机工程学),但如果随意进行,可能会导致某些文本的含义发生变化的极端情况。
不过,必须接受这一缺点,因为字形聚类取决于 Unicode 版本,甚至会受到特定于实现的定制的影响,因此使用不同 Unicode 库或版本的客户端可能会在字符串长度上存在分歧,从而导致数据损坏。
隐私注意事项
除了围绕处理用户提供的文本已经存在的隐私权注意事项之外,此 RFC 没有带来新的隐私权注意事项。
测试
fuchsia.input.text API 的实现者将负责为其实现编写适当的单元测试和集成测试。这些测试应涵盖此 RFC 的要求。
根据 Fuchsia 的兼容性测试提供的功能,可以测试实现文本编辑 API 的客户端是否更广泛地符合这些 API。例如,测试可能会向托管文本字段的客户端应用发送一系列文本编辑命令,然后验证生成的文本字段内容是否符合预期。
建议实现者在其测试数据中包含非 ASCII 字符串,包括:
- 基本多语言平面之外的标量值
- 多码位字形集群,例如
- 具有多个组合变音符号的字符
- 带有肤色和/或性别修饰符的表情符号
- 旗帜表情符号
文档
fuchsia.input.text 类的 API 文档将明确突出显示涉及字符串位置、范围和长度的任何数据类型所使用的单位。
缺点、替代方案和未知因素
字节
优点
- 在 FIDL 字段声明中,
string长度以字节为单位明确定义。 - 使用字节可以更轻松地推断内存中的大小。
- 对字节数组的数组访问为 O(1)。
缺点
需要进行额外的验证,以确保字节序列构成有效的 UTF‑8。
很容易不小心将 UTF-8 字符拆分为不完整(因此无效)的字节序列。
除非已知正在编辑的文本仅包含 ASCII 字符,否则将位置移动一个字节并不是一个有用的操作。
字形簇
优点
在文本编辑器中,插入符号几乎总是放置在字形簇边界上。
按整个字形簇选择文本可确保复杂表情符号不会以对用户不友好的方式意外拆分(例如,使用代码点,
👮🏽♀️[肤色中等的女性警察表情符号] 可能会拆分为POLICE OFFICER (U+1F46E)、EMOJI MODIFIER FITZPATRICK TYPE-4 (U+1F3FD)、ZERO WIDTH JOINER (U+200D)、FEMALE SIGN (U+2640)、VARIATION SELECTOR-16 (U+FE0F))。
缺点
字形聚类规则可能会因 Unicode 标准版本而异,并且取决于 CLDR 中的字符属性表查找。
更重要的是,聚类规则集并未完全由 Unicode 版本指定;具体细节允许在实现之间以及在语言区域之间有所不同 1。通过 FIDL 进行通信的两个组件(例如屏幕键盘和渲染文本框的运行时)可能使用不同的 Unicode 实现,因此可能会对它们所操作的文本范围做出相互冲突的假设。
字形聚类 Unicode 规范 UAX #29:Unicode 文本分段明确指出:
本文档定义了字形簇的默认规范。它可能会针对特定语言、操作或其他情况进行自定义。例如,箭头键移动可以根据语言进行调整,或者使用特定于特定字体的知识以更精细的方式移动,以便在需要编辑单个组件的情况下发挥作用。例如,这可能适用于泰北文字 Tai Tham(兰纳)的复杂编辑要求。同样,在某些情况下,逐个编辑字形簇元素可能更可取。例如,在给定系统上,退格键可能会按码点删除,而 Delete 键可能会删除整个集群。
UTF-16 代码单元
优点
- 许多第三方标准库和运行时在内部使用 UTF-16 编码来处理字符串。
缺点
- FIDL 以 UTF-8 而非 UTF-16 格式传输字符串。通过 FIDL 将新编码的单位引入文本编辑 API 完全没有根据,并且会迫使实现者在内部支持至少两种不同的编码,从而造成混淆。
- 与单个字节一样,很容易不小心将标量值拆分为不匹配的 UTF‑16 代理项。
在先技术和参考资料
Flutter
Flutter 似乎已在很大程度上迁移到在其公共 API 中使用字形集群,但其文档仍然不一致:
Dart 的
String类文档指出,“字符串由一系列 Unicode UTF-16 代码单元表示”,并且“字符串的字符采用 UTF-16 编码。解码 UTF-16(它会合并代理对)会生成 Unicode 码位”,这意味着字符是指代码单元。Flutter 未明确记录其
TextPosition或TextRange单位,将 offset 定义为“文本的字符串表示形式中紧跟在位置后面的字符的索引”,但未在此处定义 character。
Flutter 的
TextField.maxLength属性定义为文本字段中允许的最大字符数(Unicode 字形集群)。
下文将对此进行详细说明:
字符
如需详细了解字符的定义,请参阅 Pub 上的 characters 软件包,该软件包是 Flutter 用于分隔字符的工具。一般来说,即使是代理对和扩展字形聚类等复杂字符,Flutter 也会正确地将其解读为单个用户感知到的字符。
Web
JavaScript 字符是 UTF-16 代码单元。Range、Selection 和 CaretPosition 类都处理字符偏移量。
(不过,对于与 Chromium 运行时的集成,值得注意的是,Chromium 在内部使用 UTF-8 编码的字符串。)
Android
Android 的 IME API 明确使用 Java char,即 UTF-16 代码单元。例如,请参阅 android.view.inputmethod.BaseInputConnection.commitText。
macOS 和 iOS
在 Objective-C 中,NSString 文档指出:
NSString对象对符合 Unicode 标准的文本字符串进行编码,该字符串表示为一系列 UTF‑16 代码单元。所有长度、字符索引和范围均以 16 位平台字节序值表示,索引值从0开始。
不过,Swift 类 String 默认使用字形簇作为单位,并提供额外的属性来公开 Unicode 码位、UTF‑16 码元和字节。
与文本编辑相关的类使用不同的单位,具体取决于它们是源自 Objective-C 还是 Swift。UITextInput 协议使用不透明的抽象类 UITextRange 和 UITextPosition,这些类是特定于实现的。
Windows
Windows Core Text 的文档将其索引称为“应用光标位置”,描述为
一个从零开始的数字,用于指示从文本流开头到紧邻光标之前的字符数
“字符数”是指 UTF-16 代码单元,因为这是 .NET 的 System.Char 类型所表示的。