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}