This website records my snippets, patterns, and some practices.
悉曇文字在呈現時,會有將母音與子音相組合,以及子音彼此之間相組合等各種情況(孤合字,滿字,種子字…)。
下表為各字型在渲染結果上的差異:
從渲染結果來看,iOS 可直接使用系統內建字型。而 Android 則建議使用Noto Sans Siddham字型。
拆分句子為單獨字元時,則不論是iOS或Android,在孤合字,滿字的判斷上,都出現預期外的結果。
例如: 𑖭𑖿𑖪𑖯𑖮𑖯(svāhā, 娑婆訶) 為佛教真言中常見結尾。
在 iOS 中,用 String.flatMap
拆解 𑖭𑖿𑖪𑖯𑖮𑖯,會得到: 𑖭𑖿、𑖪𑖯、𑖮𑖯 而非 𑖭𑖿𑖪𑖯、𑖮𑖯。
在Android 系統中,使用 BreakIterator
結果亦同。
考慮到不論iOS/Android底層文字處理,都是透過 icu 函式庫,且拆分結果與字型無關,有必要深入瞭解原因與解法。
印度語系中,有所謂半聲,半字,又稱毗藍摩(Virama)的概念。字母表中的子音,是用一個子音 + a 組成,稱為體文,如 (𑖎, ka)。實際上需要標示單一子音時,會加上 𑖿 (毗藍摩)表示將母音的部分移除。 例如:
(𑖎𑖿 , k) = (𑖎, ka) + (𑖿, Virama)
此時,單一子音,會被稱為半字,或是孤合字(單獨使用時)。當半字再加上不同母音修飾符時,才會組成一個滿字。
(𑖎𑖲 , ku) = (𑖎, ka) + (𑖲 ,Vowel Sign U)
另外,多個子母音之間,也可以透過毗藍摩組成更複雜的字,上面範例中所用的 𑖭𑖿𑖪𑖯𑖮𑖯
即是。
(𑖭𑖿𑖪𑖯𑖮𑖯, svāhā) = (𑖭, sa) + (𑖿, Virama) + (𑖪, va) + (◌𑖯, Vowel sign Ā) + (𑖮, ha) + (◌𑖯, Vowel sign Ā)
從上面的結構,以及目前iOS/Android拆解字組的結果可知,目前icu的演算法,在拆字時,會直接將 𑖿 (Virama) 視做字組的結尾,而不是兩個半字的連接符。這是因為icu在處理悉曇文的拆解時,直接套用預設字素規則所導致的,特別是GB9規則。然而,在印度語系中,毗藍摩(Virama)取代了Zero Width Joiner(ZWJ)的功能,直接套用GB9規則,會造成拆字時,將毗藍摩(Virama)視為每一個字的結尾。
在渲染時則無此問題,單純依照字體是否支援,來決定如何渲染字型(參照 N4294,section 3.9,Default Representation 一節)。
暴力解:對於較短的字句,可以直接依照需要拆解,例如六字真言: 𑖌𑖼𑖦𑖜𑖰𑖢𑖟𑖿𑖦𑖸𑖮𑖳𑖽
透過icu的拆解結果會是: 𑖌𑖼 𑖦 𑖜𑖰 𑖢 𑖟𑖿 𑖦𑖸 𑖮𑖳𑖽
手動拆解則可以自行分配字組: 𑖌𑖼 𑖦 𑖜𑖰 𑖢 𑖟𑖿𑖦𑖸 𑖮𑖳𑖽
這種做法的好處在於直觀,但缺點在於無法複用,套用在長文時也很花時間。
引入並擴充 icu 函式庫,對印度語系中的virama 做特殊處理。目前icu對天城文的字素已有做擴充處理,應該可以直接套用,這也是正規的解法。
缺點在於引入函式庫會造成程式的擴張,也會需要理解架構以及改寫的時間。另外也不確定是否會與iOS/Android內建的功能造成衝突。
針對悉曇文的拆解,不依賴icu,而是自行對unicode進行拆解,依照悉曇文組字規則,反向寫出拆字規則。
優點:規則可以自行掌握,增減,可以快速實作,不需依賴其他函式庫。也可以用在較長的文本上。 缺點:測試資料,文本不足,無法保證涵蓋所有狀況。
依照目前的需求,因為匯入文本可以事先做處理,減少一些需要判斷的狀況(例如開頭,標點,裝飾文字等…),採用解法 3 在開發速度以及程式複雜度上,會是比較適合的選擇。
實際做法,考量到多平台相容性,用正規表示式來匹配字元會是比較好的做法:
/([\x{1158E}-\x{115AE}](?:(\u{115BF}[\x{1158E}-\x{115AE}][\x{115AF}-\x{115BB}]?)+|[\x{115AF}-\x{115BB}])?)|([\x{11580}-\x{1158D}]([\x{115BC}\x{115BD}]\x{115BE}?|\x{115BE})?)/
正規表示式內容說明:
上述表示式,粗略拆分為前後兩部分:
第一部分:
([\x{1158E}-\x{115AE}](?:(\u{115BF}[\x{1158E}-\x{115AE}][\x{115AF}-\x{115BB}]?)+|[\x{115AF}-\x{115BB}])?)
第二部分:
([\x{11580}-\x{1158D}]([\x{115BC}\x{115BD}]\x{115BE}?|\x{115BE})?)
此段用以抓取以所有的字母開頭(子音+母音),後面接上仰月點或明點,以及止韻的情況。這是用來補足抓取單一母音,以及字母有加上特殊符號的情況。
程式碼:
extension String {
var isSiddham: Bool {
contains(/[\u{11580}-\u{115C9}]/)
}
func splitSiddhamChars() -> [String] {
guard isSiddham else { return [] }
return matches(of: /([\u{1158E}-\u{115AE}](?:(\u{115BF}[\u{1158E}-\u{115AE}][\u{115AF}-\u{115BB}]?)+|[\u{115AF}-\u{115BB}])?)|([\u{11580}-\u{1158D}]([\u{115BC}\u{115BD}]\u{115BE}?|\u{115BE})?)/.matchingSemantics(.unicodeScalar)).reduce([]) { partialResult, match in
partialResult + [String(self[match.range])]
}
}
}
val pattern = "([\\x{1158E}-\\x{115AE}](?:(\\x{115BF}[\\x{1158E}-\\x{115AE}][\\x{115AF}-\\x{115BB}]?)+|[\\x{115AF}-\\x{115BB}])?)|([\\x{11580}-\\x{1158D}]([\\x{115BC}\\x{115BD}]\\x{115BE}?|\\x{115BE})?)"
val regex = Regex(pattern)
regex.findAll("test string").map {
// process each grapheme
}
–
參考文件: