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

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

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

问题
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 将作为多个独立运行时(不一定相互感知)之间的互操作机制,因此它必须提供一个明确定义的接口,该接口在实现上具有一致性且切实可行,而无需标准化任何运行时的实现细节。

利益相关方

主持人:abarth@google.com

Reviewers:

  • 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 编码的实现细节,不能用于表示任何已分配的字符。

  • 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::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 文本分段明确指出

    本文档定义了音素簇的默认规范。您可以根据特定语言、操作或其他情况对其进行自定义。例如,箭头键的移动方式可以根据语言进行量身定制,也可以使用特定字体的专有知识以更精细的方式移动,在某些情况下,这对于修改各个组件非常有用。例如,这适用于泰北文字泰语(兰纳语)的复杂编辑要求。同样,在某些情况下,逐个修改音素簇元素可能更为合适。例如,在给定系统上,Backspace 键可能会按代码点进行删除,而 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 上的 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 语言区域中,此值为 2。在 cs-CZ(捷克语)中,它应该只有一个,因为 'ch' 是一个二元字符。