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

RFC-0195:文本 API 中的位置和范围
状态已接受
领域
  • 人机交互
说明

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

问题
  • 108240
Gerrit 更改
  • 717053
作者
  • kpozin@google.com
审核人
提交日期(年-月-日)2022-08-26
审核日期(年-月-日)2022-10-25

总结

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

设计初衷

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

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 团队和部分审核人员中进行了社会化。

设计

本文档中的关键字“必须”“不得”“必需”“会”“不会”“应”“不应”“建议”“可以”和“可选”应按 IETF RFC 2119 中的说明进行解释。

背景

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

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

有多种单元可供选择,可将字符串拆分为多个单元。

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

    Unicode 标量值是 Unicode 码位的子集,是 [0x0, 0x10FFFF] 范围内的整数。从 Unicode 标量值 [0xD800, 0xDFFF] 中排除的码位称为代理码位。系统会预留这些字符,以用于 UTF-16 编码的实现细节,并且不能用于表示任何已分配的字符。

  • 字节:将字符串划分为字节的输出取决于编码。例如,FIDL 字符串使用的 UTF-8 编码是一种可变长度编码,其每个标量值由 1 到 4 个字节的序列表示。(例如,k 为 1 个字节,ك 为 2 个字节, 为 3 个字节,𐤊 为 4 个字节。)UTF‐8 标准指定如何解析字节序列,以及如何确定新标量值的开始位置。由于 UTF-8 是一种可变长度编码,因此无法确定 UTF-8 字符串中标量值的数量,也无法以恒定时间跳转到第 n 个标量值。n

  • 字素聚类:以图形方式呈现时,某些 Unicode 标量值的组合会组合成一个用户感知的“字符”,在技术上称为“字素聚类”。示例包括带变音符号的字母 (á̡)、包含性别和肤色选择器的脸部表情符号 (💂🏽‍♀️),以及组合为旗帜表情符号的双字母国家/地区代码 (🇦🇺)。将标量值合并到字素聚类的规则是因上下文而异的,并取决于从 Unicode 字符数据库中读取的属性;因此,它们可能会从一个版本更改为下一个标准版本。

虽然与 FIDL 及其 UTF‐8 字符串没有直接关系,但许多使用 UTF‐16 编码的旧版运行时具有一个额外的除法选项:

  • UTF‐16 代码单元:在 UTF‐16 编码中,每个 Unicode 标量值都由一个或两个称为 UTF‐16“代码单元”的 2 字节序列编码。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::UnicodeStringicu::StringCharacterIterator 搭配使用。例如,可以使用 setIndex32(n) 检索字符串中的第 n 个标量值。n

性能

对于字符串使用可变长度编码(例如 Rust)或 UTF‐16(例如 Dart)的运行时,按 Unicode 标量值访问字符串位置或长度是一项线性时间操作。(对于 UTF‐32 和类似的固定长度编码而言,这只是一个恒定时间运算,它们空间效率低下且不常用。)

对于频繁访问字符串长度并预计存在长字符串的用例,为了获得按比例分摊的常量时间,明智的做法是缓存长度值或以其他方式对字符串进行预处理。

工效学设计

Unicode 标量值在文本编辑和组合方面具有以下优势:

  • 这种粒度可防止将 UTF‐8 编码字符拆分为无效的字节序列。
  • 它允许(如有必要)在 Grapheme 集群内部进行修改。例如,在输入“á”(“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 文本分割明确声明了

    本文档定义了字形集群的默认规范。它可以针对特定语言、操作或其他情况进行自定义。例如,您可以按语言定制箭头键的移动,也可以利用特定于特定字体的知识进行更精细的移动,在修改各个组件会很有用的情况下。例如,这适用于泰国北部文字泰坦(兰纳)的复杂编辑要求。同样,在某些情况下,可能最好按元素修改字形聚类元素。例如,在给定的系统上,退格键可能会按代码点删除,而删除键可能会删除整个集群。

UTF‐16 代码单元

优点

  • 许多第三方标准库和运行时在内部为其字符串使用 UTF‐16 编码。

缺点

  • FIDL 传输 UTF‐8(而非 UTF‐16)的字符串。如果通过 FIDL 将新编码的单元引入文本编辑 API 是完全没有依据的,并且会强制实现人员在内部至少支持两种不同的编码,从而造成混淆。
  • 与单个字节一样,很容易无意中将标量值拆分为不匹配的 UTF‐16 代理。

早期技术和参考资料

Flutter

Flutter 似乎主要已迁移为在其公共 API 中使用 Grapheme 集群,但其文档仍不一致:

  • Dart 的 String 类文档指出“字符串由一系列 Unicode UTF‐16 代码单元表示”和“字符串的字符采用 UTF‐16 编码。解码 UTF-16(用于组合代理对)会生成 Unicode 代码点”,这意味着“字符”表示代码单元。

  • Flutter 不会明确记录其 TextPositionTextRange 单元,将 offset 定义为“紧跟在文本的字符串表示形式中相应位置的字符索引”,而不是在此处定义字符

  • Flutter 的 TextField.maxLength 属性定义为

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

    详细内容如下:

    字符
    如需了解什么是字符的具体定义,请参阅 Pub 上的 characters 软件包,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。UITextInput 协议使用不透明的抽象类 UITextRangeUITextPosition,它们是特定于实现的类。

Windows

Windows Core Text 的文档将其索引称为“应用光标位置”,如下所述

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

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


  1. 字符串 "ch" 中有多少个字素聚类?在 en-US 语言区域,就是两个值。在 cs-CZ(捷克语)中,它应该是一个数字,因为 'ch' 是一个二元图。