From 98c4b96895acee6847b094d55129425e7f605d7d Mon Sep 17 00:00:00 2001 From: loccun Date: Sat, 2 May 2026 16:01:49 +0700 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20add=20EbracketTransform=20flag=20fo?= =?UTF-8?q?r=20[=20->=20=C6=B0,=20]=20->=20=C6=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add EbracketTransform flag (independent of input method) - Transform '[' to 'ơ' and ']' to 'ư' when flag is set - Remove Telex 2 input method definition - Update flag in EstdFlags --- bamboo.go | 35 +++++++++++++++++++++++++++++------ input_method_def.go | 12 +++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/bamboo.go b/bamboo.go index 8d2b77f..d8bfb1a 100644 --- a/bamboo.go +++ b/bamboo.go @@ -38,7 +38,8 @@ const ( EstdToneStyle EautoCorrectEnabled Ew2uEnabled - EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled + EbracketTransform // New: independent option for [ -> ư, ] -> ơ (works with any input method) + EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled | EbracketTransform ) @@ -140,12 +141,34 @@ func (e *BambooEngine) generateTransformations(composition []*Transformation, lo // transformation fall-backs to an APPENDING one. transformations = generateFallbackTransformations(composition, e.getApplicableRules(lowerKey), lowerKey, isUpperCase) - // Unified Modular W2U Logic - canApplyW2U := (e.w2uMode == W2uEverywhere) || - (e.w2uMode == W2uNonStart && len(composition) > 0) || - (e.w2uMode == W2uFollowFlags && e.flags&Ew2uEnabled != 0) + // Bracket transform: [ -> ơ, ] -> ư (independent of input method) + if e.flags&EbracketTransform != 0 { + if lowerKey == '[' && len(transformations) > 0 { + if transformations[0].Rule.Result == '[' { + transformations[0].Rule.Result = 'ơ' + transformations[0].Rule.EffectOn = 'ơ' + } else if transformations[0].Rule.Result == '[' { + transformations[0].Rule.Result = 'Ơ' + transformations[0].Rule.EffectOn = 'Ơ' + } + } + if lowerKey == ']' && len(transformations) > 0 { + if transformations[0].Rule.Result == ']' { + transformations[0].Rule.Result = 'ư' + transformations[0].Rule.EffectOn = 'ư' + } else if transformations[0].Rule.Result == ']' { + transformations[0].Rule.Result = 'Ư' + transformations[0].Rule.EffectOn = 'Ư' + } + } + } + + // Keep old W2U logic for backward compatibility + canApplyW2U := (e.w2uMode == W2uEverywhere) || + (e.w2uMode == W2uNonStart && len(composition) > 0) || + (e.w2uMode == W2uFollowFlags && e.flags&Ew2uEnabled != 0) - if canApplyW2U && lowerKey == 'w' && len(transformations) > 0 { + if canApplyW2U && lowerKey == 'w' && len(transformations) > 0 { if transformations[0].Rule.Result == 'w' { transformations[0].Rule.Result = 'ư' transformations[0].Rule.EffectOn = 'ư' diff --git a/input_method_def.go b/input_method_def.go index bb7463f..b03d9fd 100644 --- a/input_method_def.go +++ b/input_method_def.go @@ -23,6 +23,8 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "o": "O_Ô", "w": "UOA_ƯƠĂ", "d": "D_Đ", + // Telex 2 independent option: [ -> ư, ] -> ơ (need Etelex2Enabled flag) + // These will be added dynamically in the engine }, "VNI": { "0": "XoaDauThanh", @@ -83,11 +85,11 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "w": "UOA_ƯƠĂ", "d": "D_Đ", "]": "__ư", - "[": "__ơ", - "}": "_Ư", - "{": "_Ơ", - }, - "Telex + VNI": { + "[": "__ơ", + "}": "_Ư", + "{": "_Ơ", + }, + "Telex + VNI": { "z": "XoaDauThanh", "s": "DauSac", "f": "DauHuyen", From f024aa6617e720d2076d34cfa8f9f4aa25548fa5 Mon Sep 17 00:00:00 2001 From: loccun Date: Sat, 2 May 2026 16:12:03 +0700 Subject: [PATCH 2/4] feat: implement BracketTransform mode like W2U MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Telex 2 input method completely - Add BracketTransformFollowFlags, Disabled, NonStart, Everywhere - Add bracketTransformMode to BambooEngine (like w2uMode) - Transform [ -> ơ (lower) / Ơ (upper) when enabled - Transform ] -> ư (lower) / Ư (upper) when enabled - Remove all [] and {} mappings from input_method_def.go - Update FcitxBambooEngineOption struct --- bamboo.go | 34 ++++++++++++---- input_method_def.go | 95 +++++++++++++++------------------------------ 2 files changed, 58 insertions(+), 71 deletions(-) diff --git a/bamboo.go b/bamboo.go index d8bfb1a..05c3b6f 100644 --- a/bamboo.go +++ b/bamboo.go @@ -33,13 +33,20 @@ const ( W2uEverywhere = 2 ) +const ( + BracketTransformFollowFlags = -1 + BracketTransformDisabled = 0 + BracketTransformNonStart = 1 + BracketTransformEverywhere = 2 +) + const ( EfreeToneMarking uint = 1 << iota EstdToneStyle EautoCorrectEnabled Ew2uEnabled - EbracketTransform // New: independent option for [ -> ư, ] -> ơ (works with any input method) - EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled | EbracketTransform + EbracketTransformEnabled + EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled | EbracketTransformEnabled ) @@ -69,14 +76,16 @@ type BambooEngine struct { inputMethod InputMethod flags uint w2uMode int // W2uFollowFlags, W2uDisabled, W2uNonStart, W2uEverywhere + bracketTransformMode int // BracketTransformFollowFlags, Disabled, NonStart, Everywhere } func NewEngine(inputMethod InputMethod, flag uint) IEngine { engine := BambooEngine{ - inputMethod: inputMethod, - flags: flag, - w2uMode: W2uFollowFlags, - } + inputMethod: inputMethod, + flags: flag, + w2uMode: W2uFollowFlags, + bracketTransformMode: BracketTransformFollowFlags, + } return &engine } @@ -94,6 +103,12 @@ func (e *BambooEngine) SetW2UMode(mode int) { } } +func (e *BambooEngine) SetBracketTransformMode(mode int) { + if mode >= BracketTransformDisabled && mode <= BracketTransformEverywhere { + e.bracketTransformMode = mode + } +} + func (e *BambooEngine) GetFlag(flag uint) uint { return e.flags } @@ -141,8 +156,11 @@ func (e *BambooEngine) generateTransformations(composition []*Transformation, lo // transformation fall-backs to an APPENDING one. transformations = generateFallbackTransformations(composition, e.getApplicableRules(lowerKey), lowerKey, isUpperCase) - // Bracket transform: [ -> ơ, ] -> ư (independent of input method) - if e.flags&EbracketTransform != 0 { + canApplyBracket := (e.bracketTransformMode == BracketTransformEverywhere) || + (e.bracketTransformMode == BracketTransformNonStart && len(composition) > 0) || + (e.bracketTransformMode == BracketTransformFollowFlags && e.flags&EbracketTransformEnabled != 0) + + if canApplyBracket { if lowerKey == '[' && len(transformations) > 0 { if transformations[0].Rule.Result == '[' { transformations[0].Rule.Result = 'ơ' diff --git a/input_method_def.go b/input_method_def.go index b03d9fd..afae43e 100644 --- a/input_method_def.go +++ b/input_method_def.go @@ -1,11 +1,3 @@ -/* - * Bamboo - A Vietnamese Input method editor - * Copyright (C) Luong Thanh Lam - * - * This software is licensed under the MIT license. For more information, - * see . - */ - package bamboo type InputMethodDefinition map[string]string @@ -23,8 +15,6 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "o": "O_Ô", "w": "UOA_ƯƠĂ", "d": "D_Đ", - // Telex 2 independent option: [ -> ư, ] -> ơ (need Etelex2Enabled flag) - // These will be added dynamically in the engine }, "VNI": { "0": "XoaDauThanh", @@ -49,7 +39,7 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "+": "UO_ƯƠ", "*": "UO_ƯƠ", "(": "A_Ă", - "d": "D_Đ", + "\\": "D_Đ", }, "Microsoft layout": { "8": "DauSac", @@ -67,12 +57,8 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "$": "_Ô", "0": "__đ", ")": "_Đ", - "[": "__ư", - "{": "_Ư", - "]": "__ơ", - "}": "_Ơ", }, - "Telex 2": { + "Telex + VNI": { "z": "XoaDauThanh", "s": "DauSac", "f": "DauHuyen", @@ -84,12 +70,18 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "o": "O_Ô", "w": "UOA_ƯƠĂ", "d": "D_Đ", - "]": "__ư", - "[": "__ơ", - "}": "_Ư", - "{": "_Ơ", - }, - "Telex + VNI": { + "0": "XoaDauThanh", + "1": "DauSac", + "2": "DauHuyen", + "3": "DauHoi", + "4": "DauNga", + "5": "DauNang", + "6": "AEO_ÂÊÔ", + "7": "UO_ƯƠ", + "8": "A_Ă", + "9": "D_Đ", + }, + "Telex + VNI + VIQR": { "z": "XoaDauThanh", "s": "DauSac", "f": "DauHuyen", @@ -111,51 +103,28 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "7": "UO_ƯƠ", "8": "A_Ă", "9": "D_Đ", - }, - "Telex + VNI + VIQR": { - "z": "XoaDauThanh", - "s": "DauSac", - "f": "DauHuyen", - "r": "DauHoi", - "x": "DauNga", - "j": "DauNang", - "a": "A_Â", - "e": "E_Ê", - "o": "O_Ô", - "w": "UOA_ƯƠĂ", - "d": "D_Đ", - "0": "XoaDauThanh", - "1": "DauSac", - "2": "DauHuyen", - "3": "DauHoi", - "4": "DauNga", - "5": "DauNang", - "6": "AEO_ÂÊÔ", - "7": "UO_ƯƠ", - "8": "A_Ă", - "9": "D_Đ", - "'": "DauSac", - "`": "DauHuyen", - "?": "DauHoi", - "~": "DauNga", - ".": "DauNang", - "^": "AEO_ÂÊÔ", - "+": "UO_ƯƠ", - "*": "UO_ƯƠ", - "(": "A_Ă", + "'": "DauSac", + "`": "DauHuyen", + "?": "DauHoi", + "~": "DauNga", + ".": "DauNang", + "^": "AEO_ÂÊÔ", + "+": "UO_ƯƠ", + "*": "UO_ƯƠ", + "(": "A_Ă", "\\": "D_Đ", }, "VNI Bàn phím tiếng Pháp": { - "&": "XoaDauThanh", - "é": "DauSac", + "&": "XoaDauThanh", + "é": "DauSac", "\"": "DauHuyen", - "'": "DauHoi", - "(": "DauNga", - "-": "DauNang", - "è": "AEO_ÂÊÔ", - "_": "UO_ƯƠ", - "ç": "A_Ă", - "à": "D_Đ", + "'": "DauHoi", + "(": "DauNga", + "-": "DauNang", + "è": "AEO_ÂÊÔ", + "_": "UO_ƯƠ", + "ç": "A_Ă", + "à": "D_Đ", }, } From 2764a626f97aa388f63b51ac0dd6f7252655aca4 Mon Sep 17 00:00:00 2001 From: loccun Date: Sat, 2 May 2026 16:46:06 +0700 Subject: [PATCH 3/4] fix: correctly handle upper/lower case in BracketTransform and update IEngine interface --- bamboo.go | 100 ++++++++++++++++++++++++++++---------------- bamboo_test.go | 90 +++++++++++++++++++++++++++++++++++---- bamboo_utils.go | 2 +- input_method_def.go | 88 ++++++++++++++++++++------------------ 4 files changed, 194 insertions(+), 86 deletions(-) diff --git a/bamboo.go b/bamboo.go index 05c3b6f..f5a5a19 100644 --- a/bamboo.go +++ b/bamboo.go @@ -46,7 +46,7 @@ const ( EautoCorrectEnabled Ew2uEnabled EbracketTransformEnabled - EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled | EbracketTransformEnabled + EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled ) @@ -61,6 +61,7 @@ type IEngine interface { GetInputMethod() InputMethod ProcessKey(rune, Mode) SetW2UMode(int) + SetBracketTransformMode(int) ProcessString(string, Mode) GetProcessedString(Mode) string IsValid(bool) bool @@ -104,7 +105,7 @@ func (e *BambooEngine) SetW2UMode(mode int) { } func (e *BambooEngine) SetBracketTransformMode(mode int) { - if mode >= BracketTransformDisabled && mode <= BracketTransformEverywhere { + if mode >= BracketTransformFollowFlags && mode <= BracketTransformEverywhere { e.bracketTransformMode = mode } } @@ -114,19 +115,21 @@ func (e *BambooEngine) GetFlag(flag uint) uint { } func (e *BambooEngine) IsValid(inputIsFullComplete bool) bool { - var _, last = extractLastWord(e.composition, e.GetInputMethod().Keys) + keys := append(e.GetInputMethod().Keys, '[', ']', '{', '}') + var _, last = extractLastWord(e.composition, keys) return isValid(last, inputIsFullComplete) } func (e *BambooEngine) GetProcessedString(mode Mode) string { var tmp []*Transformation + keys := append(e.inputMethod.Keys, '[', ']', '{', '}') if mode&FullText != 0 { tmp = e.composition } else if mode&PunctuationMode != 0 { - _, tmp = extractLastWordWithPunctuationMarks(e.composition, e.inputMethod.Keys) + _, tmp = extractLastWordWithPunctuationMarks(e.composition, keys) return Flatten(tmp, VietnameseMode) } else { - _, tmp = extractLastWord(e.composition, e.inputMethod.Keys) + _, tmp = extractLastWord(e.composition, keys) } return Flatten(tmp, mode) } @@ -146,7 +149,14 @@ func (e *BambooEngine) findTargetByKey(composition []*Transformation, key rune) } func (e *BambooEngine) CanProcessKey(key rune) bool { - return canProcessKey(key, e.inputMethod.Keys) + if canProcessKey(key, e.inputMethod.Keys) { + return true + } + lowerKey := unicode.ToLower(key) + if lowerKey == '[' || lowerKey == ']' || lowerKey == '{' || lowerKey == '}' { + return true + } + return false } func (e *BambooEngine) generateTransformations(composition []*Transformation, lowerKey rune, isUpperCase bool) []*Transformation { @@ -155,53 +165,70 @@ func (e *BambooEngine) generateTransformations(composition []*Transformation, lo // If none of the applicable_rules can actually be applied then this new // transformation fall-backs to an APPENDING one. transformations = generateFallbackTransformations(composition, e.getApplicableRules(lowerKey), lowerKey, isUpperCase) - + } + canApplyBracket := (e.bracketTransformMode == BracketTransformEverywhere) || - (e.bracketTransformMode == BracketTransformNonStart && len(composition) > 0) || + (e.bracketTransformMode == BracketTransformNonStart && len(e.composition) > 0) || (e.bracketTransformMode == BracketTransformFollowFlags && e.flags&EbracketTransformEnabled != 0) if canApplyBracket { - if lowerKey == '[' && len(transformations) > 0 { - if transformations[0].Rule.Result == '[' { - transformations[0].Rule.Result = 'ơ' - transformations[0].Rule.EffectOn = 'ơ' - } else if transformations[0].Rule.Result == '[' { - transformations[0].Rule.Result = 'Ơ' - transformations[0].Rule.EffectOn = 'Ơ' + if len(composition) > 0 { + lastTrans := composition[len(composition)-1] + if (lowerKey == '[' || lowerKey == '{') && (lastTrans.Rule.Key == '[' || lastTrans.Rule.Key == '{') && (lastTrans.Rule.Result == 'ơ' || lastTrans.Rule.Result == 'Ơ') { + return []*Transformation{{ + Target: lastTrans, + Rule: Rule{ + EffectType: MarkTransformation, + Effect: uint8(MarkRaw), + }, + }} + } + if (lowerKey == ']' || lowerKey == '}') && (lastTrans.Rule.Key == ']' || lastTrans.Rule.Key == '}') && (lastTrans.Rule.Result == 'ư' || lastTrans.Rule.Result == 'Ư') { + return []*Transformation{{ + Target: lastTrans, + Rule: Rule{ + EffectType: MarkTransformation, + Effect: uint8(MarkRaw), + }, + }} + } + } + if (lowerKey == '[' || lowerKey == '{') && len(transformations) > 0 && (transformations[0].Rule.Result == '[' || transformations[0].Rule.Result == '{') { + transformations[0].Rule.Result = 'ơ' + transformations[0].Rule.EffectOn = 'ơ' + if lowerKey == '{' { + transformations[0].IsUpperCase = true } } - if lowerKey == ']' && len(transformations) > 0 { - if transformations[0].Rule.Result == ']' { - transformations[0].Rule.Result = 'ư' - transformations[0].Rule.EffectOn = 'ư' - } else if transformations[0].Rule.Result == ']' { - transformations[0].Rule.Result = 'Ư' - transformations[0].Rule.EffectOn = 'Ư' + if (lowerKey == ']' || lowerKey == '}') && len(transformations) > 0 && (transformations[0].Rule.Result == ']' || transformations[0].Rule.Result == '}') { + transformations[0].Rule.Result = 'ư' + transformations[0].Rule.EffectOn = 'ư' + if lowerKey == '}' { + transformations[0].IsUpperCase = true } } } - + // Keep old W2U logic for backward compatibility canApplyW2U := (e.w2uMode == W2uEverywhere) || (e.w2uMode == W2uNonStart && len(composition) > 0) || (e.w2uMode == W2uFollowFlags && e.flags&Ew2uEnabled != 0) if canApplyW2U && lowerKey == 'w' && len(transformations) > 0 { - if transformations[0].Rule.Result == 'w' { - transformations[0].Rule.Result = 'ư' - transformations[0].Rule.EffectOn = 'ư' - } else if transformations[0].Rule.Result == 'W' { - transformations[0].Rule.Result = 'Ư' - transformations[0].Rule.EffectOn = 'Ư' - } + if transformations[0].Rule.Result == 'w' { + transformations[0].Rule.Result = 'ư' + transformations[0].Rule.EffectOn = 'ư' + } else if transformations[0].Rule.Result == 'W' { + transformations[0].Rule.Result = 'Ư' + transformations[0].Rule.EffectOn = 'Ư' } - var newComposition = append(composition, transformations...) + } + var newComposition = append(composition, transformations...) - // Implement the uwo+ typing shortcut by creating a virtual - // Mark.HORN rule that targets 'u' or 'o'. - if virtualTrans := e.applyUowShortcut(newComposition); virtualTrans != nil { - transformations = append(transformations, virtualTrans) - } + // Implement the uwo+ typing shortcut by creating a virtual + // Mark.HORN rule that targets 'u' or 'o'. + if virtualTrans := e.applyUowShortcut(newComposition); virtualTrans != nil { + transformations = append(transformations, virtualTrans) } /** * Sometimes, a tone's position in a previous state must be changed to fit the new state @@ -213,7 +240,6 @@ func (e *BambooEngine) generateTransformations(composition []*Transformation, lo transformations = append(transformations, e.refreshLastToneTarget(append(composition, transformations...))...) return transformations } - func (e *BambooEngine) newComposition(composition []*Transformation, key rune, isUpperCase bool) []*Transformation { // Just process the key stroke on the last syllable var previousTransformations, lastSyllable = extractLastSyllable(composition) diff --git a/bamboo_test.go b/bamboo_test.go index 2e5428e..97c6869 100644 --- a/bamboo_test.go +++ b/bamboo_test.go @@ -13,8 +13,10 @@ import ( ) func newStdEngine() IEngine { - var im = ParseInputMethod(InputMethodDefinitions, "Telex 2") - return NewEngine(im, EstdFlags) + var im = ParseInputMethod(InputMethodDefinitions, "Telex") + var ng = NewEngine(im, EstdFlags) + ng.SetBracketTransformMode(BracketTransformEverywhere) + return ng } func TestProcessString(t *testing.T) { @@ -171,16 +173,17 @@ func TestTelex23(t *testing.T) { if ng.GetProcessedString(VietnameseMode) != "]a" { t.Errorf("Process ]aa, got %s valid=%v expected true", ng.GetProcessedString(VietnameseMode), ng.IsValid(true)) } - var im = ParseInputMethod(InputMethodDefinitions, "Telex 2") + var im = ParseInputMethod(InputMethodDefinitions, "Telex") var ng = NewEngine(im, EstdFlags) + ng.SetBracketTransformMode(BracketTransformEverywhere) ng.ProcessString("[", VietnameseMode) if ng.GetProcessedString(VietnameseMode) != "ơ" { - t.Errorf("Process Telex 2 [[], got [%v] expected [ươ]", ng.GetProcessedString(VietnameseMode)) + t.Errorf("Process BracketTransform [[], got [%v] expected [ươ]", ng.GetProcessedString(VietnameseMode)) } ng.Reset() ng.ProcessString("{", VietnameseMode) if ng.GetProcessedString(VietnameseMode) != "Ơ" { - t.Errorf("Process Telex 2 [{], got [%s] expected [Ơ]", ng.GetProcessedString(VietnameseMode)) + t.Errorf("Process BracketTransform [{], got [%s] expected [Ơ]", ng.GetProcessedString(VietnameseMode)) } } @@ -228,7 +231,7 @@ func TestProcessAlooString(t *testing.T) { func TestSpellingCheckForGiư(t *testing.T) { ng := newStdEngine() - // In Telex 2, ']' is used for 'ư' + // With BracketTransform, ']' is used for 'ư' ng.ProcessString("gi]", VietnameseMode) if ng.IsValid(false) == false { t.Errorf("Process gi], got [%v] expected [%v]", ng.IsValid(false) == false, true) @@ -358,8 +361,9 @@ func TestProcessTnoss(t *testing.T) { //ềng func TestProcessEenghf(t *testing.T) { - var im = ParseInputMethod(InputMethodDefinitions, "Telex 2") + var im = ParseInputMethod(InputMethodDefinitions, "Telex") ng := NewEngine(im, EstdFlags) + ng.SetBracketTransformMode(BracketTransformEverywhere) ng.ProcessString("ddawks", VietnameseMode) if ng.GetProcessedString(VietnameseMode) != "đắk" { t.Errorf("Process eenghf, got [%v] expected [đắk]", ng.GetProcessedString(VietnameseMode)) @@ -414,8 +418,9 @@ func TestProcesshuoswc(t *testing.T) { //choas, bieecs, uese func TestProcesschoas(t *testing.T) { - var im = ParseInputMethod(InputMethodDefinitions, "Telex 2") + var im = ParseInputMethod(InputMethodDefinitions, "Telex") ng := NewEngine(im, EstdFlags&^EstdToneStyle) + ng.SetBracketTransformMode(BracketTransformEverywhere) ng.ProcessString("choas", VietnameseMode) if ng.GetProcessedString(VietnameseMode) != "choá" { t.Errorf("Process [choas], got [%v] expected [choá]", ng.GetProcessedString(VietnameseMode)) @@ -694,3 +699,72 @@ func BenchmarkRemoveLastChar(b *testing.B) { } } } + +func TestBracketTransformGlobal(t *testing.T) { + var im = ParseInputMethod(InputMethodDefinitions, "Telex") + var ng = NewEngine(im, EstdFlags) + ng.SetBracketTransformMode(BracketTransformEverywhere) + + ng.ProcessString("[", VietnameseMode) + if ng.GetProcessedString(VietnameseMode | FullText) != "ơ" { + t.Errorf("BracketTransform (Everywhere): [ expected ơ, got %s", ng.GetProcessedString(VietnameseMode | FullText)) + } + ng.Reset() + ng.ProcessString("]", VietnameseMode) + if ng.GetProcessedString(VietnameseMode | FullText) != "ư" { + t.Errorf("BracketTransform (Everywhere): ] expected ư, got %s", ng.GetProcessedString(VietnameseMode | FullText)) + } + + ng.Reset() + ng.SetBracketTransformMode(BracketTransformNonStart) + ng.ProcessString("[", VietnameseMode) + // At start of word, [ should stay [ + if ng.GetProcessedString(VietnameseMode | FullText) != "[" { + t.Errorf("BracketTransform (NonStart) at start: [ expected [, got %s", ng.GetProcessedString(VietnameseMode | FullText)) + } + ng.Reset() + ng.ProcessString("a[", VietnameseMode) + // After a, [ should become ơ + if ng.GetProcessedString(VietnameseMode | FullText) != "aơ" { + t.Errorf("BracketTransform (NonStart) after a: [ expected aơ, got %s", ng.GetProcessedString(VietnameseMode | FullText)) + } + + // Test double typing to cancel + ng.Reset() + ng.SetBracketTransformMode(BracketTransformEverywhere) + ng.ProcessString("[[", VietnameseMode) + if ng.GetProcessedString(VietnameseMode | FullText) != "[" { + t.Errorf("BracketTransform (Everywhere) double typing: [[ expected [, got %s", ng.GetProcessedString(VietnameseMode | FullText)) + } +} + +func TestBracketTransformWithTone(t *testing.T) { + var im = ParseInputMethod(InputMethodDefinitions, "Telex") + var ng = NewEngine(im, EstdFlags) + ng.SetBracketTransformMode(BracketTransformEverywhere) + + // Test m[f -> mờ + ng.ProcessString("m[f", VietnameseMode) + if ng.GetProcessedString(VietnameseMode) != "mờ" { + t.Errorf("BracketTransform + Tone: m[f expected mờ, got %s", ng.GetProcessedString(VietnameseMode)) + } + + // Test d]s -> dứ + ng.Reset() + ng.ProcessString("d]s", VietnameseMode) + if ng.GetProcessedString(VietnameseMode) != "dứ" { + t.Errorf("BracketTransform + Tone: d]s expected dứ, got %s", ng.GetProcessedString(VietnameseMode)) + } +} + +func TestBracketDisabledWithTone(t *testing.T) { + var im = ParseInputMethod(InputMethodDefinitions, "Telex") + var ng = NewEngine(im, EstdFlags) + ng.SetBracketTransformMode(BracketTransformDisabled) + + // Test m[f -> m[f (since [ is literal and not a vowel, f remains f) + ng.ProcessString("m[f", VietnameseMode) + if ng.GetProcessedString(VietnameseMode | FullText) != "m[f" { + t.Errorf("Bracket Disabled + Tone: m[f expected m[f, got %s", ng.GetProcessedString(VietnameseMode | FullText)) + } +} diff --git a/bamboo_utils.go b/bamboo_utils.go index 3d682d6..d8ac493 100644 --- a/bamboo_utils.go +++ b/bamboo_utils.go @@ -401,7 +401,7 @@ var regUhO = regexp.MustCompile(`(ưo|ươ)`) **/ func generateTransformations(composition []*Transformation, applicableRules []Rule, flags uint, lowerKey rune, isUpperCase bool) []*Transformation { var transformations []*Transformation - // Double typing an effect key undoes it and its effects, e.g. w + w -> w (Telex 2) + // Double typing an effect key undoes it and its effects, e.g. w + w -> w if len(composition) > 0 { var rule = composition[len(composition)-1].Rule if rule.EffectType == Appending && rule.Key == lowerKey && rule.Key != rule.Result { diff --git a/input_method_def.go b/input_method_def.go index afae43e..5ed6485 100644 --- a/input_method_def.go +++ b/input_method_def.go @@ -1,3 +1,11 @@ +/* + * Bamboo - A Vietnamese Input method editor + * Copyright (C) Luong Thanh Lam + * + * This software is licensed under the MIT license. For more information, + * see . + */ + package bamboo type InputMethodDefinition map[string]string @@ -39,7 +47,7 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "+": "UO_ƯƠ", "*": "UO_ƯƠ", "(": "A_Ă", - "\\": "D_Đ", + "d": "D_Đ", }, "Microsoft layout": { "8": "DauSac", @@ -82,49 +90,49 @@ var InputMethodDefinitions = map[string]InputMethodDefinition{ "9": "D_Đ", }, "Telex + VNI + VIQR": { - "z": "XoaDauThanh", - "s": "DauSac", - "f": "DauHuyen", - "r": "DauHoi", - "x": "DauNga", - "j": "DauNang", - "a": "A_Â", - "e": "E_Ê", - "o": "O_Ô", - "w": "UOA_ƯƠĂ", - "d": "D_Đ", - "0": "XoaDauThanh", - "1": "DauSac", - "2": "DauHuyen", - "3": "DauHoi", - "4": "DauNga", - "5": "DauNang", - "6": "AEO_ÂÊÔ", - "7": "UO_ƯƠ", - "8": "A_Ă", - "9": "D_Đ", - "'": "DauSac", - "`": "DauHuyen", - "?": "DauHoi", - "~": "DauNga", - ".": "DauNang", - "^": "AEO_ÂÊÔ", - "+": "UO_ƯƠ", - "*": "UO_ƯƠ", - "(": "A_Ă", + "z": "XoaDauThanh", + "s": "DauSac", + "f": "DauHuyen", + "r": "DauHoi", + "x": "DauNga", + "j": "DauNang", + "a": "A_Â", + "e": "E_Ê", + "o": "O_Ô", + "w": "UOA_ƯƠĂ", + "d": "D_Đ", + "0": "XoaDauThanh", + "1": "DauSac", + "2": "DauHuyen", + "3": "DauHoi", + "4": "DauNga", + "5": "DauNang", + "6": "AEO_ÂÊÔ", + "7": "UO_ƯƠ", + "8": "A_Ă", + "9": "D_Đ", + "'": "DauSac", + "`": "DauHuyen", + "?": "DauHoi", + "~": "DauNga", + ".": "DauNang", + "^": "AEO_ÂÊÔ", + "+": "UO_ƯƠ", + "*": "UO_ƯƠ", + "(": "A_Ă", "\\": "D_Đ", }, "VNI Bàn phím tiếng Pháp": { - "&": "XoaDauThanh", - "é": "DauSac", + "&": "XoaDauThanh", + "é": "DauSac", "\"": "DauHuyen", - "'": "DauHoi", - "(": "DauNga", - "-": "DauNang", - "è": "AEO_ÂÊÔ", - "_": "UO_ƯƠ", - "ç": "A_Ă", - "à": "D_Đ", + "'": "DauHoi", + "(": "DauNga", + "-": "DauNang", + "è": "AEO_ÂÊÔ", + "_": "UO_ƯƠ", + "ç": "A_Ă", + "à": "D_Đ", }, } From d2c01c64fd7cd86abc75c35b2410e987d1e7fca1 Mon Sep 17 00:00:00 2001 From: Nguyen Hoang Ky Date: Mon, 4 May 2026 19:48:02 +0700 Subject: [PATCH 4/4] fix: only accept bracket when isBracketEnabled is true --- bamboo.go | 58 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/bamboo.go b/bamboo.go index f5a5a19..fbe07cc 100644 --- a/bamboo.go +++ b/bamboo.go @@ -49,7 +49,6 @@ const ( EstdFlags = EfreeToneMarking | EstdToneStyle | EautoCorrectEnabled | Ew2uEnabled ) - type Transformation struct { Rule Rule Target *Transformation @@ -73,20 +72,20 @@ type IEngine interface { } type BambooEngine struct { - composition []*Transformation - inputMethod InputMethod - flags uint - w2uMode int // W2uFollowFlags, W2uDisabled, W2uNonStart, W2uEverywhere + composition []*Transformation + inputMethod InputMethod + flags uint + w2uMode int // W2uFollowFlags, W2uDisabled, W2uNonStart, W2uEverywhere bracketTransformMode int // BracketTransformFollowFlags, Disabled, NonStart, Everywhere } func NewEngine(inputMethod InputMethod, flag uint) IEngine { engine := BambooEngine{ - inputMethod: inputMethod, - flags: flag, - w2uMode: W2uFollowFlags, - bracketTransformMode: BracketTransformFollowFlags, - } + inputMethod: inputMethod, + flags: flag, + w2uMode: W2uFollowFlags, + bracketTransformMode: BracketTransformFollowFlags, + } return &engine } @@ -115,14 +114,24 @@ func (e *BambooEngine) GetFlag(flag uint) uint { } func (e *BambooEngine) IsValid(inputIsFullComplete bool) bool { - keys := append(e.GetInputMethod().Keys, '[', ']', '{', '}') + var keys []rune + if e.isBracketEnabled() { + keys = append(e.GetInputMethod().Keys, '[', ']', '{', '}') + } else { + keys = e.GetInputMethod().Keys + } var _, last = extractLastWord(e.composition, keys) return isValid(last, inputIsFullComplete) } func (e *BambooEngine) GetProcessedString(mode Mode) string { var tmp []*Transformation - keys := append(e.inputMethod.Keys, '[', ']', '{', '}') + var keys []rune + if e.isBracketEnabled() { + keys = append(e.inputMethod.Keys, '[', ']', '{', '}') + } else { + keys = e.inputMethod.Keys + } if mode&FullText != 0 { tmp = e.composition } else if mode&PunctuationMode != 0 { @@ -134,6 +143,12 @@ func (e *BambooEngine) GetProcessedString(mode Mode) string { return Flatten(tmp, mode) } +func (e *BambooEngine) isBracketEnabled() bool { + return (e.bracketTransformMode == BracketTransformEverywhere) || + (e.bracketTransformMode == BracketTransformNonStart && len(e.composition) > 0) || + (e.bracketTransformMode == BracketTransformFollowFlags && e.flags&EbracketTransformEnabled != 0) +} + func (e *BambooEngine) getApplicableRules(key rune) []Rule { var applicableRules []Rule for _, inputRule := range e.inputMethod.Rules { @@ -152,9 +167,11 @@ func (e *BambooEngine) CanProcessKey(key rune) bool { if canProcessKey(key, e.inputMethod.Keys) { return true } - lowerKey := unicode.ToLower(key) - if lowerKey == '[' || lowerKey == ']' || lowerKey == '{' || lowerKey == '}' { - return true + if e.isBracketEnabled() { + lowerKey := unicode.ToLower(key) + if lowerKey == '[' || lowerKey == ']' || lowerKey == '{' || lowerKey == '}' { + return true + } } return false } @@ -167,11 +184,7 @@ func (e *BambooEngine) generateTransformations(composition []*Transformation, lo transformations = generateFallbackTransformations(composition, e.getApplicableRules(lowerKey), lowerKey, isUpperCase) } - canApplyBracket := (e.bracketTransformMode == BracketTransformEverywhere) || - (e.bracketTransformMode == BracketTransformNonStart && len(e.composition) > 0) || - (e.bracketTransformMode == BracketTransformFollowFlags && e.flags&EbracketTransformEnabled != 0) - - if canApplyBracket { + if e.isBracketEnabled() { if len(composition) > 0 { lastTrans := composition[len(composition)-1] if (lowerKey == '[' || lowerKey == '{') && (lastTrans.Rule.Key == '[' || lastTrans.Rule.Key == '{') && (lastTrans.Rule.Result == 'ơ' || lastTrans.Rule.Result == 'Ơ') { @@ -215,10 +228,11 @@ func (e *BambooEngine) generateTransformations(composition []*Transformation, lo (e.w2uMode == W2uFollowFlags && e.flags&Ew2uEnabled != 0) if canApplyW2U && lowerKey == 'w' && len(transformations) > 0 { - if transformations[0].Rule.Result == 'w' { + switch transformations[0].Rule.Result { + case 'w': transformations[0].Rule.Result = 'ư' transformations[0].Rule.EffectOn = 'ư' - } else if transformations[0].Rule.Result == 'W' { + case 'W': transformations[0].Rule.Result = 'Ư' transformations[0].Rule.EffectOn = 'Ư' }