// Continuous re-segmentation of a raw keystream into zhuyin syllables + English // runs — one principled DP pass that REPLACES the sticky-enRun FSM and its // per-case split heuristics (overwrite→English, tone-split, space-split). // // The keystream is fundamentally ambiguous (`is`=ㄛㄋ is valid QWERTY *and* // zhuyin), so we score every possible segmentation and pick the cheapest: // - a zhuyin token must be a phonetically-legal syllable (in validBase); // a confident syllable (>=2 symbols, e.g. ㄓㄜ) is cheap, a lone single // symbol (could be a stray English letter) is expensive; // - an English run costs ~per-character, so the DP prefers to peel a real // syllable out of a letter run (python這個) but keeps genuine words whole // (is he, test123) and never splits a lone ambiguous symbol. // Irreducibly ambiguous cases (ni = ㄋㄧ vs "ni") default to the cheaper side; // the Shift English-mode toggle remains the escape hatch, as in every IME. // A small set of common English words (+ loanwords used in zh/en code-switch) // so the DP keeps real words whole even when they contain a valid zhuyin // substring (code=ㄏㄟ+de, api=ㄇㄣ+i). Not exhaustive — the Shift English mode // covers the rest. export const WORDS = new Set(('a about after all also am an and any api app are as at back be '+ 'because been best big but buy by call can code come could data day deal do does done down '+ 'driving each email end even every file find first for free from get go good google great '+ 'group had happy has have he help her here hey hi him his hot hour how i if in info is issue it '+ 'its just keyword know last let like line link list live login look mail make man many may me '+ 'meeting more most my need new next no not note now number of off ok on one online only open or '+ 'order other our out over page part people php play please post python read really right run '+ 'same say search see server service she should show sir site so some sorry sound support sure '+ 'system take team tech test text than thank thanks that the their them then there these they '+ 'thing think this those time to today too tool top try two up us use user very video want was '+ 'way we web week well what when where which who why will with word work would year yes you your' ).split(' ')); export function makeSegmenter(DACHEN, TONEK, validBase, words=WORDS){ const isDigit = c => c>='0'&&c<='9'; const isAlnum = c => isDigit(c) || c>='A'&&c<='Z' || c>='a'&&c<='z'; // all legal zhuyin syllables starting at i: {len, v, syms, hard} // hard = carries a tone or a number-row key -> a real zhuyin signal; a // pure-letter syllable (soft) mid letter-run is almost always English. function zhAt(keys,i){ const res=[]; let bopo='', lastSlot=-1, dig=false, letter=false; for(let L=0; L<3 && i+L0;i=from[i]) syls.push(tok[i]); syls.reverse(); return syls; } const symCount = s => [...s].length; // toneless syllable: chars = symbols return function segment(keys){ const n=keys.length; const dp=new Array(n+1).fill(null); dp[0]={cost:0,toks:[]}; const relax=(j,cost,tok,from)=>{ if(!dp[j]||cost=2?1.0:2.6) : (s.syms>=2?3.0:4.2)) + (s.typo?1.5:0), {t:'zh',v:s.v}, i); if(isAlnum(keys[i])) // English run (each length) for(let j=i+1;j<=n && isAlnum(keys[j-1]);j++){ const seg=keys.slice(i,j), L=j-i; // per-char cost; lone letters penalized (a 1-char English token in // zhuyin is almost always a mis-segmentation, e.g. api); known words // of length>=3 discounted so they stay whole despite a valid zhuyin // substring (code) — but the cost is floored so short dict words // ('or','is') can't become negative-cost chain fodder that the DP // strings together to undercut honest segmentations (7w|or|ld). const disc=(L>=3 && words.has(seg.toLowerCase()))?3:0; const cost=dp[i].cost + Math.max(0.9, 1 + 0.6*L + (L===1?1.5:0) - disc); relax(j, cost, {t:'en',v:seg}, i); } if(!isAlnum(keys[i]) && !DACHEN[keys[i]] && !TONEK[keys[i]]) // stray symbol relax(i+1, dp[i].cost + 1.5, {t:'en',v:keys[i]}, i); // en punctuation inside en text: a punct key flanked by English/number // context (an alnum that is NOT a tone key) is a literal char, not // zhuyin — 7-11, a-b, 0912-345. This is the only escape for a dachen-punct // key ('-'=ㄦ etc). A tone digit (3/4/6/7) is NOT english context, so a // real zhuyin final ㄦ (這兒 "5k4-", 女兒 "sm3-6") is preserved; offered // cheaply so it merges into the English run, but the DP still prefers a // valid zhuyin syllable when one parses (b.4=ㄖㄡˋ). if(!isAlnum(keys[i]) && keys[i]>' ' && keys[i]<'\x7f'){ const enCtx=c=>isAlnum(c) && !TONEK[c]; if((i>0 && enCtx(keys[i-1])) || (i+1=2 && syls.every(s=>symCount(s)>=2)){ for(const v of syls) refined.push({t:'zh',v}); continue; } } refined.push({...t}); } return refined; }; }