use core::fmt;
use core::iter::Peekable;
use core::str::Chars;
use crate::error::ParseError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Token {
Char(char),
Command(String),
BeginGroup,
EndGroup,
Superscript,
Subscript,
AlignmentTab,
MathShift,
DisplayShift,
Space,
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Char(c) => write!(f, "char:{c}"),
Self::Command(name) => write!(f, "cmd:{name}"),
Self::BeginGroup => f.write_str("{"),
Self::EndGroup => f.write_str("}"),
Self::Superscript => f.write_str("^"),
Self::Subscript => f.write_str("_"),
Self::AlignmentTab => f.write_str("&"),
Self::MathShift => f.write_str("$"),
Self::DisplayShift => f.write_str("$$"),
Self::Space => f.write_str("space"),
}
}
}
#[must_use]
pub fn format_tokens(tokens: &[Token]) -> String {
let mut out = String::new();
for (i, t) in tokens.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(&t.to_string());
}
out
}
pub fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
let mut chars = input.chars().peekable();
let mut out = Vec::new();
while let Some(c) = chars.next() {
match c {
'%' => skip_line(&mut chars),
' ' => out.push(Token::Space),
'\t' | '\n' | '\r' => {}
'{' => out.push(Token::BeginGroup),
'}' => out.push(Token::EndGroup),
'^' => out.push(Token::Superscript),
'_' => out.push(Token::Subscript),
'&' => out.push(Token::AlignmentTab),
'$' => {
if chars.peek() == Some(&'$') {
chars.next();
out.push(Token::DisplayShift);
} else {
out.push(Token::MathShift);
}
}
'\\' => out.push(command(&mut chars)?),
other => out.push(Token::Char(other)),
}
}
Ok(out)
}
fn skip_line(chars: &mut Peekable<Chars<'_>>) {
for c in chars.by_ref() {
if c == '\n' {
break;
}
}
}
fn command(chars: &mut Peekable<Chars<'_>>) -> Result<Token, ParseError> {
let Some(&first) = chars.peek() else {
return Err(ParseError::TrailingBackslash);
};
if first.is_ascii_alphabetic() {
let mut name = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_alphabetic() {
name.push(c);
chars.next();
} else {
break;
}
}
while matches!(chars.peek(), Some(' ' | '\t' | '\n' | '\r')) {
chars.next();
}
Ok(Token::Command(name))
} else {
chars.next();
Ok(Token::Command(first.to_string()))
}
}