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}