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}