use {
super::{Scope, java_case},
std::fmt::Write as _,
};
#[derive(Debug, PartialEq, Eq)]
pub(super) struct Consumed<T> {
pub(super) value: T,
pub(super) len: usize,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Escape {
Char(u32),
Property { name: String, negated: bool },
BackRef,
Named,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BackrefFold {
Ascii,
Unicode,
}
impl BackrefFold {
pub(crate) fn chars_match(self, group: char, input: char) -> bool {
group == input
|| match self {
Self::Ascii => group.eq_ignore_ascii_case(&input),
Self::Unicode => {
java_case::key(u32::from(group)) == java_case::key(u32::from(input))
}
}
}
}
pub(super) fn remove_qe_quoting(chars: &[char]) -> Vec<char> {
let mut out = Vec::with_capacity(chars.len());
let mut at = 0;
while let Some(&c) = chars.get(at) {
at += 1;
match (c, chars.get(at)) {
('\\', Some('Q')) => at = quote(chars, at + 1, &mut out),
('\\', Some(&escaped)) => {
out.extend([c, escaped]);
at += 1;
}
_ => out.push(c),
}
}
out
}
fn quote(chars: &[char], mut at: usize, out: &mut Vec<char>) -> usize {
let mut first = true;
while let Some(&c) = chars.get(at) {
at += 1;
match c {
'\\' if chars.get(at) == Some(&'E') => return at + 1,
'\\' => out.extend(['\\', '\\']),
_ if !c.is_ascii() || c.is_ascii_alphabetic() => out.push(c),
_ if c.is_ascii_digit() => {
if first {
out.extend(['\\', 'x', '3']);
}
out.push(c);
}
_ => out.extend(['\\', c]),
}
first = false;
}
at
}
pub(super) fn read_escape(chars: &[char], at: usize, in_class: bool) -> Consumed<Escape> {
let rest = &chars[at + 1..];
let Some(&kind) = rest.first() else {
return Consumed {
value: Escape::Other,
len: 1,
};
};
let after = &rest[1..];
let taking = |value, len| Some(Consumed { value, len });
let read = match kind {
't' => taking(Escape::Char(0x09), 2),
'n' => taking(Escape::Char(0x0A), 2),
'r' => taking(Escape::Char(0x0D), 2),
'f' => taking(Escape::Char(0x0C), 2),
'a' => taking(Escape::Char(0x07), 2),
'e' => taking(Escape::Char(0x1B), 2),
'x' => hex_escape(after).map(|Consumed { value, len }| Consumed {
value: Escape::Char(value),
len: 2 + len,
}),
'u' => after
.get(..4)
.and_then(parse_hex)
.and_then(|code| taking(Escape::Char(code), 6)),
'0' => octal_escape(after).map(|Consumed { value, len }| Consumed {
value: Escape::Char(value),
len: 2 + len,
}),
'c' => after
.first()
.and_then(|&control| taking(Escape::Char(u32::from(control) ^ 64), 3)),
'p' | 'P' => property_name(after).map(|Consumed { value: name, len }| {
let negated = kind == 'P';
Consumed {
value: Escape::Property { name, negated },
len: 2 + len,
}
}),
'k' if !in_class => {
delimited(after, '<', '>').and_then(|len| taking(Escape::BackRef, 2 + len))
}
'N' => delimited(after, '{', '}').and_then(|len| taking(Escape::Named, 2 + len)),
'1'..='9' if !in_class => taking(Escape::BackRef, 2),
other if !other.is_ascii_alphanumeric() => taking(Escape::Char(u32::from(other)), 2),
_ => None,
};
read.unwrap_or(Consumed {
value: Escape::Other,
len: 2,
})
}
fn parse_hex(digits: &[char]) -> Option<u32> {
let code = digits.iter().try_fold(0u32, |code, digit| {
code.checked_mul(16)?.checked_add(digit.to_digit(16)?)
})?;
(code <= u32::from(char::MAX)).then_some(code)
}
fn hex_escape(after: &[char]) -> Option<Consumed<u32>> {
if after.first() == Some(&'{') {
let close = after.iter().position(|&c| c == '}')?;
let digits = &after[1..close];
if digits.is_empty() {
return None;
}
Some(Consumed {
value: parse_hex(digits)?,
len: close + 1,
})
} else {
Some(Consumed {
value: parse_hex(after.get(..2)?)?,
len: 2,
})
}
}
fn octal_escape(after: &[char]) -> Option<Consumed<u32>> {
let digit = |at: usize| after.get(at).and_then(|c| c.to_digit(8));
let first = digit(0)?;
let taking = |value, len| Consumed { value, len };
Some(match (digit(1), digit(2)) {
(Some(second), Some(third)) if first <= 3 => taking(first * 64 + second * 8 + third, 3),
(Some(second), _) => taking(first * 8 + second, 2),
_ => taking(first, 1),
})
}
fn property_name(after: &[char]) -> Option<Consumed<String>> {
if after.first() == Some(&'{') {
let close = after.iter().position(|&c| c == '}')?;
(close > 1).then(|| Consumed {
value: after[1..close].iter().collect(),
len: close + 1,
})
} else {
after.first().map(|&c| Consumed {
value: c.to_string(),
len: 1,
})
}
}
fn delimited(after: &[char], open: char, close: char) -> Option<usize> {
let end = after.iter().position(|&c| c == close)?;
(after.first() == Some(&open) && end > 1).then_some(end + 1)
}
pub(super) fn ignorable_len(rest: &[char], unix_lines: bool) -> usize {
let is_terminator = |c: char| {
c == '\n' || (!unix_lines && matches!(c, '\r' | '\u{85}' | '\u{2028}' | '\u{2029}'))
};
let mut len = 0;
while let Some(&c) = rest.get(len) {
match c {
' ' | '\t' | '\n' | '\u{0B}' | '\u{0C}' | '\r' => len += 1,
'#' => {
len += 1;
while let Some(&comment) = rest.get(len) {
len += 1;
if is_terminator(comment) {
break;
}
}
}
_ => break,
}
}
len
}
pub(super) fn read_member(chars: &[char], at: usize) -> Option<Consumed<u32>> {
let &c = chars.get(at)?;
if c != '\\' {
return Some(Consumed {
value: u32::from(c),
len: 1,
});
}
match read_escape(chars, at, true) {
Consumed {
value: Escape::Char(code),
len,
} => Some(Consumed { value: code, len }),
_ => None,
}
}
fn ascii_letter(code: u32) -> Option<char> {
let byte = u8::try_from(code).ok().filter(u8::is_ascii_alphabetic)?;
Some(char::from(byte))
}
pub(super) fn push_member(out: &mut String, source: &[char], code: u32, in_class: bool) {
match (ascii_letter(code), source) {
(Some(letter), _) => out.push(letter),
(None, [']']) => out.push_str("\\]"),
(None, ['[']) => out.push_str("\\["),
(None, ['-']) if in_class => out.push_str("\\-"),
(None, ['^']) if in_class && out.ends_with('[') => out.push_str("\\^"),
(None, ['\\', edge @ ('<' | '>')]) => out.push(*edge),
_ => out.extend(source),
}
}
pub(super) fn push_folded(out: &mut String, source: &[char], code: u32, in_class: bool) {
let Some(letter) = ascii_letter(code) else {
push_member(out, source, code, in_class);
return;
};
let (lower, upper) = (letter.to_ascii_lowercase(), letter.to_ascii_uppercase());
if in_class {
out.push(lower);
out.push(upper);
} else {
let _ = write!(out, "[{lower}{upper}]");
}
}
pub(super) fn push_other_case_ranges(out: &mut String, lo: u32, hi: u32) {
for (first, last) in [('A', 'Z'), ('a', 'z')] {
let (start, end) = (lo.max(u32::from(first)), hi.min(u32::from(last)));
if start <= end {
let _ = write!(out, "\\x{{{:X}}}-\\x{{{:X}}}", start ^ 0x20, end ^ 0x20);
}
}
}
fn property_members(name: &str, scope: Scope) -> Option<&'static str> {
let bare = !name.contains('=') && !name.starts_with("In") && !name.starts_with("Is");
if bare {
let posix = if scope.unicode_classes {
unicode_posix_members(name, scope.case.ignore)
} else {
ascii_posix_members(name, scope.case.ignore)
};
if posix.is_some() {
return posix;
}
}
if !scope.case.ignore {
return None;
}
if is_cased_category(name) {
Some("\\p{Lu}\\p{Ll}\\p{Lt}")
} else if is_cased_property(name) {
Some("\\p{Lowercase}\\p{Uppercase}\\p{Lt}")
} else {
None
}
}
fn is_cased_category(name: &str) -> bool {
let value = match name.split_once('=') {
Some((key, value)) if matches!(key.to_lowercase().as_str(), "gc" | "general_category") => {
value
}
Some(_) => return false,
None => name.strip_prefix("Is").unwrap_or(name),
};
matches!(value, "Lu" | "Ll" | "Lt")
}
fn is_cased_property(name: &str) -> bool {
name.strip_prefix("Is").is_some_and(|short| {
matches!(
short.to_uppercase().as_str(),
"LOWERCASE" | "UPPERCASE" | "TITLECASE" | "LOWER" | "UPPER"
)
})
}
fn ascii_posix_members(name: &str, ignore_case: bool) -> Option<&'static str> {
Some(match (name, ignore_case) {
("Lower" | "Upper", true) | ("Alpha", _) => "a-zA-Z",
("Lower", false) => "a-z",
("Upper", false) => "A-Z",
("Digit", _) => "0-9",
("Alnum", _) => "0-9a-zA-Z",
("Punct", _) => "!-/:-@\\[-`{-~",
("Graph", _) => "!-~",
("Print", _) => "\\x20-~",
("Blank", _) => "\\x20\\t",
("Cntrl", _) => "\\x00-\\x1F\\x7F",
("Space", _) => "\\x20\\t\\n\\x0B\\x0C\\r",
("XDigit", _) => "0-9a-fA-F",
_ => return None,
})
}
fn unicode_posix_members(name: &str, ignore_case: bool) -> Option<&'static str> {
Some(match (name.to_uppercase().as_str(), ignore_case) {
("LOWER" | "UPPER", true) => "\\p{Lowercase}\\p{Uppercase}\\p{Lt}",
("LOWER", false) => "\\p{Lowercase}",
("UPPER", false) => "\\p{Uppercase}",
("ALPHA", _) => "\\p{Alphabetic}",
("DIGIT", _) => "\\p{Nd}",
("ALNUM", _) => "\\p{Alphabetic}\\p{Nd}",
("PUNCT", _) => "\\p{P}",
("GRAPH", _) => "[^\\p{Zs}\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cs}\\p{Cn}]",
("PRINT", _) => "[^\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cs}\\p{Cn}]",
("BLANK", _) => "\\p{Zs}\\t",
("CNTRL", _) => "\\p{Cc}",
("SPACE", _) => "\\p{White_Space}",
("XDIGIT", _) => "\\p{Nd}\\p{Hex_Digit}",
_ => return None,
})
}
pub(super) fn property_class(
name: &str,
negated: bool,
in_class: bool,
scope: Scope,
) -> Option<String> {
let members = property_members(name, scope)?;
Some(match (negated, in_class) {
(false, true) => members.to_owned(),
(false, false) => format!("[{members}]"),
(true, _) => format!("[^{members}]"),
})
}
#[cfg(test)]
mod tests {
use {super::*, assert2::check};
fn escape(text: &str, in_class: bool) -> Consumed<Escape> {
let chars: Vec<char> = text.chars().collect();
read_escape(&chars, 0, in_class)
}
#[test]
fn read_escape_reads_what_java_reads() {
let property = |name: &str, negated| Escape::Property {
name: name.to_owned(),
negated,
};
let cases = [
("\\x41", false, Escape::Char(0x41), 4),
("\\x{41}", false, Escape::Char(0x41), 6),
("\\x{1F600}", false, Escape::Char(0x1F600), 9),
("\\x{110000}", false, Escape::Other, 2),
("\\x{}", false, Escape::Other, 2),
("\\x4", false, Escape::Other, 2),
("\\u0041b", false, Escape::Char(0x41), 6),
("\\u004", false, Escape::Other, 2),
("\\0101", false, Escape::Char(0x41), 5),
("\\0777", false, Escape::Char(0o77), 4),
("\\07", false, Escape::Char(7), 3),
("\\00", false, Escape::Char(0), 3),
("\\0", false, Escape::Other, 2),
("\\cA", false, Escape::Char(1), 3),
("\\c!", false, Escape::Char(0x61), 3),
("\\t", false, Escape::Char(9), 2),
("\\.", false, Escape::Char(0x2E), 2),
("\\-", true, Escape::Char(0x2D), 2),
("\\pL", false, property("L", false), 3),
("\\P{Lu}x", false, property("Lu", true), 6),
("\\p{}", false, Escape::Other, 2),
("\\p{Lu", false, Escape::Other, 2),
("\\1", false, Escape::BackRef, 2),
("\\1", true, Escape::Other, 2),
("\\k<name>", false, Escape::BackRef, 8),
("\\k<>", false, Escape::Other, 2),
("\\N{LATIN SMALL LETTER A}", false, Escape::Named, 24),
("\\w", false, Escape::Other, 2),
("\\b", false, Escape::Other, 2),
("\\", false, Escape::Other, 1),
];
for (text, in_class, expected, len) in cases {
check!(
escape(text, in_class)
== Consumed {
value: expected,
len
},
"{text:?}"
);
}
}
#[test]
fn a_class_range_gets_the_other_case_of_the_letters_it_covers() {
let cases = [
('a', 'f', "\\x{41}-\\x{46}"),
('B', 'D', "\\x{62}-\\x{64}"),
('X', 'c', "\\x{78}-\\x{7A}\\x{41}-\\x{43}"),
('+', 'z', "\\x{61}-\\x{7A}\\x{41}-\\x{5A}"),
('0', '9', ""),
('[', '`', ""),
('\u{e9}', '\u{ff}', ""),
];
for (lo, hi, expected) in cases {
let mut out = String::new();
push_other_case_ranges(&mut out, u32::from(lo), u32::from(hi));
check!(out == expected, "{lo:?}-{hi:?}");
}
}
#[test]
fn remove_qe_quoting_rewrites_as_java_does() {
let cases = [
("abc", "abc"),
("\\Qa.b\\E", "a\\.b"),
("\\Qa.b", "a\\.b"),
("[\\Qa\\E-c]", "[a-c]"),
("[a\\Q-\\Ec]", "[a\\-c]"),
("[\\Q]\\E-c]", "[\\]-c]"),
("\\Q\\E", ""),
("a\\Q\\E+", "a+"),
("\\Q1\\E2", "\\x312"),
("\\Qab1\\E", "ab1"),
("\\Q12\\E", "\\x312"),
("\\Qa\\\\E", "a\\\\"),
("\\Q\\\\E", "\\\\"),
("\\\\Qa\\E", "\\\\Qa\\E"),
("\\\\Q.\\E", "\\\\Q.\\E"),
("\\Q.\\E\\Q*\\E", "\\.\\*"),
("x\\Qa\\Eb\\Qc\\E", "xabc"),
("\\Q\\E\\Q\\E", ""),
("\\Qa\\Eb\\E", "ab\\E"),
("\\Q<>_ #\\E", "\\<\\>\\_\\ \\#"),
("\\Q\\u00e9k\\E", "\\\\u00e9k"),
("\\Q\u{e9}.\\E", "\u{e9}\\."),
("\\Q.", "\\."),
("a\\", "a\\"),
];
for (pattern, expected) in cases {
let chars: Vec<char> = pattern.chars().collect();
let rewritten: String = remove_qe_quoting(&chars).into_iter().collect();
check!(rewritten == expected, "{pattern:?}");
}
}
#[test]
fn push_member_writes_what_fancy_regex_reads() {
let cases = [
("", "]", 0x5D, false, "\\]"),
("", "\\]", 0x5D, false, "\\]"),
("", "\\<", 0x3C, false, "<"),
("", "\\>", 0x3E, false, ">"),
("", "<", 0x3C, false, "<"),
("", "\\.", 0x2E, false, "\\."),
("", "\\x41", 0x41, false, "A"),
("", "\\0101", 0x41, false, "A"),
("", "\\t", 0x09, false, "\\t"),
("", "\u{e9}", 0xE9, false, "\u{e9}"),
("", "[", 0x5B, true, "\\["),
("", "-", 0x2D, true, "\\-"),
("[a", "-", 0x2D, true, "[a\\-"),
("[0-9A-Za-z_", "-", 0x2D, true, "[0-9A-Za-z_\\-"),
("", "-", 0x2D, false, "-"),
("", "\\-", 0x2D, true, "\\-"),
("[", "^", 0x5E, true, "[\\^"),
("[a[", "^", 0x5E, true, "[a[\\^"),
("[a", "^", 0x5E, true, "[a^"),
("[^", "^", 0x5E, true, "[^^"),
("\\[", "^", 0x5E, false, "\\[^"),
("", "^", 0x5E, false, "^"),
];
for (before, source, code, in_class, expected) in cases {
let chars: Vec<char> = source.chars().collect();
let mut out = before.to_owned();
push_member(&mut out, &chars, code, in_class);
check!(out == expected, "{before:?} then {source:?}");
}
}
}