1mod atof;
29mod character_class;
30mod color_defs;
31mod commands;
32mod environments;
33mod error;
34mod html_utils;
35mod lexer;
36mod parser;
37mod predefined;
38mod specifications;
39mod text_parser;
40mod token;
41mod token_queue;
42
43use std::num::NonZeroU16;
44
45use rustc_hash::{FxBuildHasher, FxHashMap};
46#[cfg(feature = "serde")]
47use serde::{Deserialize, Serialize};
48
49use mathml_renderer::{arena::Arena, ast::Node, fmt::new_line_and_indent};
50
51pub use self::error::LatexError;
52use self::{error::LatexErrKind, lexer::Lexer, parser::Parser, token::Token};
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum MathDisplay {
57 Inline,
59 Block,
61}
62
63#[derive(Debug, Clone, Copy, Default)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
70#[non_exhaustive]
71pub enum PrettyPrint {
72 #[default]
74 Never,
75 Always,
77 Auto,
79}
80
81#[derive(Debug, Default)]
110#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
111#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
112pub struct MathCoreConfig {
113 pub pretty_print: PrettyPrint,
115 #[cfg_attr(feature = "serde", serde(with = "tuple_vec_map"))]
117 pub macros: Vec<(String, String)>,
118 pub xml_namespace: bool,
120 pub ignore_unknown_commands: bool,
123 pub annotation: bool,
126}
127
128#[derive(Debug, Default)]
129struct CommandConfig {
130 custom_cmd_tokens: Vec<Token<'static>>,
131 custom_cmd_map: FxHashMap<String, (u8, (usize, usize))>,
132 ignore_unknown_commands: bool,
133}
134
135impl CommandConfig {
136 pub fn get_command<'config>(&'config self, command: &str) -> Option<Token<'config>> {
137 let (num_args, slice) = *self.custom_cmd_map.get(command)?;
138 let tokens = self.custom_cmd_tokens.get(slice.0..slice.1)?;
139 Some(Token::CustomCmd(num_args, tokens))
140 }
141}
142
143#[derive(Debug, Default)]
145struct Flags {
146 pretty_print: PrettyPrint,
147 xml_namespace: bool,
148 annotation: bool,
149}
150
151impl From<&MathCoreConfig> for Flags {
152 fn from(config: &MathCoreConfig) -> Self {
153 Self {
155 pretty_print: config.pretty_print,
156 xml_namespace: config.xml_namespace,
157 annotation: config.annotation,
158 }
159 }
160}
161
162#[derive(Debug, Default)]
164pub struct LatexToMathML {
165 flags: Flags,
166 equation_count: u16,
168 label_map: FxHashMap<Box<str>, NonZeroU16>,
169 cmd_cfg: Option<CommandConfig>,
170}
171
172impl LatexToMathML {
173 pub fn new(config: MathCoreConfig) -> Result<Self, (Box<LatexError>, usize, String)> {
179 Ok(Self {
180 flags: Flags::from(&config),
181 equation_count: 0,
182 label_map: FxHashMap::default(),
183 cmd_cfg: Some(parse_custom_commands(
184 config.macros,
185 config.ignore_unknown_commands,
186 )?),
187 })
188 }
189
190 pub fn convert_with_global_counter(
199 &mut self,
200 latex: &str,
201 display: MathDisplay,
202 ) -> Result<String, Box<LatexError>> {
203 convert(
204 latex,
205 display,
206 self.cmd_cfg.as_ref(),
207 &mut self.equation_count,
208 &mut self.label_map,
209 &self.flags,
210 )
211 }
212
213 #[inline]
232 pub fn convert_with_local_counter(
233 &self,
234 latex: &str,
235 display: MathDisplay,
236 ) -> Result<String, Box<LatexError>> {
237 let mut equation_count = 0;
238 let mut label_map = FxHashMap::default();
239 convert(
240 latex,
241 display,
242 self.cmd_cfg.as_ref(),
243 &mut equation_count,
244 &mut label_map,
245 &self.flags,
246 )
247 }
248
249 pub fn reset_global_counter(&mut self) {
253 self.equation_count = 0;
254 }
255}
256
257fn convert(
258 latex: &str,
259 display: MathDisplay,
260 cmd_cfg: Option<&CommandConfig>,
261 equation_count: &mut u16,
262 label_map: &mut FxHashMap<Box<str>, NonZeroU16>,
263 flags: &Flags,
264) -> Result<String, Box<LatexError>> {
265 let arena = Arena::new();
266 let ast = parse(latex, &arena, cmd_cfg, equation_count, label_map)?;
267
268 let mut output = String::new();
269 output.push_str("<math");
270 if flags.xml_namespace {
271 output.push_str(" xmlns=\"http://www.w3.org/1998/Math/MathML\"");
272 }
273 if matches!(display, MathDisplay::Block) {
274 output.push_str(" display=\"block\"");
275 }
276 output.push('>');
277
278 let pretty_print = matches!(flags.pretty_print, PrettyPrint::Always)
279 || (matches!(flags.pretty_print, PrettyPrint::Auto) && display == MathDisplay::Block);
280
281 let base_indent = if pretty_print { 1 } else { 0 };
282 if flags.annotation {
283 let children_indent = if pretty_print { 2 } else { 0 };
284 new_line_and_indent(&mut output, base_indent);
285 output.push_str("<semantics>");
286 let node = parser::node_vec_to_node(&arena, &ast, false);
287 let _ = node.emit(&mut output, children_indent);
288 new_line_and_indent(&mut output, children_indent);
289 output.push_str("<annotation encoding=\"application/x-tex\">");
290 html_utils::escape_html_content(&mut output, latex);
291 output.push_str("</annotation>");
292 new_line_and_indent(&mut output, base_indent);
293 output.push_str("</semantics>");
294 } else {
295 for node in ast {
296 let _ = node.emit(&mut output, base_indent);
301 }
302 }
303 if pretty_print {
304 output.push('\n');
305 }
306 output.push_str("</math>");
307 Ok(output)
308}
309
310fn parse<'config, 'source, 'arena>(
311 latex: &'source str,
312 arena: &'arena Arena,
313 cmd_cfg: Option<&'config CommandConfig>,
314 equation_count: &'arena mut u16,
315 label_map: &'arena mut FxHashMap<Box<str>, NonZeroU16>,
316) -> Result<Vec<&'arena Node<'arena>>, Box<LatexError>>
317where
318 'config: 'source,
319 'source: 'arena,
320{
321 let lexer = Lexer::new(latex, false, cmd_cfg);
322 let mut p = Parser::new(lexer, arena, equation_count, label_map)?;
323 let nodes = p.parse()?;
324 Ok(nodes)
325}
326
327fn parse_custom_commands(
328 macros: Vec<(String, String)>,
329 ignore_unknown_commands: bool,
330) -> Result<CommandConfig, (Box<LatexError>, usize, String)> {
331 let mut map = FxHashMap::with_capacity_and_hasher(macros.len(), FxBuildHasher);
332 let mut tokens = Vec::new();
333 for (idx, (name, definition)) in macros.into_iter().enumerate() {
334 if !is_valid_macro_name(name.as_str()) {
335 return Err((
336 Box::new(LatexError(0..0, LatexErrKind::InvalidMacroName(name))),
337 idx,
338 definition,
339 ));
340 }
341
342 let value = 'value: {
346 let mut lexer: Lexer<'static, '_> = Lexer::new(definition.as_str(), true, None);
347 let start = tokens.len();
348 loop {
349 match lexer.next_token_no_unknown_command() {
350 Ok(tokloc) => {
351 if matches!(tokloc.token(), Token::Eoi) {
352 break;
353 }
354 tokens.push(tokloc.into_token());
355 }
356 Err(err) => {
357 break 'value Err(err);
358 }
359 }
360 }
361 let end = tokens.len();
362 let num_args = lexer.parse_cmd_args().unwrap_or(0);
363 Ok((num_args, (start, end)))
364 };
365
366 match value {
367 Err(err) => {
368 return Err((err, idx, definition));
369 }
370 Ok(v) => {
371 map.insert(name, v);
372 }
373 }
374 }
375 Ok(CommandConfig {
376 custom_cmd_tokens: tokens,
377 custom_cmd_map: map,
378 ignore_unknown_commands,
379 })
380}
381
382fn is_valid_macro_name(s: &str) -> bool {
383 if s.is_empty() {
384 return false;
385 }
386 let mut chars = s.chars();
387 match (chars.next(), chars.next()) {
388 (Some(_), None) => true,
390 _ => s.bytes().all(|b| b.is_ascii_alphabetic()),
392 }
393}