Skip to main content

latexml_core/
lib.rs

1//! The Core of latexml - roughly the equivalent of TeX conversion.
2#![feature(thread_local)]
3#![allow(missing_docs)]
4#![allow(clippy::invisible_characters)] // Font metrics contain real zero-width Unicode chars from TFM files
5extern crate rustc_hash;
6
7/// Auxiliary macros
8#[macro_use]
9pub mod aux_macros;
10/// The Token and its Catcode
11#[macro_use]
12pub mod token;
13/// Common abstractions that are useful at various stages of Core processing
14#[macro_use]
15pub mod common;
16pub mod cycle_guard;
17pub mod stack_guard;
18/// Grouping Tokens together
19#[macro_use]
20pub mod tokens;
21/// The programmable API foundation for creating bindings of LaTeX sty/cls libraries
22#[macro_use]
23pub mod binding;
24/// Support for TeX-like Alignments
25pub mod alignment;
26/// TeX comments as standalone objects
27pub mod comment;
28/// All possible definitions for TeX-native commands (expandable, primitive, constructor,...)
29pub mod definition;
30/// a shared interface for digested objects
31pub mod digested;
32/// An abstraction layer over the converted XML document
33pub mod document;
34/// The Gullet is responsible for reading Tokens and other data from the Mouth
35pub mod gullet;
36/// A LaTeX-like Key-Value object
37pub mod keyval;
38/// A collection of Key-Value objects, typically from a single LaTeX argument
39pub mod keyvals;
40/// Rules for combining together characters and other text rules
41pub mod ligature;
42/// A list of `Digested` objects
43pub mod list;
44/// The mouth is a thin interface over a file, responsible for reading characters and associating
45/// them with catcodes
46pub mod mouth;
47/// The abstraction layer used by the Gullet to read arguments for the various kinds of TeX object
48/// definitions
49pub mod parameter;
50/// Rules for rewriting the constructed XML document, after core processing has completed
51pub mod rewrite;
52/// A global, singleton, mutable state - hosts almost all TeX-facing runtime information for the
53/// conversion
54#[macro_use]
55pub mod state;
56/// Code generator: dump file → compiled Rust module
57pub mod dump_codegen;
58/// Reader for Rust-native kernel dump files
59pub mod dump_reader;
60/// Writer for Rust-native kernel dump files
61pub mod dump_writer;
62/// The stomach is an abstraction responsible for digesting `Tokens` and `Register`s prepared by the
63/// Gullet into Boxes
64#[macro_use]
65pub mod stomach;
66
67/// Miri soundness model for the `runtime-bindings` re-entrant trampoline (PR #248
68/// B1). Test-only; the real path is libxml2-backed and cannot be Miri-checked.
69#[cfg(test)]
70mod runtime_bindings_reentrancy_model;
71/// Streaming XML substrate for fragmented (bounded-memory) conversion.
72pub mod sxml;
73/// A TeX-like digested Box
74pub mod tbox;
75/// Per-job structured telemetry: phase wall times, counts, resource peaks.
76/// See `docs/performance/TELEMETRY.md` for the design contract.
77pub mod telemetry;
78/// Auxilary utilities that do not participate in the main conversion abstraction
79pub mod util;
80/// Main-level wall-clock watchdog that forcibly aborts the process after a deadline.
81/// Complements the cooperative `stomach::check_timeout` polling for native-code hotspots
82/// (Marpa, libxml2, libxslt) that don't return to the digestion loop.
83pub mod watchdog;
84/// A TeX-like digested Whatsit
85pub mod whatsit;
86
87use std::{borrow::Cow, fmt, rc::Rc};
88
89use libxml::tree::Node;
90use once_cell::sync::Lazy;
91/// Initialize libxml2 for thread safety. Must be called before any libxml2
92/// operations that don't go through `libxml::parser::Parser`. Delegates to
93/// the safe wrapper in rust-libxml, which uses its own `std::sync::Once` to
94/// guarantee exactly-once initialisation even across threads.
95///
96/// See: <https://dev.w3.org/XInclude-Test-Suite/libxml2-2.4.24/doc/threads.html>
97pub fn ensure_libxml_init() { libxml::init_parser(); }
98
99/// Free this thread's accumulated engine state — the three `State`
100/// singletons (`STATE`, `STD_STATE`, `STY_STATE`) **and** the
101/// string-interner arena — returning them to a fresh baseline.
102///
103/// **Why this exists.** The engine's roots (`STATE`, `arena::ARENA`, …)
104/// are `#[thread_local]` *attribute* statics. Unlike the `thread_local!`
105/// macro, the attribute does **not** run destructors on thread exit, so a
106/// thread that builds a full engine and then exits *leaks* it (~110 MB
107/// for a typical document). The single-conversion `latexml_oxide` binary
108/// never notices — it runs one conversion and the process exits. But any
109/// process that runs **many** conversions across **many** threads
110/// (notably the test harness, where libtest spawns a fresh thread per
111/// test) accumulates one leaked engine per conversion (measured: ~4.9 GB
112/// across `50_structure`, which then trips the per-process RSS fuse in
113/// `stomach::check_timeout`). Resetting between conversions frees that
114/// memory before the thread exits (peak fell ~4.9 GB → ~2.9 GB at -j20).
115///
116/// **Why reset the interner here.** The interner *could* be kept across
117/// conversions — that is the faithful daemon design (Perl keeps its
118/// symbol table and resets only the binding stack via
119/// `pushDaemonFrame`/`popDaemonFrame` in `LaTeXML.pm`), and re-interning
120/// the same ~110k base symbols next conversion is deduped. But that only
121/// pays off when the **same thread** handles multiple conversions. The
122/// test harness gets a **fresh thread per test**, so its interner can
123/// never be reused — keeping it would just leak it on thread exit. So we
124/// reset it too. A future *thread-reusing* daemon should instead keep the
125/// interner by calling [`state::reset_thread_state`] alone (State only).
126///
127/// **Soundness.** Resetting the interner invalidates *every* live
128/// `SymStr` on the thread, so this is sound only between fully
129/// independent conversions — when the prior conversion's output has
130/// already been serialized to owned data and nothing will read a
131/// pre-reset symbol again. The test harness satisfies this (each test
132/// serializes to owned `String`s, then resets before its thread exits).
133/// It does **not** reclaim libxml2's process-global C state (parser
134/// dictionaries) — that residual (~24 MB/test) is left as-is rather than
135/// risk the global `xmlCleanupParser`.
136///
137/// THREAD-LIFECYCLE CONTRACT (PR_READINESS review): several SymStr-holding
138/// statics are NOT reset here — `pin!` call-site OnceCells (no registry;
139/// unresettable by design), gullet `DEFERRED_COMMANDS`, dump-reader
140/// `CURRENT_LOAD_CTX`, package-local caches. They are safe only because no
141/// thread READS a pre-reset SymStr after `arena::reset()`: libtest runs one
142/// test per thread, and the persistent cortex_worker never resets. Any
143/// future daemon/thread-pool that resets AND reuses a thread resurrects the
144/// phantom-symbol-aliasing bug class (see the REPORT-map fix, 7b64a48ad1)
145/// in an unfixable form — redesign the pin! cache before doing that.
146pub fn reset_thread_engine() {
147  state::reset_thread_state();
148  common::arena::reset();
149  // The error REPORT's `undefined`/`missing` maps are keyed by arena `SymStr`s,
150  // so they MUST be cleared together with the arena — otherwise a stale key
151  // resolves, in the renumbered arena, to a different string (phantom undefined).
152  // (`reset_arena_keyed_reports` is in scope via `pub use crate::common::error::*`.)
153  reset_arena_keyed_reports();
154  // The is_noexpand_family and fontmap-key memos are keyed by arena symbols —
155  // same stale-alias hazard as the REPORT maps; clear them with the arena.
156  token::reset_noexpand_family_memo();
157  binding::content::reset_fontmap_key_memo();
158}
159
160pub use crate::common::error::*;
161use crate::{
162  common::{
163    arena::SymHashMap as HashMap, dimension::Dimension, font::Font, locator::Locator, model::Model,
164    numeric_ops::NumericOps, object::Object, store::Stored,
165  },
166  definition::register::RegisterValue,
167  digested::{Digested, DigestedData},
168  document::Document,
169  state::{State, StateOptions, set_state},
170  stomach::Stomach,
171  tbox::Tbox,
172  tokens::Tokens,
173};
174
175pub static NO_PROPERTIES: Lazy<HashMap<Stored>> = Lazy::new(HashMap::default);
176
177/// The Core conversion runtime
178pub struct Core {
179  /// a list of library names to be preloaded before the main conversion begins
180  pub preload: Vec<String>,
181}
182
183/// Configuration for the Core processing
184#[derive(Default)]
185pub struct CoreOptions {
186  // First, state::related options:
187  /// a custom schema-induced model (default is `None`) for the final XML
188  pub model:            Option<Model>,
189  /// default is 0, sub-zero values are quiet, positive values are verbose
190  pub verbosity:        Option<i32>,
191  /// strict error-reporting (is this still used?)
192  pub strict:           Option<bool>,
193  /// toggle preserving comments in the XML on/off
194  pub include_comments: Option<bool>,
195  /// toggle loading raw .sty modules on/off
196  pub include_styles:   Option<bool>,
197  /// disable math parsing (enabled by default)
198  pub nomathparse:      Option<bool>,
199  /// enable source-locator (`--source-map`) tracking + emission (off by
200  /// default). See `docs/performance/SOURCE_PROVENANCE.md`.
201  pub source_map:       Option<bool>,
202  /// an optional, fixed id prefix for all xml:id attributes
203  pub documentid:       Option<String>,
204  /// the list of paths used for loading TeX sources and packages
205  pub search_paths:     Option<Vec<String>>,
206  /// the list of paths used for loading graphics assets
207  pub graphics_paths:   Option<Vec<String>>,
208  /// set an explicit encoding of the input text
209  pub input_encoding:   Option<String>,
210  /// a list of package names to preload before processing start
211  pub preload:          Option<Vec<String>>,
212}
213
214impl Core {
215  /// instantiate a new Core processor
216  pub fn new(options: CoreOptions) -> Self {
217    // Eagerly initialize the engine's `#[thread_local]` roots, LEAVES-FIRST,
218    // on THIS thread before any of them is touched re-entrantly. ARENA is the
219    // universal leaf (every root's Lazy initializer interns via arena::pin);
220    // the token constants, MODEL, the gullet roots and the STD/STY catcode
221    // templates all reach into ARENA (and the gullet/template roots build
222    // tokens). Forcing them in dependency order up front — before
223    // `set_stomach`/`set_state` below trigger their own initializers, and
224    // before expansion lazily touches the gullet/catcode roots mid-run —
225    // guarantees no root's `Lazy` init ever runs another root's init
226    // *re-entrantly*. That cross-`#[thread_local]`-during-init pattern is
227    // benign on Linux/ELF TLS but is the documented macOS hazard
228    // (rust-lang/rust#29594) behind the macOS worker-thread memory
229    // corruption in issue #217 (varying garbage node types → panics /
230    // SIGSEGV / SIGBUS, only on macOS, only in libtest's worker threads —
231    // the single-conversion main-thread CLI was never affected). Forcing
232    // just ARENA+MODEL cut the failures 4→1; this completes the set. No
233    // behavioral change on Linux (these all initialize during any
234    // conversion anyway — this only fixes the ORDER).
235    common::arena::force_init(); // leaf
236    token::force_init(); // token constants -> arena
237    common::model::force_init(); // Model::new -> arena
238    gullet::force_init(); // DEFERRED_COMMANDS / COLUMN_ENDS / GULLET -> arena
239    state::force_init(); // STD_STATE / STY_STATE templates -> arena
240    let preload = options.preload.unwrap_or_default();
241    // pass on the state::options, defaults are handled in state::new
242    let state_options = StateOptions {
243      model: options.model,
244      verbosity: options.verbosity,
245      strict: options.strict,
246      include_comments: options.include_comments,
247      documentid: options.documentid,
248      search_paths: options.search_paths,
249      graphics_paths: options.graphics_paths,
250      include_styles: options.include_styles,
251      input_encoding: options.input_encoding,
252      nomathparse: options.nomathparse,
253      source_map: options.source_map,
254      ..StateOptions::default()
255    };
256    stomach::set_stomach(Stomach::default());
257    set_state(State::new(state_options));
258    Core { preload }
259  }
260}
261/// Common operations for Box-like (digested) data
262pub trait BoxOps: Object {
263  /// If composite, unwrap into the contained digested objects (or return self)
264  fn unlist(&self) -> Vec<Digested> { Vec::new() }
265  fn unlist_ref(&self) -> Vec<Cow<'_, Digested>> { Vec::new() }
266  /// absorb the current object into the `Document` XML - returning the corresponding nodes
267  fn be_absorbed(&self, document: &mut Document) -> Result<Vec<Node>>;
268  /// be_absorbed but with allowed side-effects on the carrier (for `Alignment` only)
269  fn be_absorbed_mut(&mut self, _document: &mut Document) -> Result<Vec<Node>> {
270    self.be_absorbed(_document)
271  }
272  /// build a string representation of the underlying digested data
273  fn get_string(&self) -> Result<Cow<'_, str>>;
274  /// get the underlying tokens (preceding digestion)
275  fn get_tokens(&self) -> Option<&Tokens> { None }
276  /// deprecated: get the map of named properties. This can not be usable as long as we have any
277  /// data behind a RefCell wrapper.
278  /// Use `with_properties` instead.
279  fn get_properties(&self) -> &HashMap<Stored> { &NO_PROPERTIES }
280
281  /// execute a function using this object's named properties
282  fn with_properties<R, FnR>(&self, caller: FnR) -> R
283  where FnR: FnOnce(&HashMap<Stored>) -> R;
284  /// get a mutable reference to the map of named properties
285  fn get_properties_mut(&mut self) -> &mut HashMap<Stored> {
286    panic!("get_properties_mut called on type without mutable properties");
287  }
288  /// set a named property (allows all `Stored` types for values)
289  fn set_property<T: Into<Stored>>(&mut self, key: &str, value: T) {
290    self.get_properties_mut().insert(key, value.into());
291  }
292  /// get a single named property (with special "isSpace" check)
293  fn get_property(&self, key: &str) -> Option<Cow<'_, Stored>> {
294    self.with_properties(|props| {
295      if key == "isSpace" {
296        match props.get(key) {
297          Some(value) => Some(Cow::Owned(value.clone())),
298          None => {
299            let tex = self
300              .get_tokens()
301              .map(|tks| tks.clone().untex())
302              .unwrap_or_default(); // !
303            if !tex.is_empty() && tex.chars().all(char::is_whitespace) {
304              // Check the TeX code, not (just) the string!
305              Some(Cow::Owned(Stored::Bool(true)))
306            } else {
307              None
308            }
309          },
310        }
311      } else {
312        props.get(key).map(|v| Cow::Owned(v.clone()))
313      }
314    })
315  }
316  fn get_property_string(&self, key: &str) -> String {
317    self
318      .get_property(key)
319      .map(|v| v.to_string())
320      .unwrap_or_default()
321  }
322  /// get a mutable reference to a single named property (does NOT have the "isSpace" check)
323  fn get_property_mut(&mut self, key: &str) -> Option<&mut Stored> {
324    self.get_properties_mut().get_mut(key)
325  }
326  /// checks if a property key has been set
327  fn has_property(&self, key: &str) -> bool {
328    self.with_properties(|props| props.contains_key(key))
329  }
330  /// obtains a boolean property value (false unless `Stored::Bool`)
331  fn get_property_bool(&self, key: &str) -> bool {
332    self.with_properties(|props| matches!(props.get(key), Some(Stored::Bool(true))))
333  }
334  /// obtains the "body" of a digested object which captured it
335  fn get_body(&self) -> Result<Option<Digested>> {
336    Error!(
337      "boxops",
338      "get_body",
339      "Generic BoxOps::get_body should never be called!"
340    );
341    Ok(None)
342  }
343  /// gets the associated font, if any
344  fn get_font(&self) -> Result<Option<Rc<Font>>>;
345  /// sets an associated font
346  fn set_font(&mut self, _font: Rc<Font>) { /* no-op for types without font */
347  }
348  /// sets a "width" property, for sizing
349  fn set_width<T: Into<Stored>>(&mut self, width: T) { self.set_property("width", width); }
350
351  // For the dimensions of boxes, we'll store the (lazily) computed size as:
352  //    cached_width, cached_height, cached_depth
353  // and the explicitly requested/assigned size as
354  //    width, height, depth.
355  // Generally speaking, an XML element should only get width, height, depth
356  // attributes when they were explicitly set.
357  // However, when requesting the size of a box, you'd get either (w/ explicit size overriding)
358
359  /// gets the "width" property value, if any
360  fn get_width(&self, options: Option<HashMap<Stored>>) -> Result<Option<RegisterValue>> {
361    if !self.has_property("width") && !self.has_property("cached_width") {
362      // TODO: Restore caching?
363      // self.compute_size_store(options.unwrap_or_default())?
364      let (w, ..) = self.compute_size(options.unwrap_or_default())?;
365      return Ok(Some(RegisterValue::Dimension(w)));
366    }
367
368    // Convert MuGlue/MuDimension widths to pt (1mu = font_size/18). `\the\wd`
369    // is a dimension query — the result must be Dimension-typed. Without
370    // this conversion `\hbox{\,}` width came back as `MuGlue(3mu)` and
371    // formatted as `3.0pt` (raw mu treated as pt) instead of `1.66663pt`.
372    // Order of ops (div by 18 then mul by fs) matches Perl integer
373    // truncation: see `mu_to_pt_value` in store.rs.
374    fn coerce_mu(val: &Stored) -> Option<RegisterValue> {
375      match val {
376        Stored::MuGlue(g) => {
377          let fs = state::lookup_font()
378            .and_then(|f| f.get_size())
379            .unwrap_or(10.0);
380          let muwidth = (fs * common::numeric_ops::UNITY_F64 / 18.0) as i64;
381          let pt_scaled =
382            (g.value_of() as f64 * muwidth as f64 / common::numeric_ops::UNITY_F64).trunc();
383          Some(RegisterValue::Dimension(Dimension::new(pt_scaled as i64)))
384        },
385        Stored::MuDimension(d) => {
386          let fs = state::lookup_font()
387            .and_then(|f| f.get_size())
388            .unwrap_or(10.0);
389          let pt_scaled = (d.value_of() / 18) as f64 * fs;
390          Some(RegisterValue::Dimension(Dimension::new(pt_scaled as i64)))
391        },
392        _ => val.into(),
393      }
394    }
395    Ok(match self.get_property("width") {
396      Some(val) => coerce_mu(&val),
397      None => match self.get_property("cached_width") {
398        Some(val) => coerce_mu(&val),
399        None => Some(RegisterValue::Dimension(Dimension::default())),
400      },
401    })
402  }
403  /// sets a "height" property value, for sizing
404  fn set_height<T: Into<Stored>>(&mut self, width: T) { self.set_property("height", width); }
405  /// gets the "height" property value, if any.
406  /// Checks "height", then "cached_height", then computes from font if needed.
407  fn get_height(&self) -> Option<RegisterValue> {
408    match self.get_property("height") {
409      Some(val) => (&*val).into(),
410      None => match self.get_property("cached_height") {
411        Some(val) => (&*val).into(),
412        None => match self.compute_size(HashMap::default()) {
413          Ok((_, h, _)) => Some(RegisterValue::Dimension(h)),
414          _ => Some(RegisterValue::Dimension(Dimension::default())),
415        },
416      },
417    }
418  }
419  /// sets a "depth" property value, for sizing
420  fn set_depth<T: Into<Stored>>(&mut self, width: T) { self.set_property("depth", width); }
421  /// gets the "depth" property value, if any.
422  /// Checks "depth", then "cached_depth", then computes from font if needed.
423  fn get_depth(&self) -> Option<RegisterValue> {
424    match self.get_property("depth") {
425      Some(val) => (&*val).into(),
426      None => match self.get_property("cached_depth") {
427        Some(val) => (&*val).into(),
428        None => match self.compute_size(HashMap::default()) {
429          Ok((_, _, d)) => Some(RegisterValue::Dimension(d)),
430          _ => Some(RegisterValue::Dimension(Dimension::default())),
431        },
432      },
433    }
434  }
435  /// gets the box size as a triple of (width, height, depth)
436  /// the generic implementation is immutable and will recompute the size on each call
437  /// see `Digested::get_size` for a variant with interior mutability which caches the box size
438  fn get_size(
439    &mut self,
440    options: Option<HashMap<Stored>>,
441  ) -> Result<(
442    Dimension,
443    Dimension,
444    Dimension,
445    Dimension,
446    Dimension,
447    Dimension,
448  )> {
449    // TODO: Reintroduce caching?
450    if !(self.has_property("cached_width")
451      && self.has_property("cached_height")
452      && self.has_property("cached_depth"))
453    {
454      self.compute_size_and_cache(options.unwrap_or_default())?;
455    }
456    self.with_properties(|props| {
457      let (width, height, depth, cached_width, cached_height, cached_depth) = (
458        props.get("width"),
459        props.get("height"),
460        props.get("depth"),
461        props.get("cached_width"),
462        props.get("cached_height"),
463        props.get("cached_depth"),
464      );
465
466      // eprintln!("SIZE of {} {}", std::any::type_name::<Self>(), self.get_string()?);
467      //     . "\n preassigned: " . _showsize($$props{width},  $$props{height},  $$props{depth})
468      //     . "\n calculated : " . _showsize($$props{cached_width}, $$props{cached_height},
469      // $$props{cached_depth})     . "\n w/options " . join(',', map { $_ . "=" .
470      // ToString($options{$_}); } sort keys %options)     . "\n =>: " .
471      // _showsize($$props{width} || $$props{cached_width}, $$props{height}
472      // || $$props{cached_height}, $$props{depth} || $$props{cached_depth})     . "\n   Of " .
473      // ToString($self)) if $LaTeXML::DEBUG{size};
474      // Helper: extract a Dimension from a Stored value.
475      // Handles Dimension directly, plus Glue/MuGlue/MuDimension by extracting the base value.
476      // MuGlue/MuDimension values are in scaled mu (1mu = font_size/18);
477      // convert to scaled pt using the current font size.
478      fn stored_to_dim(s: Option<&Stored>) -> Option<Dimension> {
479        match s {
480          Some(Stored::Dimension(d)) => Some(*d),
481          Some(Stored::Glue(g)) => Some(Dimension::new(g.value_of())),
482          Some(Stored::MuGlue(g)) => {
483            // Convert mu to pt: 1mu = font_size / 18
484            let fs = state::lookup_font()
485              .and_then(|f| f.get_size())
486              .unwrap_or(10.0);
487            let muwidth = (fs * common::numeric_ops::UNITY_F64 / 18.0) as i64;
488            let pt_scaled =
489              (g.value_of() as f64 * muwidth as f64 / common::numeric_ops::UNITY_F64).trunc();
490            Some(Dimension::new(pt_scaled as i64))
491          },
492          Some(Stored::MuDimension(d)) => {
493            let fs = state::lookup_font()
494              .and_then(|f| f.get_size())
495              .unwrap_or(10.0);
496            let mu_val = d.value_of() as f64;
497            let pt_scaled = mu_val * fs / 18.0;
498            Some(Dimension::new(pt_scaled as i64))
499          },
500          _ => None,
501        }
502      }
503      Ok((
504        stored_to_dim(width).unwrap_or_else(|| stored_to_dim(cached_width).unwrap_or_default()),
505        stored_to_dim(height).unwrap_or_else(|| stored_to_dim(cached_height).unwrap_or_default()),
506        stored_to_dim(depth).unwrap_or_else(|| stored_to_dim(cached_depth).unwrap_or_default()),
507        stored_to_dim(cached_width).unwrap_or_else(|| stored_to_dim(width).unwrap_or_default()),
508        stored_to_dim(cached_height).unwrap_or_else(|| stored_to_dim(height).unwrap_or_default()),
509        stored_to_dim(cached_depth).unwrap_or_else(|| stored_to_dim(depth).unwrap_or_default()),
510      ))
511    })
512  }
513
514  /// computes and caches (via named properties) the size of a box-like object.
515  /// Perl #2798 (S5, padding slice): after computing the size, add any requested
516  /// `pad{top,bottom,left,right}` to the computed dimensions. This is the SAFE
517  /// part of `computeSizeStore` — additive and inert until the app layer sets a
518  /// `pad*` property (display math `\abovedisplayskip`/`\belowdisplayskip`,
519  /// `\overline`/`\underline` 2pt, items/equations). The riskier requested-vs-
520  /// computed merge + full-spec bypass + `isEmpty` are deliberately NOT included
521  /// here — a mechanical port of those regressed (Rust boxes don't use
522  /// width/height/depth uniformly as "requested box size"); see SYNC_STATUS U2.
523  fn compute_size_and_cache(
524    &mut self,
525    mut options: HashMap<Stored>,
526  ) -> Result<(Dimension, Dimension, Dimension)> {
527    for key in [
528      "width",
529      "height",
530      "depth",
531      "vattach",
532      "layout",
533      "totalheight",
534      "padtop",
535      "padbottom",
536      "padleft",
537      "padright",
538    ] {
539      if let Some(v) = self.get_property(key) {
540        options.insert(key, v.into_owned());
541      }
542    }
543
544    let (mut w, mut h, mut d) = self.compute_size(options.clone())?;
545    // Perl: add requested padding to the computed size.
546    fn pad_sp(s: Option<&Stored>) -> i64 {
547      match s {
548        Some(Stored::Dimension(d)) => d.value_of(),
549        Some(Stored::Glue(g)) => g.value_of(),
550        Some(Stored::Int(i)) => *i,
551        _ => 0,
552      }
553    }
554    let (padl, padr, padt, padb) = (
555      pad_sp(options.get("padleft")),
556      pad_sp(options.get("padright")),
557      pad_sp(options.get("padtop")),
558      pad_sp(options.get("padbottom")),
559    );
560    if padl != 0 || padr != 0 {
561      w = Dimension::new(w.value_of() + padl + padr);
562    }
563    if padt != 0 {
564      h = Dimension::new(h.value_of() + padt);
565    }
566    if padb != 0 {
567      d = Dimension::new(d.value_of() + padb);
568    }
569
570    if !self.has_property("cached_width") {
571      self.set_property("cached_width", w);
572    }
573    if !self.has_property("cached_height") {
574      self.set_property("cached_height", h);
575    }
576    if !self.has_property("cached_depth") {
577      self.set_property("cached_depth", d);
578    }
579    Ok((w, h, d))
580  }
581
582  /// computes and returns the size of a box-like object
583  fn compute_size(&self, options: HashMap<Stored>) -> Result<(Dimension, Dimension, Dimension)>;
584}
585
586/// The current TeX processing mode
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub enum TexMode {
589  /// TeX's math mode
590  Math,
591  /// TeX's text mode
592  Text,
593}