Skip to main content

latexml_core/definition/
constructor.rs

1use std::{borrow::Cow, fmt, rc::Rc};
2
3use libxml::tree::Node;
4
5use crate::{
6  BoxOps, Digested,
7  common::{arena::SymHashMap, error::*, font::Font, locator::Locator, object::Object},
8  definition::{
9    BeforeDigestClosure, ConstructionClosure, Definition, DigestionClosure, FontDirective,
10    PropertiesClosure, ReplacementClosure, Reversion, SizingClosure,
11  },
12  document::Document,
13  parameter::Parameters,
14  state::*,
15  stomach::digest_next_body,
16  token::*,
17  tokens::Tokens,
18  whatsit::Whatsit,
19};
20
21/// A `--source-map` construct's source extent: the union (first `from` → last
22/// `to`) of its children's spans, or `None` if none carries a position (the
23/// caller then falls back to the gullet locator). docs/performance/SOURCE_PROVENANCE.md §3.1.
24fn assemble_locator(args: &[Option<Digested>]) -> Option<Locator> {
25  args
26    .iter()
27    .flatten()
28    .filter_map(child_span)
29    .reduce(|a, b| Locator::new_range(a, b).unwrap_or(a))
30}
31
32/// A child's located span: its own `get_locator()` if set, else — under
33/// `token-locators` — recovered from the per-token origin handles still riding
34/// its reverted tokens. (Origins survive revert/re-digest; `get_locator` merely
35/// fails to aggregate undigested/composite content — §3.1.3.) Off the feature,
36/// only `get_locator` is consulted (byte-identical behavior).
37///
38/// `pub` so out-of-band construction paths that open an element *around*
39/// already-digested content — e.g. `insert_frontmatter` building `<ltx:title>`
40/// from the stored, deferred `\title{…}` boxes — can recover the same span and
41/// feed it to `Document::set_current_box_locator`.
42pub fn child_span(d: &Digested) -> Option<Locator> {
43  if let Some(l) = d.get_locator().filter(|l| l.from_line != 0) {
44    return Some(l);
45  }
46  #[cfg(feature = "token-locators")]
47  {
48    let reverted = d.revert().ok()?;
49    let origins: Vec<crate::token::TokenStart> = reverted
50      .unlist_ref()
51      .iter()
52      .filter_map(|t| crate::token::get_token_origin(t.loc))
53      .collect();
54    // Prefer genuine (read-from-source) origins: a macro's structural body
55    // literals carry an *inherited* origin (its call site) and must not widen
56    // the content-exact span of its real arguments. Fall back to the inherited
57    // origins only when nothing genuine was recovered — that is the
58    // origin-less expansion case (`\today`), where the invocation point is the
59    // only source position there is. See docs/performance/SOURCE_PROVENANCE.md §3.1.3.
60    let span = |it: &mut dyn Iterator<Item = &crate::token::TokenStart>| {
61      it.map(|o| {
62        crate::common::arena::with(o.source, |s| Locator::new(s, o.line, o.col, o.line, o.col))
63      })
64      .reduce(|a, b| Locator::new_range(a, b).unwrap_or(a))
65    };
66    span(&mut origins.iter().filter(|o| !o.inherited))
67      .or_else(|| span(&mut origins.iter().filter(|o| o.inherited)))
68  }
69  #[cfg(not(feature = "token-locators"))]
70  {
71    None
72  }
73}
74
75/// configuration for creating a new Constructor
76#[derive(Clone)]
77pub struct ConstructorOptions {
78  /// number of arguments (if any)
79  pub nargs:            Option<usize>,
80  /// bouded mode (default: false)
81  pub bounded:          bool,
82  /// begin a named mode
83  pub mode:             Option<String>,
84  /// a `SizingClosure` to estimate the size of the digested box
85  pub sizer:            Option<SizingClosure>,
86  /// custom code to run immediately before the digestion phase
87  pub before_digest:    Vec<BeforeDigestClosure>,
88  /// custom code to run immediately after the digestion phase
89  pub after_digest:     Vec<DigestionClosure>,
90  /// custom code to run immediately before the construction phase
91  pub before_construct: Vec<ConstructionClosure>,
92  /// custom code to run immediately after the construction phase
93  pub after_construct:  Vec<ConstructionClosure>,
94
95  /// switch to horizontal mode before digesting (Perl: enterHorizontal => 1)
96  pub enter_horizontal: bool,
97  /// switch to vertical mode before digesting (Perl: leaveHorizontal => 1)
98  pub leave_horizontal: bool,
99
100  // environment-specific
101  /// requires to be used in math mode
102  pub require_math:       bool,
103  /// forbids use in math mode
104  pub forbid_math:        bool,
105  /// custom directives for computing box properties
106  pub properties:         PropertiesClosure,
107  /// should it capture the body as `#body` (default: false)
108  pub capture_body:       bool,
109  /// specify a font to use, or instructions how to compute which font to use
110  pub font:               Option<FontDirective>,
111  /// custom code to run as digestion begins
112  pub after_digest_begin: Vec<DigestionClosure>,
113  /// custom code to run just before digestion ends
114  pub before_digest_end:  Vec<BeforeDigestClosure>,
115  /// custom code to run after `#body` has been digested
116  pub after_digest_body:  Vec<DigestionClosure>,
117  /// provide tokens to revert to, or custom code for computing them
118  pub reversion:          Option<Reversion>,
119  /// Local/Global scope of installing this definition (default: Local)
120  pub scope:              Option<Scope>,
121  /// is this a robust command sequence (default: false)
122  pub robust:             bool,
123  /// lock the definition for raw TeX overrides (default: false)
124  pub locked:             bool,
125  /// alternative (command sequence) name, used for reversion
126  pub alias:              Option<String>,
127}
128impl Default for ConstructorOptions {
129  fn default() -> Self {
130    ConstructorOptions {
131      nargs:              None,
132      bounded:            false,
133      before_digest:      vec![],
134      after_digest:       vec![],
135      before_construct:   vec![],
136      after_construct:    vec![],
137      mode:               None,
138      enter_horizontal:   false,
139      leave_horizontal:   false,
140      // environment-specific
141      require_math:       false,
142      forbid_math:        false,
143      properties:         Rc::new(|_whatsit| Ok(SymHashMap::default())),
144      capture_body:       false,
145      font:               None,
146      after_digest_begin: vec![],
147      before_digest_end:  vec![],
148      after_digest_body:  vec![],
149      scope:              None,
150      robust:             false,
151      locked:             false,
152      alias:              None,
153      reversion:          None,
154      sizer:              None,
155    }
156  }
157}
158impl fmt::Debug for ConstructorOptions {
159  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160    write!(
161      f,
162      "\nConstructorOptions {{nargs:{:?}, bounded:{:?}, mode:{:?}, \n\tbefore_digest:{:?}, \
163       after_digest_begin:{:?}, before_digest_end:{:?},\n\tafter_digest:{:?}, \
164       after_digest_body:{:?}, before_construct:{:?}, after_construct:{:?},\n\trequire_math:{:?}, \
165       forbid_math:{:?}, capture_body:{:?}, scope:{:?},\n\tlocked:{:?}, alias:{:?} }}\n",
166      self.nargs,
167      self.bounded,
168      self.mode,
169      self.before_digest.len(),
170      self.after_digest_begin.len(),
171      self.before_digest_end.len(),
172      self.after_digest.len(),
173      self.after_digest_body.len(),
174      self.before_construct.len(),
175      self.after_construct.len(),
176      self.require_math,
177      self.forbid_math,
178      self.capture_body,
179      self.scope,
180      self.locked,
181      self.alias
182    )
183  }
184}
185
186#[derive(Clone)]
187pub struct Constructor {
188  pub cs:                Token,
189  pub nargs:             Option<usize>,
190  pub paramlist:         Option<Parameters>,
191  pub replacement:       Option<ReplacementClosure>,
192  pub sizer:             Option<SizingClosure>,
193  pub before_digest:     Vec<BeforeDigestClosure>,
194  pub after_digest:      Vec<DigestionClosure>,
195  pub before_construct:  Vec<ConstructionClosure>,
196  pub after_construct:   Vec<ConstructionClosure>,
197  pub properties:        PropertiesClosure,
198  pub capture_body:      bool,
199  // environment-specific
200  pub after_digest_body: Vec<DigestionClosure>,
201  pub reversion:         Option<Reversion>,
202  pub alias:             Option<String>,
203}
204impl Default for Constructor {
205  fn default() -> Self {
206    Constructor {
207      cs:                T_CS!("Constructor"),
208      nargs:             None,
209      paramlist:         None,
210      replacement:       None,
211      before_digest:     vec![],
212      after_digest:      vec![],
213      before_construct:  vec![],
214      after_construct:   vec![],
215      properties:        Rc::new(|_whatsit| Ok(SymHashMap::default())),
216      capture_body:      false,
217      after_digest_body: vec![],
218      reversion:         None,
219      alias:             None,
220      sizer:             None,
221    }
222  }
223}
224impl fmt::Debug for Constructor {
225  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
226    write!(
227      f,
228      "\nConstructor {{
229        cs:{:?}
230        nargs:{:?}
231        paramlist:{:?}
232        replacement:{:?}
233        before_digest:{:?}
234        after_digest:{:?}
235        before_construct:{:?}
236        after_construct:{:?}
237        capture_body:{:?}
238        after_digest_body:{:?}
239        reversion:{:?}
240        alias:{:?}
241        sizer:{:?} }}\n",
242      self.cs,
243      self.nargs,
244      self.paramlist,
245      self.replacement.is_some(),
246      self.before_digest.len(),
247      self.after_digest.len(),
248      self.before_construct.len(),
249      self.after_construct.len(),
250      self.capture_body,
251      self.after_digest_body.len(),
252      self.reversion.is_some(),
253      self.alias,
254      self.sizer.is_some(),
255    )
256  }
257}
258
259impl PartialEq for Constructor {
260  fn eq(&self, other: &Constructor) -> bool { self.cs == other.cs }
261}
262
263impl fmt::Display for Constructor {
264  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
265    write!(
266      f,
267      "{}",
268      <Self as Definition>::stringify_type(self, "Constructor")
269    )
270  }
271}
272impl Object for Constructor {
273  fn stringify(&self) -> String { <Self as Definition>::stringify_type(self, "Constructor") }
274}
275impl Constructor {
276  /// Digest through the `Rc<Constructor>` the state table already holds, instead
277  /// of deep-cloning the definition for every invocation.
278  ///
279  /// Every Whatsit keeps a back-reference to the definition that built it
280  /// ([`Whatsit::definition`]), and [`Definition::invoke_primitive`] fills that
281  /// slot with `Rc::new(self.clone())` — a **fresh deep clone per invocation**,
282  /// carrying the `Parameters` and all three `Vec<Rc<dyn Fn…>>` hook lists with
283  /// it. Those clones are not transient: a Whatsit lives in the digested tree
284  /// for the whole Build, so a document retains one private copy of the
285  /// constructor per construct it uses. Measured with dhat (2026-07-29, debug
286  /// profile, math-dense fixture): the `Rc<Constructor>` itself was 1,215 blocks
287  /// / 349,920 B — an exact 1:1 with the 1,215 constructor invocations — plus
288  /// 3,638 blocks / 314,816 B inside `<Constructor as Clone>::clone` for the
289  /// parameters and hook vectors.
290  ///
291  /// The state table's entry is `Stored::Constructor(Rc<Constructor>)`
292  /// (`common/store.rs`), so the invoker is already holding a shareable handle;
293  /// passing it through turns that whole per-invocation cost into a refcount
294  /// bump. Sharing is sound because the definition is immutable once installed —
295  /// nothing calls `Rc::get_mut` on it, and `PartialEq for Whatsit` compares
296  /// definitions **by value** (`*self.definition == *other.definition`), so a
297  /// shared handle compares exactly as a private clone did.
298  pub fn invoke_primitive_shared(me: &Rc<Constructor>) -> Result<Vec<Digested>> {
299    me.digest_to_whatsit(Rc::clone(me) as Rc<dyn Definition>)
300  }
301
302  /// The body of constructor digestion, parameterized by the handle to install
303  /// as the Whatsit's [`Whatsit::definition`] back-reference.
304  fn digest_to_whatsit(&self, definition: Rc<dyn Definition>) -> Result<Vec<Digested>> {
305    Debug!("invoke_primitive for {:?}", self.get_cs());
306    // Call any `Before' code.
307    // TODO: profiling / tracing
308    // let profiled = state!().lookup_value("PROFILING") && ($LaTeXML::CURRENT_TOKEN || $$self{cs});
309    // let tracing = state!().lookup_value("tracingcommands");
310    // LaTeXML::Definition::startProfiling($profiled, "digest") if $profiled;
311
312    let mut result = self.execute_before_digest()?;
313
314    // info!("{" + $self->tracingCSName . "}\n" if $tracing;
315    // Get some info before we process arguments...
316    let state_font = lookup_font();
317    let ismath = lookup_bool_sym(crate::pin!("IN_MATH"));
318    // info!(target: "constructor", "invoke for {:?} ({:?})", self.get_cs(), ismath);
319    // Parse AND digest the arguments to the Constructor
320    let mut args: Vec<Option<Digested>> = match self.get_parameters() {
321      None => Vec::new(),
322      Some(params) => params.read_arguments_and_digest(self)?,
323    };
324    // info!($self->tracingArgs(@args) . "\n" if $tracing && @args;
325    let nargs = self.get_num_args();
326    args.truncate(nargs);
327
328    // Compute any extra Whatsit properties (many end up as element attributes)
329
330    let mut properties = (self.properties)(&args)?;
331    // for (key, value) in properties.iter() {
332    //   if (ref $value eq 'CODE') {
333    //     $properties{$key} = &$value($stomach, @args); } }
334
335    properties
336      .entry("font")
337      .or_insert_with(|| match state_font {
338        Some(f) => Stored::Font(Rc::clone(&f)),
339        None => Stored::Font(Rc::new(Font::text_default())), // should never happen?
340      });
341    // $properties{locator} = $stomach->getGullet->getMouth->getLocator unless defined
342    // $properties{locator};
343    properties
344      .entry("isMath")
345      .or_insert_with(|| Stored::Bool(ismath));
346    // Perl: $mode = $properties{mode} || $state->lookupValue('MODE') || 'restricted_horizontal';
347    // Set mode on whatsit so repackHorizontal can distinguish vertical vs horizontal items.
348    properties.entry("mode").or_insert_with(|| {
349      let mode = lookup_string_from_sym(crate::pin!("MODE"));
350      Stored::String(crate::common::arena::pin(if mode.is_empty() {
351        "restricted_horizontal"
352      } else {
353        &mode
354      }))
355    });
356    // $properties{level}   = $stomach->getBoxingLevel;
357
358    // Now create the Whatsit, itself.
359    // Every field is named rather than `..Whatsit::default()`: the default
360    // builds a placeholder `Rc::new(Expandable::default())` for `definition`
361    // that this literal immediately overwrites, so the functional-update form
362    // allocated and dropped one `Rc<Expandable>` per constructor invocation
363    // (dhat 2026-07-29: 1,215 blocks / 145,800 B, again 1:1 with invocations).
364    let mut whatsit = Whatsit {
365      definition,
366      args,
367      properties,
368      reversion: None,
369      dual_reversion: None,
370      locator: None,
371    };
372    // Perl `Core/Definition/Constructor.pm` L106:
373    //   `$props{locator} = $stomach->getGullet->getLocator`
374    // — capture the construct's source position at digest time. Gated on
375    // `--source-map`: the whatsit locator is consumed only by source-map
376    // stamping + (untested) error messages, so the corpus/parity path skips
377    // the per-construct `get_locator`/`arena::pin` cost and stays
378    // byte-identical (the switch gates *all* locator tracking). Without this,
379    // constructor-built elements carry `Locator::default()` (source =
380    // `locator.rs`) and the source-map user-source filter drops them
381    // (~53/265 → 128/… `article.tex` elements stamped once captured).
382    if source_map_enabled() {
383      // --source-map: the construct's source extent is the union of its
384      // children's spans (fixes the post-expansion eating-disorder, Experiment 2),
385      // falling back to the gullet locator when no child carries a position.
386      whatsit.locator =
387        assemble_locator(&whatsit.args).or_else(|| Some(crate::gullet::get_locator()));
388    }
389
390    // Call any 'After' code.
391    let mut post = self.execute_after_digest(&mut whatsit)?;
392
393    if self.capture_body {
394      let captured = digest_next_body(None)?;
395      // info!(target:"constructor:digest_next_body", "\n{:?}\n----\n",captured);
396      post.extend(captured);
397
398      // token-locators: capture_body constructs (e.g. `\lx@begin@inline@math`)
399      // carry their content as #body, not positional args, so the earlier
400      // assemble_locator (over args) missed it and fell back to the gullet point.
401      // Derive the span from the digested body and union it with any positional-
402      // arg span, so the wrapper (e.g. `ltx:Math`) spans its content. §3.1.3.
403      #[cfg(feature = "token-locators")]
404      if crate::state::source_map_enabled() {
405        if let Some(body_span) = post
406          .iter()
407          .filter_map(child_span)
408          .reduce(|a, b| Locator::new_range(a, b).unwrap_or(a))
409        {
410          whatsit.locator = Some(match whatsit.locator {
411            Some(prev) if prev.from_line != 0 => {
412              Locator::new_range(prev, body_span).unwrap_or(body_span)
413            },
414            _ => body_span,
415          });
416        }
417      }
418      whatsit.set_body(post);
419      post = vec![];
420      //info!(target: "constructor:capture", "whatsit: {:?}", whatsit);
421      // info!(target: "constructor:capture", "constructor: {:?}", self.get_cs_name());
422    }
423    let post_post = self.execute_after_digest_body(&mut whatsit)?;
424    // LaTeXML::Core::Definition::stopProfiling($profiled, 'digest') if $profiled;
425
426    // Package the result boxes
427    result.push(whatsit.into());
428    result.extend(post);
429    result.extend(post_post);
430    Ok(result)
431  }
432}
433
434impl Definition for Constructor {
435  fn before_digest(&self) -> Option<&Vec<BeforeDigestClosure>> { Some(&self.before_digest) }
436  fn after_digest(&self) -> Option<&Vec<DigestionClosure>> { Some(&self.after_digest) }
437  fn after_digest_body(&self) -> Option<&Vec<DigestionClosure>> { Some(&self.after_digest_body) }
438  fn capture_body(&self) -> bool { self.capture_body }
439  fn get_sizer(&self) -> Option<SizingClosure> { self.sizer.clone() }
440  fn invoke(&self, _once_only: bool) -> Result<Tokens> { Ok(Tokens!()) }
441  /// Digest the constructor; This should occur in the Stomach to create a Whatsit.
442  /// The whatsit which will be further processed to create the document.
443  ///
444  /// Allocates a fresh `Rc` for the Whatsit's back-reference. Callers that
445  /// already hold the state table's `Rc<Constructor>` should use
446  /// [`Constructor::invoke_primitive_shared`] instead, which reuses it — see
447  /// there for why that matters.
448  fn invoke_primitive(&self) -> Result<Vec<Digested>> {
449    self.digest_to_whatsit(Rc::new(self.clone()))
450  }
451
452  fn get_cs(&self) -> Cow<'_, Token> { Cow::Borrowed(&self.cs) }
453  fn get_cs_name(&self) -> Cow<'_, str> { Cow::Owned(self.cs.with_cs_name(ToString::to_string)) }
454  fn get_alias(&self) -> Option<&String> { self.alias.as_ref() }
455  fn get_parameters(&self) -> Option<&Parameters> { self.paramlist.as_ref() }
456  fn get_num_args(&self) -> usize {
457    match self.nargs {
458      Some(n) => n,
459      None => match self.paramlist {
460        Some(ref params) => params.get_num_args(),
461        None => 0,
462      },
463    }
464    // self.nargs = Some(nargs);
465  }
466
467  fn do_absorption(&self, document: &mut Document, whatsit: &Whatsit) -> Result<Vec<Node>> {
468    for pre_closure in &self.before_construct {
469      pre_closure(document, whatsit)?;
470    }
471
472    match self.replacement {
473      None => {
474        // info!(target:"constructor:replacement", "no replacement for {:?}", self.get_cs_name());
475      },
476      Some(ref main_closure) => {
477        main_closure(document, whatsit.get_args(), whatsit.get_properties())?
478      },
479    };
480
481    for post_closure in &self.after_construct {
482      post_closure(document, whatsit)?;
483    }
484    Ok(Vec::new())
485  }
486  fn get_reversion_spec(&self) -> Option<Reversion> { self.reversion.clone() }
487}
488
489#[cfg(test)]
490mod tests {
491  use super::*;
492
493  #[test]
494  fn constructor_options_default_all_false_none_empty() {
495    let o = ConstructorOptions::default();
496    assert!(o.nargs.is_none());
497    assert!(!o.bounded);
498    assert!(o.mode.is_none());
499    assert!(!o.enter_horizontal);
500    assert!(!o.leave_horizontal);
501    assert!(!o.require_math);
502    assert!(!o.forbid_math);
503    assert!(!o.capture_body);
504    assert!(o.font.is_none());
505    assert!(o.scope.is_none());
506    assert!(!o.robust);
507    assert!(!o.locked);
508    assert!(o.alias.is_none());
509    assert!(o.reversion.is_none());
510    assert!(o.sizer.is_none());
511    assert!(o.before_digest.is_empty());
512    assert!(o.after_digest.is_empty());
513    assert!(o.before_construct.is_empty());
514    assert!(o.after_construct.is_empty());
515    assert!(o.after_digest_begin.is_empty());
516    assert!(o.before_digest_end.is_empty());
517    assert!(o.after_digest_body.is_empty());
518  }
519
520  #[test]
521  fn constructor_default_fields() {
522    let c = Constructor::default();
523    assert!(c.nargs.is_none());
524    assert!(c.paramlist.is_none());
525    assert!(c.replacement.is_none());
526    assert!(c.sizer.is_none());
527    assert!(!c.capture_body);
528    assert!(c.alias.is_none());
529    assert!(c.reversion.is_none());
530    assert!(c.before_digest.is_empty());
531    assert!(c.after_digest.is_empty());
532    assert!(c.before_construct.is_empty());
533    assert!(c.after_construct.is_empty());
534    assert!(c.after_digest_body.is_empty());
535  }
536
537  #[test]
538  fn constructor_options_debug_includes_fields() {
539    // The hand-written Debug impl formats the struct; just verify it
540    // doesn't panic and produces a non-empty string that contains
541    // key field names.
542    let o = ConstructorOptions::default();
543    let s = format!("{o:?}");
544    assert!(s.contains("nargs"), "got {s:?}");
545    assert!(s.contains("bounded"), "got {s:?}");
546    assert!(s.contains("capture_body"), "got {s:?}");
547  }
548}