katex-parser 0.1.0

A port of the KaTeX parser: lexing and parsing LaTeX math expressions with macro expansion into a typed AST, plus a Unicode rendering backend
Documentation
use crate::error::ParseError;
use crate::macro_definition::{MacroDefinition, MacroExpansion};
use crate::macro_expander::{MacroExpander, MacroReplacement};
use crate::token::Token;

fn digit_value(text: &str) -> Option<usize> {
    if text.len() != 1 {
        return None;
    }
    let c = text.chars().next().unwrap();
    if c.is_ascii_digit() {
        Some((c as u32 - '0' as u32) as usize)
    } else if ('a'..='f').contains(&c) {
        Some((c as u32 - 'a' as u32 + 10) as usize)
    } else if ('A'..='F').contains(&c) {
        Some((c as u32 - 'A' as u32 + 10) as usize)
    } else {
        None
    }
}

fn char_code_from_token(token: &Token) -> Result<i64, ParseError> {
    if token.text == "EOF" {
        Err(ParseError::InvalidArgument {
            message: "\\char` missing argument".to_string(),
            loc: token.loc.clone(),
        })
    } else if token.text.len() > 1 && token.text.starts_with('\\') {
        Ok(token.text.chars().nth(1).unwrap() as i64)
    } else {
        Ok(token.text.chars().next().unwrap() as i64)
    }
}

fn parse_char_number(
    context: &mut MacroExpander,
    first: &Token,
    base: usize,
) -> Result<i64, ParseError> {
    let Some(value) = digit_value(&first.text).filter(|v| *v < base) else {
        return Err(ParseError::InvalidArgument {
            message: format!("Invalid base-{base} digit {}", first.text),
            loc: first.loc.clone(),
        });
    };
    let mut number = value as i64;
    loop {
        let token = context.future()?;
        match digit_value(&token.text) {
            Some(digit) if digit < base => {
                let _ = context.pop_token()?;
                number = if number > (2147483647 - digit as i64) / base as i64 {
                    2147483647
                } else {
                    number * base as i64 + digit as i64
                };
                continue;
            }
            _ => break,
        }
    }
    Ok(number)
}

pub(crate) fn char_macro(context: &mut MacroExpander) -> Result<MacroReplacement, ParseError> {
    let first = context.pop_token()?;
    let number = if first.text == "`" {
        char_code_from_token(&context.pop_token()?)?
    } else {
        let (base, digit) = if first.text == "'" {
            (8, context.pop_token()?)
        } else if first.text == "\"" {
            (16, context.pop_token()?)
        } else {
            (10, first)
        };
        parse_char_number(context, &digit, base)?
    };
    Ok(MacroReplacement::ReplacementText(format!("\\@char{{{number}}}")))
}

pub(crate) fn parse_argument_count(text: &str) -> Option<usize> {
    let mut number: u64 = 0;
    let mut saw_digit = false;
    let mut trailing_space = false;
    for c in text.chars() {
        if c.is_ascii_digit() && !trailing_space {
            saw_digit = true;
            let digit = c as u32 - '0' as u32;
            number = if number > (2147483647 - digit as u64) / 10 {
                2147483647
            } else {
                number * 10 + digit as u64
            };
        } else if matches!(c, ' ' | '\t' | '\n' | '\r') {
            if saw_digit {
                trailing_space = true;
            }
        } else {
            return None;
        }
    }
    if saw_digit {
        Some(number as usize)
    } else {
        None
    }
}

fn define_command_macro(
    context: &mut MacroExpander,
    exists_ok: bool,
    nonexists_ok: bool,
    skip_if_exists: bool,
) -> Result<MacroReplacement, ParseError> {
    let name_arg = context.consume_arg(None)?.tokens;
    if name_arg.len() != 1 {
        return Err(ParseError::InvalidArgument {
            message: "\\newcommand's first argument must be a macro name".to_string(),
            loc: None,
        });
    }
    let name = name_arg[0].text.clone();
    let exists = context.is_defined(&name);
    if exists && !exists_ok {
        return Err(ParseError::InvalidArgument {
            message: format!(
                "\\newcommand{{{name}}} attempting to redefine {name}; use \\renewcommand"
            ),
            loc: None,
        });
    }
    if !exists && !nonexists_ok {
        return Err(ParseError::InvalidArgument {
            message: format!(
                "\\renewcommand{{{name}}} when command {name} does not yet exist; use \\newcommand"
            ),
            loc: None,
        });
    }
    let mut body = context.consume_arg(None)?.tokens;
    let mut num_args = 0;
    if body.len() == 1 && body[0].text == "[" {
        num_args = parse_optional_arg_count(context)?;
        body = context.consume_arg(None)?.tokens;
    }
    if !(exists && skip_if_exists) {
        context.macros.set(
            name,
            Some(MacroDefinition::expansion(MacroExpansion {
                tokens: body,
                num_args,
                delimiters: None,
                unexpandable: false,
            })),
            false,
        );
    }
    Ok(MacroReplacement::ReplacementText(String::new()))
}

fn parse_optional_arg_count(context: &mut MacroExpander) -> Result<usize, ParseError> {
    let mut builder = String::new();
    loop {
        let token = context.expand_next_token()?;
        if token.text == "]" || token.text == "EOF" {
            break;
        }
        builder.push_str(&token.text);
    }
    let count_text = builder;
    let Some(value) = parse_argument_count(&count_text) else {
        return Err(ParseError::InvalidArgument {
            message: format!("Invalid number of arguments: {count_text}"),
            loc: None,
        });
    };
    Ok(value)
}

pub(crate) fn new_command_macro(context: &mut MacroExpander) -> Result<MacroReplacement, ParseError> {
    define_command_macro(context, false, true, false)
}

pub(crate) fn renew_command_macro(
    context: &mut MacroExpander,
) -> Result<MacroReplacement, ParseError> {
    define_command_macro(context, true, false, false)
}

pub(crate) fn provide_command_macro(
    context: &mut MacroExpander,
) -> Result<MacroReplacement, ParseError> {
    define_command_macro(context, true, true, true)
}