Skip to main content

latexml_core/definition/
math_primitive.rs

1use std::borrow::Cow;
2
3use libxml::tree::Node;
4
5use super::SizingClosure;
6// use crate::common::font::Font;
7use crate::common::arena::SymHashMap as HashMap;
8use crate::{
9  Digested,
10  common::{error::*, object::Object, store::Stored},
11  definition::{
12    BeforeDigestClosure, ConstructionClosure, Definition, DigestionClosure, FontDirective,
13    PrimitiveClosure, Reversion,
14  },
15  document::Document,
16  parameter::Parameters,
17  state::Scope,
18  token::*,
19  tokens::Tokens,
20  whatsit::Whatsit,
21};
22
23// DefMath Define a Mathematical symbol or function.
24// There are two sets of cases:
25//  (1) If the presentation appears to be TeX code, we create an XMDual,
26// since the presentation may end up with structure, etc.
27//  (2) But if the presentation is a simple string, or unicode,
28// it is just the content of the symbol; even if the function takes arguments.
29// ALSO
30//  arrange that the operator token gets cs="$cs"
31// ALSO
32//  Possibly some trick with SUMOP/INTOP affecting limits ?
33//  Well, not exactly, but....
34// HMM.... Still fishy.
35// When to make a dual ?
36// If the $presentation seems to be TeX (ie. it involves #1... but not ONLY!)
37
38#[derive(Clone)]
39pub struct MathPrimitiveOptions {
40  pub bounded:          bool,
41  pub mode:             Option<String>,
42  pub before_digest:    Vec<BeforeDigestClosure>,
43  pub after_digest:     Vec<DigestionClosure>,
44  pub before_construct: Vec<ConstructionClosure>,
45  pub after_construct:  Vec<ConstructionClosure>,
46  pub is_prefix:        bool,
47  pub scope:            Option<Scope>,
48  pub font:             Option<FontDirective>,
49  pub require_math:     bool,
50  pub forbid_math:      bool,
51  pub locked:           bool,
52  pub alias:            Option<String>,
53  pub decl_id:          Option<String>,
54  pub replace:          Option<String>,
55  pub protected:        bool,
56  pub robust:           bool,
57
58  // Math specific
59  pub name:                   Option<String>,
60  pub meaning:                Option<String>,
61  pub omcd:                   Option<String>,
62  pub reversion:              Option<Reversion>,
63  pub sizer:                  Option<SizingClosure>,
64  pub role:                   Option<String>,
65  pub operator_role:          Option<String>,
66  pub reorder:                bool,
67  pub dual:                   bool,
68  pub mathstyle:              Option<String>,
69  /// Dynamic mathstyle: compute "display"/"text" based on current font mathstyle at invocation
70  /// time Perl: mathstyle => \&doVariablesizeOp
71  pub dynamic_mathstyle:      bool,
72  pub scriptpos:              Option<String>,
73  /// Dynamic scriptpos: compute "mid"/"post" based on current font mathstyle at invocation time
74  /// Perl: scriptpos => \&doScriptpos
75  pub dynamic_scriptpos:      bool,
76  pub operator_scriptpos:     Option<usize>,
77  pub stretchy:               Option<bool>,
78  pub operator_stretchy:      Option<bool>,
79  pub nogroup:                bool,
80  pub hide_content_reversion: bool,
81  pub revert_as:              Option<Cow<'static, str>>,
82  pub lpadding:               Option<usize>,
83  pub rpadding:               Option<usize>,
84}
85impl Default for MathPrimitiveOptions {
86  fn default() -> Self {
87    MathPrimitiveOptions {
88      bounded:          false,
89      before_digest:    Vec::new(),
90      after_digest:     Vec::new(),
91      before_construct: Vec::new(),
92      after_construct:  Vec::new(),
93      mode:             None,
94      is_prefix:        false,
95      scope:            None,
96      require_math:     false,
97      forbid_math:      false,
98      locked:           false,
99      alias:            None,
100      font:             None,
101      decl_id:          None,
102      replace:          None,
103      protected:        false,
104      robust:           false,
105
106      // math-specific
107      name:                   None,
108      meaning:                None,
109      omcd:                   None,
110      reversion:              None,
111      sizer:                  None,
112      role:                   None,
113      operator_role:          None,
114      reorder:                false,
115      dual:                   false,
116      mathstyle:              None,
117      dynamic_mathstyle:      false,
118      scriptpos:              None,
119      dynamic_scriptpos:      false,
120      operator_scriptpos:     None,
121      stretchy:               None,
122      operator_stretchy:      None,
123      nogroup:                true,
124      hide_content_reversion: false,
125      revert_as:              None,
126      lpadding:               None,
127      rpadding:               None,
128    }
129  }
130}
131impl PartialEq for MathPrimitiveOptions {
132  fn eq(&self, other: &MathPrimitiveOptions) -> bool {
133    self.name == other.name && self.meaning == other.meaning && self.role == other.role
134  }
135}
136
137impl MathPrimitiveOptions {
138  pub fn to_hash_stored(&self) -> HashMap<Stored> {
139    let mut h = HashMap::default();
140    if let Some(ref meaning) = self.meaning {
141      h.insert("meaning", meaning.into());
142    }
143    if let Some(ref name) = self.name {
144      h.insert("name", name.into());
145    }
146    if let Some(ref omcd) = self.omcd {
147      h.insert("omcd", omcd.into());
148    }
149    if let Some(ref role) = self.role {
150      h.insert("role", role.into());
151    }
152    if let Some(ref decl_id) = self.decl_id {
153      h.insert("decl_id", decl_id.into());
154    }
155    if let Some(ref operator_role) = self.operator_role {
156      h.insert("operator_role", operator_role.into());
157    }
158    if let Some(ref mathstyle) = self.mathstyle {
159      h.insert("mathstyle", mathstyle.into());
160    }
161    if let Some(ref scriptpos) = self.scriptpos {
162      h.insert("scriptpos", scriptpos.into());
163    }
164    if let Some(ref operator_scriptpos) = self.operator_scriptpos {
165      h.insert(
166        "operator_scriptpos",
167        Stored::Int(*operator_scriptpos as i64),
168      );
169    }
170    if let Some(ref stretchy) = self.stretchy {
171      h.insert("stretchy", (*stretchy).into());
172    }
173    if let Some(ref stretchy) = self.operator_stretchy {
174      h.insert("operator_stretchy", (*stretchy).into());
175    }
176    if let Some(ref mode) = self.mode {
177      h.insert("mode", mode.into());
178    }
179    // TODO: Do we want to run the font closures here? Maybe?
180    if let Some(ref font_directive) = self.font {
181      h.insert("font", Stored::FontDirective(font_directive.clone()));
182    }
183    if let Some(ref lpadding) = self.lpadding {
184      h.insert("lpadding", (*lpadding).into());
185    }
186    if let Some(ref rpadding) = self.rpadding {
187      h.insert("rpadding", (*rpadding).into());
188    }
189
190    h
191  }
192
193  /// Like `to_hash_stored` but applies per-invocation overrides without
194  /// cloning the whole options. Used in DefMath closures (hot path —
195  /// one call per math token invocation).
196  pub fn to_hash_stored_with_overrides(
197    &self,
198    mode_override: Option<&'static str>,
199    mathstyle_override: Option<&'static str>,
200    scriptpos_override: Option<&'static str>,
201  ) -> HashMap<Stored> {
202    let mut h = self.to_hash_stored();
203    if let Some(m) = mode_override {
204      h.insert("mode", Stored::String(crate::common::arena::pin_static(m)));
205    }
206    if let Some(ms) = mathstyle_override {
207      h.insert(
208        "mathstyle",
209        Stored::String(crate::common::arena::pin_static(ms)),
210      );
211    }
212    if let Some(sp) = scriptpos_override {
213      h.insert(
214        "scriptpos",
215        Stored::String(crate::common::arena::pin_static(sp)),
216      );
217    }
218    h
219  }
220
221  // Attempt at emulating the `%simpletoken_options` check in Perl
222  /// Checks if complex options are present,
223  /// suggestive of using a `Constructor` instead of a `Primitive`
224  pub fn has_complex_option(&self) -> bool {
225    //DG: note that `nogroup` is true by default, so checking for it is counter-intuitive (should
226    // we even?)
227    self.bounded
228      || self.mode.is_some()
229      || !self.before_digest.is_empty()
230      || !self.after_digest.is_empty()
231      || self.is_prefix
232      || self.require_math
233      || self.forbid_math
234      || self.alias.is_some()
235      || self.decl_id.is_some()
236      || self.replace.is_some()
237      || self.reversion.is_some()
238      || self.sizer.is_some()
239      || self.operator_role.is_some()
240      || self.reorder
241      || self.dual
242      || self.operator_scriptpos.is_some()
243      || self.stretchy.is_some()
244      || self.operator_stretchy.is_some()
245      || self.hide_content_reversion
246      || self.revert_as.is_some()
247  }
248}
249
250#[derive(Clone)]
251pub struct MathPrimitive {
252  pub cs:          Token,
253  pub paramlist:   Option<Parameters>,
254  pub nargs:       Option<usize>,
255  pub replacement: Option<PrimitiveClosure>,
256  pub options:     MathPrimitiveOptions,
257  pub alias:       Option<String>,
258}
259impl Default for MathPrimitive {
260  fn default() -> Self {
261    MathPrimitive {
262      cs:          T_CS!("MathPrimitive"),
263      paramlist:   None,
264      nargs:       None,
265      replacement: None,
266      options:     MathPrimitiveOptions::default(),
267      alias:       None,
268    }
269  }
270}
271impl PartialEq for MathPrimitive {
272  fn eq(&self, other: &MathPrimitive) -> bool { self.cs == other.cs }
273}
274
275// impl fmt::Display for MathPrimitive {
276//   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
277//     todo!();
278//   }
279// }
280impl Object for MathPrimitive {
281  fn stringify(&self) -> String { <Self as Definition>::stringify_type(self, "MathPrimitive") }
282}
283impl Definition for MathPrimitive {
284  fn before_digest(&self) -> Option<&Vec<BeforeDigestClosure>> { Some(&self.options.before_digest) }
285  fn after_digest(&self) -> Option<&Vec<DigestionClosure>> { Some(&self.options.after_digest) }
286  fn invoke(&self, _once_only: bool) -> Result<Tokens> { Ok(Tokens!()) }
287  fn invoke_primitive(&self) -> Result<Vec<Digested>> {
288    // Info!("MathPrimitive", "invoke", stomach, "invoke for {:?}", self.cs);
289    // my $profiled = $state->lookupValue('PROFILING') && ($LaTeXML::CURRENT_TOKEN || $$self{cs});
290    // my $tracing = $state->lookupValue('tracingcommands');
291    // LaTeXML::Core::Definition::startProfiling($profiled, 'digest') if $profiled;
292    // print STDERR '{' . $self->tracingCSName . "}\n" if $tracing;
293    let mut result: Vec<Digested> = self.execute_before_digest()?;
294    let args = self.read_arguments()?;
295    // print STDERR $self->tracingArgs(@args) . "\n" if $tracing && @args;
296    let replacement_result = match self.replacement {
297      None => Vec::new(),
298      Some(ref closure) => closure(args)?,
299    };
300    result.extend(replacement_result);
301    let mut w = Whatsit::default();
302    let after_result = self.execute_after_digest(&mut w)?;
303    result.extend(after_result);
304
305    // LaTeXML::Core::Definition::stopProfiling($profiled, 'digest') if $profiled;
306    Ok(result)
307  }
308
309  fn do_absorption(&self, _document: &mut Document, _whatsit: &Whatsit) -> Result<Vec<Node>> {
310    fatal!(
311      Definition,
312      Unexpected,
313      "do_absorption on MathPrimitive should never be called!"
314    );
315  }
316
317  fn get_cs(&self) -> Cow<'_, Token> { Cow::Borrowed(&self.cs) }
318  fn get_cs_name(&self) -> Cow<'_, str> { Cow::Owned(self.cs.with_cs_name(ToString::to_string)) }
319  fn get_alias(&self) -> Option<&String> { self.alias.as_ref() }
320  fn get_parameters(&self) -> Option<&Parameters> { self.paramlist.as_ref() }
321  fn get_num_args(&self) -> usize {
322    match self.nargs {
323      Some(n) => n,
324      None => match self.paramlist {
325        Some(ref params) => params.get_num_args(),
326        None => 0,
327      },
328    }
329    // TODO: Rethink the memoize in this immutable setting
330    // self.nargs = Some(nargs);
331  }
332}
333
334#[cfg(test)]
335mod tests {
336  use super::*;
337
338  #[test]
339  fn math_primitive_options_default_fields() {
340    let o = MathPrimitiveOptions::default();
341    // Spot-check representative fields from the ~30-field struct.
342    assert!(!o.bounded);
343    assert!(!o.is_prefix);
344    assert!(!o.require_math);
345    assert!(!o.forbid_math);
346    assert!(!o.locked);
347    assert!(!o.robust);
348    assert!(!o.protected);
349    assert!(!o.reorder);
350    assert!(!o.dual);
351    assert!(!o.dynamic_mathstyle);
352    assert!(!o.dynamic_scriptpos);
353    assert!(o.nogroup, "nogroup defaults to true (Perl parity)");
354    assert!(!o.hide_content_reversion);
355    assert!(o.name.is_none());
356    assert!(o.meaning.is_none());
357    assert!(o.role.is_none());
358    assert!(o.operator_role.is_none());
359    assert!(o.mathstyle.is_none());
360    assert!(o.scriptpos.is_none());
361    assert!(o.operator_scriptpos.is_none());
362    assert!(o.stretchy.is_none());
363    assert!(o.operator_stretchy.is_none());
364    assert!(o.revert_as.is_none());
365    assert!(o.lpadding.is_none());
366    assert!(o.rpadding.is_none());
367    assert!(o.before_digest.is_empty());
368    assert!(o.after_digest.is_empty());
369  }
370
371  #[test]
372  fn math_primitive_options_partial_eq_by_subset() {
373    // PartialEq compares name + meaning + role only — defaults of
374    // other fields don't affect equality.
375    let mut a = MathPrimitiveOptions::default();
376    let mut b = MathPrimitiveOptions::default();
377    a.name = Some("plus".into());
378    b.name = Some("plus".into());
379    assert!(a == b, "same name equal");
380    // Changing a non-compared field still keeps them equal.
381    a.locked = true;
382    assert!(a == b);
383    // Changing a compared field breaks equality.
384    b.name = Some("times".into());
385    assert!(!(a == b));
386  }
387
388  #[test]
389  fn math_primitive_options_to_hash_stored_empty_default() {
390    // Default options with no string fields set produces an empty
391    // HashMap.
392    let o = MathPrimitiveOptions::default();
393    let h = o.to_hash_stored();
394    assert_eq!(h.len(), 0);
395  }
396
397  #[test]
398  fn math_primitive_options_to_hash_stored_with_fields() {
399    let o = MathPrimitiveOptions {
400      meaning: Some("plus".into()),
401      name: Some("+".into()),
402      ..Default::default()
403    };
404    let h = o.to_hash_stored();
405    assert!(h.contains_key("meaning"));
406    assert!(h.contains_key("name"));
407    assert!(
408      !h.contains_key("role"),
409      "role=None shouldn't populate the hash"
410    );
411  }
412
413  #[test]
414  fn math_primitive_default_fields() {
415    let m = MathPrimitive::default();
416    assert!(m.paramlist.is_none());
417    assert!(m.replacement.is_none());
418    assert!(m.alias.is_none());
419    assert!(m.nargs.is_none());
420    // options and flags live in m.options, not directly on the struct.
421    assert!(m.options.reversion.is_none());
422    assert!(!m.options.is_prefix);
423  }
424
425  #[test]
426  fn math_primitive_partial_eq_by_cs() {
427    let a = MathPrimitive::default();
428    let b = MathPrimitive::default();
429    // Both defaults have the same cs → equal.
430    assert!(a == b);
431  }
432}