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

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

要求使用萬國碼 (Unicode) 純量值做為文字編輯 API 中位置和範圍的基本單位。

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

摘要

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

提振精神

fuchsia.input.text 命名空間會提供用於文字編輯和組合的 FIDL 通訊協定,以便跨執行階段實作文字欄位、輸入法編輯器 (IME)、複製及貼上、自動更正和相關功能。這些 API 提供多種用於擷取、選取及修改文字範圍的方法。做為這些方法的設計基礎部分,API 必須針對將索引轉換為萬國碼 (Unicode) 字串的方式進行標準化。這必須確保在 Flutter 中實作的螢幕小鍵盤,在 Chromium 網頁檢視畫面中指示一個文字方塊「刪除插入點前的三個字元」,鍵盤和瀏覽器就和「三個字元」的意義以及插入點目前位置達成共識。

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

社交

這個設計是 Fuchsia HCI 團隊的 Google 文件,在以 RFC CL 身分張貼之前,經過部分審查者認可。

設計

本文件中的重要字詞「必須」、「不得」、「必要」、「應」、「不應」、「應該」、「不應該」、「建議」、「可能」和「選用」均以 IETF RFC 2119 說明描述。

背景

如需更詳細的總覽,請參閱 FIDL API 可讀性 Rubric > 字串編碼

FIDL 字串是一個位元組序列,代表 UTF‐8 編碼的 萬國碼 (Unicode) 文字。

單位有多種選項可分割字串。

  • 萬國碼 (Unicode) 純量值:在萬國碼 (Unicode) 中,文字的基本 Atom 是「萬國碼 (Unicode) 純量值」,是範圍 [0x0, 0xD7FF], [0xE000, 0x10FFFF] 中的整數,可對應至「抽象字元」。

    萬國碼純量值是 Unicode 碼點的子集,是 [0x0, 0x10FFFF] 範圍內的整數。從萬國碼 (Unicode) 純量值 [0xD800, 0xDFFF] 中排除的碼點,稱為代理碼點。這些編碼會保留在 UTF-16 編碼的實作詳細資料,無法用於代表任何指派的字元。

  • Byte:將字串除以位元組的輸出結果,取決於編碼方式。 例如,FIDL 字串使用的 UTF‐8 編碼是一種變數長度編碼,每個純量值分別以 1 到 4 個位元組的序列表示。(例如,k 是 1 個位元組,ك 是 2 個位元組, 是三個位元組,𐤊 是 4 個位元組)。UTF-8 標準會指定如何剖析位元組序列,以及判斷新的純量值的起始位置。由於 UTF-8 為變數長度編碼,因此您無法判斷 UTF-8 字串中的純量值數量,也無法在常數時間跳至第 nn-第「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🇦。(請注意,Grapheme 叢集 🇦🇺 會分割為 🇦🇺)。

實作

在內部,實作者可使用所選程式設計語言或程式庫中最適合或最便利的 Unicode 字串編碼和索引建立方式。

不過,fuchsia.input.text 中的所有通訊協定實作

  • 「必須」使用通訊協定中指定的萬國碼 (Unicode) 純量值索引,正確解讀文字位置和範圍。
  • 「必須」以萬國碼 (Unicode) 純量值索引,將文字編輯指令傳送至其他 fuchsia.input.text 實作項目。

供參考:

  • 在 Rust 中,萬國碼 (Unicode) 純量值是單一 char,而且 String&str 中的純量值可以使用 String::chars() 進行疊代。

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

  • 自 C++ 17 起,用於操控萬國碼 (Unicode) 文字的標準程式庫公用程式並不完整,因此建議改用 icu::StringCharacterIterator 搭配 icu::UnicodeString。舉例來說,使用 setIndex32(n) 即可擷取字串中第 n-th 純量值。

效能

如果執行階段使用 UTF-8 (例如 Rust) 或 UTF‐16 (例如 Dart) 做為字串使用變數長度編碼,透過萬國碼 (Unicode) 純量值存取字串位置或長度,屬於線性作業。(這只是對 UTF‐32 和類似的固定長度編碼而言,這只是一項常數時間作業,所以不會有空間效率且不常使用)。

對於經常存取字串長度且預期會出現長字串的情況,為了達到「修剪」固定時間,快取長度值或預先處理字串可能有麻煩。

人體工學

萬國碼純量值在文字編輯與組合方面有下列優點:

  • 這種精細程度可避免將 UTF‐8 編碼字元分割成無效的位元組序列。
  • 可視需要在 Grapheme 叢集內編輯。例如,如果在輸入「á」(「a」後面加上「◌́」) 後,允許使用反向間距刪除重音,而非底字母。

如要瞭解與其他選項比較的結果,請參閱「缺點、替代方案和未知」。

回溯相容性

這個 RFC 會顧慮為 Fuchsia 平台從剪輯實作的新文字編輯 API。除了轉換在 FIDL API 文字位置表示法與任何語言執行階段中偏好的呈現方式外,我們預期不會發生回溯相容性問題。

安全性考量

依據整個萬國碼 (Unicode) 純量值 (而非位元組或 UTF‐16 編碼單位) 操控文字,可降低字串遭截斷的可能性。

如果依萬國碼 (Unicode) 純量值進行原型,就有可能分割圖形叢集 (有時是不需要的 (請參閱「Ergonomics」),但若是這種情況,可能會導致部分文字發生變化的極端案例。

不過,這個缺點是必須接受這項缺點,因為 Grapheme 叢集建立時依附於 Unicode 版本,甚至須根據實作專用的自訂設定,因此採用不同萬國碼 (Unicode) 程式庫或版本的用戶端可能會不同意字串長度,導致資料損毀。

隱私權注意事項

除了已經在處理使用者提供文字時存在的隱私權考量外,這個 RFC 並未新增任何隱私權注意事項。

測試

fuchsia.input.text API 的實作者將負責為實作項目編寫適當的單元和整合測試。這些 RFC 的要求應包含在這些測試中。

Fchsia 相容性測試提供的功能而定,實作文字編輯 API 的用戶端行為可能會經過測試,是否符合這些 API 的相關規範。舉例來說,測試可能會向代管文字欄位的用戶端應用程式傳送一系列的文字編輯指令,然後驗證產生的文字欄位內容是否符合預期。

建議實作者在測試資料中加入非 ASCII 字串,包括:

  • 多程式碼點 Grapheme 叢集,例如
    • 由多個組合變音符號的字元
    • 表情符號搭配膚色和/或性別修飾符
    • 旗幟表情符號

說明文件

fuchsia.input.text 類別的 API 說明文件會明確醒目顯示與字串位置、範圍和長度有關的任何資料類型使用的單位。

缺點、替代方案和未知

位元組

優點

  • 在 FIDL 欄位宣告中,string 長度會明確定義以位元組為單位
  • 使用位元組有助於判斷記憶體中的大小。
  • 位元組陣列的陣列存取作業為 O(1)。

缺點

  • 需要額外驗證以確保位元組序列構成有效的 UTF-8。

  • 這很容易不小心將 UTF‐8 字元分割成不完整 (也因此無效) 的位元組序列。

  • 除非知道編輯的文字僅包含 ASCII 字元,否則將位置位移一位元組不是實用的作業。

Grapheme 叢集

優點

  • 在文字編輯器中,插入點幾乎一律會放在 Grapheme 叢集界線上。

  • 從整個石墨儀叢集中選取文字可確保複雜的表情符號不會意外以容易使用的方式分割 (例如使用程式碼點,👮🏽‍♀️ (帶有中膚色的女性警官表情符號) 可拆分為 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)

缺點

  • Grapheme 叢集規則可能會在 Unicode 標準版本之間變更,並取決於 CLDR 的字元屬性資料表查詢。

    更重要的是,叢集規則組合並非由 Unicode 版本完整指定,因為實作方式以及語言代碼 1 之間允許的詳細資料有所不同。透過 FIDL 通訊的兩個元件 (例如螢幕小鍵盤和轉譯文字方塊的執行階段) 可能會使用不同的 Unicode 實作方式,因此可能難以假設其操控的文字範圍。

    Grapheme 分群法的 Unicode 規格,UAX #29:萬國碼 (Unicode) 文字區隔明確狀態

    本文件定義了 Grapheme 叢集的預設規格。可以針對特定語言、作業或其他情境自訂。舉例來說,如果使用者想編輯個別元件,可以使用語言自訂箭頭鍵移動,也可以使用特定字型的知識,以更精細的方式移動。例如,這適用於北泰字母台大 (Tham) 複雜的編輯規定。同樣地,在某些情況下,按照元素編輯 Grapheme 叢集元素可能比較適合。例如,在特定系統中,Backspace 鍵可能會根據程式碼點刪除,刪除鍵則可能會刪除整個叢集。

UTF-16 程式碼單位

優點

  • 許多第三方標準程式庫和執行階段會針對其字串使用 UTF‐16 編碼。

缺點

  • FIDL 會以 UTF‐8 傳輸字串,而非 UTF‐16。透過 FIDL 將新的編碼單元引進文字編輯 API 將完全沒有發展,並會強迫實作者在內部支援至少兩種不同的編碼,進而造成混淆。
  • 就像個別位元組一樣,很容易不小心將純量值分割為沒有相符的 UTF-16 代理值。

先前的圖片和參考資料

輕快悅耳的笛聲

Flutter 似乎已大量遷移為在其公用 API 中使用 Grapheme 叢集,但說明文件不一致:

  • Dart 的 String 類別說明文件指出「字串」是以一組萬國碼 (Unicode) UTF‐16 代碼單位表示,以及「字串的字元是以 UTF‐16 編碼。解碼 UTF‐16 會結合代理值組合,產生萬國碼 (Unicode) 碼點,意味著字元代表程式碼單位

  • Flutter 不會明確記錄其 TextPositionTextRange 單位,也就是將 offset 定義為「與字串中位置相符的字元索引」,但此處不會定義字元

  • 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 語言代碼中則有兩個。在 cs-CZ (捷克文) 中,這個項目應為 1,因為 'ch'digraph