use super::prose::VERBATIM_ENVS;
use regex::Regex;
use std::sync::LazyLock;
fn neutralize_tex_special_chars(s: &str) -> String {
s.chars()
.map(|c| match c {
'$' | '%' => '?',
other => other,
})
.collect()
}
fn has_unescaped_percent(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
for i in 0..chars.len() {
if chars[i] == '%' && (i == 0 || chars[i - 1] != '\\') {
return true;
}
}
false
}
pub fn neutralize_verbatim_envs(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let mut pos = 0usize;
loop {
let Some(rel) = src[pos..].find(r"\begin{") else {
out.push_str(&src[pos..]);
break;
};
let begin_start = pos + rel;
let name_start = begin_start + r"\begin{".len();
let Some(name_rel_end) = src[name_start..].find('}') else {
out.push_str(&src[pos..]);
break;
};
let name_end = name_start + name_rel_end;
let name = &src[name_start..name_end];
if !VERBATIM_ENVS.contains(&name) {
out.push_str(&src[pos..=name_end]);
pos = name_end + 1;
continue;
}
let head_end = name_end + 1; let end_marker = format!("\\end{{{name}}}");
let Some(end_rel) = src[head_end..].find(end_marker.as_str()) else {
out.push_str(&src[pos..]);
break;
};
let tail_start = head_end + end_rel;
let whole_end = tail_start + end_marker.len();
let line_start = src[..begin_start].rfind('\n').map(|i| i + 1).unwrap_or(0);
let commented = has_unescaped_percent(&src[line_start..begin_start]);
out.push_str(&src[pos..begin_start]);
if commented {
out.push_str(&src[begin_start..whole_end]);
} else {
out.push_str(&src[begin_start..head_end]);
out.push_str(&neutralize_tex_special_chars(&src[head_end..tail_start]));
out.push_str(&src[tail_start..whole_end]);
}
pos = whole_end;
}
out
}
static VERBATIM_ARG_MACROS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\\(?:code|verb|url|email|fct|pkg|proglang)\{([^{}]*)\}").unwrap()
});
pub fn neutralize_verbatim_args(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let mut last = 0usize;
for caps in VERBATIM_ARG_MACROS.captures_iter(src) {
let whole = caps.get(0).unwrap();
let body = caps.get(1).unwrap();
out.push_str(&src[last..whole.start()]);
out.push_str(&src[whole.start()..body.start()]);
out.push_str(&neutralize_tex_special_chars(body.as_str()));
out.push_str(&src[body.end()..whole.end()]);
last = whole.end();
}
out.push_str(&src[last..]);
out
}
static MACRO_DEF_HEAD: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\\(?:(?P<env>(?:re)?newenvironment)|(?P<cmd>(?:re)?newcommand|providecommand)|(?P<def>def))\*?")
.unwrap()
});
fn skip_ws(chars: &[u8], mut i: usize) -> usize {
while i < chars.len() && matches!(chars[i], b' ' | b'\t' | b'\r' | b'\n') {
i += 1;
}
i
}
fn match_brace_group(s: &str, i: usize) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 0i32;
let mut j = i;
let n = bytes.len();
while j < n {
match bytes[j] {
b'\\' => j += 2,
b'{' => {
depth += 1;
j += 1;
}
b'}' => {
depth -= 1;
j += 1;
if depth == 0 {
return Some(j);
}
}
_ => j += 1,
}
}
None
}
fn skip_bracket_group(s: &str, i: usize) -> Option<usize> {
let bytes = s.as_bytes();
let n = bytes.len();
let mut j = i + 1;
while j < n {
match bytes[j] {
b'\\' => j += 2,
b'{' => j = match_brace_group(s, j)?,
b']' => return Some(j + 1),
_ => j += 1,
}
}
None
}
fn match_control_seq(s: &str, i: usize) -> Option<usize> {
let bytes = s.as_bytes();
if i >= bytes.len() || bytes[i] != b'\\' {
return None;
}
let mut j = i + 1;
if j < bytes.len() && ((bytes[j] as char).is_ascii_alphabetic() || bytes[j] == b'@') {
while j < bytes.len() && ((bytes[j] as char).is_ascii_alphabetic() || bytes[j] == b'@') {
j += 1;
}
Some(j)
} else if j < bytes.len() {
Some(j + 1)
} else {
Some(j)
}
}
pub fn neutralize_macro_definition_bodies(src: &str) -> String {
let mut out: Vec<u8> = src.as_bytes().to_vec();
let n = out.len();
let mut search_from = 0usize;
while let Some(m) = MACRO_DEF_HEAD.find_at(src, search_from) {
search_from = m.end();
let mut i = skip_ws(src.as_bytes(), m.end());
if i >= n {
break;
}
let is_def = MACRO_DEF_HEAD
.captures(&src[m.start()..m.end()])
.and_then(|c| c.name("def"))
.is_some();
let is_env = MACRO_DEF_HEAD
.captures(&src[m.start()..m.end()])
.and_then(|c| c.name("env"))
.is_some();
if is_def {
let Some(after_cs) = match_control_seq(src, i) else {
continue;
};
i = after_cs;
let Some(brace) = src[i..].find('{').map(|off| i + off) else {
continue;
};
i = brace;
} else if src.as_bytes()[i] == b'{' {
let Some(after) = match_brace_group(src, i) else {
continue;
};
i = after;
i = skip_ws(src.as_bytes(), i);
while i < n && src.as_bytes()[i] == b'[' {
match skip_bracket_group(src, i) {
Some(after_bracket) => {
i = after_bracket;
i = skip_ws(src.as_bytes(), i);
}
None => break,
}
}
} else {
let Some(after_cs) = match_control_seq(src, i) else {
continue;
};
i = after_cs;
i = skip_ws(src.as_bytes(), i);
while i < n && src.as_bytes()[i] == b'[' {
match skip_bracket_group(src, i) {
Some(after_bracket) => {
i = after_bracket;
i = skip_ws(src.as_bytes(), i);
}
None => break,
}
}
}
let n_bodies = if is_env { 2 } else { 1 };
for _ in 0..n_bodies {
i = skip_ws(src.as_bytes(), i);
if i >= n || src.as_bytes()[i] != b'{' {
break;
}
let Some(end) = match_brace_group(src, i) else {
break;
};
blank_begin_end_tokens(src, &mut out, i + 1, end - 1);
i = end;
search_from = i;
}
}
String::from_utf8(out)
.expect("neutralization only ever overwrites ASCII bytes with ASCII bytes")
}
fn blank_begin_end_tokens(src: &str, out: &mut [u8], start: usize, end: usize) {
let bytes = src.as_bytes();
let mut i = start;
while i < end {
if bytes[i] == b'\\' {
for word in [&b"begin"[..], &b"end"[..]] {
let word_end = i + 1 + word.len();
if word_end <= end && &bytes[i + 1..word_end] == word {
let after_ws = skip_ws(bytes, word_end);
if after_ws < end && bytes[after_ws] == b'{' {
out[i] = b' ';
break;
}
}
}
}
i += 1;
}
}
pub fn preprocess(src: &str) -> String {
let src = neutralize_macro_definition_bodies(src);
let src = neutralize_verbatim_envs(&src);
neutralize_verbatim_args(&src)
}