RFC-0195:文本 API 中的位置和范围

RFC-0195:文本 API 中的位置和范围
状态已接受
区域
  • HCI
说明

规定使用 Unicode 标量值作为文本编辑 API 中位置和范围的基本单位。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-08-26
审核日期(年-月-日)2022-10-25

摘要

我们建议 fuchsia.input.text API 应使用 Unicode 标量值 作为 文本编辑位置和范围(例如插入符号和选择)的原子单位。

设计初衷

fuchsia.input.text 命名空间将为文本编辑和撰写提供 FIDL 协议,从而实现文本字段、输入法编辑器 (IME)、复制和粘贴、自动更正及相关功能的跨运行时实现。这些 API 将包含许多用于检索、选择和修改文本范围的方法。作为这些方法设计的基本组成部分,API 必须标准化一种对 Unicode 字符串进行索引的方式。这是必需的,以确保例如当 Flutter 中实现的屏幕键盘指示 Chromium Web 视图中的文本框“删除光标之前的三个字符”时,键盘和浏览器就“三个字符”的含义以及光标当前所在的位置达成一致。

Fuchsia 的树内运行时目前尚未就字符串操作的基本 单位达成共识(请参阅 在先技术和参考资料)。我们审核过的其他几个非 Fuchsia 平台的标准 SDK 在这个问题上也不一致,而且还受到旧版设计选择的影响,在许多情况下,这些选择早于现代 Unicode 标准。

由于国际化文本编辑是面向用户的现代操作系统的一项关键功能,并且 Fuchsia 没有现成的统一标准可供采用,因此 Fuchsia 有机会通过为其跨运行时 API 选择自己的单一标准来改进现状,该标准在人体工学方面有意义,并且不受旧版设计的阻碍。

此外,由于 Fuchsia 的文本编辑 API 将充当多个独立运行时(不一定彼此了解)之间的互操作机制,因此它必须提供明确定义的接口,以便一致地实现,而无需标准化任何一个运行时的实现细节。

利益相关方

教员: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 发布之前,此设计已作为 Google 文档在 Fuchsia HCI 团队和部分审核人之间进行社交化。

设计

本文档中的关键字“MUST”“MUST NOT”“REQUIRED”“SHALL”“SHALL NOT”“SHOULD” “SHOULD NOT”“RECOMMENDED”“MAY”和“OPTIONAL”需按照 IETF RFC 2119中的说明进行解读。

背景

如需了解更详细的概览,请参阅 FIDL API 可读性评分标准 > 字符串编码

FIDL 字符串是表示 UTF-8 编码的 Unicode 文本的字节序列。

字符串可以划分为多个单位。

  • Unicode 标量值:在 Unicode 中,文本的基本原子是“Unicode 标量值”,它是范围 [0x0, 0xD7FF], [0xE000, 0x10FFFF] 中的整数,可以映射到“抽象字符”。

    Unicode 标量值是 Unicode 代码点的子集,Unicode 代码点是 范围 [0x0, 0x10FFFF] 中的整数。从 Unicode 标量值中排除的代码点 [0xD800, 0xDFFF] 称为 代理代码点。它们保留用于 UTF-16 编码的实现细节,不能用于表示任何分配的字符。

  • Byte:将字符串划分为字节的输出取决于编码。 例如,FIDL 字符串使用的 UTF-8 编码是一种可变长度编码,每个标量值由 1 到 4 个字节的序列表示。 (例如,k 是一个字节,ك 是两个字节, 是三个字节,𐤊 是四个字节。) 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()迭代 a String&str 中的标量值。

  • 在 Dart 中,这是一个 rune。可以使用 String.runes 属性迭代字符串的 Unicode 标量值。

  • 截至 C++ 17,标准库中用于操作 Unicode 文本的实用程序 尚不完整,因此建议改用 icu::UnicodeStringicu::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 没有明确记录其 TextPositionTextRange单元,将offset定义为 “字符串 表示形式中紧随文本位置的字符的索引”,但在此处未定义character

  • Flutter 的 TextField.maxLength 属性定义为

    文本字段中允许的最大字符数(Unicode 字形集群)。

    下面对此进行了进一步阐述:

    字符
    如需了解字符的具体定义,请参阅 Pub 上的 字符软件包, Flutter 使用该软件包来划定字符。一般来说,即使是复杂的字符(例如代理对和扩展字形集群),Flutter 也会正确地将它们解释为单个用户感知的字符。

Web

JavaScript 字符是 UTF-16 代码单元。 RangeSelectionCaretPosition 类都处理字符 偏移量。

(不过,对于与 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。The UITextInput 协议使用不透明的抽象 类 UITextRangeUITextPosition,这些类是 特定于实现的。

Windows

Windows Core Text 的文档将其索引称为 “应用插入符号位置”,描述如下

一个从零开始的数字,表示紧靠插入符号之前的文本流开头的字符数

“字符数”表示 UTF-16 代码单元,因为这是 .NET 的 System.Char 类型所表示的内容。


  1. 字符串 "ch" 中有多少个字形集群?在 en-US 语言区域中,有两个。在 cs-CZ(捷克语)中,应该只有一个,因为 'ch' 是 一个 二合字母