Skip to main content

latexml_core/binding/def/
builder.rs

1//! `ConstructorBuilder` — the shared lowering for `DefConstructor`.
2//!
3//! Both binding front-ends target this one builder, so they cannot drift:
4//! * the compile-time `DefConstructor!` macro (`latexml_engine`), and
5//! * the runtime Rhai script layer (`latexml_contrib::script_bindings`).
6//!
7//! It is front-end-agnostic: it takes already-native values and closures, so it
8//! lives in `latexml_core` and pulls in neither the macro machinery nor Rhai.
9//!
10//! Two kinds of options, by how much can be shared:
11//! * **Scalar options** (`mode`, `bounded`, …) go through one generic
12//!   [`ConstructorBuilder::set_option`] — the key→field mapping lives in exactly one place, so
13//!   adding a scalar option updates both front-ends at once.
14//! * **Closure options** (`afterDigest`, …) have typed setters: the field and `install` are shared,
15//!   while the closure itself is produced by whichever front-end (a macro `$body:block`, or a Rhai
16//!   trampoline).
17
18use super::dialect::{def_constructor, def_environment};
19use crate::{
20  common::{
21    def_parser::{parse_parameters, parse_prototype},
22    error::{Error, Result},
23  },
24  definition::{
25    BeforeDigestClosure, ConstructionClosure, DigestionClosure, FontDirective, PropertiesClosure,
26    ReplacementClosure, Reversion, constructor::ConstructorOptions,
27  },
28  parameter::Parameters,
29  token::Token,
30  util::text::{Delimiter, extract_bracketed},
31};
32
33/// The typed (closure/structured) option setters shared verbatim by
34/// [`ConstructorBuilder`] and [`EnvironmentBuilder`] — one macro invocation per
35/// builder keeps the two surfaces identical without a trait object.
36macro_rules! shared_hook_setters {
37  () => {
38    /// Push an `afterDigest` hook (constructor: after digestion; environment:
39    /// runs on the `\end` whatsit — Perl semantics).
40    pub fn after_digest(mut self, hook: DigestionClosure) -> Self {
41      self.options.after_digest.push(hook);
42      self
43    }
44
45    /// Push an `afterDigestBegin` hook (environments: runs on the `\begin`
46    /// whatsit right after its arguments digest).
47    pub fn after_digest_begin(mut self, hook: DigestionClosure) -> Self {
48      self.options.after_digest_begin.push(hook);
49      self
50    }
51
52    /// Push an `afterDigestBody` hook (environments / box constructors: runs on
53    /// the whatsit after the captured body digests — Perl's `afterDigestBody`).
54    pub fn after_digest_body(mut self, hook: DigestionClosure) -> Self {
55      self.options.after_digest_body.push(hook);
56      self
57    }
58
59    /// Push a `beforeDigest` hook (runs before the arguments are digested —
60    /// Perl's `beforeDigest => sub {…}`, e.g. `\footnote`'s `neutralize_font`).
61    pub fn before_digest(mut self, hook: BeforeDigestClosure) -> Self {
62      self.options.before_digest.push(hook);
63      self
64    }
65
66    /// Push a `beforeDigestEnd` hook (environments: before `\end{…}` digests).
67    pub fn before_digest_end(mut self, hook: BeforeDigestClosure) -> Self {
68      self.options.before_digest_end.push(hook);
69      self
70    }
71
72    /// Push a `beforeConstruct` hook (runs before the replacement absorbs).
73    pub fn before_construct(mut self, hook: ConstructionClosure) -> Self {
74      self.options.before_construct.push(hook);
75      self
76    }
77
78    /// Push an `afterConstruct` hook (runs after the replacement absorbs).
79    pub fn after_construct(mut self, hook: ConstructionClosure) -> Self {
80      self.options.after_construct.push(hook);
81      self
82    }
83
84    /// Set the `properties` closure (computes the whatsit's property map from
85    /// the digested args — Perl's `properties => sub {…}` / `properties => {…}`).
86    pub fn properties(mut self, props: PropertiesClosure) -> Self {
87      self.options.properties = props;
88      self
89    }
90
91    /// Set the reversion (`reversion => "…"` token form or a closure).
92    pub fn reversion(mut self, rev: Reversion) -> Self {
93      self.options.reversion = Some(rev);
94      self
95    }
96
97    /// Set the font directive (`font => { family => …, … }`).
98    pub fn font(mut self, font: FontDirective) -> Self {
99      self.options.font = Some(font);
100      self
101    }
102
103    /// Set the sizer (`sizer => sub {…}` computing (width, height, depth)).
104    pub fn sizer(mut self, sizer: crate::definition::SizingClosure) -> Self {
105      self.options.sizer = Some(sizer);
106      self
107    }
108  };
109}
110
111/// A scalar option value handed to [`ConstructorBuilder::set_option`]. Both
112/// front-ends produce these (the macro from a literal, Rhai from a `Dynamic`),
113/// so the key→field switch is single-source.
114pub enum OptionValue {
115  Str(String),
116  Bool(bool),
117  Int(i64),
118}
119
120impl OptionValue {
121  fn into_string(self) -> Result<String> {
122    match self {
123      OptionValue::Str(s) => Ok(s),
124      _ => Err(Error::from("constructor option expected a string value")),
125    }
126  }
127
128  /// Coerce to bool: `Bool` as-is, `Int` non-zero, `Str` non-empty. Public so
129  /// the runtime (Rhai) front-end's `dynamic_to_bool` can share this exact
130  /// policy instead of re-deriving it (review m4 — keeps `bounded: 1` /
131  /// `protected: "yes"` meaning the same on both front-ends).
132  pub fn into_bool(self) -> Result<bool> {
133    match self {
134      OptionValue::Bool(b) => Ok(b),
135      OptionValue::Int(i) => Ok(i != 0),
136      OptionValue::Str(s) => Ok(!s.is_empty()),
137    }
138  }
139}
140
141/// Accumulates a constructor definition and installs it via [`def_constructor`].
142pub struct ConstructorBuilder {
143  cs:          Token,
144  paramlist:   Option<Parameters>,
145  replacement: Option<ReplacementClosure>,
146  options:     ConstructorOptions,
147}
148
149impl ConstructorBuilder {
150  /// Parse the prototype (shared with the macro path via `parse_prototype`).
151  pub fn new(proto: &str) -> Result<Self> {
152    let (cs, paramlist) = parse_prototype(proto, true)?;
153    Ok(Self {
154      cs,
155      paramlist,
156      replacement: None,
157      options: ConstructorOptions::default(),
158    })
159  }
160
161  /// Set the XML replacement (template- or closure-derived; built by the caller).
162  pub fn replacement(mut self, repl: ReplacementClosure) -> Self {
163    self.replacement = Some(repl);
164    self
165  }
166
167  /// Apply a **scalar** option by name (see `apply_scalar_option`, the
168  /// single-source key→field map shared with [`EnvironmentBuilder`]).
169  pub fn set_option(mut self, key: &str, value: OptionValue) -> Result<Self> {
170    apply_scalar_option(&mut self.options, key, value)?;
171    Ok(self)
172  }
173
174  shared_hook_setters!();
175
176  /// Install the accumulated definition.
177  pub fn install(self) -> Result<()> {
178    def_constructor(self.cs, self.paramlist, self.replacement, self.options);
179    Ok(())
180  }
181}
182
183/// Apply a **scalar** option by name onto `ConstructorOptions`. THE single
184/// source of truth for the option-name → field mapping — both builders (and so
185/// both front-ends) route scalar options through here, so a new scalar option is
186/// added in exactly one place. Unknown keys are ignored (runtime-forgiving,
187/// matching Perl `%options`).
188fn apply_scalar_option(
189  options: &mut ConstructorOptions,
190  key: &str,
191  value: OptionValue,
192) -> Result<()> {
193  match key {
194    "mode" => options.mode = Some(value.into_string()?),
195    "bounded" => options.bounded = value.into_bool()?,
196    "requireMath" => options.require_math = value.into_bool()?,
197    "forbidMath" => options.forbid_math = value.into_bool()?,
198    "enterHorizontal" => options.enter_horizontal = value.into_bool()?,
199    "leaveHorizontal" => options.leave_horizontal = value.into_bool()?,
200    "captureBody" => options.capture_body = value.into_bool()?,
201    "alias" => options.alias = Some(value.into_string()?),
202    _ => log::debug!("binding builder: ignoring unknown scalar option '{key}'"),
203  }
204  Ok(())
205}
206
207/// Accumulates an environment definition and installs it via [`def_environment`]
208/// — the environment analog of [`ConstructorBuilder`], sharing the same
209/// option machinery. The prototype is the `DefEnvironment!` shape:
210/// `"{name}"` or `"{name}{}…"` (env name in braces, then the parameter list).
211pub struct EnvironmentBuilder {
212  name:        String,
213  paramlist:   Option<Parameters>,
214  replacement: Option<ReplacementClosure>,
215  options:     ConstructorOptions,
216}
217
218impl EnvironmentBuilder {
219  /// Parse the `{name}<params>` prototype (mirrors the `DefEnvironmentWO!`
220  /// macro: extract the braced name, parse the remainder as parameters against
221  /// a synthetic `\name` control sequence).
222  pub fn new(proto: &str) -> Result<Self> {
223    let mut proto = proto.trim_start().to_string();
224    let name = extract_bracketed(&mut proto, Some(&Delimiter::Brace)).ok_or_else(|| {
225      Error::from(format!(
226        "DefEnvironment prototype must start with {{name}}: {proto:?}"
227      ))
228    })?;
229    let paramlist_str = proto.trim_start().to_string();
230    let paramlist = if paramlist_str.is_empty() {
231      None
232    } else {
233      let cs = crate::T_CS!(crate::s!("\\{}", &name));
234      parse_parameters(&paramlist_str, &cs, true)?
235    };
236    Ok(Self {
237      name,
238      paramlist,
239      replacement: None,
240      options: ConstructorOptions::default(),
241    })
242  }
243
244  /// Set the XML replacement (typically referencing `#body`).
245  pub fn replacement(mut self, repl: ReplacementClosure) -> Self {
246    self.replacement = Some(repl);
247    self
248  }
249
250  /// Apply a **scalar** option by name (shared map: `apply_scalar_option`).
251  pub fn set_option(mut self, key: &str, value: OptionValue) -> Result<Self> {
252    apply_scalar_option(&mut self.options, key, value)?;
253    Ok(self)
254  }
255
256  shared_hook_setters!();
257
258  /// Install the accumulated environment definition.
259  pub fn install(self) -> Result<()> {
260    def_environment(self.name, self.paramlist, self.replacement, self.options);
261    Ok(())
262  }
263}