Skip to main content

skey_engine/
charset.rs

1//! Vietnamese charset conversion.
2//!
3//! Implements legacy charset encode/decode using the same 213-character
4//! pivot table approach as x-unikey's vnconv.  Each charset is defined
5//! by a single 213-element lookup table mapping the internal index to
6//! the charset's byte representation.
7//!
8//! Supported charsets:
9//!   Unicode (UTF-8)  — native, no conversion needed
10//!   TCVN3 (ABC)       — single-byte, the most common legacy charset
11//!   VNI-WIN           — double-byte, second most common legacy
12//!   VIQR              — ASCII quoted-readable
13//!   WinCP1258         — Windows Vietnamese code page
14//!   UnicodeDecomposed — NFD (combining diacritics)
15
16use std::collections::HashMap;
17use std::sync::OnceLock;
18
19// ── Internal 213-char index ──────────────────────────────────────────
20//
21// Layout (same as x-unikey StdVnChar):
22//   Indices  0.. 35: a group   — A,a,Á,á,À,à,Ả,ả,Ã,ã,Ạ,ạ  (12)
23//   Indices 36.. 47: b,c,d group — B,b,C,c,D,d,Đ,đ          (12+2=14? no: 36-47=12)
24//   ...actually let me refer to the UnicodeTable for the exact layout
25//
26//   0..11:   a   (12 chars: 6 tones × 2 cases)
27//   12..23:  â   (12)
28//   24..35:  ă   (12)
29//   36..41:  b,c,d (6)
30//   42..43:  đ,Đ (2)
31//   44..55:  e   (12)
32//   56..67:  ê   (12)
33//   68..73:  f,g,h (6)
34//   74..85:  i   (12)
35//   86..95:  j,k,l,m,n (10)
36//   96..107: o   (12)
37//   108..119:ô   (12)
38//   120..131:ơ   (12)
39//   132..141:p,q,r,s,t (10)
40//   142..153:u   (12)
41//   154..165:ư   (12)
42//   166..171:v,w,x (6)
43//   172..183:y   (12)
44//   184..185:z (2)
45//   186..212:Western symbols (27)
46
47const VN_CHAR_COUNT: usize = 213;
48
49// ── Unicode pivot table ──────────────────────────────────────────────
50// Ported from x-unikey data.cpp UnicodeTable[213].
51// Maps internal index → Unicode codepoint.
52
53static UNICODE_TABLE: [char; VN_CHAR_COUNT] = [
54    // 0..11: a (6 tones × 2 cases)
55    'A', 'a', '\u{00C1}', '\u{00E1}', '\u{00C0}', '\u{00E0}',
56    '\u{1EA2}', '\u{1EA3}', '\u{00C3}', '\u{00E3}', '\u{1EA0}', '\u{1EA1}',
57    // 12..23: â
58    '\u{00C2}', '\u{00E2}', '\u{1EA4}', '\u{1EA5}', '\u{1EA6}', '\u{1EA7}',
59    '\u{1EA8}', '\u{1EA9}', '\u{1EAA}', '\u{1EAB}', '\u{1EAC}', '\u{1EAD}',
60    // 24..35: ă
61    '\u{0102}', '\u{0103}', '\u{1EAE}', '\u{1EAF}', '\u{1EB0}', '\u{1EB1}',
62    '\u{1EB2}', '\u{1EB3}', '\u{1EB4}', '\u{1EB5}', '\u{1EB6}', '\u{1EB7}',
63    // 36..41: B,b,C,c,D,d
64    'B', 'b', 'C', 'c', 'D', 'd',
65    // 42..43: Đ,đ
66    '\u{0110}', '\u{0111}',
67    // 44..55: e
68    'E', 'e', '\u{00C9}', '\u{00E9}', '\u{00C8}', '\u{00E8}',
69    '\u{1EBA}', '\u{1EBB}', '\u{1EBC}', '\u{1EBD}', '\u{1EB8}', '\u{1EB9}',
70    // 56..67: ê
71    '\u{00CA}', '\u{00EA}', '\u{1EBE}', '\u{1EBF}', '\u{1EC0}', '\u{1EC1}',
72    '\u{1EC2}', '\u{1EC3}', '\u{1EC4}', '\u{1EC5}', '\u{1EC6}', '\u{1EC7}',
73    // 68..73: F,f,G,g,H,h
74    'F', 'f', 'G', 'g', 'H', 'h',
75    // 74..85: i
76    'I', 'i', '\u{00CD}', '\u{00ED}', '\u{00CC}', '\u{00EC}',
77    '\u{1EC8}', '\u{1EC9}', '\u{0128}', '\u{0129}', '\u{1ECA}', '\u{1ECB}',
78    // 86..95: J,j,K,k,L,l,M,m,N,n
79    'J', 'j', 'K', 'k', 'L', 'l', 'M', 'm', 'N', 'n',
80    // 96..107: o
81    'O', 'o', '\u{00D3}', '\u{00F3}', '\u{00D2}', '\u{00F2}',
82    '\u{1ECE}', '\u{1ECF}', '\u{00D5}', '\u{00F5}', '\u{1ECC}', '\u{1ECD}',
83    // 108..119: ô
84    '\u{00D4}', '\u{00F4}', '\u{1ED0}', '\u{1ED1}', '\u{1ED2}', '\u{1ED3}',
85    '\u{1ED4}', '\u{1ED5}', '\u{1ED6}', '\u{1ED7}', '\u{1ED8}', '\u{1ED9}',
86    // 120..131: ơ
87    '\u{01A0}', '\u{01A1}', '\u{1EDA}', '\u{1EDB}', '\u{1EDC}', '\u{1EDD}',
88    '\u{1EDE}', '\u{1EDF}', '\u{1EE0}', '\u{1EE1}', '\u{1EE2}', '\u{1EE3}',
89    // 132..141: P,p,Q,q,R,r,S,s,T,t
90    'P', 'p', 'Q', 'q', 'R', 'r', 'S', 's', 'T', 't',
91    // 142..153: u
92    'U', 'u', '\u{00DA}', '\u{00FA}', '\u{00D9}', '\u{00F9}',
93    '\u{1EE6}', '\u{1EE7}', '\u{0168}', '\u{0169}', '\u{1EE4}', '\u{1EE5}',
94    // 154..165: ư
95    '\u{01AF}', '\u{01B0}', '\u{1EE8}', '\u{1EE9}', '\u{1EEA}', '\u{1EEB}',
96    '\u{1EEC}', '\u{1EED}', '\u{1EEE}', '\u{1EEF}', '\u{1EF0}', '\u{1EF1}',
97    // 166..171: V,v,W,w,X,x
98    'V', 'v', 'W', 'w', 'X', 'x',
99    // 172..183: y
100    'Y', 'y', '\u{00DD}', '\u{00FD}', '\u{1EF2}', '\u{1EF3}',
101    '\u{1EF6}', '\u{1EF7}', '\u{1EF8}', '\u{1EF9}', '\u{1EF4}', '\u{1EF5}',
102    // 184..185: Z,z
103    'Z', 'z',
104    // 186..212: Western symbols (port from x-unikey)
105    '\u{20AC}', '\u{20A1}', '\u{0192}', '\u{201E}', '\u{2026}', '\u{2020}',
106    '\u{2021}', '\u{02C6}', '\u{2030}', '\u{0160}', '\u{2039}', '\u{0152}',
107    '\u{017D}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', '\u{2022}',
108    '\u{2013}', '\u{2014}', '\u{02DC}', '\u{2122}', '\u{0161}', '\u{203A}',
109    '\u{0153}', '\u{017E}', '\u{0178}',
110];
111
112// ── TCVN3 (ABC) single-byte table ────────────────────────────────────
113// Ported from x-unikey SingleByteTables[0].
114// Maps internal index → TCVN3 byte value.
115// 0x00 means the character is not representable in TCVN3.
116
117static TCVN3_TABLE: [u8; VN_CHAR_COUNT] = [
118    // 0..11: a
119    b'A', b'a', 0xB8, 0xB8, 0xB5, 0xB5, 0xB6, 0xB6, 0xB7, 0xB7, 0xB9, 0xB9,
120    // 12..23: â
121    0xA2, 0xA9, 0xCA, 0xCA, 0xC7, 0xC7, 0xC8, 0xC8, 0xC9, 0xC9, 0xCB, 0xCB,
122    // 24..35: ă
123    0xA1, 0xA8, 0xBE, 0xBE, 0xBB, 0xBB, 0xBC, 0xBC, 0xBD, 0xBD, 0xC6, 0xC6,
124    // 36..41: B,b,C,c,D,d
125    b'B', b'b', b'C', b'c', b'D', b'd',
126    // 42..43: Đ,đ
127    0xA5, 0xB0,
128    // 44..55: e
129    b'E', b'e', 0xC4, 0xC4, 0xBF, 0xBF, 0xC0, 0xC0, 0xC1, 0xC1, 0xC2, 0xC2,
130    // 56..67: ê
131    0xA4, 0xAB, 0xCC, 0xCC, 0xC3, 0xC3, 0xCE, 0xCE, 0xCF, 0xCF, 0xD0, 0xD0,
132    // 68..73: F,f,G,g,H,h
133    b'F', b'f', b'G', b'g', b'H', b'h',
134    // 74..85: i
135    b'I', b'i', 0xAE, 0xAE, 0xA6, 0xA6, 0xB1, 0xB1, 0xB2, 0xB2, 0xB3, 0xB3,
136    // 86..95: J,j,K,k,L,l,M,m,N,n
137    b'J', b'j', b'K', b'k', b'L', b'l', b'M', b'm', b'N', b'n',
138    // 96..107: o
139    b'O', b'o', 0xD3, 0xD3, 0xD4, 0xD4, 0xD5, 0xD5, 0xD6, 0xD6, 0xD8, 0xD8,
140    // 108..119: ô
141    0xA3, 0xAA, 0xD9, 0xD9, 0xD1, 0xD1, 0xDA, 0xDA, 0xDB, 0xDB, 0xDC, 0xDC,
142    // 120..131: ơ
143    0xA7, 0xAD, 0xE1, 0xE1, 0xDD, 0xDD, 0xDE, 0xDE, 0xDF, 0xDF, 0xE0, 0xE0,
144    // 132..141: P,p,Q,q,R,r,S,s,T,t
145    b'P', b'p', b'Q', b'q', b'R', b'r', b'S', b's', b'T', b't',
146    // 142..153: u
147    b'U', b'u', 0xE7, 0xE7, 0xE4, 0xE4, 0xE5, 0xE5, 0xE6, 0xE6, 0xE8, 0xE8,
148    // 154..165: ư
149    0xB4, 0xAC, 0xE3, 0xE3, 0xD7, 0xD7, 0xE9, 0xE9, 0xEA, 0xEA, 0xEB, 0xEB,
150    // 166..171: V,v,W,w,X,x
151    b'V', b'v', b'W', b'w', b'X', b'x',
152    // 172..183: y
153    b'Y', b'y', 0xED, 0xED, 0xEE, 0xEE, 0xF0, 0xF0, 0xF1, 0xF1, 0xEF, 0xEF,
154    // 184..185: Z,z
155    b'Z', b'z',
156    // 186..212: Western symbols
157    0x80, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
158    0x89, 0x8A, 0x8B, 0x8C, 0x8E, 0x91, 0x92, 0x93,
159    0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B,
160    0x9C, 0x9E, 0x9F,
161];
162
163// ── VNI-WIN double-byte table ────────────────────────────────────────
164// Ported from x-unikey DoubleByteTables[0].
165// Maps internal index → VNI-WIN 2-byte word (big-endian in original).
166// 0x0000 means not representable.
167
168static VNIWIN_TABLE: [u16; VN_CHAR_COUNT] = [
169    // 0..11: a
170    0x0041, 0x0061, 0xd941, 0xf961, 0xd841, 0xf861, 0xdb41, 0xfb61,
171    0xd541, 0xf561, 0xcf41, 0xef61,
172    // 12..23: â
173    0xc241, 0xe261, 0xc141, 0xe161, 0xc041, 0xe061, 0xc541, 0xe561,
174    0xc341, 0xe361, 0xc441, 0xe461,
175    // 24..35: ă
176    0xca41, 0xea61, 0xc941, 0xe961, 0xc841, 0xe861, 0xda41, 0xfa61,
177    0xdc41, 0xfc61, 0xcb41, 0xeb61,
178    // 36..41: B,b,C,c,D,d
179    0x0042, 0x0062, 0x0043, 0x0063, 0x0044, 0x0064,
180    // 42..43: Đ,đ
181    0x00d1, 0x00f1,
182    // 44..55: e
183    0x0045, 0x0065, 0xd945, 0xf965, 0xd845, 0xf865, 0xdb45, 0xfb65,
184    0xd545, 0xf565, 0xcf45, 0xef65,
185    // 56..67: ê
186    0xc245, 0xe265, 0xc145, 0xe165, 0xc045, 0xe065, 0xc545, 0xe565,
187    0xc345, 0xe365, 0xc445, 0xe465,
188    // 68..73: F,f,G,g,H,h
189    0x0046, 0x0066, 0x0047, 0x0067, 0x0048, 0x0068,
190    // 74..85: i
191    0x0049, 0x0069, 0x00cd, 0x00ed, 0x00cc, 0x00ec, 0x00c6, 0x00e6,
192    0x00d3, 0x00f3, 0x00d2, 0x00f2,
193    // 86..95: J,j,K,k,L,l,M,m,N,n
194    0x004a, 0x006a, 0x004b, 0x006b, 0x004c, 0x006c, 0x004d, 0x006d,
195    0x004e, 0x006e,
196    // 96..107: o
197    0x004f, 0x006f, 0xd94f, 0xf96f, 0xd84f, 0xf86f, 0xdb4f, 0xfb6f,
198    0xd54f, 0xf56f, 0xcf4f, 0xef6f,
199    // 108..119: ô
200    0xc24f, 0xe26f, 0xc14f, 0xe16f, 0xc04f, 0xe06f, 0xc54f, 0xe56f,
201    0xc34f, 0xe36f, 0xc44f, 0xe46f,
202    // 120..131: ơ
203    0x00d4, 0x00f4, 0xd9d4, 0xf9f4, 0xd8d4, 0xf8f4, 0xdbd4, 0xfbf4,
204    0xd5d4, 0xf5f4, 0xcfd4, 0xeff4,
205    // 132..141: P,p,Q,q,R,r,S,s,T,t
206    0x0050, 0x0070, 0x0051, 0x0071, 0x0052, 0x0072, 0x0053, 0x0073,
207    0x0054, 0x0074,
208    // 142..153: u
209    0x0055, 0x0075, 0xd955, 0xf975, 0xd855, 0xf875, 0xdb55, 0xfb75,
210    0xd555, 0xf575, 0xcf55, 0xef75,
211    // 154..165: ư
212    0x00d6, 0x00f6, 0xd9d6, 0xf9f6, 0xd8d6, 0xf8f6, 0xdbd6, 0xfbf6,
213    0xd5d6, 0xf5f6, 0xcfd6, 0xeff6,
214    // 166..171: V,v,W,w,X,x
215    0x0056, 0x0076, 0x0057, 0x0077, 0x0058, 0x0078,
216    // 172..183: y
217    0x0059, 0x0079, 0xd959, 0xf979, 0xd859, 0xf879, 0xdb59, 0xfb79,
218    0xd559, 0xf579, 0x00ce, 0x00ee,
219    // 184..185: Z,z
220    0x005a, 0x007a,
221    // 186..212: Western symbols
222    0x0080, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088,
223    0x0089, 0x008A, 0x008B, 0x008C, 0x008E, 0x0091, 0x0092, 0x0093,
224    0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B,
225    0x009C, 0x009E, 0x009F,
226];
227
228// ── VIQR output table ────────────────────────────────────────────────
229// Packed u32: byte0=base, byte1=tone1, byte2=tone2, byte3=0
230// Ported from x-unikey VIQRTable[213].
231
232static VIQR_TABLE: [u32; VN_CHAR_COUNT] = [
233    // 0..11: a
234    0x00000041, 0x00000061, 0x00002741, 0x00002761, 0x00006041, 0x00006061,
235    0x00003F41, 0x00003F61, 0x00007E41, 0x00007E61, 0x00002E41, 0x00002E61,
236    // 12..23: â
237    0x005E0041, 0x005E0061, 0x00275E41, 0x00275E61, 0x00605E41, 0x00605E61,
238    0x003F5E41, 0x003F5E61, 0x007E5E41, 0x007E5E61, 0x002E5E41, 0x002E5E61,
239    // 24..35: ă
240    0x00280041, 0x00280061, 0x00272841, 0x00272861, 0x00602841, 0x00602861,
241    0x003F2841, 0x003F2861, 0x007E2841, 0x007E2861, 0x002E2841, 0x002E2861,
242    // 36..43: B,b,C,c,D,d,Đ,đ
243    0x42, 0x62, 0x43, 0x63, 0x44, 0x64, 0x00004444, 0x00006464,
244    // 44..55: e
245    0x45, 0x65, 0x00002745, 0x00002765, 0x00006045, 0x00006065,
246    0x00003F45, 0x00003F65, 0x00007E45, 0x00007E65, 0x00002E45, 0x00002E65,
247    // 56..67: ê
248    0x005E0045, 0x005E0065, 0x00275E45, 0x00275E65, 0x00605E45, 0x00605E65,
249    0x003F5E45, 0x003F5E65, 0x007E5E45, 0x007E5E65, 0x002E5E45, 0x002E5E65,
250    // 68..85: F,f,G,g,H,h,I,i
251    0x46, 0x66, 0x47, 0x67, 0x48, 0x68,
252    0x49, 0x69, 0x00002749, 0x00002769, 0x00006049, 0x00006069,
253    0x00003F49, 0x00003F69, 0x00007E49, 0x00007E69, 0x00002E49, 0x00002E69,
254    // 86..95: J,j,K,k,L,l,M,m,N,n
255    0x4A, 0x6A, 0x4B, 0x6B, 0x4C, 0x6C, 0x4D, 0x6D, 0x4E, 0x6E,
256    // 96..107: o
257    0x4F, 0x6F, 0x0000274F, 0x0000276F, 0x0000604F, 0x0000606F,
258    0x00003F4F, 0x00003F6F, 0x00007E4F, 0x00007E6F, 0x00002E4F, 0x00002E6F,
259    // 108..119: ô
260    0x005E004F, 0x005E006F, 0x00275E4F, 0x00275E6F, 0x00605E4F, 0x00605E6F,
261    0x003F5E4F, 0x003F5E6F, 0x007E5E4F, 0x007E5E6F, 0x002E5E4F, 0x002E5E6F,
262    // 120..131: ơ
263    0x002B004F, 0x002B006F, 0x00272B4F, 0x00272B6F, 0x00602B4F, 0x00602B6F,
264    0x003F2B4F, 0x003F2B6F, 0x007E2B4F, 0x007E2B6F, 0x002E2B4F, 0x002E2B6F,
265    // 132..141: P,p,Q,q,R,r,S,s,T,t
266    0x50, 0x70, 0x51, 0x71, 0x52, 0x72, 0x53, 0x73, 0x54, 0x74,
267    // 142..153: u
268    0x55, 0x75, 0x00002755, 0x00002775, 0x00006055, 0x00006075,
269    0x00003F55, 0x00003F75, 0x00007E55, 0x00007E75, 0x00002E55, 0x00002E75,
270    // 154..165: ư
271    0x002B0055, 0x002B0075, 0x00272B55, 0x00272B75, 0x00602B55, 0x00602B75,
272    0x003F2B55, 0x003F2B75, 0x007E2B55, 0x007E2B75, 0x002E2B55, 0x002E2B75,
273    // 166..171: V,v,W,w,X,x
274    0x56, 0x76, 0x57, 0x77, 0x58, 0x78,
275    // 172..183: y
276    0x59, 0x79, 0x00002759, 0x00002779, 0x00006059, 0x00006079,
277    0x00003F59, 0x00003F79, 0x00007E59, 0x00007E79, 0x00002E59, 0x00002E79,
278    // 184..185: Z,z
279    0x5A, 0x7A,
280    // 186..212: Western symbols (pass through as-is)
281    0x80, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
282    0x89, 0x8A, 0x8B, 0x8C, 0x8E, 0x91, 0x92, 0x93,
283    0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B,
284    0x9C, 0x9E, 0x9F,
285];
286
287/// Write packed VIQR bytes from a u32: byte0 byte1 byte2 (skip 0x00).
288fn push_viqr_bytes(out: &mut Vec<u8>, w: u32) {
289    let b0 = (w & 0xFF) as u8;
290    let b1 = ((w >> 8) & 0xFF) as u8;
291    let b2 = ((w >> 16) & 0xFF) as u8;
292    if b0 != 0 { out.push(b0); }
293    if b1 != 0 { out.push(b1); }
294    if b2 != 0 { out.push(b2); }
295}
296
297// ── WinCP1258 table ──────────────────────────────────────────────────
298// Ported from x-unikey WinCP1258Pre[213].
299// Format: each u16 = (combining_mark << 8) | base_char.
300//  hi=0x00 → single byte output (base_char only).
301//  hi≠0x00 → two-byte output: [base_char, combining_mark].
302
303static WINCP1258_TABLE: [u16; VN_CHAR_COUNT] = [
304    // 0..11: a
305    0x0041, 0x0061, 0x00C1, 0x00E1, 0x00C0, 0x00E0,
306    0xD241, 0xD261, 0xDE41, 0xDE61, 0xF241, 0xF261,
307    // 12..23: â
308    0x00C2, 0x00E2, 0xECC2, 0xECE2, 0xCCC2, 0xCCE2,
309    0xD2C2, 0xD2E2, 0xDEC2, 0xDEE2, 0xF2C2, 0xF2E2,
310    // 24..35: ă
311    0x00C3, 0x00E3, 0xECC3, 0xECE3, 0xCCC3, 0xCCE3,
312    0xD2C3, 0xD2E3, 0xDEC3, 0xDEE3, 0xF2C3, 0xF2E3,
313    // 36..43: B,b,C,c,D,d,Đ,đ
314    0x0042, 0x0062, 0x0043, 0x0063, 0x0044, 0x0064, 0x00D0, 0x00F0,
315    // 44..55: e
316    0x0045, 0x0065, 0x00C9, 0x00E9, 0x00C8, 0x00E8,
317    0xD245, 0xD265, 0xDE45, 0xDE65, 0xF245, 0xF265,
318    // 56..67: ê
319    0x00CA, 0x00EA, 0xECCA, 0xECEA, 0xCCCA, 0xCCEA,
320    0xD2CA, 0xD2EA, 0xDECA, 0xDEEA, 0xF2CA, 0xF2EA,
321    // 68..73: F,f,G,g,H,h
322    0x0046, 0x0066, 0x0047, 0x0067, 0x0048, 0x0068,
323    // 74..85: i
324    0x0049, 0x0069, 0x00CD, 0x00ED, 0xCC49, 0xCC69,
325    0xD249, 0xD269, 0xDE49, 0xDE69, 0xF249, 0xF269,
326    // 86..95: J,j,K,k,L,l,M,m,N,n
327    0x004A, 0x006A, 0x004B, 0x006B, 0x004C, 0x006C,
328    0x004D, 0x006D, 0x004E, 0x006E,
329    // 96..107: o
330    0x004F, 0x006F, 0x00D3, 0x00F3, 0xCC4F, 0xCC6F,
331    0xD24F, 0xD26F, 0xDE4F, 0xDE6F, 0xF24F, 0xF26F,
332    // 108..119: ô
333    0x00D4, 0x00F4, 0xECD4, 0xECF4, 0xCCD4, 0xCCF4,
334    0xD2D4, 0xD2F4, 0xDED4, 0xDEF4, 0xF2D4, 0xF2F4,
335    // 120..131: ơ
336    0x00D5, 0x00F5, 0xECD5, 0xECF5, 0xCCD5, 0xCCF5,
337    0xD2D5, 0xD2F5, 0xDED5, 0xDEF5, 0xF2D5, 0xF2F5,
338    // 132..141: P,p,Q,q,R,r,S,s,T,t
339    0x0050, 0x0070, 0x0051, 0x0071, 0x0052, 0x0072,
340    0x0053, 0x0073, 0x0054, 0x0074,
341    // 142..153: u
342    0x0055, 0x0075, 0x00DA, 0x00FA, 0x00D9, 0x00F9,
343    0xD255, 0xD275, 0xDE55, 0xDE75, 0xF255, 0xF275,
344    // 154..165: ư
345    0x00DD, 0x00FD, 0xECDD, 0xECFD, 0xCCDD, 0xCCFD,
346    0xD2DD, 0xD2FD, 0xDEDD, 0xDEFD, 0xF2DD, 0xF2FD,
347    // 166..171: V,v,W,w,X,x
348    0x0056, 0x0076, 0x0057, 0x0077, 0x0058, 0x0078,
349    // 172..183: y
350    0x0059, 0x0079, 0xEC59, 0xEC79, 0xCC59, 0xCC79,
351    0xD259, 0xD279, 0xDE59, 0xDE79, 0xF259, 0xF279,
352    // 184..185: Z,z
353    0x005A, 0x007A,
354    // 186..212: Western symbols
355    0x0080, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086,
356    0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C,
357    0x008E, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095,
358    0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B,
359    0x009C, 0x009E, 0x009F,
360];
361
362// ── Remaining single-byte tables (port from x-unikey) ───────────────
363
364static VPS_TABLE: [u8; VN_CHAR_COUNT] = [
365    0x41, 0x61, 0xC1, 0xE1, 0x80, 0xE0, 0x81, 0xE4, 0x82, 0xE3, 0xE5, 0xE5,
366    0xC2, 0xE2, 0x83, 0xC3, 0x84, 0xC0, 0x85, 0xC4, 0xC5, 0xC5, 0xC6, 0xC6,
367    0x88, 0xE6, 0x8D, 0xA1, 0x8E, 0xA2, 0x8F, 0xA3, 0xF0, 0xA4, 0xA5, 0xA5,
368    0x42, 0x62, 0x43, 0x63, 0x44, 0x64, 0xF1, 0xC7, 0x45, 0x65, 0xC9, 0xE9,
369    0xD7, 0xE8, 0xDE, 0xC8, 0xFE, 0xEB, 0xCB, 0xCB, 0xCA, 0xEA, 0x90, 0x89,
370    0x93, 0x8A, 0x94, 0x8B, 0x95, 0xCD, 0x8C, 0x8C, 0x46, 0x66, 0x47, 0x67,
371    0x48, 0x68, 0x49, 0x69, 0xB4, 0xED, 0xB5, 0xEC, 0xB7, 0xCC, 0xB8, 0xEF,
372    0xCE, 0xCE, 0x4A, 0x6A, 0x4B, 0x6B, 0x4C, 0x6C, 0x4D, 0x6D, 0x4E, 0x6E,
373    0x4F, 0x6F, 0xB9, 0xF3, 0xBC, 0xF2, 0xBD, 0xD5, 0xBE, 0xF5, 0x86, 0x86,
374    0xD4, 0xF4, 0x96, 0xD3, 0x97, 0xD2, 0x98, 0xB0, 0x99, 0x87, 0xB6, 0xB6,
375    0xF7, 0xD6, 0x9D, 0xA7, 0x9E, 0xA9, 0x9F, 0xAA, 0xA6, 0xAB, 0xAE, 0xAE,
376    0x50, 0x70, 0x51, 0x71, 0x52, 0x72, 0x53, 0x73, 0x54, 0x74, 0x55, 0x75,
377    0xDA, 0xFA, 0xA8, 0xF9, 0xD1, 0xFB, 0xAC, 0xDB, 0xF8, 0xF8, 0xD0, 0xDC,
378    0xAD, 0xD9, 0xAF, 0xD8, 0xB1, 0xBA, 0xBB, 0xBB, 0xBF, 0xBF, 0x56, 0x76,
379    0x57, 0x77, 0x58, 0x78, 0x59, 0x79, 0xDD, 0x9A, 0xB2, 0xFF, 0xFD, 0x9B,
380    0xB3, 0xCF, 0x9C, 0x9C, 0x5A, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
381    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8E, 0x91, 0x92, 0x00, 0x00, 0x00,
382    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x00,];
383static VISCII_TABLE: [u8; VN_CHAR_COUNT] = [
384    0x41, 0x61, 0xC1, 0xE1, 0xC0, 0xE0, 0xC4, 0xE4, 0xC3, 0xE3, 0x80, 0xD5,
385    0xC2, 0xE2, 0x84, 0xA4, 0x85, 0xA5, 0x86, 0xA6, 0xE7, 0xE7, 0x87, 0xA7,
386    0xC5, 0xE5, 0x81, 0xA1, 0x82, 0xA2, 0xC6, 0xC6, 0xC7, 0xC7, 0x83, 0xA3,
387    0x42, 0x62, 0x43, 0x63, 0x44, 0x64, 0xD0, 0xF0, 0x45, 0x65, 0xC9, 0xE9,
388    0xC8, 0xE8, 0xCB, 0xEB, 0x88, 0xA8, 0x89, 0xA9, 0xCA, 0xEA, 0x8A, 0xAA,
389    0x8B, 0xAB, 0x8C, 0xAC, 0x8D, 0xAD, 0x8E, 0xAE, 0x46, 0x66, 0x47, 0x67,
390    0x48, 0x68, 0x49, 0x69, 0xCD, 0xED, 0xCC, 0xEC, 0x9B, 0xEF, 0xCE, 0xEE,
391    0x98, 0xB8, 0x4A, 0x6A, 0x4B, 0x6B, 0x4C, 0x6C, 0x4D, 0x6D, 0x4E, 0x6E,
392    0x4F, 0x6F, 0xD3, 0xF3, 0xD2, 0xF2, 0x99, 0xF6, 0xF5, 0xF5, 0x9A, 0xF7,
393    0xD4, 0xF4, 0x8F, 0xAF, 0x90, 0xB0, 0x91, 0xB1, 0x92, 0xB2, 0x93, 0xB5,
394    0xB4, 0xBD, 0x95, 0xBE, 0x96, 0xB6, 0x97, 0xB7, 0xB3, 0xDE, 0x94, 0xFE,
395    0x50, 0x70, 0x51, 0x71, 0x52, 0x72, 0x53, 0x73, 0x54, 0x74, 0x55, 0x75,
396    0xDA, 0xFA, 0xD9, 0xF9, 0x9C, 0xFC, 0x9D, 0xFB, 0x9E, 0xF8, 0xBF, 0xDF,
397    0xBA, 0xD1, 0xBB, 0xD7, 0xBC, 0xD8, 0xFF, 0xE6, 0xB9, 0xF1, 0x56, 0x76,
398    0x57, 0x77, 0x58, 0x78, 0x59, 0x79, 0xDD, 0xFD, 0x9F, 0xCF, 0xD6, 0xD6,
399    0xDB, 0xDB, 0xDC, 0xDC, 0x5A, 0x7A, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
400    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00,
401    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x00,];
402static BKHCM1_TABLE: [u8; VN_CHAR_COUNT] = [
403    0x41, 0x61, 0x80, 0xBE, 0x81, 0xBF, 0x82, 0xC0, 0x83, 0xC1, 0x84, 0xC2,
404    0x9F, 0xDD, 0x7E, 0xDE, 0xA1, 0xDF, 0xA2, 0xE0, 0xA3, 0xE1, 0xA4, 0xE2,
405    0x99, 0xD7, 0x9A, 0xD8, 0x9B, 0xD9, 0x9C, 0xDA, 0x9D, 0xDB, 0x98, 0xDC,
406    0x42, 0x62, 0x43, 0x63, 0x44, 0x64, 0x7D, 0xBD, 0x45, 0x65, 0x85, 0xC3,
407    0x86, 0xC4, 0x87, 0xC5, 0x88, 0xC6, 0x89, 0xC7, 0xA5, 0xE3, 0xA6, 0xE4,
408    0xA7, 0xE5, 0xA8, 0xE6, 0xA9, 0xE7, 0xAA, 0xE8, 0x46, 0x66, 0x47, 0x67,
409    0x48, 0x68, 0x49, 0x69, 0x8A, 0xC8, 0x8B, 0xC9, 0x8C, 0xCA, 0x8D, 0xCB,
410    0x8E, 0xCC, 0x4A, 0x6A, 0x4B, 0x6B, 0x4C, 0x6C, 0x4D, 0x6D, 0x4E, 0x6E,
411    0x4F, 0x6F, 0x8F, 0xCD, 0x90, 0xCE, 0x91, 0xCF, 0x92, 0xD0, 0x93, 0xD1,
412    0xAB, 0xE9, 0xAC, 0xEA, 0xAD, 0xEB, 0xAE, 0xEC, 0xAF, 0xED, 0xB0, 0xEE,
413    0xB1, 0xEF, 0xB2, 0xF0, 0xB3, 0xF1, 0xB4, 0xF2, 0xB5, 0xF3, 0xB6, 0xF4,
414    0x50, 0x70, 0x51, 0x71, 0x52, 0x72, 0x53, 0x73, 0x54, 0x74, 0x55, 0x75,
415    0x94, 0xD2, 0x95, 0xD3, 0x96, 0xD4, 0x97, 0xD5, 0x98, 0xD6, 0xB7, 0xF5,
416    0xB8, 0xF6, 0xB9, 0xF7, 0xBA, 0xF8, 0xBB, 0xF9, 0xBC, 0xFA, 0x56, 0x76,
417    0x57, 0x77, 0x58, 0x78, 0x59, 0x79, 0x7B, 0xFB, 0x5E, 0xFC, 0x60, 0xFD,
418    0x7C, 0xFE, 0x8E, 0xFF, 0x5A, 0x7A, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
419    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00,
420    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x00,];
421static VIETWAREF_TABLE: [u8; VN_CHAR_COUNT] = [
422    0x41, 0x61, 0xC0, 0xC0, 0xAA, 0xAA, 0xB6, 0xB6, 0xBA, 0xBA, 0xC1, 0xC1,
423    0x97, 0xA1, 0xCA, 0xCA, 0xC7, 0xC7, 0xC8, 0xC8, 0xC9, 0xC9, 0xCB, 0xCB,
424    0x96, 0x9F, 0xC5, 0xC5, 0xC2, 0xC2, 0xC3, 0xC3, 0xC4, 0xC4, 0xC6, 0xC6,
425    0x42, 0x62, 0x43, 0x63, 0x44, 0x64, 0x98, 0xA2, 0x45, 0x65, 0xCF, 0xCF,
426    0xCC, 0xCC, 0xCD, 0xCD, 0xCE, 0xCE, 0xD1, 0xD1, 0x99, 0xA3, 0xD5, 0xD5,
427    0xD2, 0xD2, 0xD3, 0xD3, 0xD4, 0xD4, 0xD6, 0xD6, 0x46, 0x66, 0x47, 0x67,
428    0x48, 0x68, 0x49, 0x69, 0xDB, 0xDB, 0xD8, 0xD8, 0xD9, 0xD9, 0xDA, 0xDA,
429    0xDC, 0xDC, 0x4A, 0x6A, 0x4B, 0x6B, 0x4C, 0x6C, 0x4D, 0x6D, 0x4E, 0x6E,
430    0x4F, 0x6F, 0xE2, 0xE2, 0xDF, 0xDF, 0xE0, 0xE0, 0xE1, 0xE1, 0xE3, 0xE3,
431    0x9A, 0xA4, 0xE7, 0xE7, 0xE4, 0xE4, 0xE5, 0xE5, 0xE6, 0xE6, 0xE8, 0xE8,
432    0x9B, 0xA5, 0xEC, 0xEC, 0xE9, 0xE9, 0xEA, 0xEA, 0xEB, 0xEB, 0xED, 0xED,
433    0x50, 0x70, 0x51, 0x71, 0x52, 0x72, 0x53, 0x73, 0x54, 0x74, 0x55, 0x75,
434    0xF2, 0xF2, 0xEE, 0xEE, 0xEF, 0xEF, 0xF1, 0xF1, 0xF3, 0xF3, 0x9C, 0xA7,
435    0xF7, 0xF7, 0xF4, 0xF4, 0xF5, 0xF5, 0xF6, 0xF6, 0xF8, 0xF8, 0x56, 0x76,
436    0x57, 0x77, 0x58, 0x78, 0x59, 0x79, 0xFC, 0xFC, 0xF9, 0xF9, 0xFA, 0xFA,
437    0xFB, 0xFB, 0xFF, 0xFF, 0x5A, 0x7A, 0x80, 0x82, 0x83, 0x84, 0x85, 0x86,
438    0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8E, 0x91, 0x92, 0x93, 0x94, 0x95,
439    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x00,];
440static ISC_TABLE: [u8; VN_CHAR_COUNT] = [
441    0x41, 0x61, 0x83, 0xB8, 0x80, 0xB5, 0x81, 0xB6, 0x82, 0xB7, 0x84, 0xB9,
442    0xA2, 0xA9, 0xC4, 0xA0, 0xC1, 0xC7, 0xC2, 0xC8, 0xC3, 0xC9, 0x86, 0xCB,
443    0xA1, 0xA8, 0xC0, 0xBE, 0xAF, 0xBB, 0xBA, 0xBC, 0xBF, 0xBD, 0x85, 0xC6,
444    0x42, 0x62, 0x43, 0x63, 0x44, 0x64, 0xA7, 0xAE, 0x45, 0x65, 0xD0, 0x8A,
445    0x87, 0xCC, 0x88, 0xCE, 0x89, 0xCF, 0xD1, 0x8B, 0xA3, 0xAA, 0xDA, 0xD5,
446    0xC5, 0xD2, 0xCD, 0xD3, 0xD9, 0xD4, 0x8C, 0xD6, 0x46, 0x66, 0x47, 0x67,
447    0x48, 0x68, 0x49, 0x69, 0x90, 0xDD, 0x8D, 0xD7, 0x8E, 0xD8, 0x8F, 0xDC,
448    0x91, 0xDE, 0x4A, 0x6A, 0x4B, 0x6B, 0x4C, 0x6C, 0x4D, 0x6D, 0x4E, 0x6E,
449    0x4F, 0x6F, 0x95, 0xE3, 0x92, 0xDF, 0x93, 0xE1, 0x94, 0xE2, 0x96, 0xE4,
450    0xA4, 0xAB, 0xFF, 0xE8, 0xDB, 0xE5, 0xE0, 0xE6, 0xF0, 0xE7, 0x97, 0xE9,
451    0xA5, 0xAC, 0x9B, 0xED, 0x98, 0xEA, 0x99, 0xEB, 0x9A, 0xEC, 0x9C, 0xEE,
452    0x50, 0x70, 0x51, 0x71, 0x52, 0x72, 0x53, 0x73, 0x54, 0x74, 0x55, 0x75,
453    0x40, 0xF3, 0x9D, 0xEF, 0x9E, 0xF1, 0x9F, 0xF2, 0x7C, 0xF4, 0xA6, 0xAD,
454    0x60, 0xF8, 0x5C, 0xF5, 0x5E, 0xF6, 0x7E, 0xF7, 0x23, 0xF9, 0x56, 0x76,
455    0x57, 0x77, 0x58, 0x78, 0x59, 0x79, 0xB3, 0xFD, 0xB0, 0xFA, 0xB1, 0xFB,
456    0xB2, 0xFC, 0xB4, 0xFE, 0x5A, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
457    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x12, 0x13, 0x14, 0x00,
458    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,];
459
460// ── Remaining double-byte tables (port from x-unikey) ────────────────
461
462static BKHCM2_TABLE: [u16; VN_CHAR_COUNT] = [
463    0x0041, 0x0061, 0xC141, 0xE161, 0xC241, 0xE261,
464    0xC341, 0xE361, 0xC441, 0xE461, 0xC541, 0xE561,
465    0x00CA, 0x00EA, 0xCBCA, 0xEBEA, 0xCCCA, 0xECEA,
466    0xCDCA, 0xEDEA, 0xCECA, 0xEEEA, 0xC5CA, 0xE5EA,
467    0x00D9, 0x00F9, 0xC6D9, 0xE6F9, 0xC7D9, 0xE7F9,
468    0xC8D9, 0xE8F9, 0xC9D9, 0xE9F9, 0xC5D9, 0xE5F9,
469    0x0042, 0x0062, 0x0043, 0x0063, 0x0044, 0x0064,
470    0x00C0, 0x00E0, 0x0045, 0x0065, 0xC145, 0xE165,
471    0xC245, 0xE265, 0xC345, 0xE365, 0xC445, 0xE465,
472    0xC545, 0xE565, 0x00CF, 0x00EF, 0xCBCF, 0xEBEF,
473    0xCCCF, 0xECEF, 0xCDCF, 0xEDEF, 0xCECF, 0xEEEF,
474    0xE5CF, 0xE5EF, 0x0046, 0x0066, 0x0047, 0x0067,
475    0x0048, 0x0068, 0x0049, 0x0069, 0x00D1, 0x00F1,
476    0x00D2, 0x00F2, 0x00D3, 0x00F3, 0x00D4, 0x00F4,
477    0x00D5, 0x00F5, 0x004A, 0x006A, 0x004B, 0x006B,
478    0x004C, 0x006C, 0x004D, 0x006D, 0x004E, 0x006E,
479    0x004F, 0x006F, 0xC14F, 0xE16F, 0xC24F, 0xE26F,
480    0xC34F, 0xE36F, 0xC44F, 0xE46F, 0xC54F, 0xE56F,
481    0x00D6, 0x00F6, 0xCBD6, 0xEBF6, 0xCCD6, 0xECF6,
482    0xCDD6, 0xEDF6, 0xCED6, 0xEEF6, 0xC5D6, 0xE5F6,
483    0x00DA, 0x00FA, 0xC1DA, 0xE1FA, 0xC2DA, 0xE2FA,
484    0xC3DA, 0xE3FA, 0xC4DA, 0xE4FA, 0xC5DA, 0xE5FA,
485    0x0050, 0x0070, 0x0051, 0x0071, 0x0052, 0x0072,
486    0x0053, 0x0073, 0x0054, 0x0074, 0x0055, 0x0075,
487    0xC155, 0xE175, 0xC255, 0xE275, 0xC355, 0xE375,
488    0xC455, 0xE475, 0xC555, 0xE575, 0x00DB, 0x00FB,
489    0xC1DB, 0xE1FB, 0xC2DB, 0xE2FB, 0xC3DB, 0xE3FB,
490    0xC4DB, 0xE4FB, 0xC5DB, 0xE5FB, 0x0056, 0x0076,
491    0x0057, 0x0077, 0x0058, 0x0078, 0x0059, 0x0079,
492    0xC159, 0xE179, 0xC259, 0xE279, 0xC359, 0xE379,
493    0xC459, 0xE479, 0xC559, 0xE579, 0x005A, 0x007A,
494    0x0080, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086,
495    0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C,
496    0x008E, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095,
497    0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B,
498    0x009C, 0x009E, 0x009F,];
499static VIETWAREX_TABLE: [u16; VN_CHAR_COUNT] = [
500    0x0041, 0x0061, 0xCF41, 0xEF61, 0xCC41, 0xEC61,
501    0xCD41, 0xED61, 0xCE41, 0xEE61, 0xDB41, 0xFB61,
502    0x00C1, 0x00E1, 0xDAC1, 0xFAE1, 0xD6C1, 0xF6E1,
503    0xD8C1, 0xF8E1, 0xD9C1, 0xF9E1, 0xDBC1, 0xFBE1,
504    0x00C0, 0x00E0, 0xD5C0, 0xF5E0, 0xD2C0, 0xF2E0,
505    0xD3C0, 0xF3E0, 0xD4C0, 0xF4E0, 0xDBC0, 0xFBE0,
506    0x0042, 0x0062, 0x0043, 0x0063, 0x0044, 0x0064,
507    0x00C2, 0x00E2, 0x0045, 0x0065, 0xCF45, 0xEF65,
508    0xCC45, 0xEC65, 0xCD45, 0xED65, 0xCE45, 0xEE65,
509    0xDB45, 0xFB65, 0x00C3, 0x00E3, 0xDAC3, 0xFAE3,
510    0xD6C3, 0xF6E3, 0xD8C3, 0xF8E3, 0xD9C3, 0xF9E3,
511    0xDBC3, 0xFBE3, 0x0046, 0x0066, 0x0047, 0x0067,
512    0x0048, 0x0068, 0x0049, 0x0069, 0x00CA, 0x00EA,
513    0x00C7, 0x00E7, 0x00C8, 0x00E8, 0x00C9, 0x00E9,
514    0x00CB, 0x00EB, 0x004A, 0x006A, 0x004B, 0x006B,
515    0x004C, 0x006C, 0x004D, 0x006D, 0x004E, 0x006E,
516    0x004F, 0x006F, 0xCF4F, 0xEF6F, 0xCC4F, 0xEC6F,
517    0xCD4F, 0xED6F, 0xCE4F, 0xEE6F, 0xDC4F, 0xFC6F,
518    0x00C4, 0x00E4, 0xDAC4, 0xFAE4, 0xD6C4, 0xF6E4,
519    0xD8C4, 0xF8E4, 0xD9C4, 0xF9E4, 0xDCC4, 0xFCE4,
520    0x00C5, 0x00E5, 0xCFC5, 0xEFE5, 0xCCC5, 0xECE5,
521    0xCDC5, 0xEDE5, 0xCEC5, 0xEEE5, 0xDCC5, 0xFCE5,
522    0x0050, 0x0070, 0x0051, 0x0071, 0x0052, 0x0072,
523    0x0053, 0x0073, 0x0054, 0x0074, 0x0055, 0x0075,
524    0xCF55, 0xEF75, 0xCC55, 0xEC75, 0xCD55, 0xED75,
525    0xCE55, 0xEE75, 0xDB55, 0xFB75, 0x00C6, 0x00E6,
526    0xCFC6, 0xEFE6, 0xCCC6, 0xECE6, 0xCDC6, 0xEDE6,
527    0xCEC6, 0xEEE6, 0xDBC6, 0xFBE6, 0x0056, 0x0076,
528    0x0057, 0x0077, 0x0058, 0x0078, 0x0059, 0x0079,
529    0xCF59, 0xEF79, 0xCC59, 0xEC79, 0xCD59, 0xED79,
530    0xCE59, 0xEE79, 0xD159, 0xF179, 0x005A, 0x007A,
531    0x0080, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086,
532    0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C,
533    0x008E, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095,
534    0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B,
535    0x009C, 0x009E, 0x009F,];
536static VNI_MAC_TABLE: [u16; VN_CHAR_COUNT] = [
537    0x0041, 0x0061, 0xF441, 0x9D61, 0xAF41, 0xBF61,
538    0xF341, 0x9E61, 0xCD41, 0x9B61, 0xEC41, 0x9561,
539    0xE541, 0x8961, 0xE741, 0x8761, 0xCB41, 0x8861,
540    0x8141, 0x8C61, 0xCC41, 0x8B61, 0x8041, 0x8A61,
541    0xE641, 0x9061, 0x8341, 0x8E61, 0xE941, 0x8F61,
542    0xF241, 0x9C61, 0x8641, 0x9F61, 0xE841, 0x9161,
543    0x0042, 0x0062, 0x0043, 0x0063, 0x0044, 0x0064,
544    0x0084, 0x0096, 0x0045, 0x0065, 0xF445, 0x9D65,
545    0xAF45, 0xBF65, 0xF345, 0x9E65, 0xCD45, 0x9B65,
546    0xEC45, 0x9565, 0xE545, 0x8965, 0xE745, 0x8765,
547    0xCB45, 0x8865, 0x8145, 0x8C65, 0xCC45, 0x8B65,
548    0x8045, 0x8A65, 0x0046, 0x0066, 0x0047, 0x0067,
549    0x0048, 0x0068, 0x0049, 0x0069, 0x00EA, 0x0092,
550    0x00ED, 0x0093, 0x00AE, 0x00BE, 0x00EE, 0x0097,
551    0x00F1, 0x0098, 0x004A, 0x006A, 0x004B, 0x006B,
552    0x004C, 0x006C, 0x004D, 0x006D, 0x004E, 0x006E,
553    0x004F, 0x006F, 0xF44F, 0x9D6F, 0xAF4F, 0xBF6F,
554    0xF34F, 0x9E6F, 0xCD4F, 0x9B6F, 0xEC4F, 0x956F,
555    0xE54F, 0x896F, 0xE74F, 0x876F, 0xCB4F, 0x886F,
556    0x814F, 0x8C6F, 0xCC4F, 0x8B6F, 0x804F, 0x8A6F,
557    0x00EF, 0x0099, 0xF4EF, 0x9D99, 0xAFEF, 0xBF99,
558    0xF3EF, 0x9E99, 0xCDEF, 0x9B99, 0xECEF, 0x9599,
559    0x0050, 0x0070, 0x0051, 0x0071, 0x0052, 0x0072,
560    0x0053, 0x0073, 0x0054, 0x0074, 0x0055, 0x0075,
561    0xF455, 0x9D75, 0xAF55, 0xBF75, 0xF355, 0x9E75,
562    0xCD55, 0x9B75, 0xEC55, 0x9575, 0x0085, 0x009A,
563    0xF485, 0x9D9A, 0xAF85, 0xBF9A, 0xF385, 0x9E9A,
564    0xCD85, 0x9B9A, 0xEC85, 0x959A, 0x0056, 0x0076,
565    0x0057, 0x0077, 0x0058, 0x0078, 0x0059, 0x0079,
566    0xF459, 0x9D79, 0xAF59, 0xBF79, 0xF359, 0x9E79,
567    0xCD59, 0x9B79, 0x00EB, 0x0094, 0x005A, 0x007A,
568    0x00DB, 0x00E2, 0x00C4, 0x00E3, 0x00C9, 0x00A0,
569    0x00E0, 0x00F6, 0x00E4, 0x003F, 0x00DC, 0x00CE,
570    0x003F, 0x00D4, 0x00D5, 0x00D2, 0x00D3, 0x00A5,
571    0x00D0, 0x00D1, 0x00F7, 0x00AA, 0x003F, 0x00DD,
572    0x00CF, 0x003F, 0x00D9,];
573
574// ── Unicode NFD (decomposed) table ───────────────────────────────────
575
576static UNICOMP_TABLE: [u32; VN_CHAR_COUNT] = [    0x00000041, 0x00000061, 0x03010041, 0x03010061,
577    0x03000041, 0x03000061, 0x03090041, 0x03090061,
578    0x03030041, 0x03030061, 0x03230041, 0x03230061,
579    0x000000C2, 0x000000E2, 0x030100C2, 0x030100E2,
580    0x030000C2, 0x030000E2, 0x030900C2, 0x030900E2,
581    0x030300C2, 0x030300E2, 0x032300C2, 0x032300E2,
582    0x00000102, 0x00000103, 0x03010102, 0x03010103,
583    0x03000102, 0x03000103, 0x03090102, 0x03090103,
584    0x03030102, 0x03030103, 0x03230102, 0x03230103,
585    0x00000042, 0x00000062, 0x00000043, 0x00000063,
586    0x00000044, 0x00000064, 0x00000110, 0x00000111,
587    0x000000D1, 0x000000F1, 0x00000045, 0x00000065,
588    0x03010045, 0x03010065, 0x03000045, 0x03000065,
589    0x03090045, 0x03090065, 0x03030045, 0x03030065,
590    0x03230045, 0x03230065, 0x000000CA, 0x000000EA,
591    0x030100CA, 0x030100EA, 0x030000CA, 0x030000EA,
592    0x030900CA, 0x030900EA, 0x030300CA, 0x030300EA,
593    0x032300CA, 0x032300EA, 0x00000046, 0x00000066,
594    0x00000047, 0x00000067, 0x00000048, 0x00000068,
595    0x00000049, 0x00000069, 0x03010049, 0x03010069,
596    0x03000049, 0x03000069, 0x03090049, 0x03090069,
597    0x03030049, 0x03030069, 0x03230049, 0x03230069,
598    0x0000004A, 0x0000006A, 0x0000004B, 0x0000006B,
599    0x0000004C, 0x0000006C, 0x0000004D, 0x0000006D,
600    0x0000004E, 0x0000006E, 0x0000004F, 0x0000006F,
601    0x0301004F, 0x0301006F, 0x0300004F, 0x0300006F,
602    0x0309004F, 0x0309006F, 0x0303004F, 0x0303006F,
603    0x0323004F, 0x0323006F, 0x000000D4, 0x000000F4,
604    0x030100D4, 0x030100F4, 0x030000D4, 0x030000F4,
605    0x030900D4, 0x030900F4, 0x030300D4, 0x030300F4,
606    0x032300D4, 0x032300F4, 0x000001A0, 0x000001A1,
607    0x030101A0, 0x030101A1, 0x030001A0, 0x030001A1,
608    0x030901A0, 0x030901A1, 0x030301A0, 0x030301A1,
609    0x032301A0, 0x032301A1, 0x00000050, 0x00000070,
610    0x00000051, 0x00000071, 0x00000052, 0x00000072,
611    0x00000053, 0x00000073, 0x00000054, 0x00000074,
612    0x00000055, 0x00000075, 0x03010055, 0x03010075,
613    0x03000055, 0x03000075, 0x03090055, 0x03090075,
614    0x03030055, 0x03030075, 0x03230055, 0x03230075,
615    0x000001AF, 0x000001B0, 0x030101AF, 0x030101B0,
616    0x030001AF, 0x030001B0, 0x030901AF, 0x030901B0,
617    0x030301AF, 0x030301B0, 0x032301AF, 0x032301B0,
618    0x00000056, 0x00000076, 0x00000057, 0x00000077,
619    0x00000058, 0x00000078, 0x00000059, 0x00000079,
620    0x03010059, 0x03010079, 0x03000059, 0x03000079,
621    0x03090059, 0x03090079, 0x03030059, 0x03030079,
622    0x03230059, 0x03230079, 0x0000005A, 0x0000007A,
623    0x000020AC, 0x000020A1, 0x00000192, 0x0000201E,
624    0x00002026, 0x00002020, 0x00002021, 0x000002C6,
625    0x00002030, 0x00000160, 0x00002039, 0x00000152,
626    0x0000017D, 0x00002018, 0x00002019, 0x0000201C,
627    0x0000201D, 0x00002022, 0x00002013, 0x00002014,
628    0x000002DC, 0x00002122, 0x00000161, 0x0000203A,
629    0x00000153,];
630
631// ── Tone removal table ──────────────────────────────────────────────
632// Ported from x-unikey StdVnNoTone[213].
633// Maps internal index → index of toneless equivalent.
634
635static NO_TONE: &[usize; VN_CHAR_COUNT] = &[
636    // 0..11: a → a
637    0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
638    // 12..23: â → â
639    12, 13, 12, 13, 12, 13, 12, 13, 12, 13, 12, 13,
640    // 24..35: ă → ă
641    24, 25, 24, 25, 24, 25, 24, 25, 24, 25, 24, 25,
642    // 36..41: b,c,d
643    36, 37, 38, 39, 40, 41,
644    // 42..43: đ
645    42, 43,
646    // 44..55: e → e
647    44, 45, 44, 45, 44, 45, 44, 45, 44, 45, 44, 45,
648    // 56..67: ê → ê
649    56, 57, 56, 57, 56, 57, 56, 57, 56, 57, 56, 57,
650    // 68..73: f,g,h
651    68, 69, 70, 71, 72, 73,
652    // 74..85: i → i
653    74, 75, 74, 75, 74, 75, 74, 75, 74, 75, 74, 75,
654    // 86..95: j,k,l,m,n
655    86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
656    // 96..107: o → o
657    96, 97, 96, 97, 96, 97, 96, 97, 96, 97, 96, 97,
658    // 108..119: ô → ô
659    108, 109, 108, 109, 108, 109, 108, 109, 108, 109, 108, 109,
660    // 120..131: ơ → ơ
661    120, 121, 120, 121, 120, 121, 120, 121, 120, 121, 120, 121,
662    // 132..141: p,q,r,s,t
663    132, 133, 134, 135, 136, 137, 138, 139, 140, 141,
664    // 142..153: u → u
665    142, 143, 142, 143, 142, 143, 142, 143, 142, 143, 142, 143,
666    // 154..165: ư → ư
667    154, 155, 154, 155, 154, 155, 154, 155, 154, 155, 154, 155,
668    // 166..171: v,w,x
669    166, 167, 168, 169, 170, 171,
670    // 172..183: y → y
671    172, 173, 172, 173, 172, 173, 172, 173, 172, 173, 172, 173,
672    // 184..185: z
673    184, 185,
674    // 186..212: Western symbols (no tone)
675    186, 187, 188, 189, 190, 191, 192, 193,
676    194, 195, 196, 197, 198, 199, 200, 201,
677    202, 203, 204, 205, 206, 207, 208, 209,
678    210, 211, 212,
679];
680
681// ── Public API ───────────────────────────────────────────────────────
682
683/// Supported Vietnamese charsets.
684#[derive(Clone, Copy, PartialEq, Eq, Debug)]
685pub enum VietCharset {
686    Unicode = 0,
687    TCVN3 = 1,
688    VNIWin = 2,
689    WinCP1258 = 3,
690    VIQR = 4,
691    // Remaining single-byte charsets from x-unikey
692    VPS = 5,
693    VISCII = 6,
694    BKHCM1 = 7,
695    VietwareF = 8,
696    ISC = 9,
697    // Remaining double-byte charsets
698    BKHCM2 = 10,
699    VietwareX = 11,
700    VNIMac = 12,
701    // Unicode NFD (decomposed)
702    UnicodeDecomposed = 13,
703}
704
705// ── Lazy-built reverse lookup maps ───────────────────────────────────
706
707fn unicode_to_index_map() -> &'static HashMap<char, usize> {
708    static MAP: OnceLock<HashMap<char, usize>> = OnceLock::new();
709    MAP.get_or_init(|| {
710        let mut m = HashMap::with_capacity(VN_CHAR_COUNT);
711        for (i, &ch) in UNICODE_TABLE.iter().enumerate() {
712            m.insert(ch, i);
713        }
714        m
715    })
716}
717
718fn tcvn3_to_index_map() -> &'static HashMap<u8, usize> {
719    static MAP: OnceLock<HashMap<u8, usize>> = OnceLock::new();
720    MAP.get_or_init(|| {
721        let mut m = HashMap::with_capacity(VN_CHAR_COUNT);
722        for (i, &b) in TCVN3_TABLE.iter().enumerate() {
723            m.insert(b, i);
724        }
725        m
726    })
727}
728
729fn vniwin_to_index_map() -> &'static HashMap<u16, usize> {
730    static MAP: OnceLock<HashMap<u16, usize>> = OnceLock::new();
731    MAP.get_or_init(|| {
732        let mut m = HashMap::with_capacity(VN_CHAR_COUNT);
733        for (i, &w) in VNIWIN_TABLE.iter().enumerate() {
734            m.insert(w, i);
735        }
736        m
737    })
738}
739
740// ── Encode / Decode ─────────────────────────────────────────────────
741
742/// Get the output table for a single-byte charset.
743fn sb_table(cs: VietCharset) -> &'static [u8] {
744    match cs {
745        VietCharset::TCVN3 => &TCVN3_TABLE,
746        VietCharset::VPS => &VPS_TABLE,
747        VietCharset::VISCII => &VISCII_TABLE,
748        VietCharset::BKHCM1 => &BKHCM1_TABLE,
749        VietCharset::VietwareF => &VIETWAREF_TABLE,
750        VietCharset::ISC => &ISC_TABLE,
751        _ => &TCVN3_TABLE,
752    }
753}
754
755/// Get the output table for a double-byte charset (little-endian word).
756fn db_table(cs: VietCharset) -> &'static [u16] {
757    match cs {
758        VietCharset::VNIWin => &VNIWIN_TABLE,
759        VietCharset::BKHCM2 => &BKHCM2_TABLE,
760        VietCharset::VietwareX => &VIETWAREX_TABLE,
761        VietCharset::VNIMac => &VNI_MAC_TABLE,
762        _ => &VNIWIN_TABLE,
763    }
764}
765
766fn is_single_byte(cs: VietCharset) -> bool {
767    matches!(cs, VietCharset::TCVN3 | VietCharset::VPS | VietCharset::VISCII
768        | VietCharset::BKHCM1 | VietCharset::VietwareF | VietCharset::ISC)
769}
770
771fn is_double_byte(cs: VietCharset) -> bool {
772    matches!(cs, VietCharset::VNIWin | VietCharset::BKHCM2
773        | VietCharset::VietwareX | VietCharset::VNIMac)
774}
775
776/// Encode a UTF-8 string to target charset bytes.
777pub fn encode(input: &str, charset: VietCharset) -> Vec<u8> {
778    match charset {
779        VietCharset::Unicode => input.as_bytes().to_vec(),
780        cs if is_single_byte(cs) => {
781            encode_sb(input, sb_table(cs))
782        }
783        cs if is_double_byte(cs) => {
784            encode_db(input, db_table(cs))
785        }
786        VietCharset::WinCP1258 => encode_wincp(input),
787        VietCharset::VIQR => encode_viqr(input),
788        VietCharset::UnicodeDecomposed => encode_nfd(input),
789        _ => input.as_bytes().to_vec(),
790    }
791}
792
793fn encode_sb(input: &str, table: &[u8]) -> Vec<u8> {
794    let map = unicode_to_index_map();
795    let mut out = Vec::with_capacity(input.len());
796    for ch in input.chars() {
797        if let Some(&idx) = map.get(&ch) {
798            out.push(table[idx]);
799        } else {
800            let mut buf = [0u8; 4];
801            let s = ch.encode_utf8(&mut buf);
802            out.extend_from_slice(s.as_bytes());
803        }
804    }
805    out
806}
807
808fn encode_db(input: &str, table: &[u16]) -> Vec<u8> {
809    let map = unicode_to_index_map();
810    let mut out = Vec::with_capacity(input.len() * 2);
811    for ch in input.chars() {
812        if let Some(&idx) = map.get(&ch) {
813            let w = table[idx];
814            out.push((w & 0xFF) as u8);
815            out.push((w >> 8) as u8);
816        } else if ch.is_ascii() {
817            out.push(ch as u8);
818        } else {
819            let mut buf = [0u8; 4];
820            let s = ch.encode_utf8(&mut buf);
821            out.extend_from_slice(s.as_bytes());
822        }
823    }
824    out
825}
826
827fn encode_wincp(input: &str) -> Vec<u8> {
828    let map = unicode_to_index_map();
829    let mut out = Vec::with_capacity(input.len() * 2);
830    for ch in input.chars() {
831        if let Some(&idx) = map.get(&ch) {
832            let w = WINCP1258_TABLE[idx];
833            out.push((w & 0xFF) as u8);
834            let hi = (w >> 8) as u8;
835            if hi != 0 { out.push(hi); }
836        } else if ch.is_ascii() {
837            out.push(ch as u8);
838        } else {
839            let mut buf = [0u8; 4];
840            let s = ch.encode_utf8(&mut buf);
841            out.extend_from_slice(s.as_bytes());
842        }
843    }
844    out
845}
846
847fn encode_viqr(input: &str) -> Vec<u8> {
848    let map = unicode_to_index_map();
849    let mut out = Vec::new();
850    for ch in input.chars() {
851        if let Some(&idx) = map.get(&ch) {
852            push_viqr_bytes(&mut out, VIQR_TABLE[idx]);
853        } else if ch.is_ascii() {
854            out.push(ch as u8);
855        } else {
856            let mut buf = [0u8; 4];
857            let s = ch.encode_utf8(&mut buf);
858            out.extend_from_slice(s.as_bytes());
859        }
860    }
861    out
862}
863
864fn encode_nfd(input: &str) -> Vec<u8> {
865    let map = unicode_to_index_map();
866    let mut out = Vec::with_capacity(input.len() * 6);
867    for ch in input.chars() {
868        if let Some(&idx) = map.get(&ch) {
869            let w = UNICOMP_TABLE[idx];
870            let base = w & 0xFFFF;
871            let comb = (w >> 16) & 0xFFFF;
872            // Encode base char as UTF-8
873            if let Some(c) = char::from_u32(base) {
874                let mut buf = [0u8; 4];
875                let s = c.encode_utf8(&mut buf);
876                out.extend_from_slice(s.as_bytes());
877            }
878            // Encode combining mark as UTF-8 (if present)
879            if comb != 0 {
880                if let Some(c) = char::from_u32(comb) {
881                    let mut buf = [0u8; 4];
882                    let s = c.encode_utf8(&mut buf);
883                    out.extend_from_slice(s.as_bytes());
884                }
885            }
886        } else {
887            let mut buf = [0u8; 4];
888            let s = ch.encode_utf8(&mut buf);
889            out.extend_from_slice(s.as_bytes());
890        }
891    }
892    out
893}
894
895/// Decode charset bytes to a UTF-8 string.
896pub fn decode(input: &[u8], charset: VietCharset) -> String {
897    match charset {
898        VietCharset::Unicode => String::from_utf8_lossy(input).to_string(),
899        VietCharset::WinCP1258 => {
900            let _map = unicode_to_index_map();
901            // Build reverse: (base_byte, combining_byte) → index
902            let mut rev: HashMap<(u8, u8), usize> = HashMap::with_capacity(VN_CHAR_COUNT);
903            for (i, &w) in WINCP1258_TABLE.iter().enumerate() {
904                let lo = (w & 0xFF) as u8;
905                let hi = (w >> 8) as u8;
906                rev.insert((lo, hi), i);
907            }
908            let mut out = String::with_capacity(input.len());
909            let mut i = 0;
910            while i < input.len() {
911                let lo = input[i];
912                // Try 2-byte WinCP1258 pair (combining mark follows base)
913                if i + 1 < input.len() {
914                    let hi = input[i + 1];
915                    if let Some(&idx) = rev.get(&(lo, hi)) {
916                        out.push(UNICODE_TABLE[idx]);
917                        i += 2;
918                        continue;
919                    }
920                }
921                // Try single-byte entry
922                if let Some(&idx) = rev.get(&(lo, 0)) {
923                    out.push(UNICODE_TABLE[idx]);
924                } else {
925                    out.push(lo as char);
926                }
927                i += 1;
928            }
929            out
930        }
931        VietCharset::VIQR => {
932            // VIQR decode: use the engine's built-in VIQR converter
933            let s = String::from_utf8_lossy(input);
934            crate::engine::convert_viqr(&s)
935        }
936        VietCharset::TCVN3 => {
937            let map = tcvn3_to_index_map();
938            let mut out = String::with_capacity(input.len());
939            for &b in input {
940                if let Some(&idx) = map.get(&b) {
941                    out.push(UNICODE_TABLE[idx]);
942                } else {
943                    out.push(b as char);
944                }
945            }
946            out
947        }
948        VietCharset::VNIWin => {
949            let map = vniwin_to_index_map();
950            let mut out = String::with_capacity(input.len());
951            let mut i = 0;
952            while i < input.len() {
953                // Try 2-byte VNI-WIN pair
954                if i + 1 < input.len() {
955                    let lo = input[i];
956                    let hi = input[i + 1];
957                    let w = lo as u16 | ((hi as u16) << 8);
958                    if let Some(&idx) = map.get(&w) {
959                        out.push(UNICODE_TABLE[idx]);
960                        i += 2;
961                        continue;
962                    }
963                }
964                // Fallback: single byte pass-through
965                out.push(input[i] as char);
966                i += 1;
967            }
968            out
969        }
970        // Generic: all single-byte charsets decode the same way
971        cs if is_single_byte(cs) => {
972            let table = sb_table(cs);
973            let mut rev: HashMap<u8, usize> = HashMap::with_capacity(VN_CHAR_COUNT);
974            for (i, &b) in table.iter().enumerate() {
975                rev.insert(b, i);
976            }
977            let mut out = String::with_capacity(input.len());
978            for &b in input {
979                if let Some(&idx) = rev.get(&b) {
980                    out.push(UNICODE_TABLE[idx]);
981                } else {
982                    out.push(b as char);
983                }
984            }
985            out
986        }
987        // Generic: all double-byte charsets decode the same way
988        cs if is_double_byte(cs) => {
989            let table = db_table(cs);
990            let mut rev: HashMap<u16, usize> = HashMap::with_capacity(VN_CHAR_COUNT);
991            for (i, &w) in table.iter().enumerate() {
992                rev.insert(w, i);
993            }
994            let mut out = String::with_capacity(input.len());
995            let mut i = 0;
996            while i < input.len() {
997                if i + 1 < input.len() {
998                    let lo = input[i];
999                    let hi = input[i + 1];
1000                    let w = lo as u16 | ((hi as u16) << 8);
1001                    if let Some(&idx) = rev.get(&w) {
1002                        out.push(UNICODE_TABLE[idx]);
1003                        i += 2;
1004                        continue;
1005                    }
1006                }
1007                out.push(input[i] as char);
1008                i += 1;
1009            }
1010            out
1011        }
1012        // NFD: pass through (decode as UTF-8)
1013        VietCharset::UnicodeDecomposed => String::from_utf8_lossy(input).to_string(),
1014        // All other variants (single-byte, double-byte) handled above via guard arms
1015        _ => String::from_utf8_lossy(input).to_string(),
1016    }
1017}
1018
1019/// Remove tone marks from a Vietnamese UTF-8 string.
1020/// e.g. "tiếng Việt" → "tiêng Viêt"
1021pub fn remove_tone(input: &str) -> String {
1022    let map = unicode_to_index_map();
1023    let mut out = String::with_capacity(input.len());
1024    for ch in input.chars() {
1025        if let Some(&idx) = map.get(&ch) {
1026            let toneless_idx = NO_TONE[idx];
1027            out.push(UNICODE_TABLE[toneless_idx]);
1028        } else {
1029            out.push(ch);
1030        }
1031    }
1032    out
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038
1039    #[test]
1040    fn unicode_table_size() {
1041        assert_eq!(UNICODE_TABLE.len(), 213);
1042        assert_eq!(TCVN3_TABLE.len(), 213);
1043        assert_eq!(VNIWIN_TABLE.len(), 213);
1044        assert_eq!(NO_TONE.len(), 213);
1045    }
1046
1047    #[test]
1048    fn unicode_roundtrip() {
1049        let input = "tiếng Việt";
1050        let encoded = encode(input, VietCharset::Unicode);
1051        let decoded = decode(&encoded, VietCharset::Unicode);
1052        assert_eq!(decoded, input);
1053    }
1054
1055    #[test]
1056    fn tcvn3_roundtrip() {
1057        let input = "tiếng Việt";
1058        let encoded = encode(input, VietCharset::TCVN3);
1059        let decoded = decode(&encoded, VietCharset::TCVN3);
1060        assert_eq!(decoded, input);
1061    }
1062
1063    #[test]
1064    fn vniwin_roundtrip() {
1065        let input = "tiếng Việt";
1066        let encoded = encode(input, VietCharset::VNIWin);
1067        let decoded = decode(&encoded, VietCharset::VNIWin);
1068        assert_eq!(decoded, input);
1069    }
1070
1071    #[test]
1072    fn all_vietnamese_vowels() {
1073        // Test all 12 vowels × 6 tones
1074        let all_vowels = "aàáảãạ âầấẩẫậ ăằắẳẵặ eèéẻẽẹ êềếểễệ iìíỉĩị oòóỏõọ ôồốổỗộ ơờớởỡợ uùúủũụ ưừứửữự yỳýỷỹỵ";
1075        for charset in [VietCharset::TCVN3, VietCharset::VNIWin] {
1076            let encoded = encode(all_vowels, charset);
1077            let decoded = decode(&encoded, charset);
1078            assert_eq!(decoded, all_vowels, "roundtrip failed for {:?}", charset);
1079        }
1080    }
1081
1082    #[test]
1083    fn remove_tone_works() {
1084        assert_eq!(remove_tone("tiếng"), "tiêng");
1085        assert_eq!(remove_tone("Việt"), "Viêt");
1086        assert_eq!(remove_tone("được"), "đươc");
1087        assert_eq!(remove_tone("không"), "không"); // ô has no tone
1088        assert_eq!(remove_tone("xin chào"), "xin chao");
1089    }
1090
1091    #[test]
1092    fn wincp1258_roundtrip() {
1093        let input = "tiếng Việt";
1094        let encoded = encode(input, VietCharset::WinCP1258);
1095        let decoded = decode(&encoded, VietCharset::WinCP1258);
1096        assert_eq!(decoded, input);
1097    }
1098
1099    #[test]
1100    fn viqr_output() {
1101        let input = "tiếng Việt";
1102        let encoded = encode(input, VietCharset::VIQR);
1103        // VIQR output should be ASCII-only
1104        assert!(encoded.iter().all(|&b| b <= 0x7F));
1105        // Should contain recognizable VIQR patterns
1106        let s = String::from_utf8(encoded).unwrap();
1107        assert!(s.contains("tie^'ng") || s.contains("tie^'ng") || s.len() > 0);
1108    }
1109
1110    #[test]
1111    fn english_pass_through() {
1112        let input = "Hello world 123!";
1113        for charset in [VietCharset::TCVN3, VietCharset::VNIWin, VietCharset::WinCP1258, VietCharset::Unicode] {
1114            let encoded = encode(input, charset);
1115            let decoded = decode(&encoded, charset);
1116            assert_eq!(decoded, input, "English pass-through failed for {:?}", charset);
1117        }
1118    }
1119}