Skip to main content

sup_xml_core/
charsets.rs

1#![forbid(unsafe_code)]  // see CONTRIBUTING.md § "Unsafe policy"
2
3//! Character classification for XML 1.0 names.
4//!
5//! # ASCII lookup tables
6//!
7//! Two 256-entry tables let callers classify ASCII bytes with a single array
8//! access.  Bytes ≥ 0x80 are always zero in the table; callers must fall back
9//! to the Unicode range functions for those.
10//!
11//! - [`ASCII_XML_NAME`] — XML `Name` (§2.3): `:` counts as a name-start char.
12//! - [`ASCII_NCNAME`]   — XPath `NCName`: `:` is a separator, not part of a name.
13//!
14//! # Unicode range functions
15//!
16//! Non-ASCII characters are checked with `is_name_start_char` / `is_name_char_unicode`
17//! (5th edition, the current default) or `is_name_start_char_4e` / `is_name_char_4e`
18//! (4th edition, enabled via `ParseOptions::xml10_fourth_edition`).
19//!
20//! Both editions agree on all ASCII characters; the tables above handle those
21//! uniformly.  The non-ASCII functions cover ≥ 0x80 only.
22
23// ── ASCII lookup tables ───────────────────────────────────────────────────────
24
25/// Byte is an ASCII name-start character (A–Z, a–z, `_`, and for XML Name, `:`).
26pub const NS: u8 = 0x01;
27/// Byte is an ASCII name character but NOT a name-start (`0`–`9`, `-`, `.`).
28pub const NC: u8 = 0x02;
29
30/// XML `Name` lookup table.  Includes `:` as a name-start char (§2.3).
31/// Bytes ≥ 0x80 are `0`; use [`is_name_start_char`] / [`is_name_char_unicode`]
32/// (or their 4th-edition counterparts) for those.
33pub const ASCII_XML_NAME: [u8; 256] = {
34    let mut t = [0u8; 256];
35    let mut b = b'A' as usize; while b <= b'Z' as usize { t[b] = NS; b += 1; }
36    let mut b = b'a' as usize; while b <= b'z' as usize { t[b] = NS; b += 1; }
37    t[b'_' as usize] = NS;
38    t[b':' as usize] = NS;
39    let mut b = b'0' as usize; while b <= b'9' as usize { t[b] = NC; b += 1; }
40    t[b'-' as usize] = NC;
41    t[b'.' as usize] = NC;
42    t
43};
44
45/// Boolean fast-path: 1 iff the byte is an ASCII `NameChar` (`NS|NC`) per
46/// XML 1.0 § 2.3 production [4a].  Non-ASCII bytes (≥ 0x80) are `0` so the
47/// tight inner loop in `scan_name_raw` exits and falls into the explicit
48/// UTF-8 path.  Layout matches `ASCII_XML_NAME` for `NS|NC`.
49///
50/// Kept as `u8` (not `bool`) so a single `if ASCII_XML_NAME_CHAR[b] != 0`
51/// compiles to a cmp-against-zero rather than a load-and-zext.  In a tight
52/// 8-wide chunk this lets LLVM vectorize via SIMD compares / pshufb.
53pub const ASCII_XML_NAME_CHAR: [u8; 256] = {
54    let mut t = [0u8; 256];
55    let mut b = b'A' as usize; while b <= b'Z' as usize { t[b] = 1; b += 1; }
56    let mut b = b'a' as usize; while b <= b'z' as usize { t[b] = 1; b += 1; }
57    let mut b = b'0' as usize; while b <= b'9' as usize { t[b] = 1; b += 1; }
58    t[b'_' as usize] = 1;
59    t[b':' as usize] = 1;
60    t[b'-' as usize] = 1;
61    t[b'.' as usize] = 1;
62    t
63};
64
65/// XPath `NCName` lookup table.  Excludes `:` (it is a separator in XPath).
66/// Bytes ≥ 0x80 are `0`; use [`is_name_start_char`] / [`is_name_char_unicode`]
67/// for those.
68pub const ASCII_NCNAME: [u8; 256] = {
69    let mut t = [0u8; 256];
70    let mut b = b'A' as usize; while b <= b'Z' as usize { t[b] = NS; b += 1; }
71    let mut b = b'a' as usize; while b <= b'z' as usize { t[b] = NS; b += 1; }
72    t[b'_' as usize] = NS;
73    // ':' intentionally omitted
74    let mut b = b'0' as usize; while b <= b'9' as usize { t[b] = NC; b += 1; }
75    t[b'-' as usize] = NC;
76    t[b'.' as usize] = NC;
77    t
78};
79
80// ── 5th-edition non-ASCII ranges (XML 1.0 §2.3, 2008 revision) ───────────────
81//
82// These functions cover only bytes ≥ 0x80.  ASCII characters are handled by
83// the lookup tables above.
84
85/// Returns `true` if `c` (≥ U+0080) is a valid XML 1.0 5th-edition name-start
86/// character.  The `:` and `_` cases are ASCII and handled by the tables.
87pub fn is_name_start_char(c: char) -> bool {
88    matches!(c,
89        '\u{C0}'..='\u{D6}'
90        | '\u{D8}'..='\u{F6}'
91        | '\u{F8}'..='\u{2FF}'
92        | '\u{370}'..='\u{37D}'
93        | '\u{37F}'..='\u{1FFF}'
94        | '\u{200C}'..='\u{200D}'
95        | '\u{2070}'..='\u{218F}'
96        | '\u{2C00}'..='\u{2FEF}'
97        | '\u{3001}'..='\u{D7FF}'
98        | '\u{F900}'..='\u{FDCF}'
99        | '\u{FDF0}'..='\u{FFFD}'
100        | '\u{10000}'..='\u{EFFFF}'
101    )
102}
103
104/// Returns `true` if `c` (≥ U+0080) is a valid XML 1.0 5th-edition name
105/// character.  ASCII name chars (`0`–`9`, `-`, `.`) are handled by the tables.
106pub fn is_name_char_unicode(c: char) -> bool {
107    is_name_start_char(c)
108        || matches!(c,
109            '\u{B7}'
110            | '\u{0300}'..='\u{036F}'
111            | '\u{203F}'..='\u{2040}'
112        )
113}
114
115// ── 4th-edition tables (XML 1.0 Appendix B, pre-2008) ────────────────────────
116//
117// These productions were removed in the 5th edition.  Enabled via
118// `ParseOptions::xml10_fourth_edition` (equivalent to libxml2 `XML_PARSE_OLD10`).
119//
120// Source: https://www.w3.org/TR/2006/REC-xml-20060816/#CharClasses
121
122/// XML 1.0 §2.3 NameStartChar — 4th edition (`Letter | '_' | ':'`).
123pub fn is_name_start_char_4e(c: char) -> bool {
124    matches!(c, '_' | ':') || is_base_char(c) || is_ideographic(c)
125}
126
127/// XML 1.0 §2.3 NameChar — 4th edition.
128pub fn is_name_char_4e(c: char) -> bool {
129    is_name_start_char_4e(c)
130        || is_xml_digit(c)
131        || matches!(c, '.' | '-')
132        || is_combining_char(c)
133        || is_extender(c)
134}
135
136fn is_base_char(c: char) -> bool {
137    matches!(c,
138        '\u{0041}'..='\u{005A}' | '\u{0061}'..='\u{007A}' |
139        '\u{00C0}'..='\u{00D6}' | '\u{00D8}'..='\u{00F6}' |
140        '\u{00F8}'..='\u{00FF}' |
141        '\u{0100}'..='\u{0131}' | '\u{0134}'..='\u{013E}' |
142        '\u{0141}'..='\u{0148}' | '\u{014A}'..='\u{017E}' |
143        '\u{0180}'..='\u{01C3}' | '\u{01CD}'..='\u{01F0}' |
144        '\u{01F4}'..='\u{01F5}' | '\u{01FA}'..='\u{0217}' |
145        '\u{0250}'..='\u{02A8}' | '\u{02BB}'..='\u{02C1}' |
146        '\u{0386}' |
147        '\u{0388}'..='\u{038A}' | '\u{038C}' | '\u{038E}'..='\u{03A1}' |
148        '\u{03A3}'..='\u{03CE}' | '\u{03D0}'..='\u{03D6}' |
149        '\u{03DA}' | '\u{03DC}' | '\u{03DE}' | '\u{03E0}' |
150        '\u{03E2}'..='\u{03F3}' |
151        '\u{0401}'..='\u{040C}' | '\u{040E}'..='\u{044F}' |
152        '\u{0451}'..='\u{045C}' | '\u{045E}'..='\u{0481}' |
153        '\u{0490}'..='\u{04C4}' | '\u{04C7}'..='\u{04C8}' |
154        '\u{04CB}'..='\u{04CC}' | '\u{04D0}'..='\u{04EB}' |
155        '\u{04EE}'..='\u{04F5}' | '\u{04F8}'..='\u{04F9}' |
156        '\u{0531}'..='\u{0556}' | '\u{0559}' |
157        '\u{0561}'..='\u{0586}' |
158        '\u{05D0}'..='\u{05EA}' | '\u{05F0}'..='\u{05F2}' |
159        '\u{0621}'..='\u{063A}' | '\u{0641}'..='\u{064A}' |
160        '\u{0671}'..='\u{06B7}' | '\u{06BA}'..='\u{06BE}' |
161        '\u{06C0}'..='\u{06CE}' | '\u{06D0}'..='\u{06D3}' | '\u{06D5}' |
162        '\u{06E5}'..='\u{06E6}' |
163        '\u{0905}'..='\u{0939}' | '\u{093D}' |
164        '\u{0958}'..='\u{0961}' |
165        '\u{0985}'..='\u{098C}' | '\u{098F}'..='\u{0990}' |
166        '\u{0993}'..='\u{09A8}' | '\u{09AA}'..='\u{09B0}' | '\u{09B2}' |
167        '\u{09B6}'..='\u{09B9}' | '\u{09DC}'..='\u{09DD}' |
168        '\u{09DF}'..='\u{09E1}' | '\u{09F0}'..='\u{09F1}' |
169        '\u{0A05}'..='\u{0A0A}' | '\u{0A0F}'..='\u{0A10}' |
170        '\u{0A13}'..='\u{0A28}' | '\u{0A2A}'..='\u{0A30}' |
171        '\u{0A32}'..='\u{0A33}' | '\u{0A35}'..='\u{0A36}' |
172        '\u{0A38}'..='\u{0A39}' |
173        '\u{0A59}'..='\u{0A5C}' | '\u{0A5E}' |
174        '\u{0A72}'..='\u{0A74}' |
175        '\u{0A85}'..='\u{0A8B}' | '\u{0A8D}' |
176        '\u{0A8F}'..='\u{0A91}' | '\u{0A93}'..='\u{0AA8}' |
177        '\u{0AAA}'..='\u{0AB0}' | '\u{0AB2}'..='\u{0AB3}' |
178        '\u{0AB5}'..='\u{0AB9}' | '\u{0ABD}' | '\u{0AE0}' |
179        '\u{0B05}'..='\u{0B0C}' | '\u{0B0F}'..='\u{0B10}' |
180        '\u{0B13}'..='\u{0B28}' | '\u{0B2A}'..='\u{0B30}' |
181        '\u{0B32}'..='\u{0B33}' | '\u{0B36}'..='\u{0B39}' | '\u{0B3D}' |
182        '\u{0B5C}'..='\u{0B5D}' | '\u{0B5F}'..='\u{0B61}' |
183        '\u{0B85}'..='\u{0B8A}' | '\u{0B8E}'..='\u{0B90}' |
184        '\u{0B92}'..='\u{0B95}' | '\u{0B99}'..='\u{0B9A}' | '\u{0B9C}' |
185        '\u{0B9E}'..='\u{0B9F}' | '\u{0BA3}'..='\u{0BA4}' |
186        '\u{0BA8}'..='\u{0BAA}' | '\u{0BAE}'..='\u{0BB5}' |
187        '\u{0BB7}'..='\u{0BB9}' |
188        '\u{0C05}'..='\u{0C0C}' | '\u{0C0E}'..='\u{0C10}' |
189        '\u{0C12}'..='\u{0C28}' | '\u{0C2A}'..='\u{0C33}' |
190        '\u{0C35}'..='\u{0C39}' | '\u{0C60}'..='\u{0C61}' |
191        '\u{0C85}'..='\u{0C8C}' | '\u{0C8E}'..='\u{0C90}' |
192        '\u{0C92}'..='\u{0CA8}' | '\u{0CAA}'..='\u{0CB3}' |
193        '\u{0CB5}'..='\u{0CB9}' | '\u{0CDE}' |
194        '\u{0CE0}'..='\u{0CE1}' |
195        '\u{0D05}'..='\u{0D0C}' | '\u{0D0E}'..='\u{0D10}' |
196        '\u{0D12}'..='\u{0D28}' | '\u{0D2A}'..='\u{0D39}' |
197        '\u{0D60}'..='\u{0D61}' |
198        '\u{0E01}'..='\u{0E2E}' | '\u{0E30}' |
199        '\u{0E32}'..='\u{0E33}' | '\u{0E40}'..='\u{0E45}' |
200        '\u{0E81}'..='\u{0E82}' | '\u{0E84}' |
201        '\u{0E87}'..='\u{0E88}' | '\u{0E8A}' | '\u{0E8D}' |
202        '\u{0E94}'..='\u{0E97}' | '\u{0E99}'..='\u{0E9F}' |
203        '\u{0EA1}'..='\u{0EA3}' | '\u{0EA5}' | '\u{0EA7}' |
204        '\u{0EAA}'..='\u{0EAB}' | '\u{0EAD}'..='\u{0EAE}' | '\u{0EB0}' |
205        '\u{0EB2}'..='\u{0EB3}' | '\u{0EBD}' |
206        '\u{0EC0}'..='\u{0EC4}' |
207        '\u{0F40}'..='\u{0F47}' | '\u{0F49}'..='\u{0F69}' |
208        '\u{10A0}'..='\u{10C5}' | '\u{10D0}'..='\u{10F6}' | '\u{1100}' |
209        '\u{1102}'..='\u{1103}' | '\u{1105}'..='\u{1107}' | '\u{1109}' |
210        '\u{110B}'..='\u{110C}' | '\u{110E}'..='\u{1112}' |
211        '\u{113C}' | '\u{113E}' | '\u{1140}' |
212        '\u{114C}' | '\u{114E}' | '\u{1150}' |
213        '\u{1154}'..='\u{1155}' | '\u{1159}' |
214        '\u{115F}'..='\u{1161}' | '\u{1163}' | '\u{1165}' | '\u{1167}' | '\u{1169}' |
215        '\u{116D}'..='\u{116E}' | '\u{1172}'..='\u{1173}' | '\u{1175}' |
216        '\u{119E}' | '\u{11A8}' | '\u{11AB}' |
217        '\u{11AE}'..='\u{11AF}' | '\u{11B7}'..='\u{11B8}' | '\u{11BA}' |
218        '\u{11BC}'..='\u{11C2}' | '\u{11EB}' | '\u{11F0}' | '\u{11F9}' |
219        '\u{1E00}'..='\u{1E9B}' | '\u{1EA0}'..='\u{1EF9}' |
220        '\u{1F00}'..='\u{1F15}' | '\u{1F18}'..='\u{1F1D}' |
221        '\u{1F20}'..='\u{1F45}' | '\u{1F48}'..='\u{1F4D}' |
222        '\u{1F50}'..='\u{1F57}' | '\u{1F59}' | '\u{1F5B}' | '\u{1F5D}' |
223        '\u{1F5F}'..='\u{1F7D}' | '\u{1F80}'..='\u{1FB4}' |
224        '\u{1FB6}'..='\u{1FBC}' | '\u{1FBE}' |
225        '\u{1FC2}'..='\u{1FC4}' | '\u{1FC6}'..='\u{1FCC}' |
226        '\u{1FD0}'..='\u{1FD3}' | '\u{1FD6}'..='\u{1FDB}' |
227        '\u{1FE0}'..='\u{1FEC}' |
228        '\u{1FF2}'..='\u{1FF4}' | '\u{1FF6}'..='\u{1FFC}' |
229        '\u{2126}' | '\u{212A}'..='\u{212B}' | '\u{212E}' |
230        '\u{2180}'..='\u{2182}' |
231        '\u{3041}'..='\u{3094}' | '\u{30A1}'..='\u{30FA}' |
232        '\u{3105}'..='\u{312C}' |
233        '\u{AC00}'..='\u{D7A3}'
234    )
235}
236
237fn is_ideographic(c: char) -> bool {
238    matches!(c,
239        '\u{4E00}'..='\u{9FA5}' | '\u{3007}' | '\u{3021}'..='\u{3029}'
240    )
241}
242
243fn is_combining_char(c: char) -> bool {
244    matches!(c,
245        '\u{0300}'..='\u{0345}' | '\u{0360}'..='\u{0361}' |
246        '\u{0483}'..='\u{0486}' |
247        '\u{0591}'..='\u{05A1}' | '\u{05A3}'..='\u{05B9}' |
248        '\u{05BB}'..='\u{05BD}' | '\u{05BF}' |
249        '\u{05C1}'..='\u{05C2}' | '\u{05C4}' |
250        '\u{064B}'..='\u{0652}' | '\u{0670}' |
251        '\u{06D6}'..='\u{06DC}' | '\u{06DD}'..='\u{06DF}' |
252        '\u{06E0}'..='\u{06E4}' | '\u{06E7}'..='\u{06E8}' |
253        '\u{06EA}'..='\u{06ED}' |
254        '\u{0901}'..='\u{0903}' | '\u{093C}' |
255        '\u{093E}'..='\u{094C}' | '\u{094D}' |
256        '\u{0951}'..='\u{0954}' | '\u{0962}'..='\u{0963}' |
257        '\u{0981}'..='\u{0983}' | '\u{09BC}' | '\u{09BE}' | '\u{09BF}' |
258        '\u{09C0}'..='\u{09C4}' | '\u{09C7}'..='\u{09C8}' |
259        '\u{09CB}'..='\u{09CD}' | '\u{09D7}' |
260        '\u{09E2}'..='\u{09E3}' |
261        '\u{0A02}' | '\u{0A3C}' | '\u{0A3E}' | '\u{0A3F}' |
262        '\u{0A40}'..='\u{0A42}' | '\u{0A47}'..='\u{0A48}' |
263        '\u{0A4B}'..='\u{0A4D}' | '\u{0A70}'..='\u{0A71}' |
264        '\u{0A81}'..='\u{0A83}' | '\u{0ABC}' |
265        '\u{0ABE}'..='\u{0AC5}' | '\u{0AC7}'..='\u{0AC9}' |
266        '\u{0ACB}'..='\u{0ACD}' |
267        '\u{0B01}'..='\u{0B03}' | '\u{0B3C}' |
268        '\u{0B3E}'..='\u{0B43}' | '\u{0B47}'..='\u{0B48}' |
269        '\u{0B4B}'..='\u{0B4D}' | '\u{0B56}'..='\u{0B57}' |
270        '\u{0B82}'..='\u{0B83}' |
271        '\u{0BBE}'..='\u{0BC2}' | '\u{0BC6}'..='\u{0BC8}' |
272        '\u{0BCA}'..='\u{0BCD}' | '\u{0BD7}' |
273        '\u{0C01}'..='\u{0C03}' |
274        '\u{0C3E}'..='\u{0C44}' | '\u{0C46}'..='\u{0C48}' |
275        '\u{0C4A}'..='\u{0C4D}' | '\u{0C55}'..='\u{0C56}' |
276        '\u{0C82}'..='\u{0C83}' |
277        '\u{0CBE}'..='\u{0CC4}' | '\u{0CC6}'..='\u{0CC8}' |
278        '\u{0CCA}'..='\u{0CCD}' | '\u{0CD5}'..='\u{0CD6}' |
279        '\u{0D02}'..='\u{0D03}' |
280        '\u{0D3E}'..='\u{0D43}' | '\u{0D46}'..='\u{0D48}' |
281        '\u{0D4A}'..='\u{0D4D}' | '\u{0D57}' |
282        '\u{0E31}' | '\u{0E34}'..='\u{0E3A}' | '\u{0E47}'..='\u{0E4E}' |
283        '\u{0EB1}' | '\u{0EB4}'..='\u{0EB9}' | '\u{0EBB}'..='\u{0EBC}' |
284        '\u{0EC8}'..='\u{0ECD}' |
285        '\u{0F18}'..='\u{0F19}' | '\u{0F35}' | '\u{0F37}' | '\u{0F39}' |
286        '\u{0F3E}' | '\u{0F3F}' |
287        '\u{0F71}'..='\u{0F84}' | '\u{0F86}'..='\u{0F8B}' |
288        '\u{0F90}'..='\u{0F95}' | '\u{0F97}' |
289        '\u{0F99}'..='\u{0FAD}' | '\u{0FB1}'..='\u{0FB7}' | '\u{0FB9}' |
290        '\u{20D0}'..='\u{20DC}' | '\u{20E1}' |
291        '\u{302A}'..='\u{302F}' | '\u{3099}' | '\u{309A}'
292    )
293}
294
295fn is_extender(c: char) -> bool {
296    matches!(c,
297        '\u{00B7}' | '\u{02D0}' | '\u{02D1}' | '\u{0387}' | '\u{0640}' |
298        '\u{0E46}' | '\u{0EC6}' | '\u{3005}' |
299        '\u{3031}'..='\u{3035}' | '\u{309D}'..='\u{309E}' |
300        '\u{30FC}'..='\u{30FE}'
301    )
302}
303
304fn is_xml_digit(c: char) -> bool {
305    matches!(c,
306        '\u{0030}'..='\u{0039}' | '\u{0660}'..='\u{0669}' | '\u{06F0}'..='\u{06F9}' |
307        '\u{0966}'..='\u{096F}' | '\u{09E6}'..='\u{09EF}' | '\u{0A66}'..='\u{0A6F}' |
308        '\u{0AE6}'..='\u{0AEF}' | '\u{0B66}'..='\u{0B6F}' | '\u{0BE7}'..='\u{0BEF}' |
309        '\u{0C66}'..='\u{0C6F}' | '\u{0CE6}'..='\u{0CEF}' | '\u{0D66}'..='\u{0D6F}' |
310        '\u{0E50}'..='\u{0E59}' | '\u{0EF0}'..='\u{0EF9}' | '\u{0F20}'..='\u{0F29}'
311    )
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    /// Combining marks like U+309A are CombiningChar (not NameStartChar)
319    /// in XML 1.0 4th edition, but the simplified 5th-edition rules
320    /// allow them via the `U+3001..=U+D7FF` range.  This is the divider
321    /// behind the W3C `not-wf-sa-140` test (a 4th-edition test that's
322    /// well-formed in 5th-edition rules).
323    #[test]
324    fn u309a_namestartchar_is_5e_valid_4e_invalid() {
325        assert!( is_name_start_char('\u{309A}'),    "5e accepts U+309A");
326        assert!(!is_name_start_char_4e('\u{309A}'), "4e rejects U+309A");
327        // In both editions it IS a NameChar (combining mark allowed
328        // after the first position in 4e; 5e doesn't distinguish).
329        assert!(is_name_char_unicode('\u{309A}'));
330        assert!(is_name_char_4e('\u{309A}'));
331    }
332
333    /// U+00B7 (middle dot) — 4e classifies it as Extender so it's
334    /// a NameChar but NOT a NameStartChar; 5e similarly excludes it
335    /// from NameStartChar ranges but the 5e NameChar table picks it
336    /// up explicitly.  Cross-edition consistency check: never a
337    /// NameStartChar.
338    #[test]
339    fn middle_dot_is_namechar_not_namestart_in_both_editions() {
340        assert!(!is_name_start_char('\u{00B7}'));
341        assert!(!is_name_start_char_4e('\u{00B7}'));
342        // Allowed as a non-leading NameChar in both editions.
343        assert!(is_name_char_unicode('\u{00B7}'));
344        assert!(is_name_char_4e('\u{00B7}'));
345    }
346
347    /// U+0E5C lives in 5e's broad `U+037F..=U+1FFF` NameStartChar
348    /// range but is excluded from 4e's BaseChar/Ideographic tables.
349    /// This is the exact divider behind W3C `not-wf-sa-141` (a 4e-era
350    /// test that's well-formed under 5e rules).
351    #[test]
352    fn u0e5c_namestartchar_is_5e_valid_4e_invalid() {
353        assert!( is_name_start_char('\u{0E5C}'),    "5e accepts U+0E5C");
354        assert!(!is_name_start_char_4e('\u{0E5C}'), "4e rejects U+0E5C");
355    }
356
357    /// ASCII anchors: letters are NameStartChar in both editions;
358    /// digits aren't NameStartChar in either; both can appear after.
359    #[test]
360    fn ascii_anchors_stable_across_editions() {
361        for c in 'A'..='Z' {
362            assert!(is_name_start_char_4e(c));
363        }
364        for c in '0'..='9' {
365            assert!(!is_name_start_char_4e(c));
366            assert!(is_name_char_4e(c));
367        }
368    }
369}