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::ast::{AtomFamily, ParseNode};
use crate::error::ParseError;
use crate::function_registry::{ArgType, FunctionContext, FunctionParser, FunctionSpec};

use super::require_function_arg;

fn delimiter_size(func_name: &str) -> Result<usize, ParseError> {
    match func_name {
        "\\bigl" | "\\bigr" | "\\bigm" | "\\big" => Ok(1),
        "\\Bigl" | "\\Bigr" | "\\Bigm" | "\\Big" => Ok(2),
        "\\biggl" | "\\biggr" | "\\biggm" | "\\bigg" => Ok(3),
        "\\Biggl" | "\\Biggr" | "\\Biggm" | "\\Bigg" => Ok(4),
        _ => Err(ParseError::InternalInvariant {
            message: format!("Unknown delimiter sizing function {func_name}"),
        }),
    }
}

fn delimiter_mclass(func_name: &str) -> Result<AtomFamily, ParseError> {
    match func_name {
        "\\bigl" | "\\Bigl" | "\\biggl" | "\\Biggl" => Ok(AtomFamily::Mopen),
        "\\bigr" | "\\Bigr" | "\\biggr" | "\\Biggr" => Ok(AtomFamily::Mclose),
        "\\bigm" | "\\Bigm" | "\\biggm" | "\\Biggm" => Ok(AtomFamily::Mrel),
        "\\big" | "\\Big" | "\\bigg" | "\\Bigg" => Ok(AtomFamily::Mord),
        _ => Err(ParseError::InternalInvariant {
            message: format!("Unknown delimiter sizing function {func_name}"),
        }),
    }
}

fn delimiter_text(node: &ParseNode) -> Option<String> {
    match node {
        ParseNode::Atom { text, .. }
        | ParseNode::MathOrd { text, .. }
        | ParseNode::TextOrd { text, .. }
        | ParseNode::Spacing { text, .. }
        | ParseNode::AccentToken { text, .. }
        | ParseNode::OperatorToken { text, .. } => Some(text.clone()),
        _ => None,
    }
}

fn is_delimiter(text: &str) -> bool {
    matches!(
        text,
        "("
            | "\\lparen"
            | ")"
            | "\\rparen"
            | "["
            | "\\lbrack"
            | "]"
            | "\\rbrack"
            | "\\{"
            | "\\lbrace"
            | "\\}"
            | "\\rbrace"
            | "\\lfloor"
            | "\\rfloor"
            | "⌊"
            | "⌋"
            | "\\lceil"
            | "\\rceil"
            | "⌈"
            | "⌉"
            | "<"
            | ">"
            | "\\langle"
            | "⟨"
            | "\\rangle"
            | "⟩"
            | "\\lt"
            | "\\gt"
            | "\\lvert"
            | "\\rvert"
            | "\\lVert"
            | "\\rVert"
            | "\\lgroup"
            | "\\rgroup"
            | "⟮"
            | "⟯"
            | "\\lmoustache"
            | "\\rmoustache"
            | "⎰"
            | "⎱"
            | "/"
            | "\\backslash"
            | "|"
            | "\\vert"
            | "\\|"
            | "\\Vert"
            | "\\uparrow"
            | "\\Uparrow"
            | "\\downarrow"
            | "\\Downarrow"
            | "\\updownarrow"
            | "\\Updownarrow"
            | "."
    )
}

fn checked_delimiter(node: &ParseNode, func_name: &str) -> Result<String, ParseError> {
    match delimiter_text(node) {
        Some(text) if is_delimiter(&text) => Ok(text),
        Some(text) => Err(ParseError::InvalidArgument {
            message: format!("Invalid delimiter '{text}' after '{func_name}'"),
            loc: None,
        }),
        None => Err(ParseError::InvalidArgument {
            message: format!("Invalid delimiter type after '{func_name}'"),
            loc: None,
        }),
    }
}

pub(crate) fn delim_sizing_spec() -> FunctionSpec {
    FunctionSpec {
        names: vec![
            "\\bigl".to_string(),
            "\\Bigl".to_string(),
            "\\biggl".to_string(),
            "\\Biggl".to_string(),
            "\\bigr".to_string(),
            "\\Bigr".to_string(),
            "\\biggr".to_string(),
            "\\Biggr".to_string(),
            "\\bigm".to_string(),
            "\\Bigm".to_string(),
            "\\biggm".to_string(),
            "\\Biggm".to_string(),
            "\\big".to_string(),
            "\\Big".to_string(),
            "\\bigg".to_string(),
            "\\Bigg".to_string(),
        ],
        num_args: 1,
        arg_types: vec![ArgType::PrimitiveArg],
        handler: Some(delim_sizing_handler),
        ..Default::default()
    }
}

fn delim_sizing_handler(
    _parser: &mut dyn FunctionParser,
    context: &FunctionContext,
    args: &[ParseNode],
    _opt_args: &[Option<ParseNode>],
) -> Result<ParseNode, ParseError> {
    Ok(ParseNode::DelimSizing {
        mode: context.mode,
        size: delimiter_size(&context.func_name)?,
        mclass: delimiter_mclass(&context.func_name)?,
        delim: checked_delimiter(
            &require_function_arg(args, 0, &context.func_name)?,
            &context.func_name,
        )?,
    })
}

pub(crate) fn left_right_closing_spec() -> FunctionSpec {
    FunctionSpec {
        names: vec!["\\right".to_string()],
        num_args: 1,
        arg_types: vec![ArgType::PrimitiveArg],
        primitive: true,
        handler: Some(left_right_closing_handler),
        ..Default::default()
    }
}

fn left_right_closing_handler(
    parser: &mut dyn FunctionParser,
    context: &FunctionContext,
    args: &[ParseNode],
    _opt_args: &[Option<ParseNode>],
) -> Result<ParseNode, ParseError> {
    Ok(ParseNode::LeftRightRight {
        mode: context.mode,
        delim: checked_delimiter(
            &require_function_arg(args, 0, &context.func_name)?,
            &context.func_name,
        )?,
        color: parser.current_color()?,
    })
}

pub(crate) fn left_right_spec() -> FunctionSpec {
    FunctionSpec {
        names: vec!["\\left".to_string()],
        num_args: 1,
        arg_types: vec![ArgType::PrimitiveArg],
        primitive: true,
        handler: Some(left_right_handler),
        ..Default::default()
    }
}

fn left_right_handler(
    parser: &mut dyn FunctionParser,
    context: &FunctionContext,
    args: &[ParseNode],
    _opt_args: &[Option<ParseNode>],
) -> Result<ParseNode, ParseError> {
    parser.parse_left_right(&checked_delimiter(
        &require_function_arg(args, 0, &context.func_name)?,
        &context.func_name,
    )?)
}

pub(crate) fn middle_spec() -> FunctionSpec {
    FunctionSpec {
        names: vec!["\\middle".to_string()],
        num_args: 1,
        arg_types: vec![ArgType::PrimitiveArg],
        primitive: true,
        handler: Some(middle_handler),
        ..Default::default()
    }
}

fn middle_handler(
    parser: &mut dyn FunctionParser,
    context: &FunctionContext,
    args: &[ParseNode],
    _opt_args: &[Option<ParseNode>],
) -> Result<ParseNode, ParseError> {
    let delim = checked_delimiter(
        &require_function_arg(args, 0, &context.func_name)?,
        &context.func_name,
    )?;
    if !parser.in_left_right() {
        return Err(ParseError::InvalidArgument {
            message: "\\middle without preceding \\left".to_string(),
            loc: None,
        });
    }
    Ok(ParseNode::Middle {
        mode: context.mode,
        delim,
    })
}