Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ mod replace_fn;
#[cfg(feature = "regex-engine")]
mod unicode17;
#[cfg(feature = "regex-engine")]
mod unicode17_data;
#[cfg(feature = "regex-engine")]
use class_range_validate::has_out_of_order_double_dash_class_range;
#[cfg(feature = "regex-engine")]
pub use compile::js_regexp_compile_value;
Expand Down
10 changes: 5 additions & 5 deletions crates/perry-runtime/src/regex/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1432,13 +1432,13 @@ pub(super) fn js_regex_to_rust(pattern: &str) -> String {
// mis-compiling (Node also rejects `\P{RGI_Emoji}`).
// All other properties pass through to the crate unchanged.
match parse_unicode_property(&chars, i) {
// Unicode-17.0 expansions the crate's UCD-16 tables get
// wrong (`unicode17::expand`); precedes never-match below.
Some((value, negated, end))
if super::unicode17::script_replacement(&value, negated, in_class)
.is_some() =>
if super::unicode17::expand(&value, negated, in_class).is_some() =>
{
result.push_str(
&super::unicode17::script_replacement(&value, negated, in_class)
.unwrap(),
&super::unicode17::expand(&value, negated, in_class).unwrap(),
);
i = end;
}
Expand Down Expand Up @@ -1980,7 +1980,7 @@ mod tests {
(r"[\d-z]", r"[\d\-z]"),
(r"[a\w-]", r"[a\w\-]"),
(r"[a-\d]", r"[a\-\d]"),
(r"[\p{L}-x]", r"[\p{L}\-x]"),
(r"[\p{Greek}-x]", r"[\p{Greek}\-x]"),
] {
assert_eq!(js_regex_to_rust(src), expect, "src={src}");
assert!(
Expand Down
46 changes: 46 additions & 0 deletions crates/perry-runtime/src/regex/unicode17.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,49 @@ pub(super) fn script_replacement(value: &str, negated: bool, in_class: bool) ->
(false, false) => format!("[{body}]"),
})
}

/// Expand a `\p{...}`/`\P{...}` for a property carrying a Unicode-17.0 delta or a
/// full replacement (see `unicode17_data`), or return `None` (pass-through to the
/// `regex` crate's bundled UCD-16 view). `value` is already normalized
/// (lowercased, `_`/spaces removed, any `gc=`/`general_category=` prefix
/// stripped by the caller) — the same key space `unicode17_data::u17_expansion`
/// is keyed on and the same spelling the crate's own property parser accepts.
///
/// * `Delta(d)` — the crate is correct at UCD 16 and UCD 17 only *added* the
/// ranges `d`; union them into the crate class: `[\p{value}d]` (positive),
/// `[^\p{value}d]` (negated), `\p{value}d` (positive class member). A negated
/// *in-class* member can't express "complement of the union" as a class-union
/// term, so it falls back to the crate's `\P{value}` (correct except for the
/// handful of freshly-added UCD-17 points, and unexercised by Test262 — every
/// generated case uses the anchored `/^\p{…}+$/` / `/^\P{…}+$/` forms).
/// * `Full(f)` — the crate can't represent the property (`Script=Unknown`,
/// `Changes_When_NFKC_Casefolded`) or UCD 17 *removed* points (so a union would
/// over-match); `f` replaces it wholesale, exactly like `script_replacement`.
pub(super) fn u17_replacement(value: &str, negated: bool, in_class: bool) -> Option<String> {
use super::unicode17_data::{u17_expansion, U17Expansion};
Some(match u17_expansion(value)? {
U17Expansion::Delta(d) => match (in_class, negated) {
(false, false) => format!("[\\p{{{value}}}{d}]"),
(false, true) => format!("[^\\p{{{value}}}{d}]"),
(true, false) => format!("\\p{{{value}}}{d}"),
(true, true) => format!("\\P{{{value}}}"),
},
U17Expansion::Full(f) => match (in_class, negated) {
(true, true) => String::new(),
(true, false) => f.to_string(),
(false, true) => format!("[^{f}]"),
(false, false) => format!("[{f}]"),
},
})
}

/// Unicode-17.0 expansion for a `\p{...}`/`\P{...}` property, or `None` to pass
/// the property through to the `regex` crate's bundled UCD-16 view unchanged.
/// Tries the four brand-new U17 scripts (`script_replacement`, #6068) first,
/// then the U17 deltas/overrides for pre-existing properties (`u17_replacement`).
/// `value` is normalized as documented on those two functions. Grammar dispatch
/// calls this single entry point so the property-escape arm stays compact.
pub(super) fn expand(value: &str, negated: bool, in_class: bool) -> Option<String> {
script_replacement(value, negated, in_class)
.or_else(|| u17_replacement(value, negated, in_class))
}
Loading
Loading