pub(crate) type Lexed = (char, bool);
pub(crate) fn is_ident(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub(super) fn code_only(text: &str) -> String {
let lexed = lex(text);
let mut out = String::with_capacity(lexed.len());
let mut i = 0;
while i < lexed.len() {
if let Some(end) = test_module_end(&lexed, i) {
i = end;
continue;
}
out.push(lexed[i].0);
i += 1;
}
out
}
pub(crate) fn lex(text: &str) -> Vec<Lexed> {
let s: Vec<char> = text.chars().collect();
let mut out = Vec::with_capacity(s.len());
let mut i = 0;
while i < s.len() {
let next = s.get(i + 1).copied();
let end = match (s[i], next) {
('/', Some('/')) => {
let end = s[i..]
.iter()
.position(|&c| c == '\n')
.map_or(s.len(), |n| i + n);
i = end;
continue;
}
('/', Some('*')) => {
i = block_comment_end(&s, i);
out.push((' ', true));
continue;
}
('"', _) => Some(string_end(&s, i + 1, 0)),
('r', _) => raw_string_start(&s, i).map(|(hashes, body)| string_end(&s, body, hashes)),
('\'', _) => char_literal_end(&s, i),
_ => None,
};
match end {
Some(end) => {
out.extend(s[i..end].iter().map(|&c| (c, false)));
i = end;
}
None => {
out.push((s[i], true));
i += 1;
}
}
}
out
}
fn block_comment_end(s: &[char], at: usize) -> usize {
let mut depth = 0usize;
let mut i = at;
while i + 1 < s.len() {
match (s[i], s[i + 1]) {
('/', '*') => depth += 1,
('*', '/') => {
depth -= 1;
if depth == 0 {
return i + 2;
}
}
_ => {
i += 1;
continue;
}
}
i += 2;
}
s.len()
}
fn string_end(s: &[char], body: usize, hashes: usize) -> usize {
let mut i = body;
while i < s.len() {
if hashes == 0 && s[i] == '\\' {
i += 2;
continue;
}
if s[i] == '"' && (1..=hashes).all(|n| s.get(i + n) == Some(&'#')) {
return i + 1 + hashes;
}
i += 1;
}
s.len()
}
fn raw_string_start(s: &[char], at: usize) -> Option<(usize, usize)> {
let before = at.checked_sub(1).map(|p| s[p]);
let prefixed = match before {
Some('b') => !at.checked_sub(2).is_some_and(|p| is_ident(s[p])),
Some(c) => !is_ident(c),
None => true,
};
if !prefixed {
return None;
}
let hashes = s[at + 1..].iter().take_while(|&&c| c == '#').count();
(s.get(at + 1 + hashes) == Some(&'"')).then_some((hashes, at + 2 + hashes))
}
fn char_literal_end(s: &[char], at: usize) -> Option<usize> {
if s.get(at + 1) == Some(&'\\') {
let close = s.iter().skip(at + 3).take(10).position(|&c| c == '\'')?;
return Some(at + 3 + close + 1);
}
(s.get(at + 1).is_some_and(|&c| c != '\'') && s.get(at + 2) == Some(&'\'')).then_some(at + 3)
}
fn test_module_end(src: &[Lexed], at: usize) -> Option<usize> {
let mut i = eat(src, at, "#[cfg(test)]")?;
loop {
i = skip_ws(src, i);
if eat(src, i, "#[").is_none() {
break;
}
i = matching(src, i + 1, '[', ']')?;
}
if let Some(after) = eat(src, i, "pub") {
i = skip_ws(src, after);
if eat(src, i, "(").is_some() {
i = skip_ws(src, matching(src, i, '(', ')')?);
}
}
let after_mod = eat(src, i, "mod")?;
let name = skip_ws(src, after_mod);
if name == after_mod {
return None;
}
i = name;
while src.get(i).is_some_and(|&(c, code)| code && is_ident(c)) {
i += 1;
}
if i == name {
return None;
}
i = skip_ws(src, i);
match src.get(i) {
Some((';', true)) => Some(i + 1),
Some(('{', true)) => matching(src, i, '{', '}'),
_ => None,
}
}
pub(crate) fn eat(src: &[Lexed], at: usize, needle: &str) -> Option<usize> {
let mut i = at;
for want in needle.chars() {
match src.get(i) {
Some(&(c, true)) if c == want => i += 1,
_ => return None,
}
}
Some(i)
}
pub(crate) fn skip_ws(src: &[Lexed], mut i: usize) -> usize {
while src
.get(i)
.is_some_and(|&(c, code)| code && c.is_whitespace())
{
i += 1;
}
i
}
pub(crate) fn matching(src: &[Lexed], at: usize, open: char, close: char) -> Option<usize> {
let mut depth = 0usize;
for (i, &(c, code)) in src.iter().enumerate().skip(at) {
if code && c == open {
depth += 1;
} else if code && c == close {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(i + 1);
}
}
}
None
}