use crate::node::Span;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Tok<'src> {
pub(crate) kind: TokKind<'src>,
pub(crate) span: Span,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TokKind<'src> {
ControlWord(&'src str),
ControlSymbol(char),
BeginGroup,
EndGroup,
Sup,
Sub,
AlignTab,
Tie,
MathShift,
Space,
Char(char),
}
pub(crate) fn lex(source: &str) -> Vec<Tok<'_>> {
let bytes = source.as_bytes();
let mut toks = Vec::new();
let mut iter = source.char_indices().peekable();
while let Some((start, ch)) = iter.next() {
match ch {
'%' => {
for (_, c) in iter.by_ref() {
if c == '\n' {
break;
}
}
}
'\\' => {
match iter.peek().copied() {
Some((_, c)) if c.is_ascii_alphabetic() => {
let mut end = start + 1;
while let Some(&(i, c)) = iter.peek() {
if c.is_ascii_alphabetic() {
end = i + c.len_utf8();
iter.next();
} else {
break;
}
}
while let Some(&(_, c)) = iter.peek() {
if c.is_whitespace() {
iter.next();
} else {
break;
}
}
let name = name_slice(source, start + 1, end);
toks.push(Tok {
kind: TokKind::ControlWord(name),
span: Span::new(start, end),
});
}
Some((i, c)) => {
iter.next();
toks.push(Tok {
kind: TokKind::ControlSymbol(c),
span: Span::new(start, i + c.len_utf8()),
});
}
None => {
toks.push(Tok {
kind: TokKind::Char('\\'),
span: Span::new(start, start + 1),
});
}
}
}
'{' => toks.push(single(TokKind::BeginGroup, start, 1)),
'}' => toks.push(single(TokKind::EndGroup, start, 1)),
'^' => toks.push(single(TokKind::Sup, start, 1)),
'_' => toks.push(single(TokKind::Sub, start, 1)),
'&' => toks.push(single(TokKind::AlignTab, start, 1)),
'~' => toks.push(single(TokKind::Tie, start, 1)),
'$' => toks.push(single(TokKind::MathShift, start, 1)),
c if c.is_whitespace() => {
let mut end = start + c.len_utf8();
while let Some(&(i, c2)) = iter.peek() {
if c2.is_whitespace() {
end = i + c2.len_utf8();
iter.next();
} else {
break;
}
}
toks.push(Tok {
kind: TokKind::Space,
span: Span::new(start, end),
});
}
c => {
let _ = bytes;
toks.push(Tok {
kind: TokKind::Char(c),
span: Span::new(start, start + c.len_utf8()),
});
}
}
}
toks
}
#[inline(always)]
fn single(kind: TokKind<'static>, start: usize, len: usize) -> Tok<'static> {
Tok {
kind,
span: Span::new(start, start + len),
}
}
#[inline(always)]
fn name_slice(source: &str, start: usize, end: usize) -> &str {
source.get(start..end).unwrap_or("")
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds<'a>(src: &'a str) -> Vec<TokKind<'a>> {
lex(src).into_iter().map(|t| t.kind).collect()
}
#[test]
fn control_words_consume_trailing_space() {
assert_eq!(
kinds(r"\pi x"),
vec![TokKind::ControlWord("pi"), TokKind::Char('x')]
);
}
#[test]
fn control_word_span_excludes_trailing_space() {
let toks = lex(r"\pi x");
assert_eq!(toks[0].span, Span::new(0, 3));
assert_eq!(toks[1].span, Span::new(4, 5));
}
#[test]
fn control_symbols() {
assert_eq!(
kinds(r"\\ \, \{"),
vec![
TokKind::ControlSymbol('\\'),
TokKind::Space,
TokKind::ControlSymbol(','),
TokKind::Space,
TokKind::ControlSymbol('{'),
]
);
}
#[test]
fn control_space() {
assert_eq!(
kinds(r"a\ b"),
vec![
TokKind::Char('a'),
TokKind::ControlSymbol(' '),
TokKind::Char('b'),
]
);
}
#[test]
fn specials() {
assert_eq!(
kinds("{x}^2_i&~$"),
vec![
TokKind::BeginGroup,
TokKind::Char('x'),
TokKind::EndGroup,
TokKind::Sup,
TokKind::Char('2'),
TokKind::Sub,
TokKind::Char('i'),
TokKind::AlignTab,
TokKind::Tie,
TokKind::MathShift,
]
);
}
#[test]
fn comments_run_to_eol() {
assert_eq!(
kinds("a% comment ^ _ $\nb"),
vec![TokKind::Char('a'), TokKind::Char('b'),]
);
}
#[test]
fn escaped_percent_is_a_control_symbol() {
assert_eq!(
kinds(r"\%b"),
vec![TokKind::ControlSymbol('%'), TokKind::Char('b'),]
);
}
#[test]
fn whitespace_collapses() {
assert_eq!(
kinds("a \t\n b"),
vec![TokKind::Char('a'), TokKind::Space, TokKind::Char('b'),]
);
}
#[test]
fn trailing_backslash_degrades_to_char() {
assert_eq!(kinds("a\\"), vec![TokKind::Char('a'), TokKind::Char('\\')]);
}
#[test]
fn unicode_chars_lex_with_correct_spans() {
let toks = lex("αβ");
assert_eq!(toks.len(), 2);
assert_eq!(toks[0].span, Span::new(0, 2));
assert_eq!(toks[1].span, Span::new(2, 4));
}
}