mod flags;
mod groups;
mod guard;
mod inner;
pub(crate) use flags::{strip_leading_inline_flags, strip_leading_zero_width_assertions};
pub(crate) use groups::{
expand_leading_charclass_prefixes, expand_leading_literal_alternation_with_tail,
MAX_CHARCLASS_PREFIX_EXPANSION,
};
pub(crate) use guard::{split_leading_boundary_guard, strip_leading_boundary_guard};
#[cfg(test)]
pub(crate) use inner::extract_inner_literals;
pub(crate) use inner::{
is_escaped_literal, regex_has_required_literal_run, MIN_DISTINCTIVE_INFIX_CHARS,
MIN_INNER_LITERAL_CHARS,
};
use crate::types::MIN_LITERAL_PREFIX_CHARS;
fn split_top_level_alternatives(pattern: &str) -> Option<Vec<&str>> {
let mut parts = Vec::new();
let mut start = 0usize;
let mut depth = 0usize;
let mut in_class = false;
let mut escaped = false;
for (index, ch) in pattern.char_indices() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'[' if !in_class => in_class = true,
']' if in_class => in_class = false,
'(' if !in_class => depth += 1,
')' if !in_class => depth = depth.saturating_sub(1),
'|' if !in_class && depth == 0 => {
parts.push(&pattern[start..index]);
start = index + ch.len_utf8();
}
_ => {}
}
}
if parts.is_empty() {
return None;
}
parts.push(&pattern[start..]);
Some(parts)
}
pub(crate) fn extract_literal_prefixes(pattern: &str) -> Vec<String> {
let pattern = strip_leading_inline_flags(pattern);
let pattern = strip_leading_zero_width_assertions(pattern);
if let Some(parts) = split_top_level_alternatives(pattern) {
let mut prefixes = Vec::new();
for part in parts {
let branch_prefixes = extract_literal_prefixes(part);
if branch_prefixes.is_empty() {
return Vec::new();
}
prefixes.extend(branch_prefixes);
}
return prefixes;
}
if let Some(rest) = strip_leading_boundary_guard(pattern) {
let inner = extract_literal_prefixes(rest);
if !inner.is_empty() {
return inner;
}
}
if pattern.starts_with('(') && pattern.contains('|') {
let mut depth = 0;
let mut end_idx = None;
for (i, ch) in pattern.char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
end_idx = Some(i);
break;
}
}
_ => {}
}
}
if let Some(end) = end_idx {
let inner = groups::strip_group_prefix(&pattern[1..end]);
let mut parts = Vec::new();
let mut start = 0;
let mut d = 0;
for (i, ch) in inner.char_indices() {
match ch {
'(' => d += 1,
')' => d -= 1,
'|' if d == 0 => {
parts.push(&inner[start..i]);
start = i + 1;
}
_ => {}
}
}
parts.push(&inner[start..]);
let mut results = Vec::new();
for part in parts {
if let Some(p) = extract_literal_prefix(part) {
results.push(p);
} else {
results.clear();
break;
}
}
if !results.is_empty() {
return results;
}
}
}
if let Some(p) = extract_literal_prefix(pattern) {
return vec![p];
}
if let Some(expanded) = expand_leading_charclass_prefixes(pattern) {
return expanded;
}
if let Some(expanded) = expand_leading_literal_alternation_with_tail(pattern) {
return expanded;
}
Vec::new()
}
pub(crate) fn leading_literal_run(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
match ch {
'\\' => match chars.next() {
Some(next) if is_escaped_literal(next) => out.push(next),
_ => break,
},
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | ':' | '=' => out.push(ch),
_ => break,
}
}
out
}
pub(crate) fn extract_literal_prefix(pattern: &str) -> Option<String> {
let pattern = strip_leading_inline_flags(pattern);
let pattern = strip_leading_zero_width_assertions(pattern);
let mut prefix = String::new();
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
let Some(next) = chars.next() else {
break;
};
if is_escaped_literal(next) {
prefix.push(next);
} else {
break;
}
}
'[' | '.' | '+' | '|' | '^' | '$' => break,
'*' | '?' => {
prefix.pop();
break;
}
'{' => {
if chars.peek() == Some(&'0') {
prefix.pop();
}
break;
}
'(' => {
let group_start = chars.clone().collect::<String>();
let optional_group =
groups::leading_group_parts(&group_start).is_some_and(|(_, tail)| {
tail.starts_with('?') || tail.starts_with('*') || tail.starts_with("{0")
});
if optional_group {
} else if let Some(alternatives) = groups::extract_group_alternatives(&group_start)
{
if let Some(first) = alternatives.first() {
let common: String = first
.chars()
.enumerate()
.take_while(|(i, c)| {
alternatives
.iter()
.all(|alt| alt.chars().nth(*i) == Some(*c))
})
.map(|(_, c)| c)
.collect();
if !common.is_empty() {
prefix.push_str(&common);
}
}
} else if let Some(inner) = groups::extract_plain_group_inner(&group_start) {
if let Some(inner_prefix) = extract_literal_prefix(inner) {
prefix.push_str(&inner_prefix);
}
}
break;
}
_ => {
prefix.push(ch);
}
}
}
if prefix.len() >= MIN_LITERAL_PREFIX_CHARS {
Some(prefix)
} else {
None
}
}