Skip to main content

latexml/util/
preset.rs

1//! Reusable engine presets that don't depend on the test harness.
2//!
3//! Extracted from `util/test.rs` (audit DEP-02, 2026-05-18) so the
4//! `latexmlmath_oxide` standalone binary can build without the test
5//! harness's `glob`/`phf` dependencies. The two helpers here have no
6//! ties to the harness machinery — they just construct a minimal
7//! `Core` and tokenize a single inline formula.
8
9use latexml_core::{Core, CoreOptions, document::Document, state};
10use latexml_math_parser::node_to_grammar_lexemes;
11use libxml::tree::Node;
12
13use crate::core_interface::DigestionAPI;
14
15/// Provide a default `Core` engine preloaded with `article.cls`
16/// and `amsmath.sty` — the minimum needed to digest most formulae.
17pub fn new_test_engine() -> Core {
18  let core_engine = Core::new(CoreOptions {
19    // `LaTeX.pool` FIRST — a class preload must not be the thing that drags the
20    // pool in. `\@pushfilename` changes meaning when the pool (and the kernel dump
21    // behind it) loads: preloading `article.cls` alone pushes the class's filename
22    // with the pre-pool `\@pushfilename` (which never touches
23    // `\g__hook_name_stack_seq`), then pops it with the real expl3 `\@popfilename`
24    // that the pool installed — popping a seq that only ever saw the *inner*
25    // packages' pushes. Hence "LaTeX hooks Error: Extra \PopDefaultHookLabel" on
26    // every latexmlmath_oxide run. Same order ar5iv's preload list uses.
27    // See SYNC_STATUS "`--preload=<cls>` hook-stack imbalance" — the underlying
28    // ordering bug is still open; this preset just stops provoking it.
29    preload: Some(
30      ["LaTeX.pool", "article.cls", "amsmath.sty"]
31        .map(|x| x.to_string())
32        .to_vec(),
33    ),
34    verbosity: Some(-2),
35    search_paths: None,
36    nomathparse: Some(true),
37    include_comments: Some(false),
38    ..CoreOptions::default()
39  });
40  // Shared model loader — see crate::load_latexml_default_model.
41  crate::load_latexml_default_model();
42  state::set_bindings_dispatch(latexml_core::common::native_dispatcher(
43    latexml_package::dispatch,
44  ));
45  state::add_binding_names(latexml_package::binding_names());
46  core_engine
47}
48
49/// Simple tokenization of a single formula, without any custom preloads
50/// beyond latex and amsmath.
51pub fn lex_single_tex_formula(
52  tex: &str,
53  latexml: &mut Core,
54) -> (Vec<String>, Vec<Node>, Option<Node>, Document) {
55  let xml_result = latexml.convert_file(format!("literal:\\[ {tex} \\]"));
56  assert!(xml_result.is_ok(), "{:?}", xml_result.err());
57  let mut doc = xml_result.unwrap();
58
59  match doc.findnode("//*[local-name()='XMath']", None) {
60    Some(math) => {
61      let mut idx = 0;
62      let (lexemes, nodes) = node_to_grammar_lexemes(&math, &mut idx);
63      (lexemes, nodes, Some(math), doc)
64    },
65    None => (Vec::new(), Vec::new(), None, doc),
66  }
67}