1use 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 static REGEX_UCD_VERSION: Cell<UnicodeVersion> =
30 const { Cell::new(UnicodeVersion::Latest) };
31}
32
33pub 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
44pub(super) fn current_ucd_version() -> UnicodeVersion {
50 REGEX_UCD_VERSION.with(|c| c.get())
51}
52
53pub 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
66fn 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
88fn 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 "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
125fn 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 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 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
195fn 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
219pub 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), (0x0D, 0x0D), (0x20, 0x20), ]))
231}
232
233pub 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
253fn 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
271pub 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
281fn 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
301fn block_ranges(name: &str) -> Option<Vec<(u32, u32)>> {
305 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
327fn block_table() -> &'static [(&'static str, u32, u32)] {
333 &[
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 ("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 ("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 ("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 ("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 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}