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::{Measurement, Mode, ParseNode};
use crate::error::ParseError;
use crate::function_registry::{ArgType, FunctionContext, FunctionParser, FunctionSpec};
use crate::settings::TrustContext;

use super::{
    parse_decimal, parse_size_measurement, require_function_arg, trim_ascii_spaces,
    valid_size_unit,
};

pub(crate) fn includegraphics_spec() -> FunctionSpec {
    FunctionSpec {
        names: vec!["\\includegraphics".to_string()],
        num_args: 1,
        num_optional_args: 1,
        arg_types: vec![ArgType::RawArg, ArgType::UrlArg],
        handler: Some(includegraphics_handler),
        ..Default::default()
    }
}

fn graphics_number(text: &str) -> Option<String> {
    let chars: Vec<char> = text.chars().collect();
    let len = chars.len();
    let mut index = 0;
    let mut sign = String::new();
    if index < len && (chars[index] == '+' || chars[index] == '-') {
        sign.push(chars[index]);
        index = 1;
        while index < len && chars[index] == ' ' {
            index += 1;
        }
    }
    let start = index;
    while index < len && chars[index].is_ascii_digit() {
        index += 1;
    }
    let before = index - start;
    let mut after = 0;
    if index < len && chars[index] == '.' {
        index += 1;
        let fraction_start = index;
        while index < len && chars[index].is_ascii_digit() {
            index += 1;
        }
        after = index - fraction_start;
    }
    if index != len || (before == 0 && after == 0) {
        None
    } else {
        let tail: String = chars[start..].iter().collect();
        Some(sign + &tail)
    }
}

fn graphics_size(text: &str) -> Result<Measurement, ParseError> {
    let text = trim_ascii_spaces(text);
    if let Some(number_text) = graphics_number(text) {
        return Ok(Measurement {
            number: parse_decimal(&number_text)?,
            unit: "bp".to_string(),
        });
    }
    match parse_size_measurement(text)? {
        Some(size) if valid_size_unit(&size.unit) => Ok(size),
        Some(size) => Err(ParseError::InvalidArgument {
            message: format!("Invalid unit: '{}' in \\includegraphics.", size.unit),
            loc: None,
        }),
        None => Err(ParseError::InvalidArgument {
            message: format!("Invalid size: '{text}' in \\includegraphics"),
            loc: None,
        }),
    }
}

fn graphics_option_items(value: &str) -> Vec<&str> {
    value.split(',').collect()
}

fn graphics_option_pair(item: &str) -> Option<(String, String)> {
    let parts: Vec<&str> = item.split('=').collect();
    if parts.len() != 2 {
        None
    } else {
        Some((
            trim_ascii_spaces(parts[0]).to_string(),
            trim_ascii_spaces(parts[1]).to_string(),
        ))
    }
}

fn graphics_default_alt(src: &str) -> String {
    let chars: Vec<char> = src.chars().collect();
    let mut name_start = 0usize;
    let mut extension: isize = -1;
    for (index, c) in chars.iter().enumerate() {
        if *c == '/' || *c == '\\' {
            name_start = index + 1;
            extension = -1;
        } else if *c == '.' {
            extension = index as isize;
        }
    }
    if extension < name_start as isize {
        String::new()
    } else {
        chars[name_start..extension as usize].iter().collect()
    }
}

fn parse_graphics_options(
    value: &str,
) -> Result<(Measurement, Measurement, Measurement, String), ParseError> {
    let mut width = Measurement {
        number: 0.0,
        unit: "em".to_string(),
    };
    let mut height = Measurement {
        number: 0.9,
        unit: "em".to_string(),
    };
    let mut totalheight = Measurement {
        number: 0.0,
        unit: "em".to_string(),
    };
    let mut alt = String::new();
    for item in graphics_option_items(value) {
        let Some((key, value)) = graphics_option_pair(item) else {
            continue;
        };
        match key.as_str() {
            "alt" => alt = value,
            "width" => width = graphics_size(&value)?,
            "height" => height = graphics_size(&value)?,
            "totalheight" => totalheight = graphics_size(&value)?,
            _ => {
                return Err(ParseError::InvalidArgument {
                    message: format!("Invalid key: '{key}' in \\includegraphics."),
                    loc: None,
                })
            }
        }
    }
    Ok((width, height, totalheight, alt))
}

fn includegraphics_handler(
    parser: &mut dyn FunctionParser,
    context: &FunctionContext,
    args: &[ParseNode],
    opt_args: &[Option<ParseNode>],
) -> Result<ParseNode, ParseError> {
    let (width, height, totalheight, mut alt) = match opt_args.first().and_then(|o| o.as_ref()) {
        Some(ParseNode::Raw { string, .. }) => parse_graphics_options(string)?,
        Some(_) => {
            return Err(ParseError::InternalInvariant {
                message: "Expected raw graphics options".to_string(),
            })
        }
        None => parse_graphics_options("")?,
    };
    let src = match require_function_arg(args, 0, &context.func_name)? {
        ParseNode::Url { url, .. } => url,
        _ => {
            return Err(ParseError::InternalInvariant {
                message: "Expected URL argument for \\includegraphics".to_string(),
            })
        }
    };
    if alt.is_empty() {
        alt = graphics_default_alt(&src);
    }
    if parser.is_trusted(TrustContext::UrlTrust {
        command: "\\includegraphics".to_string(),
        url: src.clone(),
        protocol: None,
    }) {
        Ok(ParseNode::IncludeGraphics {
            mode: context.mode,
            alt,
            width,
            height,
            totalheight,
            src,
        })
    } else {
        Ok(ParseNode::TextOrd {
            mode: Mode::Text,
            loc: None,
            text: alt,
        })
    }
}