Skip to content

Commit cc159f8

Browse files
committed
Typewriter // MixedAlnum: Fix edge cases with lowerCamelCase.
1 parent c42085b commit cc159f8

2 files changed

Lines changed: 83 additions & 2 deletions

File tree

Packages/vChewing_Typewriter/Sources/Typewriter/Typewriter/Typewriter_MixedAlphanumerical.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,21 @@ public struct MixedAlphanumericalTypewriter<Handler: InputHandlerProtocol>: Type
138138
// BPMF 失敗,恢復 buffer 並繼續嘗試 auto-split
139139
handler.mixedAlphanumericalBuffer = originalMixedBuffer
140140
}
141+
// 若 buffer 含多個大寫字母且有小寫字母(camelCase/縮寫,如 cOS、macOS),
142+
// 即使 shouldPreferASCIIWordOnSpace 為 true,仍應嘗試 auto-split,
143+
// 以支援「cOS + 注音」這類混輸。
144+
// 條件:≥2 個大寫 + ≥1 個小寫,可區分於簡單首字大寫詞(Hello、Mac)或全大寫詞(HELLO)。
145+
let bufferHasMultipleUppercaseAndLowercase: Bool = {
146+
let buffer = handler.mixedAlphanumericalBuffer
147+
let hasLower = buffer.range(of: "[a-z]", options: .regularExpression) != nil
148+
let ucCount = buffer.unicodeScalars.filter { "A"..."Z" ~= $0 }.count
149+
return hasLower && ucCount >= 2
150+
}()
141151
// 當 buffer 較長(>= 5 字元)時,即使 shouldPreferASCIIWordOnSpace 為 true
142152
// 也嘗試 auto-split,以支援 hello你好 等長 ASCII prefix 的混輸情境。
143153
if !shouldPreferASCIIWordOnSpace || twoCharPrefixIsPhonetic
144-
|| handler.mixedAlphanumericalBuffer.count >= 5 {
154+
|| handler.mixedAlphanumericalBuffer.count >= 5
155+
|| bufferHasMultipleUppercaseAndLowercase {
145156
// 先嘗試無 word-like 限制的 auto-split(fallback),以正確保留常見雙字母前綴(如 ai)。
146157
// 若 fallback 失敗,再嘗試 word-like 限制,以支援 hello你好 類型混輸。
147158
if tryAutoSplitASCIIAndPhoneticSuffix(
@@ -633,6 +644,13 @@ public struct MixedAlphanumericalTypewriter<Handler: InputHandlerProtocol>: Type
633644
if trialComposer.hasIntonation(withNothingElse: true) { return nil }
634645
}
635646

647+
// 後綴若包含大寫字母,不得被視為注音後綴。
648+
// 大寫字母只能透過 Shift 鍵入,在 mixed mode 下明確代表 ASCII 意圖。
649+
// 若允許大寫字母進入注音 composer(因 receiveKey 對大小寫不敏感),
650+
// 則「macOS 」這類 camelCase 輸入會被誤拆為 ASCII 前綴 + 注音後綴。
651+
let suffixHasUppercase = suffixText.range(of: "[A-Z]", options: .regularExpression) != nil
652+
guard !suffixHasUppercase else { return nil }
653+
636654
trialComposer.clear()
637655
// 在評估 mixed 輸入時,暫時停用自動糾正。否則 auto-correct 會讓不同的 suffix
638656
// 坍縮到同一個 reading key(例如「zj/4」被糾正為「ㄈㄥˋ」而與「z/4」相同),

Packages/vChewing_Typewriter/Tests/TypewriterTests/InputHandlerTests_Cases4.swift

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extension InputHandlerTests {
3131
/// - Parameters:
3232
/// - thePrefix: 要測試的前綴。
3333
/// - Warning: This unit test case is not designed for dynamic phonabet layouts.
34-
@Test(arguments: ["", "ai", "AI", "hello", "Hello"])
34+
@Test(arguments: ["", "ai", "ello", "cOS"])
3535
func test_IH400_MixedAlnumKanjiInputTest_Izanami(
3636
_ thePrefix: String
3737
) throws {
@@ -1163,6 +1163,69 @@ extension InputHandlerTests {
11631163
#expect(testHandler.committableDisplayText(sansReading: true) == "", "5k4 應為 ㄓㄜˋ=這")
11641164
}
11651165

1166+
// MARK: — camelCase 後綴不得被誤判為注音
1167+
1168+
/// camelCase 英文詞中大寫字母不得被 auto-split 誤判為注音後綴。
1169+
/// 此測試鎖住 bug:`macOS ` 被誤拆成 `ma` + 注音後綴 `OS`。
1170+
@Test
1171+
func test_IH429_MixedCamelCaseSuffixNotTreatedAsPhonetic() throws {
1172+
let (testHandler, testSession) = try prepareMixedModeHandler()
1173+
1174+
// macOS:大寫後綴 "OS" 不得被當作注音
1175+
typeSentence("macOS ")
1176+
#expect(testSession.recentCommissions.joined() == "macOS ")
1177+
#expect(testHandler.mixedAlphanumericalBuffer.isEmpty)
1178+
#expect(testHandler.composer.isEmpty)
1179+
1180+
testHandler.clear()
1181+
testSession.recentCommissions.removeAll()
1182+
1183+
// This:大寫開頭的純 ASCII 單字應維持完整
1184+
typeSentence("This ")
1185+
#expect(testSession.recentCommissions.joined() == "This ")
1186+
#expect(testHandler.mixedAlphanumericalBuffer.isEmpty)
1187+
#expect(testHandler.composer.isEmpty)
1188+
}
1189+
1190+
// MARK: — CamelCase 縮寫作為 ASCII 前綴 + 注音後綴
1191+
1192+
/// camelCase 縮寫(如 cOS / macOS)作為 ASCII 前綴時,
1193+
/// 後續注音輸入應正確拆分,而非整段被當作 ASCII 提交。
1194+
@Test
1195+
func test_IH430_MixedCamelCasePrefixWithPhoneticSuffix() throws {
1196+
let (testHandler, testSession) = try prepareMixedModeHandler()
1197+
testHandler.currentLM.setOptions { cfg in
1198+
cfg.alwaysSupplyETenDOSUnigrams = true
1199+
}
1200+
1201+
// cOS + ㄆ(Dachen26 鍵序 q )
1202+
typeSentence("cOSq ")
1203+
let allCommissions = testSession.recentCommissions.joined()
1204+
#expect(allCommissions.contains("cOS"), "cOS prefix should be committed as ASCII")
1205+
#expect(testHandler.assembler.length == 1, "Assembler should have one reading")
1206+
#expect(testHandler.assembler.actualKeys.last == "", "Reading should be ㄆ")
1207+
1208+
testHandler.clear()
1209+
testSession.resetInputHandler(forceComposerCleanup: true)
1210+
1211+
// macOS + ㄆ(Dachen26 鍵序 q )
1212+
typeSentence("macOSq ")
1213+
let allCommissions2 = testSession.recentCommissions.joined()
1214+
#expect(allCommissions2.contains("macOS"), "macOS prefix should be committed as ASCII")
1215+
#expect(testHandler.assembler.length == 1, "Assembler should have one reading")
1216+
#expect(testHandler.assembler.actualKeys.last == "", "Reading should be ㄆ")
1217+
1218+
testHandler.clear()
1219+
testSession.resetInputHandler(forceComposerCleanup: true)
1220+
1221+
// cOS + ㄇㄛ(Dachen26 鍵序 ai )
1222+
typeSentence("cOSai ")
1223+
let allCommissions3 = testSession.recentCommissions.joined()
1224+
#expect(allCommissions3.contains("cOS"), "cOS prefix should be committed as ASCII")
1225+
#expect(testHandler.assembler.length == 1, "Assembler should have one reading")
1226+
#expect(testHandler.assembler.actualKeys.last == "ㄇㄛ", "Reading should be ㄇㄛ")
1227+
}
1228+
11661229
// MARK: - Fileprivate Helpers.
11671230

11681231
fileprivate func prepareMixedModeHandler() throws -> (handler: MockInputHandler, session: MockSession) {

0 commit comments

Comments
 (0)