use std::cell::RefCell;
type SubstExprFn = fn(&str) -> String;
thread_local! {
pub static SUBST_EXPR_HOOK: RefCell<Option<SubstExprFn>> =
const { RefCell::new(None) };
static SUBMATCHES: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
pub fn current_submatch(n: usize) -> String {
SUBMATCHES.with(|s| s.borrow().get(n).cloned().unwrap_or_default())
}
pub fn has_submatch_context() -> bool {
SUBMATCHES.with(|s| !s.borrow().is_empty())
}
#[derive(Debug, Clone)]
struct Class {
negated: bool,
items: Vec<ClassItem>,
}
#[derive(Debug, Clone)]
enum ClassItem {
Ch(char),
Range(char, char),
Digit,
Word,
Space,
Alpha,
Lower,
Upper,
Hex,
Head,
Octal,
Print,
PrintNoDigit,
Ident,
IdentNoDigit,
Keyword,
KeywordNoDigit,
Alnum,
Blank,
Cntrl,
Graph,
Punct,
LowerU,
UpperU,
Whitespace,
}
impl ClassItem {
fn folds_under_ic(&self) -> bool {
matches!(self, ClassItem::Ch(_) | ClassItem::Range(_, _))
}
fn contains(&self, c: char) -> bool {
match self {
ClassItem::Ch(x) => *x == c,
ClassItem::Range(a, b) => *a <= c && c <= *b,
ClassItem::Digit => c.is_ascii_digit(),
ClassItem::Word => c.is_ascii_alphanumeric() || c == '_',
ClassItem::Space => c == ' ' || c == '\t',
ClassItem::Alpha => c.is_ascii_alphabetic(),
ClassItem::Lower => c.is_ascii_lowercase(),
ClassItem::Upper => c.is_ascii_uppercase(),
ClassItem::Hex => c.is_ascii_hexdigit(),
ClassItem::Head => c.is_ascii_alphabetic() || c == '_',
ClassItem::Octal => ('0'..='7').contains(&c),
ClassItem::Print => is_printable(c),
ClassItem::PrintNoDigit => is_printable(c) && !c.is_ascii_digit(),
ClassItem::Ident => is_ident_char(c),
ClassItem::IdentNoDigit => is_ident_char(c) && !c.is_ascii_digit(),
ClassItem::Keyword => is_keyword_char(c),
ClassItem::KeywordNoDigit => is_keyword_char(c) && !c.is_ascii_digit(),
ClassItem::Alnum => c.is_ascii_alphanumeric(),
ClassItem::Blank => c == ' ' || c == '\t',
ClassItem::Cntrl => c.is_ascii_control(),
ClassItem::Graph => c.is_ascii_graphic(),
ClassItem::Punct => c.is_ascii_punctuation(),
ClassItem::LowerU => c.is_lowercase(),
ClassItem::UpperU => c.is_uppercase(),
ClassItem::Whitespace => matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c'),
}
}
}
fn is_printable(c: char) -> bool {
let n = c as u32;
(0x20..=0x7E).contains(&n) || n >= 0xA0
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || matches!(c as u32, 0xC0..=0xFF)
}
fn is_keyword_char(c: char) -> bool {
is_ident_char(c) || ((c as u32) > 0xFF && c.is_alphanumeric())
}
impl Class {
fn matches(&self, c: char, ic: bool) -> bool {
let hit = self.items.iter().any(|it| {
if ic && it.folds_under_ic() {
it.contains(c.to_ascii_lowercase()) || it.contains(c.to_ascii_uppercase())
} else {
it.contains(c)
}
});
hit ^ self.negated
}
}
#[derive(Debug, Clone)]
enum Node {
Lit(char),
Any,
Bol,
Eol,
WordB(bool),
MatchStart,
MatchEnd,
BackRef(usize),
OptSeq(Vec<Node>),
Class(Class),
Group(Vec<Branch>, Option<usize>),
}
#[derive(Debug, Clone)]
struct Atom {
node: Node,
min: u32,
max: u32,
greedy: bool,
}
type Branch = Vec<Atom>;
pub struct Regex {
branches: Vec<Branch>,
ngroups: usize,
forced_ic: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Captures {
pub groups: Vec<Option<(usize, usize)>>,
}
impl Captures {
pub fn whole(&self) -> (usize, usize) {
self.groups[0].unwrap_or((0, 0))
}
}
const INF: u32 = u32::MAX;
fn preprocess_magic(pat: &str) -> String {
if !pat.contains("\\v") {
return pat.to_string();
}
let chars: Vec<char> = pat.chars().collect();
let mut out = String::new();
let mut i = 0;
let mut very = false;
const OPS: &str = "(){}+?=|<>";
while i < chars.len() {
let c = chars[i];
if c == '\\' && i + 1 < chars.len() {
match chars[i + 1] {
'v' => {
very = true;
i += 2;
continue;
}
'm' => {
very = false;
i += 2;
continue;
}
_ => {}
}
}
if !very {
out.push(c);
i += 1;
continue;
}
match c {
'\\' => {
if let Some(&n) = chars.get(i + 1) {
if OPS.contains(n) {
out.push(n); } else {
out.push('\\'); out.push(n);
}
i += 2;
} else {
out.push('\\');
i += 1;
}
}
'[' => {
out.push('[');
i += 1;
if chars.get(i) == Some(&'^') {
out.push('^');
i += 1;
}
if chars.get(i) == Some(&']') {
out.push(']');
i += 1;
}
while i < chars.len() && chars[i] != ']' {
if chars[i] == '\\' && i + 1 < chars.len() {
out.push('\\');
out.push(chars[i + 1]);
i += 2;
} else {
out.push(chars[i]);
i += 1;
}
}
if i < chars.len() {
out.push(']');
i += 1;
}
}
'%' if chars.get(i + 1) == Some(&'(') => {
out.push_str("\\%("); i += 2;
}
_ if OPS.contains(c) => {
out.push('\\'); out.push(c);
i += 1;
}
_ => {
out.push(c); i += 1;
}
}
}
out
}
struct Parser {
p: Vec<char>,
i: usize,
ngroups: usize,
forced_ic: Option<bool>,
}
impl Parser {
fn peek(&self) -> Option<char> {
self.p.get(self.i).copied()
}
fn peek2(&self) -> Option<char> {
self.p.get(self.i + 1).copied()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek();
if c.is_some() {
self.i += 1;
}
c
}
fn alternation(&mut self) -> Vec<Branch> {
let mut branches = vec![self.concat()];
while self.peek() == Some('\\') && self.peek2() == Some('|') {
self.i += 2;
branches.push(self.concat());
}
branches
}
fn concat(&mut self) -> Branch {
let mut atoms = Vec::new();
loop {
match (self.peek(), self.peek2()) {
(None, _) => break,
(Some('\\'), Some('|')) | (Some('\\'), Some(')')) => break,
_ => {}
}
match self.quantified(atoms.is_empty()) {
Some(a) => atoms.push(a),
None => break,
}
}
atoms
}
fn quantified(&mut self, at_start: bool) -> Option<Atom> {
let node = self.atom(at_start)?;
let (min, max, greedy) = self.quantifier();
Some(Atom {
node,
min,
max,
greedy,
})
}
fn quantifier(&mut self) -> (u32, u32, bool) {
match (self.peek(), self.peek2()) {
(Some('*'), _) => {
self.i += 1;
(0, INF, true)
}
(Some('\\'), Some('+')) => {
self.i += 2;
(1, INF, true)
}
(Some('\\'), Some('?')) | (Some('\\'), Some('=')) => {
self.i += 2;
(0, 1, true)
}
(Some('\\'), Some('{')) => {
self.i += 2;
self.brace_count()
}
_ => (1, 1, true),
}
}
fn brace_count(&mut self) -> (u32, u32, bool) {
let mut greedy = true;
if self.peek() == Some('-') {
greedy = false;
self.i += 1;
}
let lo = self.read_int();
let mut hi = lo;
if self.peek() == Some(',') {
self.i += 1;
hi = self.read_int_or(INF);
}
if self.peek() == Some('\\') && self.peek2() == Some('}') {
self.i += 2;
} else if self.peek() == Some('}') {
self.i += 1;
}
let lo = lo.unwrap_or(0);
let hi = hi.unwrap_or(if lo == 0 { INF } else { lo });
(lo, hi, greedy)
}
fn read_int(&mut self) -> Option<u32> {
let start = self.i;
let mut n = 0u32;
while let Some(c) = self.peek() {
if c.is_ascii_digit() {
n = n * 10 + (c as u32 - '0' as u32);
self.i += 1;
} else {
break;
}
}
if self.i > start {
Some(n)
} else {
None
}
}
fn read_int_or(&mut self, default: u32) -> Option<u32> {
Some(self.read_int().unwrap_or(default))
}
fn atom(&mut self, at_start: bool) -> Option<Node> {
let c = self.peek()?;
match c {
'.' => {
self.i += 1;
Some(Node::Any)
}
'^' if at_start => {
self.i += 1;
Some(Node::Bol)
}
'$' if self.is_eol_pos() => {
self.i += 1;
Some(Node::Eol)
}
'[' => {
self.i += 1;
Some(Node::Class(self.bracket()))
}
'\\' => self.escape(),
_ => {
self.i += 1;
Some(Node::Lit(c))
}
}
}
fn is_eol_pos(&self) -> bool {
matches!(
(self.p.get(self.i + 1), self.p.get(self.i + 2)),
(None, _) | (Some('\\'), Some('|')) | (Some('\\'), Some(')'))
)
}
fn escape(&mut self) -> Option<Node> {
self.i += 1; let c = self.bump()?;
Some(match c {
'(' => {
let idx = self.ngroups + 1;
self.ngroups = idx;
let branches = self.alternation();
self.close_group();
Node::Group(branches, Some(idx))
}
'%' if self.peek() == Some('(') => {
self.i += 1; let branches = self.alternation();
self.close_group();
Node::Group(branches, None)
}
'%' if self.peek() == Some('[') => {
self.i += 1; let mut nodes = Vec::new();
while self.peek().is_some() && self.peek() != Some(']') {
match self.atom(false) {
Some(n) => nodes.push(n),
None => break,
}
}
if self.peek() == Some(']') {
self.i += 1; }
Node::OptSeq(nodes)
}
'<' => Node::WordB(true),
'>' => Node::WordB(false),
'z' if self.peek() == Some('s') => {
self.i += 1;
Node::MatchStart
}
'z' if self.peek() == Some('e') => {
self.i += 1;
Node::MatchEnd
}
'd' => class_atom(false, ClassItem::Digit),
'D' => class_atom(true, ClassItem::Digit),
'w' => class_atom(false, ClassItem::Word),
'W' => class_atom(true, ClassItem::Word),
's' => class_atom(false, ClassItem::Space),
'S' => class_atom(true, ClassItem::Space),
'a' => class_atom(false, ClassItem::Alpha),
'A' => class_atom(true, ClassItem::Alpha),
'l' => class_atom(false, ClassItem::Lower),
'u' => class_atom(false, ClassItem::Upper),
'x' => class_atom(false, ClassItem::Hex),
'h' => class_atom(false, ClassItem::Head),
'H' => class_atom(true, ClassItem::Head),
'o' => class_atom(false, ClassItem::Octal),
'O' => class_atom(true, ClassItem::Octal),
'p' => class_atom(false, ClassItem::Print),
'P' => class_atom(false, ClassItem::PrintNoDigit),
'i' => class_atom(false, ClassItem::Ident),
'I' => class_atom(false, ClassItem::IdentNoDigit),
'k' => class_atom(false, ClassItem::Keyword),
'K' => class_atom(false, ClassItem::KeywordNoDigit),
'c' => {
self.forced_ic = Some(true);
return self.atom(false);
}
'C' => {
self.forced_ic = Some(false);
return self.atom(false);
}
d @ '1'..='9' => Node::BackRef(d as usize - '0' as usize),
't' => Node::Lit('\t'),
'n' => Node::Lit('\n'),
'r' => Node::Lit('\r'),
'e' => Node::Lit('\x1b'),
other => Node::Lit(other),
})
}
fn close_group(&mut self) {
if self.peek() == Some('\\') && self.peek2() == Some(')') {
self.i += 2;
}
}
fn bracket(&mut self) -> Class {
let mut negated = false;
if self.peek() == Some('^') {
negated = true;
self.i += 1;
}
let mut items = Vec::new();
if self.peek() == Some(']') {
items.push(ClassItem::Ch(']'));
self.i += 1;
}
while let Some(c) = self.peek() {
if c == ']' {
self.i += 1;
break;
}
if c == '[' && self.peek2() == Some(':') {
if let Some(mut posix) = self.posix_class() {
items.append(&mut posix);
continue;
}
}
self.i += 1;
if self.peek() == Some('-') && self.peek2().is_some() && self.peek2() != Some(']') {
self.i += 1;
let hi = self.bump().unwrap();
items.push(ClassItem::Range(c, hi));
} else {
items.push(ClassItem::Ch(c));
}
}
Class { negated, items }
}
fn posix_class(&mut self) -> Option<Vec<ClassItem>> {
let mut j = self.i + 2; let mut name = String::new();
while let Some(ch) = self.p.get(j).copied() {
if ch == ':' {
break;
}
if !ch.is_ascii_alphabetic() {
return None;
}
name.push(ch);
j += 1;
}
if self.p.get(j).copied() != Some(':') || self.p.get(j + 1).copied() != Some(']') {
return None;
}
let items = posix_class_items(&name)?;
self.i = j + 2; Some(items)
}
}
fn posix_class_items(name: &str) -> Option<Vec<ClassItem>> {
let item = match name {
"alnum" => ClassItem::Alnum,
"alpha" => ClassItem::Alpha,
"blank" => ClassItem::Blank,
"cntrl" => ClassItem::Cntrl,
"digit" => ClassItem::Digit,
"graph" => ClassItem::Graph,
"lower" => ClassItem::LowerU,
"print" => ClassItem::Print,
"punct" => ClassItem::Punct,
"space" => ClassItem::Whitespace,
"upper" => ClassItem::UpperU,
"xdigit" => ClassItem::Hex,
"tab" => ClassItem::Ch('\t'),
"escape" => ClassItem::Ch('\x1b'),
"backspace" => ClassItem::Ch('\x08'),
"return" => ClassItem::Ch('\r'),
"ident" => ClassItem::Ident,
"keyword" => ClassItem::Keyword,
_ => return None,
};
Some(vec![item])
}
fn class_atom(negated: bool, item: ClassItem) -> Node {
Node::Class(Class {
negated,
items: vec![item],
})
}
impl Regex {
pub fn compile(pat: &str) -> Regex {
let pat = preprocess_magic(pat);
let mut parser = Parser {
p: pat.chars().collect(),
i: 0,
ngroups: 0,
forced_ic: None,
};
let branches = parser.alternation();
Regex {
branches,
ngroups: parser.ngroups,
forced_ic: parser.forced_ic,
}
}
fn effective_ic(&self, ic: bool) -> bool {
self.forced_ic.unwrap_or(ic)
}
pub fn find(&self, text: &[char], ic: bool) -> Option<Captures> {
self.find_from(text, ic, 0)
}
pub fn find_from(&self, text: &[char], ic: bool, from: usize) -> Option<Captures> {
let ic = self.effective_ic(ic);
for start in from..=text.len() {
let mut groups = vec![None; self.ngroups + 3];
if let Some(end) = self.match_alt(&self.branches, text, start, &mut groups, ic) {
let s = match groups[self.ngroups + 1] {
Some((zp, _)) => zp,
None => start,
};
let e = match groups[self.ngroups + 2] {
Some((ep, _)) => ep,
None => end,
};
groups[0] = Some((s, e));
groups.truncate(self.ngroups + 1);
return Some(Captures { groups });
}
}
None
}
pub fn is_match(&self, text: &[char], ic: bool) -> bool {
self.find(text, ic).is_some()
}
fn match_alt(
&self,
branches: &[Branch],
text: &[char],
pos: usize,
groups: &mut Vec<Option<(usize, usize)>>,
ic: bool,
) -> Option<usize> {
for b in branches {
if let Some(end) = self.match_atoms(b, text, pos, groups, ic) {
return Some(end);
}
}
None
}
fn match_atoms(
&self,
atoms: &[Atom],
text: &[char],
pos: usize,
groups: &mut Vec<Option<(usize, usize)>>,
ic: bool,
) -> Option<usize> {
let Some((atom, rest)) = atoms.split_first() else {
return Some(pos);
};
let mut positions = vec![pos];
let mut cur = pos;
let mut count = 0u32;
while count < atom.max {
match self.match_one(&atom.node, text, cur, groups, ic) {
Some(next) if next > cur => {
positions.push(next);
cur = next;
count += 1;
}
Some(next) if next == cur && count < atom.min => {
positions.push(next);
break;
}
_ => break,
}
}
let max_k = positions.len() - 1;
if (max_k as u32) < atom.min {
return None;
}
let order: Vec<usize> = if atom.greedy {
(atom.min as usize..=max_k).rev().collect()
} else {
(atom.min as usize..=max_k).collect()
};
for k in order {
if let Some(end) = self.match_atoms(rest, text, positions[k], groups, ic) {
return Some(end);
}
}
None
}
fn match_one(
&self,
node: &Node,
text: &[char],
pos: usize,
groups: &mut Vec<Option<(usize, usize)>>,
ic: bool,
) -> Option<usize> {
match node {
Node::Lit(c) => {
let ch = *text.get(pos)?;
if char_eq(ch, *c, ic) {
Some(pos + 1)
} else {
None
}
}
Node::Any => {
let ch = *text.get(pos)?;
if ch != '\n' {
Some(pos + 1)
} else {
None
}
}
Node::Class(cl) => {
let ch = *text.get(pos)?;
if cl.matches(ch, ic) {
Some(pos + 1)
} else {
None
}
}
Node::Bol => (pos == 0).then_some(pos),
Node::Eol => (pos == text.len()).then_some(pos),
Node::MatchStart => {
let slot = self.ngroups + 1;
if let Some(cell) = groups.get_mut(slot) {
*cell = Some((pos, pos));
}
Some(pos)
}
Node::MatchEnd => {
let slot = self.ngroups + 2;
if let Some(cell) = groups.get_mut(slot) {
*cell = Some((pos, pos));
}
Some(pos)
}
Node::WordB(start) => {
let before = pos > 0 && is_word(text[pos - 1]);
let after = pos < text.len() && is_word(text[pos]);
let ok = if *start {
!before && after
} else {
before && !after
};
ok.then_some(pos)
}
Node::OptSeq(nodes) => {
let mut p = pos;
for n in nodes {
match self.match_one(n, text, p, groups, ic) {
Some(next) => p = next,
None => break,
}
}
Some(p)
}
Node::BackRef(n) => {
match groups.get(*n).copied().flatten() {
Some((s, e)) => {
let len = e - s;
if pos + len <= text.len()
&& (0..len).all(|k| char_eq(text[pos + k], text[s + k], ic))
{
Some(pos + len)
} else {
None
}
}
None => Some(pos),
}
}
Node::Group(branches, capidx) => {
let end = self.match_alt(branches, text, pos, groups, ic)?;
if let Some(idx) = capidx {
groups[*idx] = Some((pos, end));
}
Some(end)
}
}
}
}
fn char_eq(a: char, b: char, ic: bool) -> bool {
if ic {
a.eq_ignore_ascii_case(&b)
} else {
a == b
}
}
fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub fn regex_match(pat: &str, subject: &str, ic: bool) -> bool {
let chars: Vec<char> = subject.chars().collect();
Regex::compile(pat).is_match(&chars, ic)
}
pub fn regex_matchstr(pat: &str, subject: &str, ic: bool) -> String {
let chars: Vec<char> = subject.chars().collect();
match Regex::compile(pat).find(&chars, ic) {
Some(caps) => {
let (s, e) = caps.whole();
chars[s..e].iter().collect()
}
None => String::new(),
}
}
pub fn regex_search_nth(
pat: &str,
subject: &str,
ic: bool,
from: usize,
nth: i64,
) -> Option<(i64, i64, Vec<String>)> {
let chars: Vec<char> = subject.chars().collect();
let re = Regex::compile(pat);
let mut pos = from.min(chars.len());
let mut remaining = nth.max(1);
loop {
let caps = re.find_from(&chars, ic, pos)?;
let (s, e) = caps.whole();
remaining -= 1;
if remaining <= 0 {
let mut groups: Vec<String> = caps
.groups
.iter()
.map(|g| match g {
Some((gs, ge)) => chars[*gs..*ge].iter().collect(),
None => String::new(),
})
.collect();
groups.resize(10, String::new());
return Some((s as i64, e as i64, groups));
}
pos = if e > s { e } else { s + 1 };
if pos > chars.len() {
return None;
}
}
}
pub fn regex_match_index(pat: &str, subject: &str, ic: bool) -> i64 {
let chars: Vec<char> = subject.chars().collect();
Regex::compile(pat)
.find(&chars, ic)
.map_or(-1, |c| c.whole().0 as i64)
}
pub fn regex_matchend(pat: &str, subject: &str, ic: bool) -> i64 {
let chars: Vec<char> = subject.chars().collect();
Regex::compile(pat)
.find(&chars, ic)
.map_or(-1, |c| c.whole().1 as i64)
}
pub fn regex_matchstrpos(pat: &str, subject: &str, ic: bool) -> (String, i64, i64) {
let chars: Vec<char> = subject.chars().collect();
match Regex::compile(pat).find(&chars, ic) {
Some(caps) => {
let (s, e) = caps.whole();
(chars[s..e].iter().collect(), s as i64, e as i64)
}
None => (String::new(), -1, -1),
}
}
pub fn regex_matchlist(pat: &str, subject: &str, ic: bool) -> Vec<String> {
let chars: Vec<char> = subject.chars().collect();
match Regex::compile(pat).find(&chars, ic) {
Some(caps) => {
let mut out: Vec<String> = caps
.groups
.iter()
.map(|g| match g {
Some((s, e)) => chars[*s..*e].iter().collect(),
None => String::new(),
})
.collect();
out.resize(10, String::new());
out
}
None => Vec::new(),
}
}
pub fn regex_substitute(subject: &str, pat: &str, sub: &str, flags: &str) -> String {
let chars: Vec<char> = subject.chars().collect();
let re = Regex::compile(pat);
let global = flags.contains('g');
let ic = flags.contains('i');
let sub_expr = sub.strip_prefix("\\=");
let mut out = String::new();
let mut tail = 0usize;
let mut zero_width: Option<usize> = None;
loop {
let mut found = None;
for start in tail..=chars.len() {
let mut groups = vec![None; re.ngroups + 1];
if let Some(end) = re.match_alt(
&re.branches,
&chars,
start,
&mut groups,
re.effective_ic(ic),
) {
groups[0] = Some((start, end));
found = Some((start, end, groups));
break;
}
}
let Some((s, e, groups)) = found else {
break;
};
if s == e {
if zero_width == Some(s) {
if tail < chars.len() {
out.push(chars[tail]);
tail += 1;
continue;
}
break;
}
zero_width = Some(s);
}
out.extend(&chars[tail..s]);
if let Some(expr) = sub_expr {
let subs: Vec<String> = groups
.iter()
.map(|g| match g {
Some((a, b)) => chars[*a..*b].iter().collect(),
None => String::new(),
})
.collect();
SUBMATCHES.with(|m| *m.borrow_mut() = subs);
let hook = SUBST_EXPR_HOOK.with(|h| *h.borrow());
let rep = hook.map(|f| f(expr)).unwrap_or_default();
out.push_str(&rep);
} else {
out.push_str(&expand_sub(sub, &chars, &groups));
}
tail = e;
if tail >= chars.len() {
break;
}
if !global {
break;
}
}
out.extend(&chars[tail.min(chars.len())..]);
out
}
#[derive(Default)]
struct SubCase {
one_shot: Option<bool>, sustained: Option<bool>,
}
impl SubCase {
fn push(&mut self, out: &mut String, c: char) {
let upper = self.one_shot.take().or(self.sustained);
match upper {
Some(true) => out.extend(c.to_uppercase()),
Some(false) => out.extend(c.to_lowercase()),
None => out.push(c),
}
}
fn push_str(&mut self, out: &mut String, s: impl IntoIterator<Item = char>) {
for c in s {
self.push(out, c);
}
}
}
fn expand_sub(sub: &str, chars: &[char], groups: &[Option<(usize, usize)>]) -> String {
let s: Vec<char> = sub.chars().collect();
let mut out = String::new();
let mut cs = SubCase::default();
let mut i = 0;
while i < s.len() {
match s[i] {
'&' => {
if let Some((a, b)) = groups.first().copied().flatten() {
cs.push_str(&mut out, chars[a..b].iter().copied());
}
i += 1;
}
'\\' if i + 1 < s.len() => {
let n = s[i + 1];
match n {
'0'..='9' => {
let g = n as usize - '0' as usize;
if let Some(Some((a, b))) = groups.get(g) {
cs.push_str(&mut out, chars[*a..*b].iter().copied());
}
}
'n' => out.push('\0'),
't' => out.push('\t'),
'r' => out.push('\r'),
'\\' => cs.push(&mut out, '\\'),
'&' => cs.push(&mut out, '&'),
'u' => cs.one_shot = Some(true),
'l' => cs.one_shot = Some(false),
'U' => cs.sustained = Some(true),
'L' => cs.sustained = Some(false),
'e' | 'E' => {
cs.sustained = None;
cs.one_shot = None;
}
other => cs.push(&mut out, other),
}
i += 2;
}
c => {
cs.push(&mut out, c);
i += 1;
}
}
}
out
}
pub fn regex_split(subject: &str, pat: &str, ic: bool, keepempty: bool) -> Vec<String> {
let chars: Vec<char> = subject.chars().collect();
let n = chars.len();
let re = Regex::compile(pat);
let eic = re.effective_ic(ic);
let find_from = |from: usize| -> Option<(usize, usize)> {
let mut p = from;
while p <= n {
let mut groups = vec![None; re.ngroups + 3];
if let Some(end) = re.match_alt(&re.branches, &chars, p, &mut groups, eic) {
let startp = groups[re.ngroups + 1].map_or(p, |(zp, _)| zp);
let endp = groups[re.ngroups + 2].map_or(end, |(ep, _)| ep);
return Some((startp, endp));
}
p += 1;
}
None
};
let mut out: Vec<String> = Vec::new();
let mut str = 0usize; let mut col = 0usize; loop {
let at_end = str >= n;
if at_end && !keepempty {
break;
}
let m = if at_end { None } else { find_from(str + col) };
let (startp, endp) = m.unwrap_or((n, n));
let end = if m.is_some() { startp } else { n };
if keepempty || end > str || (!out.is_empty() && !at_end && m.is_some() && end < endp) {
out.push(chars[str..end].iter().collect());
}
if m.is_none() {
break;
}
col = if endp > str { 0 } else { 1 };
str = endp;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_atoms_and_anchors() {
assert!(regex_match("foo", "a foo b", false));
assert!(regex_match("^foo", "foobar", false));
assert!(!regex_match("^foo", "a foo", false));
assert!(regex_match("bar$", "foobar", false));
assert!(regex_match("f.o", "fxo", false));
}
#[test]
fn quantifiers_and_classes() {
assert!(regex_match("ab*c", "ac", false));
assert!(regex_match("ab*c", "abbbc", false));
assert!(regex_match("a\\+", "aaa", false));
assert!(regex_match("\\d\\+", "x42y", false));
assert!(regex_match("[a-c]\\{2}", "xbcx", false));
assert!(!regex_match("[^0-9]", "5", false));
}
#[test]
fn groups_alt_wordbound_case() {
assert!(regex_match("\\(foo\\|bar\\)", "a bar", false));
assert!(regex_match("\\<word\\>", "a word here", false));
assert!(!regex_match("\\<ord\\>", "word", false));
assert!(regex_match("FOO", "foo", true)); assert!(regex_match("\\cfoo", "FOO", false)); }
#[test]
fn matchstr_and_substitute() {
assert_eq!(regex_matchstr("\\d\\+", "ab123cd", false), "123");
assert_eq!(regex_substitute("foobar", "o", "0", ""), "f0obar");
assert_eq!(regex_substitute("foobar", "o", "0", "g"), "f00bar");
assert_eq!(
regex_substitute("2024-06", "\\(\\d\\+\\)-\\(\\d\\+\\)", "\\2/\\1", ""),
"06/2024"
);
}
#[test]
fn split_on_pattern() {
assert_eq!(
regex_split("a1b2c", "\\d", false, false),
vec!["a", "b", "c"]
);
}
}