Skip to main content

latexml_core/definition/
primitive.rs

1use std::borrow::Cow;
2
3use libxml::tree::Node;
4
5use crate::{
6  Digested,
7  common::{arena::SymHashMap, error::*, object::Object, store::Stored},
8  definition::{
9    BeforeDigestClosure, Definition, DigestionClosure, FontDirective, PrimitiveBody, Reversion,
10  },
11  document::Document,
12  parameter::Parameters,
13  pin,
14  state::Scope,
15  tbox::Tbox,
16  token::*,
17  tokens::Tokens,
18  whatsit::Whatsit,
19};
20
21#[derive(Clone, Default)]
22pub struct PrimitiveOptions {
23  pub bounded:          bool,
24  pub is_prefix:        bool,
25  pub require_math:     bool,
26  pub forbid_math:      bool,
27  pub robust:           bool,
28  pub locked:           bool,
29  pub enter_horizontal: bool,
30  pub leave_horizontal: bool,
31  pub nargs:            Option<usize>,
32  pub scope:            Option<Scope>,
33  pub font:             Option<FontDirective>,
34  pub mode:             Option<String>,
35  pub alias:            Option<String>,
36  pub before_digest:    Vec<BeforeDigestClosure>,
37  pub after_digest:     Vec<DigestionClosure>,
38  pub reversion:        Option<Reversion>,
39  /// The fontinfo lookup key for `\font`-defined primitives. See
40  /// `Primitive::font_id`.
41  pub font_id:          Option<crate::common::arena::data::SymStr>,
42}
43
44#[derive(Clone)]
45pub struct Primitive {
46  pub cs:            Token,
47  pub paramlist:     Option<Parameters>,
48  // TODO: we have a case where the replacement is a simple string/character
49  //       which gets auto-wrapped with a Tbox during invoke.
50  pub replacement:   Option<PrimitiveBody>,
51  pub before_digest: Vec<BeforeDigestClosure>,
52  pub after_digest:  Vec<DigestionClosure>,
53  pub alias:         Option<String>,
54  pub nargs:         Option<usize>,
55  pub reversion:     Option<Reversion>,
56  pub is_prefix:     bool,
57  /// Set on `\font`-defined primitives (Perl `LaTeXML::Core::Definition::FontDef::fontID`).
58  /// Holds the value-table key under which this CS's fontinfo hash lives
59  /// (e.g. `\tenrm` → `Some("fontinfo_\\tenrm")`). Lets the dumper round-trip
60  /// font-defined primitives via Perl's `FD(<cs>)` record (see
61  /// `Core/Dumper.pm` L356-389) — closures aren't serializable but the
62  /// font_id + the dumped `Stored::Font` value at that key let the reader
63  /// rebuild an equivalent merge-font Primitive.
64  pub font_id:       Option<crate::common::arena::data::SymStr>,
65}
66impl Default for Primitive {
67  fn default() -> Self {
68    Primitive {
69      cs:            T_CS!("Primitive"),
70      paramlist:     None,
71      replacement:   None,
72      alias:         None,
73      before_digest: Vec::new(),
74      after_digest:  Vec::new(),
75      nargs:         None,
76      reversion:     None,
77      is_prefix:     false,
78      font_id:       None,
79    }
80  }
81}
82impl PartialEq for Primitive {
83  fn eq(&self, other: &Primitive) -> bool { self.cs == other.cs }
84}
85
86// impl fmt::Display for Primitive {
87//   fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
88//     todo!();
89//   }
90// }
91impl Object for Primitive {
92  fn stringify(&self) -> String { <Self as Definition>::stringify_type(self, "Primitive") }
93}
94impl Definition for Primitive {
95  fn before_digest(&self) -> Option<&Vec<BeforeDigestClosure>> { Some(&self.before_digest) }
96  fn after_digest(&self) -> Option<&Vec<DigestionClosure>> { Some(&self.after_digest) }
97  fn is_prefix(&self) -> bool { self.is_prefix }
98
99  fn invoke(&self, _once_only: bool) -> Result<Tokens> { Ok(Tokens!()) }
100  fn invoke_primitive(&self) -> Result<Vec<Digested>> {
101    Debug!("primitive invoke for {:?}", self.cs);
102    // my $profiled = $state->lookupValue('PROFILING') && ($LaTeXML::CURRENT_TOKEN || $$self{cs});
103    // my $tracing = $state->lookupValue('tracingcommands');
104    // LaTeXML::Core::Definition::startProfiling($profiled, 'digest') if $profiled;
105    // print STDERR '{' . $self->tracingCSName . "}\n" if $tracing;
106    let mut invoked_boxes: Vec<Digested> = self.execute_before_digest()?;
107    let args = self.read_arguments()?;
108    // print STDERR $self->tracingArgs(@args) . "\n" if $tracing && @args;
109    match self.replacement {
110      Some(PrimitiveBody::Closure(ref closure)) => invoked_boxes.extend(closure(args)?),
111      Some(PrimitiveBody::String(symbol)) => {
112        // Perl L67: $stomach->enterHorizontal if defined $replacement
113        crate::stomach::enter_horizontal();
114        let cs_token = self
115          .alias
116          .as_ref()
117          .map(|alias| Token::from(alias.as_str()))
118          .unwrap_or(self.cs);
119        let mut box_tokens = vec![cs_token];
120        // Perl L69: append revertArguments for parameterized string primitives
121        if let Some(ref params) = self.paramlist {
122          for arg in &args {
123            box_tokens.extend(arg.revert()?.unlist());
124          }
125          let _ = params; // acknowledge usage
126        }
127        let box_props = SymHashMap::default();
128        invoked_boxes.push(Digested::from(Tbox::new(
129          symbol,
130          None,
131          None,
132          Tokens::new(box_tokens),
133          box_props,
134        )));
135      },
136      None => {
137        // Perl: Box(undef, undef, undef, Tokens($self->getCSorAlias, ...), isEmpty => 1)
138        // Even with no replacement, Perl creates a Box with the CS as reversion and isEmpty flag.
139        // This is essential for font switches (\rm, \it, etc.) to appear in tex attributes.
140        let cs_token = self
141          .alias
142          .as_ref()
143          .map(|alias| Token::from(alias.as_str()))
144          .unwrap_or(self.cs);
145        let box_tokens = vec![cs_token];
146        // TODO: add revert_arguments for ArgWrap type when needed
147        let mut box_props = SymHashMap::default();
148        box_props.insert("isEmpty", Stored::Bool(true));
149        invoked_boxes.push(Digested::from(Tbox::new(
150          pin!(""),
151          None,
152          None,
153          Tokens::new(box_tokens),
154          box_props,
155        )));
156      },
157    }
158    if !self.after_digest.is_empty() {
159      // optimize to avoid needless generation of whatsits
160      let mut w = Whatsit::default();
161      let after_boxes = self.execute_after_digest(&mut w)?;
162      invoked_boxes.extend(after_boxes);
163    }
164
165    // LaTeXML::Core::Definition::stopProfiling($profiled, 'digest') if $profiled;
166    Ok(invoked_boxes)
167  }
168
169  fn do_absorption(&self, _document: &mut Document, _whatsit: &Whatsit) -> Result<Vec<Node>> {
170    fatal!(
171      Definition,
172      Unexpected,
173      "do_absorption on Primitive should never be called!"
174    );
175  }
176
177  fn get_cs(&self) -> Cow<'_, Token> { Cow::Borrowed(&self.cs) }
178  fn get_cs_name(&self) -> Cow<'_, str> { Cow::Owned(self.cs.with_cs_name(ToString::to_string)) }
179  fn get_alias(&self) -> Option<&String> { self.alias.as_ref() }
180  fn get_parameters(&self) -> Option<&Parameters> { self.paramlist.as_ref() }
181
182  fn get_num_args(&self) -> usize {
183    match self.nargs {
184      Some(n) => n,
185      None => match self.paramlist {
186        Some(ref params) => params.get_num_args(),
187        None => 0,
188      },
189    }
190    // TODO: Rethink the memoize in this immutable setting
191    // self.nargs = Some(nargs);
192  }
193}
194
195#[cfg(test)]
196mod tests {
197  use super::*;
198  use crate::common::arena;
199
200  #[test]
201  fn primitive_default_fields() {
202    let p = Primitive::default();
203    assert_eq!(arena::to_string(p.cs.text), "Primitive");
204    assert!(p.paramlist.is_none());
205    assert!(p.replacement.is_none());
206    assert!(p.alias.is_none());
207    assert!(p.before_digest.is_empty());
208    assert!(p.after_digest.is_empty());
209    assert!(p.nargs.is_none());
210    assert!(p.reversion.is_none());
211    assert!(!p.is_prefix);
212  }
213
214  #[test]
215  fn primitive_partial_eq_by_cs() {
216    // PartialEq compares by cs only — Perl parity (closures can't
217    // be structurally compared).
218    // Primitive doesn't derive Debug, so assert_eq! / assert_ne!
219    // can't format it on failure — use plain equality checks.
220    let mut a = Primitive::default();
221    let mut b = Primitive::default();
222    a.cs = T_CS!("\\foo");
223    b.cs = T_CS!("\\foo");
224    assert!(a == b, "same cs should compare equal");
225    b.cs = T_CS!("\\bar");
226    assert!(!(a == b), "different cs should not be equal");
227  }
228
229  #[test]
230  fn primitive_is_prefix_reflects_field() {
231    let mut p = Primitive::default();
232    assert!(!p.is_prefix());
233    p.is_prefix = true;
234    assert!(p.is_prefix());
235  }
236
237  #[test]
238  fn primitive_get_num_args_zero_without_params() {
239    let p = Primitive::default();
240    assert_eq!(p.get_num_args(), 0);
241  }
242
243  #[test]
244  fn primitive_get_num_args_uses_nargs_override() {
245    // If nargs is explicitly set, it takes precedence over paramlist.
246    let p = Primitive {
247      nargs: Some(3),
248      ..Default::default()
249    };
250    assert_eq!(p.get_num_args(), 3);
251  }
252
253  #[test]
254  fn primitive_before_digest_ref_returns_some_empty() {
255    let p = Primitive::default();
256    let bd = p.before_digest().expect("Some(&Vec)");
257    assert!(bd.is_empty());
258  }
259
260  #[test]
261  fn primitive_after_digest_ref_returns_some_empty() {
262    let p = Primitive::default();
263    let ad = p.after_digest().expect("Some(&Vec)");
264    assert!(ad.is_empty());
265  }
266
267  #[test]
268  fn primitive_get_parameters_none_by_default() {
269    let p = Primitive::default();
270    assert!(p.get_parameters().is_none());
271  }
272
273  #[test]
274  fn primitive_options_default_all_false() {
275    let o = PrimitiveOptions::default();
276    assert!(!o.bounded);
277    assert!(!o.is_prefix);
278    assert!(!o.require_math);
279    assert!(!o.forbid_math);
280    assert!(!o.robust);
281    assert!(!o.locked);
282    assert!(!o.enter_horizontal);
283    assert!(!o.leave_horizontal);
284    assert!(o.nargs.is_none());
285    assert!(o.scope.is_none());
286    assert!(o.font.is_none());
287    assert!(o.mode.is_none());
288    assert!(o.alias.is_none());
289    assert!(o.before_digest.is_empty());
290    assert!(o.after_digest.is_empty());
291    assert!(o.reversion.is_none());
292  }
293}