Skip to main content

sup_xml_core/regex/
unicode.rs

1//! Unicode category and block tables for XSD §F.1 `\p{...}` escapes.
2//!
3//! General categories come from the `unicode-properties` crate.
4//! Block ranges come from XSD §F.1.1 and are hand-encoded below —
5//! the list is short and stable, and the spec pins the names so a
6//! drift between Unicode revisions isn't visible to schema authors.
7//!
8//! Categories are materialized into [`ClassSet`]s lazily on first
9//! reference and cached in a `OnceLock` so repeated `\p{L}` matches
10//! pay no per-match cost.
11
12use std::cell::Cell;
13use std::collections::HashMap;
14use std::sync::{Mutex, OnceLock};
15use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory};
16
17use super::class::ClassSet;
18use super::ucd::{self, UnicodeVersion};
19
20thread_local! {
21    /// Which Unicode snapshot to consult while compiling the
22    /// `\p{...}` properties of the next pattern.  Stays at
23    /// `Latest` (the build-time UCD shipped with
24    /// `unicode-properties`) for production callers; the conformance
25    /// runner pushes `V6_0` / `V9_0` around the version-locked
26    /// W3C test sets via [`with_unicode_version`].  Read by
27    /// [`property_set`] at pattern-compile time so the resulting
28    /// NFA's `ClassSet`s bake in the right Unicode boundaries.
29    static REGEX_UCD_VERSION: Cell<UnicodeVersion> =
30        const { Cell::new(UnicodeVersion::Latest) };
31}
32
33/// Run `f` with the regex engine's bundled Unicode snapshot
34/// temporarily set to `version`.  Restores the previous value on
35/// return.  Only affects pattern *compilation*; once compiled, an
36/// NFA's classes are baked in.
37pub fn with_unicode_version<R>(version: UnicodeVersion, f: impl FnOnce() -> R) -> R {
38    let prev = REGEX_UCD_VERSION.with(|c| c.replace(version));
39    let r = f();
40    REGEX_UCD_VERSION.with(|c| c.set(prev));
41    r
42}
43
44/// Snapshot the active Unicode version for the regex engine's
45/// compile-time `\p{...}` resolution.  Read by the module-level
46/// compile-cache when building the cache key so a pattern cached
47/// under one UCD snapshot isn't returned to a caller asking under
48/// another.
49pub(super) fn current_ucd_version() -> UnicodeVersion {
50    REGEX_UCD_VERSION.with(|c| c.get())
51}
52
53/// Look up the [`ClassSet`] for an XSD `\p{...}` body.  Returns
54/// `None` for unknown names — callers turn that into a compile-time
55/// error citing the offending escape.
56pub fn property_set(name: &str) -> Option<&'static ClassSet> {
57    if let Some(block_name) = name.strip_prefix("Is") {
58        return block_set(block_name);
59    }
60    match REGEX_UCD_VERSION.with(|c| c.get()) {
61        UnicodeVersion::Latest => category_set(name),
62        v                      => category_set_versioned(name, v),
63    }
64}
65
66/// Look up a General_Category property against a bundled historical
67/// UCD snapshot (Unicode 6.0 or 9.0).  Builds the [`ClassSet`] from
68/// the snapshot's range table on first reference and caches it so
69/// repeated `\p{Lu}` queries against the same version stay cheap.
70fn category_set_versioned(
71    name: &str, version: UnicodeVersion,
72) -> Option<&'static ClassSet> {
73    static CACHE: OnceLock<Mutex<HashMap<(UnicodeVersion, String), &'static ClassSet>>>
74        = OnceLock::new();
75    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
76    {
77        let g = cache.lock().unwrap();
78        if let Some(&cs) = g.get(&(version, name.to_string())) {
79            return Some(cs);
80        }
81    }
82    let built = build_versioned_category(name, version)?;
83    let leaked: &'static ClassSet = Box::leak(Box::new(built));
84    cache.lock().unwrap().insert((version, name.to_string()), leaked);
85    Some(leaked)
86}
87
88/// Compose a `ClassSet` for `name` from the bundled UCD snapshot.
89/// Subcategories (`Lu`, `Nd`, …) come straight from the table;
90/// the seven group letters (`L`, `M`, `N`, `P`, `S`, `Z`, `C`)
91/// are the union of their member subcategories.
92fn build_versioned_category(
93    name: &str, version: UnicodeVersion,
94) -> Option<ClassSet> {
95    if let Some(ranges) = ucd::category(name, version) {
96        let pairs: Vec<(u32, u32)> = ranges.iter()
97            .map(|r| (r.start, r.end))
98            .collect();
99        return Some(ClassSet::from_sorted_ranges(pairs));
100    }
101    let members: &[&str] = match name {
102        "L"  => &["Lu", "Ll", "Lt", "Lm", "Lo"],
103        "M"  => &["Mn", "Mc", "Me"],
104        "N"  => &["Nd", "Nl", "No"],
105        "P"  => &["Pc", "Pd", "Ps", "Pe", "Pi", "Pf", "Po"],
106        "S"  => &["Sm", "Sc", "Sk", "So"],
107        "Z"  => &["Zs", "Zl", "Zp"],
108        "C"  => &["Cc", "Cf", "Cs", "Co", "Cn"],
109        // XSD §F.1.1 also accepts `LC` (Cased Letter = Lu ∪ Ll ∪ Lt).
110        "LC" => &["Lu", "Ll", "Lt"],
111        _    => return None,
112    };
113    let mut acc = ClassSet::empty();
114    for m in members {
115        if let Some(ranges) = ucd::category(m, version) {
116            let pairs: Vec<(u32, u32)> = ranges.iter()
117                .map(|r| (r.start, r.end))
118                .collect();
119            acc = acc.union(&ClassSet::from_sorted_ranges(pairs));
120        }
121    }
122    Some(acc)
123}
124
125// ── categories ─────────────────────────────────────────────────────────────
126
127/// XSD §F.1.1 — `\p{L}`, `\p{Lu}`, …; both group abbreviations
128/// (`L`, `M`, `N`, …) and full subcategories (`Lu`, `Ll`, …).
129fn category_set(name: &str) -> Option<&'static ClassSet> {
130    macro_rules! cached_cat {
131        ($name:literal, $build:expr) => {{
132            static CELL: OnceLock<ClassSet> = OnceLock::new();
133            if name == $name {
134                return Some(CELL.get_or_init(|| $build));
135            }
136        }};
137    }
138
139    // Group categories — defined as the union of their members.
140    cached_cat!("L", build_group(GeneralCategoryGroup::Letter));
141    cached_cat!("M", build_group(GeneralCategoryGroup::Mark));
142    cached_cat!("N", build_group(GeneralCategoryGroup::Number));
143    cached_cat!("P", build_group(GeneralCategoryGroup::Punctuation));
144    cached_cat!("S", build_group(GeneralCategoryGroup::Symbol));
145    cached_cat!("Z", build_group(GeneralCategoryGroup::Separator));
146    cached_cat!("C", build_group(GeneralCategoryGroup::Other));
147
148    // Subcategories — one cell each, built by predicate over the
149    // full Unicode space on first reference.  The build is ~1 ms.
150    use unicode_properties::GeneralCategory as G;
151    cached_cat!("Lu", build_filtered(|c| c.general_category() == G::UppercaseLetter));
152    cached_cat!("Ll", build_filtered(|c| c.general_category() == G::LowercaseLetter));
153    cached_cat!("Lt", build_filtered(|c| c.general_category() == G::TitlecaseLetter));
154    cached_cat!("Lm", build_filtered(|c| c.general_category() == G::ModifierLetter));
155    cached_cat!("Lo", build_filtered(|c| c.general_category() == G::OtherLetter));
156
157    cached_cat!("Mn", build_filtered(|c| c.general_category() == G::NonspacingMark));
158    cached_cat!("Mc", build_filtered(|c| c.general_category() == G::SpacingMark));
159    cached_cat!("Me", build_filtered(|c| c.general_category() == G::EnclosingMark));
160
161    cached_cat!("Nd", build_filtered(|c| c.general_category() == G::DecimalNumber));
162    cached_cat!("Nl", build_filtered(|c| c.general_category() == G::LetterNumber));
163    cached_cat!("No", build_filtered(|c| c.general_category() == G::OtherNumber));
164
165    cached_cat!("Pc", build_filtered(|c| c.general_category() == G::ConnectorPunctuation));
166    cached_cat!("Pd", build_filtered(|c| c.general_category() == G::DashPunctuation));
167    cached_cat!("Ps", build_filtered(|c| c.general_category() == G::OpenPunctuation));
168    cached_cat!("Pe", build_filtered(|c| c.general_category() == G::ClosePunctuation));
169    cached_cat!("Pi", build_filtered(|c| c.general_category() == G::InitialPunctuation));
170    cached_cat!("Pf", build_filtered(|c| c.general_category() == G::FinalPunctuation));
171    cached_cat!("Po", build_filtered(|c| c.general_category() == G::OtherPunctuation));
172
173    cached_cat!("Sm", build_filtered(|c| c.general_category() == G::MathSymbol));
174    cached_cat!("Sc", build_filtered(|c| c.general_category() == G::CurrencySymbol));
175    cached_cat!("Sk", build_filtered(|c| c.general_category() == G::ModifierSymbol));
176    cached_cat!("So", build_filtered(|c| c.general_category() == G::OtherSymbol));
177
178    cached_cat!("Zs", build_filtered(|c| c.general_category() == G::SpaceSeparator));
179    cached_cat!("Zl", build_filtered(|c| c.general_category() == G::LineSeparator));
180    cached_cat!("Zp", build_filtered(|c| c.general_category() == G::ParagraphSeparator));
181
182    cached_cat!("Cc", build_filtered(|c| c.general_category() == G::Control));
183    cached_cat!("Cf", build_filtered(|c| c.general_category() == G::Format));
184    cached_cat!("Cs", build_filtered(|c| c.general_category() == G::Surrogate));
185    cached_cat!("Co", build_filtered(|c| c.general_category() == G::PrivateUse));
186    cached_cat!("Cn", build_filtered(|c| c.general_category() == G::Unassigned));
187
188    None
189}
190
191fn build_group(group: GeneralCategoryGroup) -> ClassSet {
192    build_filtered(move |c| c.general_category_group() == group)
193}
194
195/// Walk the Unicode scalar space, collecting all codepoints
196/// satisfying `f` into a sorted range list.  Runs once per category
197/// per process.
198fn build_filtered(f: impl Fn(char) -> bool) -> ClassSet {
199    let mut ranges: Vec<(u32, u32)> = Vec::new();
200    let mut cur: Option<(u32, u32)> = None;
201    for cp in 0u32..=0x10_FFFF {
202        let Some(c) = char::from_u32(cp) else { continue };
203        if f(c) {
204            match &mut cur {
205                Some((_, hi)) if *hi + 1 == cp => *hi = cp,
206                _ => {
207                    if let Some(r) = cur.take() { ranges.push(r); }
208                    cur = Some((cp, cp));
209                }
210            }
211        } else if let Some(r) = cur.take() {
212            ranges.push(r);
213        }
214    }
215    if let Some(r) = cur { ranges.push(r); }
216    ClassSet::from_sorted_ranges(ranges)
217}
218
219// ── XSD `\s` and `\w` ─────────────────────────────────────────────────────
220
221/// `\s` per XSD §F.1.4 — the four XML whitespace characters.  Not
222/// the Unicode whitespace set (`Zs ∪ Zl ∪ Zp`); the spec is
223/// explicit.
224pub fn xsd_whitespace() -> &'static ClassSet {
225    static CELL: OnceLock<ClassSet> = OnceLock::new();
226    CELL.get_or_init(|| ClassSet::from_sorted_ranges(vec![
227        (0x09, 0x0A),  // tab, LF
228        (0x0D, 0x0D),  // CR
229        (0x20, 0x20),  // space
230    ]))
231}
232
233/// `\w` per XSD §F.1.4 — the universe minus `\p{P} ∪ \p{Z} ∪ \p{C}`.
234/// Reads [`REGEX_UCD_VERSION`] so the `P` / `Z` / `C` categories
235/// come from the right Unicode snapshot when a version-locked test
236/// is active; falls back to the latest UCD otherwise.
237pub fn xsd_word() -> &'static ClassSet {
238    let v = REGEX_UCD_VERSION.with(|c| c.get());
239    match v {
240        UnicodeVersion::Latest => {
241            static CELL: OnceLock<ClassSet> = OnceLock::new();
242            CELL.get_or_init(|| {
243                let p = category_set("P").expect("P category");
244                let z = category_set("Z").expect("Z category");
245                let c = category_set("C").expect("C category");
246                ClassSet::universe().subtract(p).subtract(z).subtract(c)
247            })
248        }
249        _ => versioned_word(v),
250    }
251}
252
253/// Per-version cache for `\w`.  Built from the bundled UCD
254/// snapshot's `P` / `Z` / `C` ranges on first reference.
255fn versioned_word(version: UnicodeVersion) -> &'static ClassSet {
256    static CACHE: OnceLock<Mutex<HashMap<UnicodeVersion, &'static ClassSet>>>
257        = OnceLock::new();
258    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
259    if let Some(&cs) = cache.lock().unwrap().get(&version) {
260        return cs;
261    }
262    let p = category_set_versioned("P", version).expect("versioned P");
263    let z = category_set_versioned("Z", version).expect("versioned Z");
264    let c = category_set_versioned("C", version).expect("versioned C");
265    let cs = ClassSet::universe().subtract(p).subtract(z).subtract(c);
266    let leaked: &'static ClassSet = Box::leak(Box::new(cs));
267    cache.lock().unwrap().insert(version, leaked);
268    leaked
269}
270
271/// `\d` per XSD §F.1.4 — equivalent to `\p{Nd}`.  Honours the
272/// current [`REGEX_UCD_VERSION`] so a version-locked test runs
273/// against the right Unicode snapshot.
274pub fn xsd_digit() -> &'static ClassSet {
275    match REGEX_UCD_VERSION.with(|c| c.get()) {
276        UnicodeVersion::Latest => category_set("Nd").expect("Nd category"),
277        v                      => category_set_versioned("Nd", v).expect("versioned Nd"),
278    }
279}
280
281// ── blocks ─────────────────────────────────────────────────────────────────
282
283/// XSD §F.1.1 named Unicode block — `\p{IsBasicLatin}` and friends.
284/// Names are stripped of their leading `Is` by the caller.
285fn block_set(name: &str) -> Option<&'static ClassSet> {
286    let ranges = block_ranges(name)?;
287    static MAP: OnceLock<std::sync::Mutex<rustc_hash::FxHashMap<&'static str, &'static ClassSet>>>
288        = OnceLock::new();
289    let map = MAP.get_or_init(Default::default);
290    let mut guard = map.lock().unwrap();
291    if let Some(&cs) = guard.get(name) {
292        return Some(cs);
293    }
294    let cs: &'static ClassSet =
295        Box::leak(Box::new(ClassSet::from_sorted_ranges(ranges)));
296    let canonical = block_name_static(name)?;
297    guard.insert(canonical, cs);
298    Some(cs)
299}
300
301/// All ranges for a named Unicode block per XSD §F.1.1.  Most
302/// blocks are a single contiguous range; a few (PrivateUse,
303/// HighSurrogates) cover multiple disjoint code-point ranges.
304fn block_ranges(name: &str) -> Option<Vec<(u32, u32)>> {
305    // XSD §F.1.1: PrivateUse spans the BMP private-use area and
306    // both supplementary private-use planes.  Implement as a
307    // multi-range union so `\p{IsPrivateUse}` matches every PUA
308    // codepoint (the conformance suite exercises Plane-15 / 16).
309    if name == "PrivateUse" || name == "PrivateUseArea" {
310        return Some(vec![
311            (0xE000,   0xF8FF),
312            (0xF0000,  0xFFFFD),
313            (0x100000, 0x10FFFD),
314        ]);
315    }
316    block_table().iter()
317        .find(|(n, _, _)| *n == name)
318        .map(|&(_, lo, hi)| vec![(lo, hi)])
319}
320
321fn block_name_static(name: &str) -> Option<&'static str> {
322    block_table().iter()
323        .find(|(n, _, _)| *n == name)
324        .map(|&(n, _, _)| n)
325}
326
327/// `(name, lo, hi)` triples for XSD §F.1.1's block list.  Codepoint
328/// ranges from Unicode `Blocks.txt`; names match the XSD spec
329/// verbatim (note: `IsLatin-1Supplement` is written without a
330/// hyphen in XSD's table, but real-world schemas use both — we
331/// accept both forms).
332fn block_table() -> &'static [(&'static str, u32, u32)] {
333    // Kept short on purpose — the full XSD §F.1.1 table has ~150
334    // entries and most are vanishingly rare in schemas.  Add
335    // entries here as users hit `\p{IsX}` names we don't yet
336    // recognise; the engine errors at compile time so missing
337    // names surface immediately.
338    // Full XSD §F.1.1 / Unicode 3.1 block list.  Names match the
339    // XSD spec verbatim; some have alias spellings (with/without
340    // hyphens) — both included so real-world schemas using either
341    // form resolve.
342    &[
343        ("BasicLatin",                              0x0000, 0x007F),
344        ("Latin-1Supplement",                       0x0080, 0x00FF),
345        ("Latin1Supplement",                        0x0080, 0x00FF),
346        ("LatinExtended-A",                         0x0100, 0x017F),
347        ("LatinExtended-B",                         0x0180, 0x024F),
348        ("IPAExtensions",                           0x0250, 0x02AF),
349        ("SpacingModifierLetters",                  0x02B0, 0x02FF),
350        ("CombiningDiacriticalMarks",               0x0300, 0x036F),
351        ("Greek",                                   0x0370, 0x03FF),
352        // Unicode 4.1+ renamed the block to "Greek and Coptic" while
353        // splitting out a separate Coptic block; XSD §F.1.1 keeps the
354        // legacy name, but many real-world schemas and the W3C
355        // conformance suite use the modern alias.
356        ("GreekandCoptic",                          0x0370, 0x03FF),
357        ("Cyrillic",                                0x0400, 0x04FF),
358        ("Armenian",                                0x0530, 0x058F),
359        ("Hebrew",                                  0x0590, 0x05FF),
360        ("Arabic",                                  0x0600, 0x06FF),
361        ("Syriac",                                  0x0700, 0x074F),
362        ("Thaana",                                  0x0780, 0x07BF),
363        ("Devanagari",                              0x0900, 0x097F),
364        ("Bengali",                                 0x0980, 0x09FF),
365        ("Gurmukhi",                                0x0A00, 0x0A7F),
366        ("Gujarati",                                0x0A80, 0x0AFF),
367        ("Oriya",                                   0x0B00, 0x0B7F),
368        ("Tamil",                                   0x0B80, 0x0BFF),
369        ("Telugu",                                  0x0C00, 0x0C7F),
370        ("Kannada",                                 0x0C80, 0x0CFF),
371        ("Malayalam",                               0x0D00, 0x0D7F),
372        ("Sinhala",                                 0x0D80, 0x0DFF),
373        ("Thai",                                    0x0E00, 0x0E7F),
374        ("Lao",                                     0x0E80, 0x0EFF),
375        ("Tibetan",                                 0x0F00, 0x0FFF),
376        ("Myanmar",                                 0x1000, 0x109F),
377        ("Georgian",                                0x10A0, 0x10FF),
378        ("HangulJamo",                              0x1100, 0x11FF),
379        ("Ethiopic",                                0x1200, 0x137F),
380        ("Cherokee",                                0x13A0, 0x13FF),
381        ("UnifiedCanadianAboriginalSyllabics",      0x1400, 0x167F),
382        ("Ogham",                                   0x1680, 0x169F),
383        ("Runic",                                   0x16A0, 0x16FF),
384        ("Khmer",                                   0x1780, 0x17FF),
385        ("Mongolian",                               0x1800, 0x18AF),
386        ("LatinExtendedAdditional",                 0x1E00, 0x1EFF),
387        ("GreekExtended",                           0x1F00, 0x1FFF),
388        ("GeneralPunctuation",                      0x2000, 0x206F),
389        ("SuperscriptsandSubscripts",               0x2070, 0x209F),
390        ("CurrencySymbols",                         0x20A0, 0x20CF),
391        ("CombiningMarksforSymbols",                0x20D0, 0x20FF),
392        // Unicode 4.1+ alias.
393        ("CombiningDiacriticalMarksforSymbols",     0x20D0, 0x20FF),
394        ("LetterlikeSymbols",                       0x2100, 0x214F),
395        ("NumberForms",                             0x2150, 0x218F),
396        ("Arrows",                                  0x2190, 0x21FF),
397        ("MathematicalOperators",                   0x2200, 0x22FF),
398        ("MiscellaneousTechnical",                  0x2300, 0x23FF),
399        ("ControlPictures",                         0x2400, 0x243F),
400        ("OpticalCharacterRecognition",             0x2440, 0x245F),
401        ("EnclosedAlphanumerics",                   0x2460, 0x24FF),
402        ("BoxDrawing",                              0x2500, 0x257F),
403        ("BlockElements",                           0x2580, 0x259F),
404        ("GeometricShapes",                         0x25A0, 0x25FF),
405        ("MiscellaneousSymbols",                    0x2600, 0x26FF),
406        ("Dingbats",                                0x2700, 0x27BF),
407        ("BraillePatterns",                         0x2800, 0x28FF),
408        ("CJKRadicalsSupplement",                   0x2E80, 0x2EFF),
409        ("KangxiRadicals",                          0x2F00, 0x2FDF),
410        ("IdeographicDescriptionCharacters",        0x2FF0, 0x2FFF),
411        ("CJKSymbolsandPunctuation",                0x3000, 0x303F),
412        ("Hiragana",                                0x3040, 0x309F),
413        ("Katakana",                                0x30A0, 0x30FF),
414        ("Bopomofo",                                0x3100, 0x312F),
415        ("HangulCompatibilityJamo",                 0x3130, 0x318F),
416        ("Kanbun",                                  0x3190, 0x319F),
417        ("BopomofoExtended",                        0x31A0, 0x31BF),
418        ("EnclosedCJKLettersandMonths",             0x3200, 0x32FF),
419        ("CJKCompatibility",                        0x3300, 0x33FF),
420        ("CJKUnifiedIdeographsExtensionA",          0x3400, 0x4DB5),
421        ("CJKUnifiedIdeographs",                    0x4E00, 0x9FFF),
422        ("YiSyllables",                             0xA000, 0xA48F),
423        ("YiRadicals",                              0xA490, 0xA4CF),
424        ("HangulSyllables",                         0xAC00, 0xD7AF),
425        ("HighSurrogates",                          0xD800, 0xDB7F),
426        ("HighPrivateUseSurrogates",                0xDB80, 0xDBFF),
427        ("LowSurrogates",                           0xDC00, 0xDFFF),
428        ("PrivateUse",                              0xE000, 0xF8FF),
429        ("PrivateUseArea",                          0xE000, 0xF8FF),
430        ("CJKCompatibilityIdeographs",              0xF900, 0xFAFF),
431        ("AlphabeticPresentationForms",             0xFB00, 0xFB4F),
432        ("ArabicPresentationForms-A",               0xFB50, 0xFDFF),
433        ("ArabicPresentationFormsA",                0xFB50, 0xFDFF),
434        ("CombiningHalfMarks",                      0xFE20, 0xFE2F),
435        ("CJKCompatibilityForms",                   0xFE30, 0xFE4F),
436        ("SmallFormVariants",                       0xFE50, 0xFE6F),
437        ("ArabicPresentationForms-B",               0xFE70, 0xFEFF),
438        ("ArabicPresentationFormsB",                0xFE70, 0xFEFF),
439        ("HalfwidthandFullwidthForms",              0xFF00, 0xFFEF),
440        ("Specials",                                0xFFF0, 0xFFFF),
441        // Supplementary planes
442        ("OldItalic",                               0x10300, 0x1032F),
443        ("Gothic",                                  0x10330, 0x1034F),
444        ("Deseret",                                 0x10400, 0x1044F),
445        ("ByzantineMusicalSymbols",                 0x1D000, 0x1D0FF),
446        ("MusicalSymbols",                          0x1D100, 0x1D1FF),
447        ("MathematicalAlphanumericSymbols",         0x1D400, 0x1D7FF),
448        ("CJKUnifiedIdeographsExtensionB",          0x20000, 0x2A6D6),
449        ("CJKCompatibilityIdeographsSupplement",    0x2F800, 0x2FA1F),
450        ("Tags",                                    0xE0000, 0xE007F),
451        // Plane-15 / Plane-16 supplementary private-use areas.  XSD
452        // §F.1.1 names these "SupplementaryPrivateUseArea-A" and
453        // "SupplementaryPrivateUseArea-B"; older XSD revisions used
454        // a single "PrivateUse" group that covered both, hence the
455        // multi-range handling for "PrivateUse" above.
456        ("SupplementaryPrivateUseArea-A",           0xF0000, 0xFFFFD),
457        ("SupplementaryPrivateUseAreaA",            0xF0000, 0xFFFFD),
458        ("SupplementaryPrivateUseArea-B",           0x100000, 0x10FFFD),
459        ("SupplementaryPrivateUseAreaB",            0x100000, 0x10FFFD),
460    ]
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn whitespace_is_xml_four() {
469        let s = xsd_whitespace();
470        assert!(s.contains(' '));
471        assert!(s.contains('\t'));
472        assert!(s.contains('\n'));
473        assert!(s.contains('\r'));
474        assert!(!s.contains('\u{A0}'), "non-breaking space is not XSD \\s");
475    }
476
477    #[test]
478    fn digit_is_decimal_only() {
479        let d = xsd_digit();
480        assert!(d.contains('0'));
481        assert!(d.contains('9'));
482        assert!(d.contains('\u{0660}'), "Arabic-Indic digit");
483        // Ethiopic digits (U+1369..U+1371) were Nd in Unicode 3.2,
484        // reclassified to No (OtherNumber) in Unicode 6.2.  XSD §F.1
485        // pins to the runtime Unicode database, so they're not in our
486        // Nd set on modern toolchains.  Some XSTS tests assume the
487        // older classification; we follow the current spec.
488        assert!(!d.contains('\u{1369}'), "Ethiopic digits are not Nd in modern Unicode");
489        assert!(!d.contains('a'));
490    }
491
492    #[test]
493    fn category_letter() {
494        let l = property_set("L").unwrap();
495        assert!(l.contains('a'));
496        assert!(l.contains('中'));
497        assert!(!l.contains('1'));
498        assert!(!l.contains(' '));
499    }
500
501    #[test]
502    fn category_uppercase() {
503        let lu = property_set("Lu").unwrap();
504        assert!(lu.contains('A'));
505        assert!(!lu.contains('a'));
506    }
507
508    #[test]
509    fn block_basic_latin() {
510        let b = property_set("IsBasicLatin").unwrap();
511        assert!(b.contains('A'));
512        assert!(b.contains('\u{7F}'));
513        assert!(!b.contains('\u{80}'));
514    }
515
516    #[test]
517    fn unknown_property_returns_none() {
518        assert!(property_set("NotARealCategory").is_none());
519        assert!(property_set("IsBogusBlock").is_none());
520    }
521
522    #[test]
523    fn word_excludes_punctuation_and_whitespace() {
524        let w = xsd_word();
525        assert!(w.contains('a'));
526        assert!(w.contains('1'));
527        assert!(!w.contains(' '));
528        assert!(!w.contains(','));
529        assert!(!w.contains('!'));
530    }
531}