#[allow(clippy::module_inception)]
pub mod engine;
pub mod lexer;
pub mod primitives;
pub mod token;
pub mod utils;
pub use engine::{Engine, MacroDb, MacroDef};
pub use lexer::{detokenize, tokenize, Lexer};
pub use primitives::{parse_definitions, DefinitionKind};
pub use token::{TexToken, TokenList};
#[derive(Debug, Clone)]
pub enum EngineWarning {
DepthExceeded { max_depth: usize },
TokenLimitExceeded { max_tokens: usize },
ArgumentParsingFailed {
macro_name: String,
error_kind: ArgumentErrorType,
},
LaTeX3Skipped { token_count: usize },
UnsupportedPrimitive { name: String },
LetTargetNotFound { name: String, target: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgumentErrorType {
RunawayArgument,
PatternMismatch,
Other(String),
}
impl std::fmt::Display for ArgumentErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RunawayArgument => write!(f, "Runaway argument"),
Self::PatternMismatch => write!(f, "Pattern mismatch"),
Self::Other(msg) => write!(f, "{}", msg),
}
}
}
impl EngineWarning {
pub fn message(&self) -> String {
match self {
EngineWarning::DepthExceeded { max_depth } => {
format!(
"Macro expansion depth exceeded maximum ({}). Possible infinite recursion.",
max_depth
)
}
EngineWarning::TokenLimitExceeded { max_tokens } => {
format!(
"Macro expansion produced too many tokens (exceeded {}). Possible infinite loop or exponential expansion.",
max_tokens
)
}
EngineWarning::ArgumentParsingFailed {
macro_name,
error_kind,
} => {
format!(
"Macro '\\{}' argument parsing failed: {}",
macro_name, error_kind
)
}
EngineWarning::LaTeX3Skipped { token_count } => {
format!(
"LaTeX3 block (\\ExplSyntaxOn ... \\ExplSyntaxOff) skipped ({} tokens). \
LaTeX3/expl3 syntax is not supported.",
token_count
)
}
EngineWarning::UnsupportedPrimitive { name } => {
format!(
"Unsupported TeX primitive '\\{}' encountered. \
This may produce incorrect output.",
name
)
}
EngineWarning::LetTargetNotFound { name, target } => {
format!(
"\\let\\{}\\{}: target '\\{}' not found in macro database. \
Built-in LaTeX commands cannot be copied with \\let.",
name, target, target
)
}
}
}
}
impl std::fmt::Display for EngineWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message())
}
}
#[derive(Debug, Clone)]
pub struct ExpandResult {
pub output: String,
pub warnings: Vec<EngineWarning>,
}
pub fn expand_latex(input: &str) -> String {
let mut engine = Engine::new();
let tokens = tokenize(input);
let expanded = engine.process(tokens);
detokenize(&expanded)
}
pub fn expand_latex_with_warnings(input: &str, math_mode: bool) -> ExpandResult {
let mut engine = if math_mode {
Engine::new_math_mode()
} else {
Engine::new()
};
let tokens = tokenize(input);
let expanded = engine.process(tokens);
let warnings = engine.take_structured_warnings();
let output = detokenize(&expanded);
ExpandResult { output, warnings }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_latex_simple() {
let input = r"\newcommand{\hello}{world} \hello";
let output = expand_latex(input);
assert!(output.contains("world"));
assert!(!output.contains(r"\hello"));
}
#[test]
fn test_expand_latex_with_args() {
let input = r"\newcommand{\pair}[2]{\langle #1, #2\rangle} \pair{a}{b}";
let output = expand_latex(input);
assert!(output.contains(r"\langle a, b\rangle"));
}
#[test]
fn test_expand_latex_nested_braces() {
let input = r"\newcommand{\pair}[2]{\langle #1, #2\rangle} \pair{a^2}{\frac{\pi}{2}}";
let output = expand_latex(input);
assert!(output.contains(r"\langle a^2, \frac{\pi}{2}\rangle"));
}
#[test]
fn test_expand_latex_recursive() {
let input = r"\newcommand{\double}[1]{#1#1} \double{x}";
let output = expand_latex(input);
assert!(output.contains("xx"));
}
#[test]
fn test_expand_latex_with_warnings_success() {
let input = r"\newcommand{\x}{y} \x";
let result = expand_latex_with_warnings(input, false);
assert!(result.output.contains("y"));
assert!(result.warnings.is_empty());
}
#[test]
fn test_expand_latex_with_warnings_math_mode() {
let input = r"\newcommand{\x}{\ifmmode MATH\else TEXT\fi} \x";
let result_text = expand_latex_with_warnings(input, false);
assert!(
result_text.output.contains("TEXT"),
"Expected TEXT in: {}",
result_text.output
);
let result_math = expand_latex_with_warnings(input, true);
assert!(
result_math.output.contains("MATH"),
"Expected MATH in: {}",
result_math.output
);
}
#[test]
fn test_expand_latex_blackboard_bold() {
let input = r"\newcommand{\R}{\mathbb{R}} \R";
let output = expand_latex(input);
assert!(
output.contains(r"\mathbb{R}"),
"Expected \\mathbb{{R}} in: {}",
output
);
}
}