RFC-0195:Text API 中的位置和範圍

RFC-0195:文字 API 中的位置和範圍
狀態已接受
區域
  • HCI
說明

規定在文字編輯 API 中,位置和範圍的基本單位為 Unicode 純量值。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-08-26
審查日期 (年-月-日)2022-10-25

摘要

我們建議 fuchsia.input.text API 使用萬國碼純量值做為文字編輯位置和範圍 (例如插入號和選取範圍) 的最小單位。

提振精神

fuchsia.input.text 命名空間會提供文字編輯和撰寫的 FIDL 通訊協定,以便跨執行階段實作文字欄位、輸入法編輯器 (IME)、複製及貼上、自動修正和相關功能。這些 API 將包含多種方法,可擷取、選取及修改文字範圍。這些方法設計的基本部分是,API 必須標準化索引至 Unicode 字串的方式。舉例來說,如果以 Flutter 實作的螢幕小鍵盤指示 Chromium 網頁檢視區塊中的文字方塊「刪除插入號前方的三個字元」,鍵盤和瀏覽器必須就「三個字元」的意義和插入號目前的位置達成共識,才能確保正常運作。

Fuchsia 樹狀結構內執行階段目前尚未就字串操控的基本單位達成共識 (請參閱「先前技術和參考資料」)。我們審查的其他幾個非 Fuchsia 平台標準 SDK,在這方面也存在不一致的情況,而且還受到舊版設計選擇的影響,許多情況下,這些選擇都早於現代 Unicode 標準。

由於國際化文字編輯是現代面向使用者的作業系統的重要功能,且 Fuchsia 沒有可採用的現有統一標準,因此 Fuchsia 有機會選擇自己的單一標準,做為跨執行階段 API,改善現狀,不僅符合人體工學,也不會受到舊版設計的阻礙。

此外,由於 Fuchsia 的文字編輯 API 會做為多個獨立執行階段之間的互通機制,這些執行階段不一定會彼此瞭解,因此必須提供明確定義的介面,方便一致地實作,但不會將任何一個執行階段的實作細節標準化。

利害關係人

協助人員:abarth@google.com

審查者:

  • 紫紅色 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

社會化

這項設計以 Google 文件的形式,在 Fuchsia HCI 團隊和部分審查人員之間傳閱,之後才以 RFC CL 的形式發布。

設計

本文中的「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] 範圍內的整數,可對應至「抽象字元」。

    萬國碼純量值是萬國碼碼點的子集,屬於 [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&str 中的純量值可使用 String::chars() 疊代。

  • 在 Dart 中,這是指 rune。字串的 Unicode 純量值可使用 String.runes 屬性疊代。

  • 自 C++ 17 起,標準程式庫的 Unicode 文字操控公用程式並不完整,因此建議改用 icu::UnicodeStringicu::StringCharacterIterator。舉例來說,字串中的第 n 個純量值可以使用 setIndex32(n) 擷取。

效能

對於使用 UTF‑8 (例如 Rust) 或 UTF‑16 (例如 Dart) 等變長編碼的執行階段,以 Unicode 純量值存取字串位置或長度是線性時間作業。(只有 UTF‑32 和類似的固定長度編碼才能以常數時間運作,但這類編碼的空間效率不佳,因此不常用。)

如果使用案例經常存取字串長度,且預期會出現長字串,建議快取長度值或預先處理字串,以達到攤銷常數時間。

人體工學

萬國碼純量值在文字編輯和撰寫方面具有下列優點:

  • 這種精細程度可避免將 UTF-8 編碼字元分割成無效的位元組序列。
  • 必要時,可透過這個字串在字素叢集內編輯。舉例來說,輸入「á」(先輸入「a」,再輸入「◌́」) 後,可以按 Backspace 鍵刪除重音符號,但無法刪除基本字母。

如要比較其他選項,請參閱「缺點、替代方案和未知事項」。

回溯相容性

這份 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 中使用字素叢集,但其說明文件仍不一致:

  • 根據 Dart 的 String 類別文件,「字串是由一連串的 Unicode UTF-16 碼元表示」,且「字串的字元是以 UTF-16 編碼。解碼 UTF‑16 (結合替代配對) 會產生 Unicode 碼點,這表示「字元」是指「程式碼單元」

  • Flutter 並未明確記錄 TextPositionTextRange 單位,而是將 offset 定義為「緊接在文字字串表示法中位置後方的字元索引」,但未在此處定義 character

  • Flutter 的 TextField.maxLength 屬性定義如下:

    文字欄位中允許的字元數 (Unicode 字形叢集) 上限。

    詳情請參閱下文:

    字元
    如要瞭解字元的具體定義,請參閱 Pub 上的字元套件,Flutter 會使用這個套件來劃分字元。一般來說,即使是複雜的字元 (例如替代配對和擴充字素叢集),Flutter 也會正確解讀為使用者感知到的單一字元。

網路

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'二合字母。