use std::{borrow::Cow, fmt, ops::Range};
use crate::concat_string;
#[derive(Clone)]
pub struct HybridRegex(HybridRegexInner);
#[derive(Clone)]
enum HybridRegexInner {
Optimized(regex::Regex),
Ecma(regress::Regex),
}
impl fmt::Debug for HybridRegex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
HybridRegexInner::Optimized(regex) => {
f.debug_tuple("Optimized").field(®ex.as_str()).finish()
}
HybridRegexInner::Ecma(_) => f.write_str("Ecma(..)"),
}
}
}
#[derive(Debug)]
pub struct HybridMatches<'regex, 'text>(regress::Matches<'regex, 'text>);
#[derive(Debug, Clone)]
pub struct HybridMatch(regress::Match);
impl Iterator for HybridMatches<'_, '_> {
type Item = HybridMatch;
#[inline(never)]
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(HybridMatch)
}
}
impl HybridMatch {
pub fn range(&self) -> &Range<usize> {
&self.0.range
}
pub fn group_count(&self) -> usize {
1 + self.0.captures.len()
}
pub fn group(&self, index: usize) -> Option<Range<usize>> {
self.0.group(index)
}
}
impl From<&str> for HybridRegex {
fn from(pattern: &str) -> Self {
HybridRegex::new(pattern).unwrap_or_else(|err| {
panic!("failed to create HybridRegex from {pattern}, error details: {err}")
})
}
}
impl HybridRegex {
#[inline(never)]
pub fn new(pattern: &str) -> anyhow::Result<Self> {
let regex_pattern = Self::get_regex_pattern(pattern, "");
match regex::Regex::new(®ex_pattern).map(Self::from_optimized) {
Ok(reg) => Ok(reg),
Err(_) => Self::new_ecma(pattern),
}
}
pub fn from_optimized(regex: regex::Regex) -> Self {
Self(HybridRegexInner::Optimized(regex))
}
#[inline(never)]
pub fn new_ecma(pattern: &str) -> anyhow::Result<Self> {
regress::Regex::new(pattern).map(HybridRegexInner::Ecma).map(Self).map_err(anyhow::Error::from)
}
#[inline(never)]
pub fn with_flags(pattern: &str, flags: &str) -> anyhow::Result<Self> {
let regex_pattern = Self::get_regex_pattern(pattern, flags);
match regex::Regex::new(®ex_pattern).map(Self::from_optimized) {
Ok(reg) => Ok(reg),
Err(_) => regress::Regex::with_flags(pattern, flags)
.map(HybridRegexInner::Ecma)
.map(Self)
.map_err(anyhow::Error::from),
}
}
pub fn as_optimized(&self) -> Option<®ex::Regex> {
match &self.0 {
HybridRegexInner::Optimized(regex) => Some(regex),
HybridRegexInner::Ecma(_) => None,
}
}
#[inline(never)]
pub fn find_from<'regex, 'text>(
&'regex self,
text: &'text str,
start: usize,
) -> Option<HybridMatches<'regex, 'text>> {
match &self.0 {
HybridRegexInner::Optimized(_) => None,
HybridRegexInner::Ecma(regex) => Some(HybridMatches(regex.find_from(text, start))),
}
}
pub fn regex_pattern(&self) -> Option<&str> {
match &self.0 {
HybridRegexInner::Optimized(regex) => Some(regex.as_str()),
HybridRegexInner::Ecma(_) => None,
}
}
fn get_regex_pattern(pattern: &str, flags: &str) -> String {
concat_string!("(?R", flags, ")", pattern)
}
pub fn matches(&self, text: &str) -> bool {
match &self.0 {
HybridRegexInner::Optimized(regex) => regex.is_match(text),
HybridRegexInner::Ecma(_) => self.matches_ecma(text),
}
}
pub fn replace<'a>(&self, haystack: &'a str, replacement: &str) -> Cow<'a, str> {
match &self.0 {
HybridRegexInner::Optimized(regex) => regex.replace(haystack, replacement),
HybridRegexInner::Ecma(_) => {
self.replace_ecma(haystack, replacement)
}
}
}
pub fn replace_all<'a>(&self, haystack: &'a str, replacement: &str) -> Cow<'a, str> {
match &self.0 {
HybridRegexInner::Optimized(regex) => regex.replace_all(haystack, replacement),
HybridRegexInner::Ecma(_) => self.replace_all_ecma(haystack, replacement),
}
}
#[inline(never)]
fn matches_ecma(&self, text: &str) -> bool {
self.find_from(text, 0).is_some_and(|mut matches| matches.next().is_some())
}
#[inline(never)]
fn replace_ecma<'a>(&self, haystack: &'a str, replacement: &str) -> Cow<'a, str> {
let HybridRegexInner::Ecma(regex) = &self.0 else { unreachable!() };
let Some(mut matches) = self.find_from(haystack, 0) else { unreachable!() };
if matches.next().is_none() {
return Cow::Borrowed(haystack);
}
Cow::Owned(regex.replace(haystack, replacement))
}
#[inline(never)]
fn replace_all_ecma<'a>(&self, haystack: &'a str, replacement: &str) -> Cow<'a, str> {
let HybridRegexInner::Ecma(regex) = &self.0 else { unreachable!() };
let Some(mut matches) = self.find_from(haystack, 0) else { unreachable!() };
if matches.next().is_none() {
return Cow::Borrowed(haystack);
}
Cow::Owned(regex.replace_all(haystack, replacement))
}
#[cfg(test)]
fn uses_ecma(&self) -> bool {
matches!(self.0, HybridRegexInner::Ecma(_))
}
}
#[cfg(test)]
mod test {
use crate::js_regex::HybridRegex;
#[test]
fn with_flags() {
let reg = HybridRegex::with_flags("a", "i").unwrap();
assert!(reg.matches("A"));
let reg = HybridRegex::new("a").unwrap();
assert!(!reg.matches("A"));
}
#[test]
fn regress_replace_all() {
let reg = HybridRegex::new(r"\d+(?!\d)").unwrap();
assert!(reg.uses_ecma());
assert_eq!(reg.replace_all("111aa111", "1"), "1aa1");
}
#[test]
fn ecma_replace_expands_replacement_tokens() {
let reg = HybridRegex::new(r"^@app(?!/(?:excluded))(/.*)?$").unwrap();
assert!(reg.uses_ecma());
assert_eq!(reg.replace("@app/utils", "/abs/src/app$1"), "/abs/src/app/utils");
let reg = HybridRegex::new(r"(foo)(?!bar)").unwrap();
assert!(reg.uses_ecma());
assert_eq!(reg.replace("foo baz", "$0:$$:$1:$2"), "foo:$:foo: baz");
}
#[test]
fn ecma_replace_all_expands_replacement_tokens() {
let reg = HybridRegex::new(r"(\d+)(?=px)").unwrap();
assert!(reg.uses_ecma());
assert_eq!(reg.replace_all("10px 20px", "$1rem"), "10rempx 20rempx");
}
#[test]
fn js_regex_compat_dot() {
let dot_reg = HybridRegex::new(".").unwrap();
assert!(!dot_reg.matches("\n"));
assert!(!dot_reg.matches("\r\n"));
let dots_reg = HybridRegex::with_flags(".", "s").unwrap();
assert!(dots_reg.matches("\n"));
assert!(dots_reg.matches("\r\n"));
assert!(dots_reg.matches("\u{2028}"));
assert!(dots_reg.matches("\u{2029}"));
}
#[test]
fn js_regex_compat_multiline() {
let reg = HybridRegex::with_flags("a$", "m").unwrap();
assert!(reg.matches("a\n"));
assert!(reg.matches("a\r\n"));
}
}