use std::cmp::Ordering;
#[rustfmt::skip]
static LETTERS: &[(u32, u32)] = &[
(0x00C0, 0x00D6), (0x00D8, 0x00F6), (0x00F8, 0x0131), (0x0134, 0x013E), (0x0141, 0x0148),
(0x014A, 0x017E), (0x0180, 0x01C3), (0x01CD, 0x01F0), (0x01F4, 0x01F5), (0x01FA, 0x0217),
(0x0250, 0x02A8), (0x02BB, 0x02C1), (0x0386, 0x0386), (0x0388, 0x038A), (0x038C, 0x038C),
(0x038E, 0x03A1), (0x03A3, 0x03CE), (0x03D0, 0x03D6), (0x03DA, 0x03DA), (0x03DC, 0x03DC),
(0x03DE, 0x03DE), (0x03E0, 0x03E0), (0x03E2, 0x03F3), (0x0401, 0x040C), (0x040E, 0x044F),
(0x0451, 0x045C), (0x045E, 0x0481), (0x0490, 0x04C4), (0x04C7, 0x04C8), (0x04CB, 0x04CC),
(0x04D0, 0x04EB), (0x04EE, 0x04F5), (0x04F8, 0x04F9), (0x0531, 0x0556), (0x0559, 0x0559),
(0x0561, 0x0586), (0x05D0, 0x05EA), (0x05F0, 0x05F2), (0x0621, 0x063A), (0x0641, 0x064A),
(0x0671, 0x06B7), (0x06BA, 0x06BE), (0x06C0, 0x06CE), (0x06D0, 0x06D3), (0x06D5, 0x06D5),
(0x06E5, 0x06E6), (0x0905, 0x0939), (0x093D, 0x093D), (0x0958, 0x0961), (0x0985, 0x098C),
(0x098F, 0x0990), (0x0993, 0x09A8), (0x09AA, 0x09B0), (0x09B2, 0x09B2), (0x09B6, 0x09B9),
(0x09DC, 0x09DD), (0x09DF, 0x09E1), (0x09F0, 0x09F1), (0x0A05, 0x0A0A), (0x0A0F, 0x0A10),
(0x0A13, 0x0A28), (0x0A2A, 0x0A30), (0x0A32, 0x0A33), (0x0A35, 0x0A36), (0x0A38, 0x0A39),
(0x0A59, 0x0A5C), (0x0A5E, 0x0A5E), (0x0A72, 0x0A74), (0x0A85, 0x0A8B), (0x0A8D, 0x0A8D),
(0x0A8F, 0x0A91), (0x0A93, 0x0AA8), (0x0AAA, 0x0AB0), (0x0AB2, 0x0AB3), (0x0AB5, 0x0AB9),
(0x0ABD, 0x0ABD), (0x0AE0, 0x0AE0), (0x0B05, 0x0B0C), (0x0B0F, 0x0B10), (0x0B13, 0x0B28),
(0x0B2A, 0x0B30), (0x0B32, 0x0B33), (0x0B36, 0x0B39), (0x0B3D, 0x0B3D), (0x0B5C, 0x0B5D),
(0x0B5F, 0x0B61), (0x0B85, 0x0B8A), (0x0B8E, 0x0B90), (0x0B92, 0x0B95), (0x0B99, 0x0B9A),
(0x0B9C, 0x0B9C), (0x0B9E, 0x0B9F), (0x0BA3, 0x0BA4), (0x0BA8, 0x0BAA), (0x0BAE, 0x0BB5),
(0x0BB7, 0x0BB9), (0x0C05, 0x0C0C), (0x0C0E, 0x0C10), (0x0C12, 0x0C28), (0x0C2A, 0x0C33),
(0x0C35, 0x0C39), (0x0C60, 0x0C61), (0x0C85, 0x0C8C), (0x0C8E, 0x0C90), (0x0C92, 0x0CA8),
(0x0CAA, 0x0CB3), (0x0CB5, 0x0CB9), (0x0CDE, 0x0CDE), (0x0CE0, 0x0CE1), (0x0D05, 0x0D0C),
(0x0D0E, 0x0D10), (0x0D12, 0x0D28), (0x0D2A, 0x0D39), (0x0D60, 0x0D61), (0x0E01, 0x0E2E),
(0x0E30, 0x0E30), (0x0E32, 0x0E33), (0x0E40, 0x0E45), (0x0E81, 0x0E82), (0x0E84, 0x0E84),
(0x0E87, 0x0E88), (0x0E8A, 0x0E8A), (0x0E8D, 0x0E8D), (0x0E94, 0x0E97), (0x0E99, 0x0E9F),
(0x0EA1, 0x0EA3), (0x0EA5, 0x0EA5), (0x0EA7, 0x0EA7), (0x0EAA, 0x0EAB), (0x0EAD, 0x0EAE),
(0x0EB0, 0x0EB0), (0x0EB2, 0x0EB3), (0x0EBD, 0x0EBD), (0x0EC0, 0x0EC4), (0x0F40, 0x0F47),
(0x0F49, 0x0F69), (0x10A0, 0x10C5), (0x10D0, 0x10F6), (0x1100, 0x1100), (0x1102, 0x1103),
(0x1105, 0x1107), (0x1109, 0x1109), (0x110B, 0x110C), (0x110E, 0x1112), (0x113C, 0x113C),
(0x113E, 0x113E), (0x1140, 0x1140), (0x114C, 0x114C), (0x114E, 0x114E), (0x1150, 0x1150),
(0x1154, 0x1155), (0x1159, 0x1159), (0x115F, 0x1161), (0x1163, 0x1163), (0x1165, 0x1165),
(0x1167, 0x1167), (0x1169, 0x1169), (0x116D, 0x116E), (0x1172, 0x1173), (0x1175, 0x1175),
(0x119E, 0x119E), (0x11A8, 0x11A8), (0x11AB, 0x11AB), (0x11AE, 0x11AF), (0x11B7, 0x11B8),
(0x11BA, 0x11BA), (0x11BC, 0x11C2), (0x11EB, 0x11EB), (0x11F0, 0x11F0), (0x11F9, 0x11F9),
(0x1E00, 0x1E9B), (0x1EA0, 0x1EF9), (0x1F00, 0x1F15), (0x1F18, 0x1F1D), (0x1F20, 0x1F45),
(0x1F48, 0x1F4D), (0x1F50, 0x1F57), (0x1F59, 0x1F59), (0x1F5B, 0x1F5B), (0x1F5D, 0x1F5D),
(0x1F5F, 0x1F7D), (0x1F80, 0x1FB4), (0x1FB6, 0x1FBC), (0x1FBE, 0x1FBE), (0x1FC2, 0x1FC4),
(0x1FC6, 0x1FCC), (0x1FD0, 0x1FD3), (0x1FD6, 0x1FDB), (0x1FE0, 0x1FEC), (0x1FF2, 0x1FF4),
(0x1FF6, 0x1FFC), (0x2126, 0x2126), (0x212A, 0x212B), (0x212E, 0x212E), (0x2180, 0x2182),
(0x3007, 0x3007), (0x3021, 0x3029), (0x3041, 0x3094), (0x30A1, 0x30FA), (0x3105, 0x312C),
(0x4E00, 0x9FA5), (0xAC00, 0xD7A3),
];
#[rustfmt::skip]
static TRAILING: &[(u32, u32)] = &[
(0x00B7, 0x00B7), (0x02D0, 0x02D1), (0x0300, 0x0345), (0x0360, 0x0361), (0x0387, 0x0387),
(0x0483, 0x0486), (0x0591, 0x05A1), (0x05A3, 0x05B9), (0x05BB, 0x05BD), (0x05BF, 0x05BF),
(0x05C1, 0x05C2), (0x05C4, 0x05C4), (0x0640, 0x0640), (0x064B, 0x0652), (0x0660, 0x0669),
(0x0670, 0x0670), (0x06D6, 0x06E4), (0x06E7, 0x06E8), (0x06EA, 0x06ED), (0x06F0, 0x06F9),
(0x0901, 0x0903), (0x093C, 0x093C), (0x093E, 0x094D), (0x0951, 0x0954), (0x0962, 0x0963),
(0x0966, 0x096F), (0x0981, 0x0983), (0x09BC, 0x09BC), (0x09BE, 0x09C4), (0x09C7, 0x09C8),
(0x09CB, 0x09CD), (0x09D7, 0x09D7), (0x09E2, 0x09E3), (0x09E6, 0x09EF), (0x0A02, 0x0A02),
(0x0A3C, 0x0A3C), (0x0A3E, 0x0A42), (0x0A47, 0x0A48), (0x0A4B, 0x0A4D), (0x0A66, 0x0A71),
(0x0A81, 0x0A83), (0x0ABC, 0x0ABC), (0x0ABE, 0x0AC5), (0x0AC7, 0x0AC9), (0x0ACB, 0x0ACD),
(0x0AE6, 0x0AEF), (0x0B01, 0x0B03), (0x0B3C, 0x0B3C), (0x0B3E, 0x0B43), (0x0B47, 0x0B48),
(0x0B4B, 0x0B4D), (0x0B56, 0x0B57), (0x0B66, 0x0B6F), (0x0B82, 0x0B83), (0x0BBE, 0x0BC2),
(0x0BC6, 0x0BC8), (0x0BCA, 0x0BCD), (0x0BD7, 0x0BD7), (0x0BE7, 0x0BEF), (0x0C01, 0x0C03),
(0x0C3E, 0x0C44), (0x0C46, 0x0C48), (0x0C4A, 0x0C4D), (0x0C55, 0x0C56), (0x0C66, 0x0C6F),
(0x0C82, 0x0C83), (0x0CBE, 0x0CC4), (0x0CC6, 0x0CC8), (0x0CCA, 0x0CCD), (0x0CD5, 0x0CD6),
(0x0CE6, 0x0CEF), (0x0D02, 0x0D03), (0x0D3E, 0x0D43), (0x0D46, 0x0D48), (0x0D4A, 0x0D4D),
(0x0D57, 0x0D57), (0x0D66, 0x0D6F), (0x0E31, 0x0E31), (0x0E34, 0x0E3A), (0x0E46, 0x0E4E),
(0x0E50, 0x0E59), (0x0EB1, 0x0EB1), (0x0EB4, 0x0EB9), (0x0EBB, 0x0EBC), (0x0EC6, 0x0EC6),
(0x0EC8, 0x0ECD), (0x0ED0, 0x0ED9), (0x0F18, 0x0F19), (0x0F20, 0x0F29), (0x0F35, 0x0F35),
(0x0F37, 0x0F37), (0x0F39, 0x0F39), (0x0F3E, 0x0F3F), (0x0F71, 0x0F84), (0x0F86, 0x0F8B),
(0x0F90, 0x0F95), (0x0F97, 0x0F97), (0x0F99, 0x0FAD), (0x0FB1, 0x0FB7), (0x0FB9, 0x0FB9),
(0x20D0, 0x20DC), (0x20E1, 0x20E1), (0x3005, 0x3005), (0x302A, 0x302F), (0x3031, 0x3035),
(0x3099, 0x309A), (0x309D, 0x309E), (0x30FC, 0x30FE),
];
pub(crate) fn is_ncname(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
is_name_start(first) && chars.all(is_name_char)
}
fn is_name_start(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_alphabetic() || c == '_';
}
in_table(c, LETTERS)
}
fn is_name_char(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-');
}
in_table(c, LETTERS) || in_table(c, TRAILING)
}
fn in_table(c: char, table: &[(u32, u32)]) -> bool {
let c = c as u32;
table
.binary_search_by(|&(lo, hi)| {
if hi < c {
Ordering::Less
} else if lo > c {
Ordering::Greater
} else {
Ordering::Equal
}
})
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_names() {
assert!(is_ncname("div"));
assert!(is_ncname("_x"));
assert!(is_ncname("a-b.c_1"));
assert!(!is_ncname(""));
assert!(!is_ncname("1a"));
assert!(!is_ncname("-a"));
assert!(!is_ncname(".a"));
assert!(!is_ncname("di[v"));
assert!(!is_ncname("a b"));
assert!(!is_ncname("a:b"));
}
#[test]
fn non_ascii_names() {
assert!(is_ncname("nsé"));
assert!(is_ncname("é"));
assert!(is_ncname("Ελληνικά"));
assert!(is_ncname("русский"));
assert!(is_ncname("中文"));
assert!(is_ncname("a·b"));
assert!(!is_ncname("·a"));
assert!(is_ncname("a\u{301}"));
assert!(!is_ncname("\u{301}a"));
assert!(!is_ncname("di\u{a0}v"));
assert!(!is_ncname("ns\u{242}"));
assert!(!is_ncname("\u{10330}"));
}
#[test]
fn tables_are_sorted_and_disjoint() {
for table in [LETTERS, TRAILING] {
for pair in table.windows(2) {
let ((lo, hi), (next_lo, _)) = (pair[0], pair[1]);
assert!(lo <= hi, "range {lo:#X}-{hi:#X} is inverted");
assert!(
hi + 1 < next_lo,
"range {lo:#X}-{hi:#X} touches {next_lo:#X}"
);
}
}
for &(lo, hi) in TRAILING {
for c in lo..=hi {
let c = char::from_u32(c).expect("table holds scalar values");
assert!(!in_table(c, LETTERS), "{c:?} is in both tables");
}
}
}
}