use regex::Regex;
use std::collections::BTreeSet as Set;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::process;
pub struct Properties {
unicode_version: (u8, u8, u8),
xid_start: Set<u32>,
xid_continue: Set<u32>,
}
impl Properties {
pub const fn unicode_version(&self) -> (u8, u8, u8) {
self.unicode_version
}
pub fn is_xid_start(&self, ch: char) -> bool {
self.xid_start.contains(&(ch as u32))
}
pub fn is_xid_continue(&self, ch: char) -> bool {
self.xid_continue.contains(&(ch as u32))
}
}
pub fn parse_xid_properties(ucd_dir: &Path) -> Properties {
let filename = "DerivedCoreProperties.txt";
let path = ucd_dir.join(filename);
let contents = fs::read_to_string(path).unwrap_or_else(|err| {
let suggestion =
"Download from https://www.unicode.org/Public/latest/ucd/UCD.zip and unzip.";
let _ = writeln!(io::stderr(), "{}: {err}\n{suggestion}", ucd_dir.display());
process::exit(1);
});
let mut properties = Properties {
unicode_version: parse_unicode_version(filename, &contents),
xid_start: Set::new(),
xid_continue: Set::new(),
};
for (i, line) in contents.lines().enumerate() {
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
let (lo, hi, name) = parse_line(line).unwrap_or_else(|| {
let _ = writeln!(io::stderr(), "{filename} line {i} is unexpected:\n{line}");
process::exit(1);
});
let set = match name {
"XID_Start" => &mut properties.xid_start,
"XID_Continue" => &mut properties.xid_continue,
_ => continue,
};
set.extend(lo..=hi);
}
properties
}
fn parse_line(line: &str) -> Option<(u32, u32, &str)> {
let (mut codepoint, rest) = line.split_once(';')?;
let (lo, hi);
codepoint = codepoint.trim();
if let Some((a, b)) = codepoint.split_once("..") {
lo = parse_codepoint(a)?;
hi = parse_codepoint(b)?;
} else {
lo = parse_codepoint(codepoint)?;
hi = lo;
}
let name = rest.trim().split('#').next()?.trim_end();
Some((lo, hi, name))
}
fn parse_codepoint(s: &str) -> Option<u32> {
u32::from_str_radix(s, 16).ok()
}
fn parse_unicode_version(filename: &str, contents: &str) -> (u8, u8, u8) {
let (name, extension) = filename
.rsplit_once('.')
.expect("Failed to split file name into name and extension");
let re = Regex::new(&format!(r"^# {name}-(\d+)\.(\d+)\.(\d+)\.{extension}\n")).unwrap();
let caps = re
.captures(contents)
.expect("Failed to find unicode version in unicode data");
let (_, [major, minor, patch]) = caps.extract();
[major, minor, patch]
.map(|s| s.parse().expect("Failed to parse unicode version"))
.into()
}