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}
155
156pub use crate::common::error::*;
157use crate::{
158 common::{
159 arena::SymHashMap as HashMap, dimension::Dimension, font::Font, locator::Locator, model::Model,
160 numeric_ops::NumericOps, object::Object, store::Stored,
161 },
162 definition::register::RegisterValue,
163 digested::{Digested, DigestedData},
164 document::Document,
165 state::{State, StateOptions, set_state},
166 stomach::Stomach,
167 tbox::Tbox,
168 tokens::Tokens,
169};
170
171pub static NO_PROPERTIES: Lazy<HashMap<Stored>> = Lazy::new(HashMap::default);
172
173/// The Core conversion runtime
174pub struct Core {
175 /// a list of library names to be preloaded before the main conversion begins
176 pub preload: Vec<String>,
177}
178
179/// Configuration for the Core processing
180#[derive(Default)]
181pub struct CoreOptions {
182 // First, state::related options:
183 /// a custom schema-induced model (default is `None`) for the final XML
184 pub model: Option<Model>,
185 /// default is 0, sub-zero values are quiet, positive values are verbose
186 pub verbosity: Option<i32>,
187 /// strict error-reporting (is this still used?)
188 pub strict: Option<bool>,
189 /// toggle preserving comments in the XML on/off
190 pub include_comments: Option<bool>,
191 /// toggle loading raw .sty modules on/off
192 pub include_styles: Option<bool>,
193 /// disable math parsing (enabled by default)
194 pub nomathparse: Option<bool>,
195 /// enable source-locator (`--source-map`) tracking + emission (off by
196 /// default). See `docs/performance/SOURCE_PROVENANCE.md`.
197 pub source_map: Option<bool>,
198 /// an optional, fixed id prefix for all xml:id attributes
199 pub documentid: Option<String>,
200 /// the list of paths used for loading TeX sources and packages
201 pub search_paths: Option<Vec<String>>,
202 /// the list of paths used for loading graphics assets
203 pub graphics_paths: Option<Vec<String>>,
204 /// set an explicit encoding of the input text
205 pub input_encoding: Option<String>,
206 /// a list of package names to preload before processing start
207 pub preload: Option<Vec<String>>,
208}
209
210impl Core {
211 /// instantiate a new Core processor
212 pub fn new(options: CoreOptions) -> Self {
213 // Eagerly initialize the engine's `#[thread_local]` roots, LEAVES-FIRST,
214 // on THIS thread before any of them is touched re-entrantly. ARENA is the
215 // universal leaf (every root's Lazy initializer interns via arena::pin);
216 // the token constants, MODEL, the gullet roots and the STD/STY catcode
217 // templates all reach into ARENA (and the gullet/template roots build
218 // tokens). Forcing them in dependency order up front — before
219 // `set_stomach`/`set_state` below trigger their own initializers, and
220 // before expansion lazily touches the gullet/catcode roots mid-run —
221 // guarantees no root's `Lazy` init ever runs another root's init
222 // *re-entrantly*. That cross-`#[thread_local]`-during-init pattern is
223 // benign on Linux/ELF TLS but is the documented macOS hazard
224 // (rust-lang/rust#29594) behind the macOS worker-thread memory
225 // corruption in issue #217 (varying garbage node types → panics /
226 // SIGSEGV / SIGBUS, only on macOS, only in libtest's worker threads —
227 // the single-conversion main-thread CLI was never affected). Forcing
228 // just ARENA+MODEL cut the failures 4→1; this completes the set. No
229 // behavioral change on Linux (these all initialize during any
230 // conversion anyway — this only fixes the ORDER).
231 common::arena::force_init(); // leaf
232 token::force_init(); // token constants -> arena
233 common::model::force_init(); // Model::new -> arena
234 gullet::force_init(); // DEFERRED_COMMANDS / COLUMN_ENDS / GULLET -> arena
235 state::force_init(); // STD_STATE / STY_STATE templates -> arena
236 let preload = options.preload.unwrap_or_default();
237 // pass on the state::options, defaults are handled in state::new
238 let state_options = StateOptions {
239 model: options.model,
240 verbosity: options.verbosity,
241 strict: options.strict,
242 include_comments: options.include_comments,
243 documentid: options.documentid,
244 search_paths: options.search_paths,
245 graphics_paths: options.graphics_paths,
246 include_styles: options.include_styles,
247 input_encoding: options.input_encoding,
248 nomathparse: options.nomathparse,
249 source_map: options.source_map,
250 ..StateOptions::default()
251 };
252 stomach::set_stomach(Stomach::default());
253 set_state(State::new(state_options));
254 Core { preload }
255 }
256}
257/// Common operations for Box-like (digested) data
258pub trait BoxOps: Object {
259 /// If composite, unwrap into the contained digested objects (or return self)
260 fn unlist(&self) -> Vec<Digested> { Vec::new() }
261 fn unlist_ref(&self) -> Vec<Cow<'_, Digested>> { Vec::new() }
262 /// absorb the current object into the `Document` XML - returning the corresponding nodes
263 fn be_absorbed(&self, document: &mut Document) -> Result<Vec<Node>>;
264 /// be_absorbed but with allowed side-effects on the carrier (for `Alignment` only)
265 fn be_absorbed_mut(&mut self, _document: &mut Document) -> Result<Vec<Node>> {
266 self.be_absorbed(_document)
267 }
268 /// build a string representation of the underlying digested data
269 fn get_string(&self) -> Result<Cow<'_, str>>;
270 /// get the underlying tokens (preceding digestion)
271 fn get_tokens(&self) -> Option<&Tokens> { None }
272 /// deprecated: get the map of named properties. This can not be usable as long as we have any
273 /// data behind a RefCell wrapper.
274 /// Use `with_properties` instead.
275 fn get_properties(&self) -> &HashMap<Stored> { &NO_PROPERTIES }
276
277 /// execute a function using this object's named properties
278 fn with_properties<R, FnR>(&self, caller: FnR) -> R
279 where FnR: FnOnce(&HashMap<Stored>) -> R;
280 /// get a mutable reference to the map of named properties
281 fn get_properties_mut(&mut self) -> &mut HashMap<Stored> {
282 panic!("get_properties_mut called on type without mutable properties");
283 }
284 /// set a named property (allows all `Stored` types for values)
285 fn set_property<T: Into<Stored>>(&mut self, key: &str, value: T) {
286 self.get_properties_mut().insert(key, value.into());
287 }
288 /// get a single named property (with special "isSpace" check)
289 fn get_property(&self, key: &str) -> Option<Cow<'_, Stored>> {
290 self.with_properties(|props| {
291 if key == "isSpace" {
292 match props.get(key) {
293 Some(value) => Some(Cow::Owned(value.clone())),
294 None => {
295 let tex = self
296 .get_tokens()
297 .map(|tks| tks.clone().untex())
298 .unwrap_or_default(); // !
299 if !tex.is_empty() && tex.chars().all(char::is_whitespace) {
300 // Check the TeX code, not (just) the string!
301 Some(Cow::Owned(Stored::Bool(true)))
302 } else {
303 None
304 }
305 },
306 }
307 } else {
308 props.get(key).map(|v| Cow::Owned(v.clone()))
309 }
310 })
311 }
312 fn get_property_string(&self, key: &str) -> String {
313 self
314 .get_property(key)
315 .map(|v| v.to_string())
316 .unwrap_or_default()
317 }
318 /// get a mutable reference to a single named property (does NOT have the "isSpace" check)
319 fn get_property_mut(&mut self, key: &str) -> Option<&mut Stored> {
320 self.get_properties_mut().get_mut(key)
321 }
322 /// checks if a property key has been set
323 fn has_property(&self, key: &str) -> bool {
324 self.with_properties(|props| props.contains_key(key))
325 }
326 /// obtains a boolean property value (false unless `Stored::Bool`)
327 fn get_property_bool(&self, key: &str) -> bool {
328 self.with_properties(|props| matches!(props.get(key), Some(Stored::Bool(true))))
329 }
330 /// obtains the "body" of a digested object which captured it
331 fn get_body(&self) -> Result<Option<Digested>> {
332 Error!(
333 "boxops",
334 "get_body",
335 "Generic BoxOps::get_body should never be called!"
336 );
337 Ok(None)
338 }
339 /// gets the associated font, if any
340 fn get_font(&self) -> Result<Option<Rc<Font>>>;
341 /// sets an associated font
342 fn set_font(&mut self, _font: Rc<Font>) { /* no-op for types without font */
343 }
344 /// sets a "width" property, for sizing
345 fn set_width<T: Into<Stored>>(&mut self, width: T) { self.set_property("width", width); }
346
347 // For the dimensions of boxes, we'll store the (lazily) computed size as:
348 // cached_width, cached_height, cached_depth
349 // and the explicitly requested/assigned size as
350 // width, height, depth.
351 // Generally speaking, an XML element should only get width, height, depth
352 // attributes when they were explicitly set.
353 // However, when requesting the size of a box, you'd get either (w/ explicit size overriding)
354
355 /// gets the "width" property value, if any
356 fn get_width(&self, options: Option<HashMap<Stored>>) -> Result<Option<RegisterValue>> {
357 if !self.has_property("width") && !self.has_property("cached_width") {
358 // TODO: Restore caching?
359 // self.compute_size_store(options.unwrap_or_default())?
360 let (w, ..) = self.compute_size(options.unwrap_or_default())?;
361 return Ok(Some(RegisterValue::Dimension(w)));
362 }
363
364 // Convert MuGlue/MuDimension widths to pt (1mu = font_size/18). `\the\wd`
365 // is a dimension query — the result must be Dimension-typed. Without
366 // this conversion `\hbox{\,}` width came back as `MuGlue(3mu)` and
367 // formatted as `3.0pt` (raw mu treated as pt) instead of `1.66663pt`.
368 // Order of ops (div by 18 then mul by fs) matches Perl integer
369 // truncation: see `mu_to_pt_value` in store.rs.
370 fn coerce_mu(val: &Stored) -> Option<RegisterValue> {
371 match val {
372 Stored::MuGlue(g) => {
373 let fs = state::lookup_font()
374 .and_then(|f| f.get_size())
375 .unwrap_or(10.0);
376 let muwidth = (fs * common::numeric_ops::UNITY_F64 / 18.0) as i64;
377 let pt_scaled =
378 (g.value_of() as f64 * muwidth as f64 / common::numeric_ops::UNITY_F64).trunc();
379 Some(RegisterValue::Dimension(Dimension::new(pt_scaled as i64)))
380 },
381 Stored::MuDimension(d) => {
382 let fs = state::lookup_font()
383 .and_then(|f| f.get_size())
384 .unwrap_or(10.0);
385 let pt_scaled = (d.value_of() / 18) as f64 * fs;
386 Some(RegisterValue::Dimension(Dimension::new(pt_scaled as i64)))
387 },
388 _ => val.into(),
389 }
390 }
391 Ok(match self.get_property("width") {
392 Some(val) => coerce_mu(&val),
393 None => match self.get_property("cached_width") {
394 Some(val) => coerce_mu(&val),
395 None => Some(RegisterValue::Dimension(Dimension::default())),
396 },
397 })
398 }
399 /// sets a "height" property value, for sizing
400 fn set_height<T: Into<Stored>>(&mut self, width: T) { self.set_property("height", width); }
401 /// gets the "height" property value, if any.
402 /// Checks "height", then "cached_height", then computes from font if needed.
403 fn get_height(&self) -> Option<RegisterValue> {
404 match self.get_property("height") {
405 Some(val) => (&*val).into(),
406 None => match self.get_property("cached_height") {
407 Some(val) => (&*val).into(),
408 None => match self.compute_size(HashMap::default()) {
409 Ok((_, h, _)) => Some(RegisterValue::Dimension(h)),
410 _ => Some(RegisterValue::Dimension(Dimension::default())),
411 },
412 },
413 }
414 }
415 /// sets a "depth" property value, for sizing
416 fn set_depth<T: Into<Stored>>(&mut self, width: T) { self.set_property("depth", width); }
417 /// gets the "depth" property value, if any.
418 /// Checks "depth", then "cached_depth", then computes from font if needed.
419 fn get_depth(&self) -> Option<RegisterValue> {
420 match self.get_property("depth") {
421 Some(val) => (&*val).into(),
422 None => match self.get_property("cached_depth") {
423 Some(val) => (&*val).into(),
424 None => match self.compute_size(HashMap::default()) {
425 Ok((_, _, d)) => Some(RegisterValue::Dimension(d)),
426 _ => Some(RegisterValue::Dimension(Dimension::default())),
427 },
428 },
429 }
430 }
431 /// gets the box size as a triple of (width, height, depth)
432 /// the generic implementation is immutable and will recompute the size on each call
433 /// see `Digested::get_size` for a variant with interior mutability which caches the box size
434 fn get_size(
435 &mut self,
436 options: Option<HashMap<Stored>>,
437 ) -> Result<(
438 Dimension,
439 Dimension,
440 Dimension,
441 Dimension,
442 Dimension,
443 Dimension,
444 )> {
445 // TODO: Reintroduce caching?
446 if !(self.has_property("cached_width")
447 && self.has_property("cached_height")
448 && self.has_property("cached_depth"))
449 {
450 self.compute_size_and_cache(options.unwrap_or_default())?;
451 }
452 self.with_properties(|props| {
453 let (width, height, depth, cached_width, cached_height, cached_depth) = (
454 props.get("width"),
455 props.get("height"),
456 props.get("depth"),
457 props.get("cached_width"),
458 props.get("cached_height"),
459 props.get("cached_depth"),
460 );
461
462 // eprintln!("SIZE of {} {}", std::any::type_name::<Self>(), self.get_string()?);
463 // . "\n preassigned: " . _showsize($$props{width}, $$props{height}, $$props{depth})
464 // . "\n calculated : " . _showsize($$props{cached_width}, $$props{cached_height},
465 // $$props{cached_depth}) . "\n w/options " . join(',', map { $_ . "=" .
466 // ToString($options{$_}); } sort keys %options) . "\n =>: " .
467 // _showsize($$props{width} || $$props{cached_width}, $$props{height}
468 // || $$props{cached_height}, $$props{depth} || $$props{cached_depth}) . "\n Of " .
469 // ToString($self)) if $LaTeXML::DEBUG{size};
470 // Helper: extract a Dimension from a Stored value.
471 // Handles Dimension directly, plus Glue/MuGlue/MuDimension by extracting the base value.
472 // MuGlue/MuDimension values are in scaled mu (1mu = font_size/18);
473 // convert to scaled pt using the current font size.
474 fn stored_to_dim(s: Option<&Stored>) -> Option<Dimension> {
475 match s {
476 Some(Stored::Dimension(d)) => Some(*d),
477 Some(Stored::Glue(g)) => Some(Dimension::new(g.value_of())),
478 Some(Stored::MuGlue(g)) => {
479 // Convert mu to pt: 1mu = font_size / 18
480 let fs = state::lookup_font()
481 .and_then(|f| f.get_size())
482 .unwrap_or(10.0);
483 let muwidth = (fs * common::numeric_ops::UNITY_F64 / 18.0) as i64;
484 let pt_scaled =
485 (g.value_of() as f64 * muwidth as f64 / common::numeric_ops::UNITY_F64).trunc();
486 Some(Dimension::new(pt_scaled as i64))
487 },
488 Some(Stored::MuDimension(d)) => {
489 let fs = state::lookup_font()
490 .and_then(|f| f.get_size())
491 .unwrap_or(10.0);
492 let mu_val = d.value_of() as f64;
493 let pt_scaled = mu_val * fs / 18.0;
494 Some(Dimension::new(pt_scaled as i64))
495 },
496 _ => None,
497 }
498 }
499 Ok((
500 stored_to_dim(width).unwrap_or_else(|| stored_to_dim(cached_width).unwrap_or_default()),
501 stored_to_dim(height).unwrap_or_else(|| stored_to_dim(cached_height).unwrap_or_default()),
502 stored_to_dim(depth).unwrap_or_else(|| stored_to_dim(cached_depth).unwrap_or_default()),
503 stored_to_dim(cached_width).unwrap_or_else(|| stored_to_dim(width).unwrap_or_default()),
504 stored_to_dim(cached_height).unwrap_or_else(|| stored_to_dim(height).unwrap_or_default()),
505 stored_to_dim(cached_depth).unwrap_or_else(|| stored_to_dim(depth).unwrap_or_default()),
506 ))
507 })
508 }
509
510 /// computes and caches (via named properties) the size of a box-like object.
511 /// Perl #2798 (S5, padding slice): after computing the size, add any requested
512 /// `pad{top,bottom,left,right}` to the computed dimensions. This is the SAFE
513 /// part of `computeSizeStore` — additive and inert until the app layer sets a
514 /// `pad*` property (display math `\abovedisplayskip`/`\belowdisplayskip`,
515 /// `\overline`/`\underline` 2pt, items/equations). The riskier requested-vs-
516 /// computed merge + full-spec bypass + `isEmpty` are deliberately NOT included
517 /// here — a mechanical port of those regressed (Rust boxes don't use
518 /// width/height/depth uniformly as "requested box size"); see SYNC_STATUS U2.
519 fn compute_size_and_cache(
520 &mut self,
521 mut options: HashMap<Stored>,
522 ) -> Result<(Dimension, Dimension, Dimension)> {
523 for key in [
524 "width",
525 "height",
526 "depth",
527 "vattach",
528 "layout",
529 "totalheight",
530 "padtop",
531 "padbottom",
532 "padleft",
533 "padright",
534 ] {
535 if let Some(v) = self.get_property(key) {
536 options.insert(key, v.into_owned());
537 }
538 }
539
540 let (mut w, mut h, mut d) = self.compute_size(options.clone())?;
541 // Perl: add requested padding to the computed size.
542 fn pad_sp(s: Option<&Stored>) -> i64 {
543 match s {
544 Some(Stored::Dimension(d)) => d.value_of(),
545 Some(Stored::Glue(g)) => g.value_of(),
546 Some(Stored::Int(i)) => *i,
547 _ => 0,
548 }
549 }
550 let (padl, padr, padt, padb) = (
551 pad_sp(options.get("padleft")),
552 pad_sp(options.get("padright")),
553 pad_sp(options.get("padtop")),
554 pad_sp(options.get("padbottom")),
555 );
556 if padl != 0 || padr != 0 {
557 w = Dimension::new(w.value_of() + padl + padr);
558 }
559 if padt != 0 {
560 h = Dimension::new(h.value_of() + padt);
561 }
562 if padb != 0 {
563 d = Dimension::new(d.value_of() + padb);
564 }
565
566 if !self.has_property("cached_width") {
567 self.set_property("cached_width", w);
568 }
569 if !self.has_property("cached_height") {
570 self.set_property("cached_height", h);
571 }
572 if !self.has_property("cached_depth") {
573 self.set_property("cached_depth", d);
574 }
575 Ok((w, h, d))
576 }
577
578 /// computes and returns the size of a box-like object
579 fn compute_size(&self, options: HashMap<Stored>) -> Result<(Dimension, Dimension, Dimension)>;
580}
581
582/// The current TeX processing mode
583#[derive(Debug, Clone, PartialEq, Eq)]
584pub enum TexMode {
585 /// TeX's math mode
586 Math,
587 /// TeX's text mode
588 Text,
589}