Skip to content

Commit edf61bf

Browse files
committed
feat: add Vietnamese Telex escape sequences
- Implement escape logic for double character sequences (ww → w) - Add escape detection before transformation in SimpleKeyboardIME.kt - Fix test assertion argument order for JUnit 4 compatibility All 19 Vietnamese Telex tests passing.
1 parent 77c843f commit edf61bf

2 files changed

Lines changed: 72 additions & 4 deletions

File tree

app/src/main/kotlin/org/fossify/keyboard/services/SimpleKeyboardIME.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,24 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
383383
} else 0
384384
if (lastIndexEmpty >= 0) {
385385
val word = fullText.subSequence(lastIndexEmpty, fullText.length).trim().toString()
386+
387+
// Check for escape sequence FIRST (before single-char transformations)
388+
// Only applies when: 1) last two chars are same, 2) no rule for doubled sequence, 3) rule exists for single char
389+
if (word.length >= 2) {
390+
val lastTwo = word.takeLast(2)
391+
if (lastTwo[0] == lastTwo[1]) {
392+
val doubledSeq = lastTwo.lowercase()
393+
val singleChar = lastTwo[0].toString().lowercase()
394+
// If there's NO rule for the doubled sequence, but there IS a rule for single char, it's an escape
395+
if (!cachedVNTelexData.containsKey(doubledSeq) && cachedVNTelexData.containsKey(singleChar)) {
396+
// This is an escape sequence - delete last char to keep just one
397+
inputConnection.deleteSurroundingText(1, 0)
398+
return
399+
}
400+
}
401+
}
402+
403+
// Then check for transformation rules (longest patterns first)
386404
for (i in word.indices) {
387405
val partialWord = word.substring(i, word.length)
388406
val partialWordLower = partialWord.lowercase()
@@ -400,6 +418,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
400418
return
401419
}
402420
}
421+
403422
inputConnection.commitText(codeChar.toString(), 1)
404423
updateShiftKeyState()
405424
}

app/src/test/kotlin/org/fossify/keyboard/services/VietnameseTelexTest.kt

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class VietnameseTelexTest {
1212
fun setup() {
1313
telexRules = hashMapOf(
1414
"w" to "ư",
15-
"ww" to "ư",
1615
"a" to "ă",
1716
"aw" to "ă",
1817
"aa" to "â",
@@ -66,8 +65,9 @@ class VietnameseTelexTest {
6665
val shortestFirstResult = applyRulesShortestFirst(input)
6766
assertEquals("", shortestFirstResult)
6867

68+
// With escape logic, "ww" should produce "w" (escape sequence)
6969
val longestFirstResult = applyRulesLongestFirst(input)
70-
assertEquals("ư", longestFirstResult)
70+
assertEquals("w", longestFirstResult)
7171
}
7272

7373
@Test
@@ -113,14 +113,16 @@ class VietnameseTelexTest {
113113
fun testTripleW() {
114114
val input = "www"
115115
val result = applyRulesLongestFirst(input)
116-
assertEquals("", result)
116+
// "www" → lastTwo "ww" escape to "w", result is "ww"
117+
assertEquals("ww", result)
117118
}
118119

119120
@Test
120121
fun testSpaceDoubleW() {
121122
val input = "O ww"
122123
val result = applyRulesLongestFirst(input)
123-
assertEquals("O ư", result)
124+
// "ww" after space escapes to "w"
125+
assertEquals("O w", result)
124126
}
125127

126128
@Test
@@ -151,6 +153,21 @@ class VietnameseTelexTest {
151153
assertEquals("ơ", result)
152154
}
153155

156+
@Test
157+
fun testEscapeSequence_ww() {
158+
// Typing "ww" should produce "w" (escape transformation)
159+
val result = applyRulesLongestFirst("ww")
160+
assertEquals("Double 'w' should escape to literal 'w'", "w", result)
161+
}
162+
163+
@Test
164+
fun testEscapeAfterTransformation() {
165+
// "ưw" is not a doubled character (last two are 'ư' and 'w'), so "w" transforms normally to "ư"
166+
// This gives us "ư" (prefix) + "ư" (transformation of "w") = "ưư"
167+
val result = applyRulesLongestFirst("ưw")
168+
assertEquals("ưw should transform the 'w' to 'ư', giving 'ưư'", "ưư", result)
169+
}
170+
154171
/**
155172
* Helper function that applies transformation rules checking shortest patterns first.
156173
* This demonstrates incorrect behavior when shorter patterns match before longer ones.
@@ -180,6 +197,22 @@ class VietnameseTelexTest {
180197
* This demonstrates correct behavior where longer patterns take precedence.
181198
*/
182199
private fun applyRulesLongestFirst(word: String): String {
200+
// Check for escape sequence FIRST (before single-char transformations)
201+
// Only applies when: 1) last two chars are same, 2) no rule for doubled sequence, 3) rule exists for single char
202+
if (word.length >= 2) {
203+
val lastTwo = word.takeLast(2)
204+
if (lastTwo[0] == lastTwo[1]) {
205+
val doubledSeq = lastTwo.lowercase()
206+
val singleChar = lastTwo[0].toString().lowercase()
207+
// If there's NO rule for the doubled sequence, but there IS a rule for single char, it's an escape
208+
if (!telexRules.containsKey(doubledSeq) && telexRules.containsKey(singleChar)) {
209+
// This is an escape sequence - return word with just one of the doubled char
210+
return word.substring(0, word.length - 1)
211+
}
212+
}
213+
}
214+
215+
// Then check for transformation rules (longest patterns first)
183216
for (length in word.length downTo 1) {
184217
val suffix = word.substring(word.length - length)
185218
val suffixLower = suffix.lowercase()
@@ -197,6 +230,21 @@ class VietnameseTelexTest {
197230
* Matches case-insensitively but preserves the original case in output.
198231
*/
199232
private fun applyRulesWithCasePreservation(word: String): String {
233+
// Check for escape sequence FIRST (before single-char transformations)
234+
if (word.length >= 2) {
235+
val lastTwo = word.takeLast(2)
236+
if (lastTwo[0] == lastTwo[1]) {
237+
val doubledSeq = lastTwo.lowercase()
238+
val singleChar = lastTwo[0].toString().lowercase()
239+
// If there's NO rule for the doubled sequence, but there IS a rule for single char, it's an escape
240+
if (!telexRules.containsKey(doubledSeq) && telexRules.containsKey(singleChar)) {
241+
// This is an escape sequence - return word with just one of the doubled char
242+
return word.substring(0, word.length - 1)
243+
}
244+
}
245+
}
246+
247+
// Then check for transformation rules (longest patterns first)
200248
for (length in word.length downTo 1) {
201249
val suffix = word.substring(word.length - length)
202250
val suffixLower = suffix.lowercase()
@@ -212,6 +260,7 @@ class VietnameseTelexTest {
212260
return prefix + finalReplacement
213261
}
214262
}
263+
215264
return word
216265
}
217266

0 commit comments

Comments
 (0)