Skip to main content

latexml_core/common/relaxng/
mod.rs

1//! Native Rust port of LaTeXML's `LaTeXML::Common::Model::RelaxNG`.
2//!
3//! Walks a RelaxNG XML schema, builds an in-memory pattern AST, simplifies
4//! it (binding/grammar/include resolution, definition recording), and can
5//! emit the LaTeX manual.tex consumed by `latexmlman.sty` for schema
6//! documentation.
7//!
8//! Three sub-modules carry the implementation, mirroring the natural
9//! sections of the upstream Perl source:
10//!
11//! * [`scan`]      — RNG XML → AST  (port of `scanPattern` etc., L100–390).
12//! * [`simplify`]  — AST normalization (port of `simplify*`, L438–525).
13//! * [`tex`]       — schema-doc TeX emission (port of `documentModules`, `toTeX*`, L550–815).
14//!
15//! The shared state — definition tables, element index, "Used by" graph —
16//! lives on [`Relaxng`] and is populated by `scan` + `simplify`, then
17//! consumed by `tex`. The Perl original mutates `$$self{...}` from
18//! several methods at once; the Rust version threads `&mut self` the
19//! same way.
20
21use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
22
23use crate::{
24  common::{model::LTX_NAMESPACE, xml::XML_NS},
25  document::Document,
26};
27
28pub mod embedded;
29pub mod scan;
30pub mod simplify;
31pub mod tex;
32
33// ----- AST ----------------------------------------------------------------
34
35/// Combiner kind on a `<define>` element.
36///
37/// Bare `<define>` is `Group`; `<define combine="choice">` is `Choice`;
38/// `<define combine="interleave">` is `Interleave`. Mirrors the suffix on
39/// upstream's `def`/`defchoice`/`definterleave` ops.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum DefCombiner {
42  Group,
43  Choice,
44  Interleave,
45}
46
47/// Combiner kind for a `<group|interleave|choice|optional|zeroOrMore|
48/// oneOrMore|list>` pattern wrapper.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum CombineOp {
51  Group,
52  Interleave,
53  Choice,
54  Optional,
55  ZeroOrMore,
56  OneOrMore,
57  List,
58}
59
60/// One node in the RelaxNG AST, mirroring Perl `RelaxNG.pm`'s
61/// `[$op, $name, @forms]` arrays.
62///
63/// The names here line up 1:1 with the Perl op strings:
64///
65/// | Perl op            | Rust variant       |
66/// |--------------------|--------------------|
67/// | `ref`              | [`Pattern::Ref`]   |
68/// | `parentref`        | [`Pattern::ParentRef`] |
69/// | `elementref`       | [`Pattern::ElementRef`] (added during simplify) |
70/// | `def`/`defchoice`/`definterleave` | [`Pattern::Def`] (combiner discriminates) |
71/// | `element`          | [`Pattern::Element`] |
72/// | `attribute`        | [`Pattern::Attribute`] |
73/// | `start`            | [`Pattern::Start`] |
74/// | `value`            | [`Pattern::Value`] |
75/// | `data`             | [`Pattern::Data`] |
76/// | `doc`              | [`Pattern::Doc`] |
77/// | `combination`      | [`Pattern::Combination`] |
78/// | `grammar`          | [`Pattern::Grammar`] |
79/// | `module`           | [`Pattern::Module`] |
80/// | `override`         | [`Pattern::Override`] (consumed by simplify) |
81/// | `'#PCDATA'` (string leaf) | [`Pattern::Text`] |
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum Pattern {
84  /// Reference to a defined pattern. `qname` is the bare name during
85  /// `scan` and `binding:name` after `simplify`.
86  Ref { qname: String },
87  /// Reference to a parent grammar's defined pattern (replaced by
88  /// `Ref` during simplify).
89  ParentRef { qname: String },
90  /// Reference to an element by tag name (introduced during simplify
91  /// when a `Def` resolves to a single `Element`).
92  ElementRef { qname: String },
93  /// `<define>` (or `combine="choice"|"interleave"`).
94  Def {
95    combiner: DefCombiner,
96    name:     String,
97    body:     Vec<Pattern>,
98  },
99  /// `<element name="...">CONTENT</element>`.
100  Element { name: String, body: Vec<Pattern> },
101  /// `<attribute name="...">CONTENT</attribute>`.
102  Attribute { name: String, body: Vec<Pattern> },
103  /// `<start>...</start>`.
104  Start { body: Vec<Pattern> },
105  /// `<value>X</value>` — a literal value (typically for attributes).
106  Value(String),
107  /// `<data type="X"/>` — a typed datum.
108  Data(String),
109  /// `<a:documentation>X</a:documentation>` — annotation text.
110  Doc(String),
111  /// `<group|interleave|choice|optional|zeroOrMore|oneOrMore|list>...</...>`.
112  ///
113  /// `<mixed>` is normalised here into `Combination { Interleave, [Text, …] }`.
114  Combination { op: CombineOp, body: Vec<Pattern> },
115  /// `<grammar>...</grammar>` — defines a fresh symbol scope. Replaced
116  /// by its `start` pattern after simplify.
117  Grammar { name: String, body: Vec<Pattern> },
118  /// External / included module: contents from a separate schema file,
119  /// recorded in [`Relaxng::modules`] for documentation.
120  Module { name: String, body: Vec<Pattern> },
121  /// `<include>...</include>` with override rules (consumed by simplify
122  /// — patches the inner `Module` and disappears).
123  Override {
124    module:       Box<Pattern>,
125    replacements: Vec<Pattern>,
126  },
127  /// `#PCDATA` — text leaf.
128  Text,
129}
130
131// ----- Schema state -------------------------------------------------------
132
133/// Internal representation of a RelaxNG schema. Built by [`scan`] and
134/// [`simplify`]; consumed by [`tex`] (and, for runtime validation,
135/// would be consumed by `Model::add_tag_content` etc.).
136///
137/// The mutable fields beyond `name` and `modules` are populated during
138/// `simplify`:
139///
140/// * [`elementdefs`](Relaxng::elementdefs) — pattern qname → element tag, when a pattern resolves
141///   to a single element.
142/// * [`element_reverse_defs`](Relaxng::element_reverse_defs) — inverse of `elementdefs`.
143/// * [`elements`](Relaxng::elements) — element tag → list of body patterns, accumulating across
144///   overrides / re-definitions.
145/// * [`defs`](Relaxng::defs) — pattern qname → its (combined) body pattern.
146/// * [`def_combiner`](Relaxng::def_combiner) — pattern qname → the combiner that won the most
147///   recent definition.
148/// * [`uses_name`](Relaxng::uses_name) — pattern qname → set of containers that reference it: `pattern:QNAME`
149///   for refs at define scope, `element:TAG@pattern:HOST` for refs inside an `element TAG {…}`
150///   hosted by define HOST (bare `element:TAG` when the element sits outside any define). Drives
151///   the "Used by" lists in the schema docs; `tex::symbol_uses` reports the element or the host
152///   pattern, whichever identifies the definition uniquely.
153/// * [`internal_grammars`](Relaxng::internal_grammars) — counter for naming embedded `<grammar>`
154///   blocks (`grammar1`,
155///   `grammar2`, …).
156#[derive(Debug)]
157pub struct Relaxng {
158  /// Top-level schema name (typically the .rng filename without ext).
159  pub name:    String,
160  /// Modules in document-order. Populated by [`simplify`]; each entry is
161  /// a `Pattern::Module` whose body is populated retroactively (the
162  /// Perl push-then-extend pattern).
163  pub modules: Vec<Pattern>,
164
165  pub elementdefs:          HashMap<String, String>,
166  pub element_reverse_defs: HashMap<String, String>,
167  pub elements:             HashMap<String, Vec<Pattern>>,
168  pub defs:                 HashMap<String, Pattern>,
169  pub def_combiner:         HashMap<String, DefCombiner>,
170  pub uses_name:            HashMap<String, HashSet<String>>,
171  pub internal_grammars:    u32,
172
173  /// Document-namespace prefix → URI, populated as the scanner sees
174  /// `xmlns:` attributes on RelaxNG nodes.
175  pub document_namespaces: HashMap<String, String>,
176
177  /// The master grammar's `<grammar ns="…">` URI — populated by the
178  /// first call to `scan_external` (i.e. the schema entry point).
179  /// Subsequent included grammars don't overwrite it. Used by the
180  /// schema-doc emitter to auto-register the corresponding namespace
181  /// prefix for elision in display names.
182  pub primary_namespace: Option<String>,
183
184  /// Namespace prefixes whose `prefix:` part should be elided from
185  /// rendered display names in the schema docs (`clean_tex_name`),
186  /// since they're contextually obvious for the schema. Auto-populated
187  /// from `primary_namespace` when the schema-doc emission starts —
188  /// e.g. LaTeXML's `default namespace = "http://dlmf.nist.gov/LaTeXML"`
189  /// (mapped to the `ltx` prefix) becomes a strip-prefix so display
190  /// names read `para` rather than `ltx:para`.
191  pub display_strip_prefixes: Vec<String>,
192}
193
194impl Default for Relaxng {
195  fn default() -> Self {
196    Relaxng {
197      name:                   String::from("LaTeXML"),
198      modules:                Vec::new(),
199      elementdefs:            HashMap::default(),
200      element_reverse_defs:   HashMap::default(),
201      elements:               HashMap::default(),
202      defs:                   HashMap::default(),
203      def_combiner:           HashMap::default(),
204      uses_name:              HashMap::default(),
205      internal_grammars:      0,
206      document_namespaces:    HashMap::default(),
207      primary_namespace:      None,
208      display_strip_prefixes: Vec::new(),
209    }
210  }
211}
212
213impl Relaxng {
214  /// Construct an empty schema state. Use [`Self::load_schema`] to
215  /// populate from an RNG file.
216  pub fn new(name: impl Into<String>) -> Self {
217    Relaxng {
218      name: name.into(),
219      ..Self::default()
220    }
221  }
222
223  /// Register a `prefix → URI` binding ahead of scanning. Mirrors
224  /// `Model::register_namespace` for standalone callers (which don't
225  /// have a live `Model` to consult). Callers that already populated
226  /// the schema's `xmlns:` declarations dynamically don't need this;
227  /// it's intended for namespaces that trang flattens away — the most
228  /// common case is a `.rnc` whose `default namespace = "..."` carries
229  /// no prefix, so the URI is preserved on `<grammar ns="..."/>` but
230  /// no `xmlns:` survives. Later calls overwrite earlier ones.
231  pub fn register_namespace(&mut self, prefix: impl Into<String>, uri: impl Into<String>) {
232    self.document_namespaces.insert(prefix.into(), uri.into());
233  }
234
235  /// Register the prefixes that `Model::new_default()` ships with the
236  /// LaTeXML schema (`xml`, `ltx`, `svg`, `xlink`, `m`, `xhtml`). The
237  /// runtime `Model` resolves these via its own registry, so this is
238  /// only needed for *standalone* tooling (the `genschema_oxide`
239  /// binary, integration tests against `LaTeXML.rng`) where we don't
240  /// have a Model object to consult. Returns `&mut self` for chaining.
241  pub fn with_latexml_defaults(&mut self) -> &mut Self {
242    self.register_namespace("xml", XML_NS);
243    self.register_namespace("ltx", LTX_NAMESPACE);
244    self.register_namespace("svg", "http://www.w3.org/2000/svg");
245    self.register_namespace("xlink", "http://www.w3.org/1999/xlink");
246    self.register_namespace("m", "http://www.w3.org/1998/Math/MathML");
247    self.register_namespace("xhtml", "http://www.w3.org/1999/xhtml");
248    self
249  }
250
251  /// Register a namespace prefix to elide from rendered display names
252  /// in the schema docs. Idempotent — duplicates are dropped.
253  pub fn register_display_prefix_strip(&mut self, prefix: impl Into<String>) {
254    let prefix = prefix.into();
255    if !self.display_strip_prefixes.contains(&prefix) {
256      self.display_strip_prefixes.push(prefix);
257    }
258  }
259
260  /// Look up `primary_namespace` in `document_namespaces` and, if it
261  /// resolves to a non-empty prefix, register that prefix for elision
262  /// from rendered display names. Call after scan + simplify, before
263  /// `tex::document_modules`. No-op when there's no primary namespace
264  /// or when its prefix is the default (empty) one.
265  ///
266  /// When two prefixes map to the same URI (rare — a schema author
267  /// could declare `xmlns:foo="…"` and `xmlns:bar="…"` to the same
268  /// namespace), the lexicographically smallest prefix wins. We sort
269  /// the candidates explicitly so the chosen prefix is identical
270  /// between builds: `document_namespaces` is a `FxHashMap`, whose
271  /// iteration order isn't a stable contract.
272  pub fn auto_strip_primary_namespace(&mut self) {
273    let Some(uri) = self.primary_namespace.clone() else {
274      return;
275    };
276    let mut candidates: Vec<&String> = self
277      .document_namespaces
278      .iter()
279      .filter(|(p, u)| !p.is_empty() && u.as_str() == uri.as_str())
280      .map(|(p, _)| p)
281      .collect();
282    candidates.sort();
283    if let Some(p) = candidates.first().map(|s| (*s).clone()) {
284      self.register_display_prefix_strip(p);
285    }
286  }
287
288  /// Insert a `<?latexml RelaxNGSchema="..."?>` processing instruction
289  /// on the given document.
290  pub fn add_schema_declaration(&self, document: &mut Document) {
291    let mut attributes = HashMap::default();
292    attributes.insert(String::from("RelaxNGSchema"), self.name.clone());
293    document
294      .insert_pi("latexml", Some(attributes))
295      .expect("should never fail");
296  }
297
298  /// Load + scan + simplify the schema named in `self.name` (or
299  /// `name_override`). Searches `search_paths` for the .rng file. After
300  /// success, the AST sits in [`Self::modules`] and the lookup tables
301  /// are populated.
302  pub fn load_schema(
303    &mut self,
304    name: &str,
305    search_paths: &[&std::path::Path],
306  ) -> Result<(), scan::ScanError> {
307    let raw = scan::scan_external(self, name, None, search_paths)?;
308    let _start = simplify::simplify_top(self, raw);
309    Ok(())
310  }
311}