Skip to main content

latexml_core/
common.rs

1pub mod arena;
2pub mod color;
3#[macro_use]
4pub mod error;
5pub mod cleaners;
6pub mod def_parser;
7pub mod dimension;
8pub mod float;
9pub mod font;
10pub mod glue;
11pub mod ligature;
12pub mod local_assignments;
13pub mod locator;
14pub mod mathchar;
15pub mod model;
16pub mod mudimension;
17pub mod muglue;
18pub mod number;
19pub mod numeric_ops;
20pub mod object;
21pub mod pair;
22pub mod relaxng;
23pub mod store;
24pub mod xml;
25
26use std::rc::Rc;
27
28use crate::{common::error::*, fmt};
29
30#[derive(Clone, Debug)]
31pub enum InputFormat {
32  TeX,
33  Bib,
34}
35#[derive(Clone, Debug)]
36pub enum OutputFormat {
37  TeX,
38  Box,
39  XML,
40  HTML5,
41  XHTML,
42}
43#[derive(Clone, Debug)]
44pub enum DataSize {
45  Math,
46  Fragment,
47  Document,
48  Archive,
49}
50
51#[derive(Clone, Debug)]
52pub enum DigestionMode {
53  TeX,
54  LaTeX,
55  AmSTeX,
56  BibTeX,
57}
58impl fmt::Display for DigestionMode {
59  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60    use self::DigestionMode::*;
61    let formatted = match *self {
62      TeX => "TeX",
63      LaTeX => "LaTeX",
64      AmSTeX => "AmSTeX",
65      BibTeX => "BibTeX",
66    };
67    write!(f, "{formatted}")
68  }
69}
70
71impl DigestionMode {
72  pub fn extension(&self) -> String {
73    match *self {
74      DigestionMode::TeX | DigestionMode::LaTeX | DigestionMode::AmSTeX => "tex",
75      DigestionMode::BibTeX => "bib",
76    }
77    .to_string()
78  }
79}
80
81/// A compiled-binding dispatcher: given a request name, either declines
82/// (`None`, so the next resolution tier is tried) or reports the load outcome
83/// (`Some(Ok(()))` / `Some(Err(_))`). It resolves *compiled-in* bindings, which
84/// have no source file, so it carries no path — the load note announces such a
85/// binding by its module-proxy name (`<name>_sty.rs`).
86pub type BindingDispatcher = Rc<dyn Fn(&str) -> Option<Result<()>>>;
87
88/// The on-disk source a binding was loaded from, when it has one: `Some(path)`
89/// for a runtime *file* binding (e.g. a `.rhai` script), `None` for a
90/// compiled-in binding. Threaded through the resolving dispatcher's result so
91/// the "(Loading …)" note can name the real path (#560) — rather than a State
92/// side-channel.
93pub type BindingSource = Option<String>;
94
95/// The **resolving** binding dispatcher installed as the single, ordered
96/// resolution chain (see `converter::install_binding_dispatch`). Unlike a bare
97/// [`BindingDispatcher`], a successful load reports the [`BindingSource`] it
98/// loaded from, so a file binding announces its real path and a compiled one
99/// its proxy name.
100pub type ResolvingBindingDispatcher = Rc<dyn Fn(&str) -> Option<Result<BindingSource>>>;
101
102/// Lift a **native** (compiled-in) binding dispatcher — one with no source file
103/// to name — into a [`ResolvingBindingDispatcher`]: a successful load reports
104/// `None` source, so the caller announces it by its module-proxy name. Pairs
105/// with the runtime `rhai_dispatch` tier, which reports the real `.rhai` path.
106pub fn native_dispatcher(
107  f: impl Fn(&str) -> Option<Result<()>> + 'static,
108) -> ResolvingBindingDispatcher {
109  Rc::new(move |request| f(request).map(|r| r.map(|()| None)))
110}
111
112/// Perl: LABEL_MAPPING_HOOK => sub { ($label, $ctr, $norefnum) => ($refnum, $id) }
113/// Returns (optional refnum string, optional id string)
114pub type LabelMappingHook = Rc<dyn Fn(&str, &str, bool) -> (Option<String>, Option<String>)>;
115
116#[derive(Clone)]
117pub struct Config {
118  pub verbosity:               i32,
119  pub format:                  OutputFormat,
120  pub whatsin:                 DataSize,
121  pub whatsout:                DataSize,
122  pub preamble:                Option<String>,
123  pub postamble:               Option<String>,
124  pub mode:                    Option<DigestionMode>,
125  pub bindings_dispatch:       Option<BindingDispatcher>,
126  pub extra_bindings_dispatch: Option<BindingDispatcher>,
127  /// Packages to preload before processing (e.g. --preload=ar5iv.sty)
128  pub preload:                 Option<Vec<String>>,
129  /// Additional search paths for finding packages/inputs (e.g. --path=dir)
130  pub search_paths:            Option<Vec<String>>,
131  /// Whether to include XML comments in output (--nocomments sets false)
132  pub include_comments:        Option<bool>,
133  /// Strict error-reporting (`--strict`; State `STRICT`, Perl Core.pm L43)
134  pub strict:                  Option<bool>,
135  /// Raw-load `.sty` AND `.cls` sources (`--includestyles`). WARNING: one flag
136  /// enables raw TeX loading of both packages and classes — it sets State
137  /// `INCLUDE_STYLES` and `INCLUDE_CLASSES` together (Perl Core.pm L55-57).
138  pub include_styles:          Option<bool>,
139  /// Whether to skip math parsing (--nomathparse)
140  pub nomathparse:             Option<bool>,
141  /// Whether to track + emit source locators (`--source-map`). Off by
142  /// default; gates both per-token start capture and per-element
143  /// `data-sourcepos` stamping. See `docs/performance/SOURCE_PROVENANCE.md`.
144  pub source_map:              Option<bool>,
145  /// Input encoding for translating source bytes to LaTeXML's internal UTF-8
146  /// (`--inputencoding`; Perl Config.pm L57, Core.pm L60-61 which assigns
147  /// State `PERL_INPUT_ENCODING`, default utf-8). Consumed by the Mouth's
148  /// per-line decode; only affects byte→UTF-8 translation, never catcodes
149  /// (for those, use the inputenc package). `None` ⇒ the utf-8 default.
150  pub inputencoding:           Option<String>,
151  /// Streaming (fragmented) core conversion: `Some(budget)` interleaves
152  /// digest→build in fragments of ~`budget` boxes, spilling closed subtrees
153  /// to disk so peak RSS is bounded by fragment size instead of document
154  /// size. `None` (default) = the eager path — also the Perl-parity path
155  /// (Perl is strictly digest-all→build-all; interleaving is a sanctioned
156  /// divergence, activated only by the `--streaming` flag or when the
157  /// projected need exceeds the memory cap). The budget is a box count on
158  /// the same per-box basis as the box-list guards.
159  pub streaming:               Option<usize>,
160}
161impl Default for Config {
162  fn default() -> Self {
163    Config {
164      verbosity:               1,
165      format:                  OutputFormat::XML,
166      whatsin:                 DataSize::Document,
167      whatsout:                DataSize::Document,
168      preamble:                None,
169      postamble:               None,
170      mode:                    Some(DigestionMode::LaTeX),
171      bindings_dispatch:       None,
172      extra_bindings_dispatch: None,
173      preload:                 None,
174      search_paths:            None,
175      include_comments:        None,
176      strict:                  None,
177      include_styles:          None,
178      nomathparse:             None,
179      source_map:              None,
180      inputencoding:           None,
181      streaming:               None,
182    }
183  }
184}
185
186#[cfg(test)]
187mod tests {
188  use super::*;
189
190  #[test]
191  fn digestion_mode_display() {
192    assert_eq!(format!("{}", DigestionMode::TeX), "TeX");
193    assert_eq!(format!("{}", DigestionMode::LaTeX), "LaTeX");
194    assert_eq!(format!("{}", DigestionMode::AmSTeX), "AmSTeX");
195    assert_eq!(format!("{}", DigestionMode::BibTeX), "BibTeX");
196  }
197
198  #[test]
199  fn digestion_mode_extension() {
200    assert_eq!(DigestionMode::TeX.extension(), "tex");
201    assert_eq!(DigestionMode::LaTeX.extension(), "tex");
202    assert_eq!(DigestionMode::AmSTeX.extension(), "tex");
203    assert_eq!(DigestionMode::BibTeX.extension(), "bib");
204  }
205
206  #[test]
207  fn config_default_fields() {
208    let c = Config::default();
209    assert_eq!(c.verbosity, 1);
210    assert!(matches!(c.format, OutputFormat::XML));
211    assert!(matches!(c.whatsin, DataSize::Document));
212    assert!(matches!(c.whatsout, DataSize::Document));
213    assert!(c.preamble.is_none());
214    assert!(c.postamble.is_none());
215    assert!(matches!(c.mode, Some(DigestionMode::LaTeX)));
216    assert!(c.bindings_dispatch.is_none());
217    assert!(c.extra_bindings_dispatch.is_none());
218    assert!(c.preload.is_none());
219    assert!(c.search_paths.is_none());
220    assert!(c.include_comments.is_none());
221    assert!(c.nomathparse.is_none());
222    assert!(c.source_map.is_none());
223  }
224
225  #[test]
226  fn config_clone_preserves_fields() {
227    let c = Config {
228      verbosity: 5,
229      preload: Some(vec!["ar5iv.sty".to_string()]),
230      ..Default::default()
231    };
232    let c2 = c.clone();
233    assert_eq!(c2.verbosity, 5);
234    assert_eq!(c2.preload.as_ref().unwrap(), &vec!["ar5iv.sty".to_string()]);
235  }
236
237  #[test]
238  fn input_format_variants() {
239    // Debug trait at minimum is derived; Clone too.
240    let _ = InputFormat::TeX;
241    let _ = InputFormat::Bib;
242    let cloned = InputFormat::TeX.clone();
243    assert!(matches!(cloned, InputFormat::TeX));
244  }
245
246  #[test]
247  fn output_format_variants() {
248    let _ = OutputFormat::TeX;
249    let _ = OutputFormat::Box;
250    let _ = OutputFormat::XML;
251    let _ = OutputFormat::HTML5;
252    let _ = OutputFormat::XHTML;
253    let cloned = OutputFormat::XML.clone();
254    assert!(matches!(cloned, OutputFormat::XML));
255  }
256
257  #[test]
258  fn data_size_variants() {
259    let _ = DataSize::Math;
260    let _ = DataSize::Fragment;
261    let _ = DataSize::Document;
262    let _ = DataSize::Archive;
263    let cloned = DataSize::Document.clone();
264    assert!(matches!(cloned, DataSize::Document));
265  }
266}