use fancy_regex::Captures;
use super::ex::ExError;
#[derive(Clone, Copy, PartialEq, Eq)]
enum MagicLevel {
Very,
Magic,
No,
VeryNo,
}
pub fn translate_pattern(input: &str) -> Result<String, ExError> {
let chars: Vec<char> = input.chars().collect();
let mut out = String::new();
let mut magic = MagicLevel::Magic;
let mut branch_start = true;
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '\\' {
i += 1;
let Some(&d) = chars.get(i) else {
out.push_str("\\\\");
break;
};
i += 1;
translate_escape(d, &chars, &mut i, &mut magic, &mut out, &mut branch_start)?;
continue;
}
match magic {
MagicLevel::Very => emit_very_magic(&chars, &mut i, &mut out, &mut branch_start)?,
MagicLevel::Magic => emit_magic(&chars, &mut i, &mut out, &mut branch_start)?,
MagicLevel::No => emit_nomagic(&chars, &mut i, &mut out, &mut branch_start),
MagicLevel::VeryNo => {
escape_literal(chars[i], &mut out);
i += 1;
branch_start = false;
}
}
}
Ok(out)
}
fn translate_escape(
d: char,
chars: &[char],
i: &mut usize,
magic: &mut MagicLevel,
out: &mut String,
branch_start: &mut bool,
) -> Result<(), ExError> {
match d {
'v' => {
*magic = MagicLevel::Very;
return Ok(());
}
'm' => {
*magic = MagicLevel::Magic;
return Ok(());
}
'M' => {
*magic = MagicLevel::No;
return Ok(());
}
'V' => {
*magic = MagicLevel::VeryNo;
return Ok(());
}
_ => {}
}
if matches!(d, '(' | ')' | '+' | '?' | '=' | '|' | '{' | '<' | '>') {
if *magic == MagicLevel::Very {
escape_literal(d, out);
*branch_start = false;
} else {
emit_group_atom(d, chars, i, out, branch_start)?;
}
return Ok(());
}
match d {
'%' => {
if chars.get(*i) == Some(&'(') {
*i += 1;
out.push_str("(?:");
*branch_start = true;
} else {
return Err(ExError::UnsupportedPattern(format!(
"\\%{}",
chars.get(*i).copied().unwrap_or(' ')
)));
}
}
'z' => {
return Err(ExError::UnsupportedPattern(format!(
"\\z{}",
chars.get(*i).copied().unwrap_or(' ')
)));
}
'@' => return Err(ExError::UnsupportedPattern("\\@".to_owned())),
'1'..='9' => {
out.push('\\');
out.push(d);
*branch_start = false;
}
'd' | 'D' | 'w' | 'W' | 's' | 'S' => {
out.push('\\');
out.push(d);
*branch_start = false;
}
'a' => push_class(out, "[A-Za-z]", branch_start),
'A' => push_class(out, "[^A-Za-z]", branch_start),
'l' => push_class(out, "[a-z]", branch_start),
'L' => push_class(out, "[^a-z]", branch_start),
'u' => push_class(out, "[A-Z]", branch_start),
'U' => push_class(out, "[^A-Z]", branch_start),
'x' => push_class(out, "[0-9A-Fa-f]", branch_start),
'X' => push_class(out, "[^0-9A-Fa-f]", branch_start),
'o' => push_class(out, "[0-7]", branch_start),
'O' => push_class(out, "[^0-7]", branch_start),
'h' => push_class(out, "[A-Za-z_]", branch_start),
'H' => push_class(out, "[^A-Za-z_]", branch_start),
'k' | 'i' => push_class(out, "\\w", branch_start),
't' => push_class(out, "\\t", branch_start),
'n' => push_class(out, "\\n", branch_start),
'r' => push_class(out, "\\r", branch_start),
_ => {
escape_literal(d, out);
*branch_start = false;
}
}
Ok(())
}
fn emit_group_atom(
d: char,
chars: &[char],
i: &mut usize,
out: &mut String,
branch_start: &mut bool,
) -> Result<(), ExError> {
match d {
'(' => {
out.push('(');
*branch_start = true;
}
')' => {
out.push(')');
*branch_start = false;
}
'|' => {
out.push('|');
*branch_start = true;
}
'+' => {
out.push('+');
*branch_start = false;
}
'?' | '=' => {
out.push('?');
*branch_start = false;
}
'{' => {
out.push_str(&take_brace(chars, i));
*branch_start = false;
}
'<' => {
out.push_str("\\b(?=\\w)");
*branch_start = false;
}
'>' => {
out.push_str("(?<=\\w)\\b");
*branch_start = false;
}
_ => unreachable!("emit_group_atom only handles ( ) + ? = | {{ < >"),
}
Ok(())
}
fn emit_very_magic(
chars: &[char],
i: &mut usize,
out: &mut String,
branch_start: &mut bool,
) -> Result<(), ExError> {
let c = chars[*i];
match c {
'(' | ')' | '|' | '{' | '<' | '>' => {
*i += 1;
return emit_group_atom(c, chars, i, out, branch_start);
}
'+' | '?' | '=' | '*' if *branch_start => {
escape_literal(c, out);
*branch_start = false;
}
'+' => {
out.push('+');
*branch_start = false;
}
'?' | '=' => {
out.push('?');
*branch_start = false;
}
'*' => {
out.push('*');
*branch_start = false;
}
'.' => {
out.push('.');
*branch_start = false;
}
'^' => out.push('^'), '$' => {
push_dollar(chars, *i, true, out);
*branch_start = false;
}
'[' => copy_class(chars, i, out, branch_start)?,
'%' => {
if chars.get(*i + 1) == Some(&'(') {
*i += 2;
out.push_str("(?:");
*branch_start = true;
return Ok(());
}
return Err(ExError::UnsupportedPattern(format!(
"%{}",
chars.get(*i + 1).copied().unwrap_or(' ')
)));
}
'@' => return Err(ExError::UnsupportedPattern("@".to_owned())),
'&' => return Err(ExError::UnsupportedPattern("&".to_owned())),
_ => {
escape_literal(c, out);
*branch_start = false;
}
}
if !matches!(c, '[' | '%') {
*i += 1;
}
Ok(())
}
fn emit_magic(
chars: &[char],
i: &mut usize,
out: &mut String,
branch_start: &mut bool,
) -> Result<(), ExError> {
let c = chars[*i];
match c {
'.' => {
out.push('.');
*branch_start = false;
}
'*' => {
if *branch_start {
out.push_str("\\*");
} else {
out.push('*');
}
*branch_start = false;
}
'^' => {
if *branch_start {
out.push('^'); } else {
out.push_str("\\^");
*branch_start = false;
}
}
'$' => {
push_dollar(chars, *i, false, out);
*branch_start = false;
}
'[' => {
copy_class(chars, i, out, branch_start)?;
return Ok(()); }
'~' => return Err(ExError::UnsupportedPattern("~".to_owned())),
_ => {
escape_literal(c, out);
*branch_start = false;
}
}
*i += 1;
Ok(())
}
fn emit_nomagic(chars: &[char], i: &mut usize, out: &mut String, branch_start: &mut bool) {
let c = chars[*i];
match c {
'^' if *branch_start => out.push('^'), '$' => {
push_dollar(chars, *i, false, out);
*branch_start = false;
}
_ => {
escape_literal(c, out);
*branch_start = false;
}
}
*i += 1;
}
fn push_dollar(chars: &[char], i: usize, very: bool, out: &mut String) {
let rest = &chars[i + 1..];
let anchor = match rest.first() {
None => true,
Some(&')') | Some(&'|') if very => true,
Some(&'\\') if !very => matches!(rest.get(1), Some(')') | Some('|')),
_ => false,
};
if anchor {
out.push('$');
} else {
out.push_str("\\$");
}
}
fn push_class(out: &mut String, s: &str, branch_start: &mut bool) {
out.push_str(s);
*branch_start = false;
}
fn copy_class(
chars: &[char],
i: &mut usize,
out: &mut String,
branch_start: &mut bool,
) -> Result<(), ExError> {
out.push('[');
*i += 1; if chars.get(*i) == Some(&'^') {
out.push('^');
*i += 1;
}
if chars.get(*i) == Some(&']') {
out.push_str("\\]");
*i += 1;
}
while let Some(&c) = chars.get(*i) {
if c == ']' {
out.push(']');
*i += 1;
*branch_start = false;
return Ok(());
}
if c == '\\' {
if let Some(&n) = chars.get(*i + 1) {
out.push('\\');
out.push(n);
*i += 2;
continue;
}
}
out.push(c);
*i += 1;
}
Err(ExError::UnsupportedPattern("unterminated [ ]".to_owned()))
}
fn take_brace(chars: &[char], i: &mut usize) -> String {
let mut inner = String::new();
while let Some(&c) = chars.get(*i) {
*i += 1;
if c == '}' {
break;
}
inner.push(c);
}
let (lazy, body) = match inner.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, inner.as_str()),
};
if body.is_empty() {
return if lazy {
"*?".to_owned()
} else {
"*".to_owned()
};
}
let body = if let Some(stripped) = body.strip_prefix(',') {
format!("0,{stripped}")
} else {
body.to_owned()
};
if lazy {
format!("{{{body}}}?")
} else {
format!("{{{body}}}")
}
}
fn escape_literal(c: char, out: &mut String) {
if matches!(
c,
'.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' | '\\'
) {
out.push('\\');
}
out.push(c);
}
pub fn expand_replacement(template: &str, caps: &Captures<'_, str>) -> String {
let chars: Vec<char> = template.chars().collect();
let mut out = String::new();
let mut one: Option<bool> = None;
let mut region: Option<bool> = None;
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '&' {
push_group(caps, 0, &mut out, &mut one, region);
i += 1;
continue;
}
if c != '\\' {
push_cased(c, &mut out, &mut one, region);
i += 1;
continue;
}
i += 1;
let Some(&d) = chars.get(i) else {
push_cased('\\', &mut out, &mut one, region);
break;
};
i += 1;
match d {
'0'..='9' => push_group(caps, d as usize - '0' as usize, &mut out, &mut one, region),
'&' => push_cased('&', &mut out, &mut one, region),
'\\' => push_cased('\\', &mut out, &mut one, region),
'u' => one = Some(true),
'l' => one = Some(false),
'U' => region = Some(true),
'L' => region = Some(false),
'e' | 'E' => region = None,
't' => push_cased('\t', &mut out, &mut one, region),
'r' | 'n' => push_cased('\n', &mut out, &mut one, region),
other => push_cased(other, &mut out, &mut one, region),
}
}
out
}
fn push_group(
caps: &Captures<'_, str>,
n: usize,
out: &mut String,
one: &mut Option<bool>,
region: Option<bool>,
) {
if let Some(m) = caps.get(n) {
for ch in m.as_str().chars() {
push_cased(ch, out, one, region);
}
}
}
fn push_cased(ch: char, out: &mut String, one: &mut Option<bool>, region: Option<bool>) {
match one.take().or(region) {
Some(true) => out.extend(ch.to_uppercase()),
Some(false) => out.extend(ch.to_lowercase()),
None => out.push(ch),
}
}
#[cfg(test)]
mod tests {
use super::*;
use fancy_regex::Regex;
fn tr(p: &str) -> String {
translate_pattern(p).expect("translatable")
}
#[test]
fn literal_text_is_unchanged() {
assert_eq!(tr("foo"), "foo");
}
#[test]
fn magic_grouping_and_quantifiers() {
assert_eq!(tr(r"\(foo\)\+"), "(foo)+");
assert_eq!(tr(r"a\|b"), "a|b");
assert_eq!(tr(r"\(ab\)\?"), "(ab)?");
assert_eq!(tr(r"\%(ab\)"), "(?:ab)");
}
#[test]
fn bare_metachars_are_literal_in_magic() {
assert_eq!(tr("a+b"), r"a\+b");
assert_eq!(tr("(x)"), r"\(x\)");
assert_eq!(tr("a|b"), r"a\|b");
}
#[test]
fn dot_star_anchors_classes() {
assert_eq!(tr("f.o"), "f.o");
assert_eq!(tr(r"f\.o"), r"f\.o");
assert_eq!(tr("ab*"), "ab*");
assert_eq!(tr("^foo$"), "^foo$");
assert_eq!(tr("[a-z0-9]"), "[a-z0-9]");
}
#[test]
fn leading_star_is_literal() {
assert_eq!(tr("*x"), r"\*x");
}
#[test]
fn dollar_anchor_vs_literal() {
assert_eq!(tr("a$"), "a$"); assert_eq!(tr(r"a$b"), r"a\$b"); assert_eq!(tr(r"\(a$\)"), r"(a$)"); }
#[test]
fn word_boundaries_use_lookaround() {
assert_eq!(tr(r"\<word\>"), r"\b(?=\w)word(?<=\w)\b");
}
#[test]
fn vim_character_classes_expand() {
assert_eq!(tr(r"\d\+"), r"\d+");
assert_eq!(tr(r"\a"), "[A-Za-z]");
assert_eq!(tr(r"\x"), "[0-9A-Fa-f]");
assert_eq!(tr(r"\l\u"), "[a-z][A-Z]");
}
#[test]
fn very_magic_mode() {
assert_eq!(tr(r"\v(foo|bar)+"), "(foo|bar)+");
assert_eq!(tr(r"\va{2,3}"), "a{2,3}");
assert_eq!(tr(r"\v\(lit\)"), r"\(lit\)");
}
#[test]
fn nomagic_and_verynomagic() {
assert_eq!(tr(r"\Mfoo.bar"), r"foo\.bar");
assert_eq!(tr(r"\M\(x\)\+"), "(x)+");
assert_eq!(tr(r"\Va.b"), r"a\.b");
}
#[test]
fn lazy_and_bounded_quantifiers() {
assert_eq!(tr(r"a\{-}"), "a*?");
assert_eq!(tr(r"a\{2,4}"), "a{2,4}");
assert_eq!(tr(r"a\{-1,3}"), "a{1,3}?");
assert_eq!(tr(r"a\{,3}"), "a{0,3}");
assert_eq!(tr(r"a\{2}"), "a{2}");
}
#[test]
fn pattern_backreference_passes_through() {
assert_eq!(tr(r"\(.\)\1"), r"(.)\1");
}
#[test]
fn unsupported_atoms_are_rejected() {
assert!(matches!(
translate_pattern(r"foo\zsbar"),
Err(ExError::UnsupportedPattern(_))
));
assert!(matches!(
translate_pattern(r"\(x\)\@="),
Err(ExError::UnsupportedPattern(_))
));
assert!(matches!(
translate_pattern(r"\%[ab]"),
Err(ExError::UnsupportedPattern(_))
));
}
#[test]
fn translated_patterns_compile_and_match() {
let re = Regex::new(&tr(r"\(\w\+\)\s\+\(\w\+\)")).unwrap();
assert!(re.is_match("hello world").unwrap());
let dbl = Regex::new(&tr(r"\(.\)\1")).unwrap();
assert!(dbl.is_match("aa").unwrap());
assert!(!dbl.is_match("ab").unwrap());
}
fn caps<'t>(pat: &str, hay: &'t str) -> Captures<'t, str> {
Regex::new(pat)
.unwrap()
.captures(hay)
.unwrap()
.expect("a match")
}
#[test]
fn replacement_backreferences_and_whole_match() {
let c = caps(r"(\w+) (\w+)", "hello world");
assert_eq!(expand_replacement(r"\2 \1", &c), "world hello");
assert_eq!(expand_replacement(r"[&]", &c), "[hello world]");
assert_eq!(expand_replacement(r"\0!", &c), "hello world!");
}
#[test]
fn replacement_case_modifiers() {
let c = caps(r"(\w+)", "hello");
assert_eq!(expand_replacement(r"\U\1", &c), "HELLO");
assert_eq!(expand_replacement(r"\u\1", &c), "Hello"); assert_eq!(expand_replacement(r"\U\1\E!", &c), "HELLO!");
let c2 = caps(r"(\w+)", "HELLO");
assert_eq!(expand_replacement(r"\l\1", &c2), "hELLO");
assert_eq!(expand_replacement(r"\L\1", &c2), "hello");
}
#[test]
fn replacement_literals_and_escapes() {
let c = caps(r"(x)", "x");
assert_eq!(expand_replacement(r"\&", &c), "&"); assert_eq!(expand_replacement(r"a\tb", &c), "a\tb"); assert_eq!(expand_replacement(r"\\", &c), "\\"); }
}