Skip to main content

latexml_core/common/relaxng/
simplify.rs

1//! AST normalization. Port of `RelaxNG.pm` lines 397–525.
2//!
3//! Walks the raw AST produced by [`super::scan`] under a binding context
4//! (the enclosing `<grammar>` name) and:
5//!
6//! * resolves `Ref` / `ParentRef` qnames against the binding/parent-binding,
7//! * records every reference site in [`Relaxng::uses_name`] (powers the "Used by:" lists in the
8//!   schema docs),
9//! * registers `Element` body patterns under [`Relaxng::elements`],
10//! * combines `Def`s into a single canonical `Combination` per qname in [`Relaxng::defs`] (with
11//!   [`Relaxng::def_combiner`] tracking which combiner won), tracking the singleton-element-def
12//!   shortcut into [`Relaxng::elementdefs`] / [`Relaxng::element_reverse_defs`],
13//! * resolves `Override`s by patching the wrapped module before re-running simplify on the patched
14//!   form,
15//! * preserves document-order of [`Relaxng::modules`].
16//!
17//! The simplifier is shape-preserving in its return value: every input
18//! pattern (except Override and singleton-element-defs) emerges with
19//! the same shape but possibly-rewritten qnames and recursively
20//! simplified bodies. Side effects on `rng` are the substantive output.
21
22use super::{CombineOp, DefCombiner, Pattern, Relaxng};
23
24/// Top-level simplifier. Maps [`simplify`] across each top-level form.
25pub fn simplify_top(rng: &mut Relaxng, raw: Vec<Pattern>) -> Vec<Pattern> {
26  raw
27    .into_iter()
28    .flat_map(|p| simplify(rng, p, "", "", None))
29    .collect()
30}
31
32/// Recursive normalizer.
33///
34/// `binding` is the qname-prefix of the currently-enclosing `<grammar>`
35/// (so a `<ref name="X"/>` inside `<grammar>foo</grammar>` resolves to
36/// `foo:X`). `parent` is the binding of the enclosing grammar one
37/// level up — used when this node is a `<parentRef/>`. `container`,
38/// when present, names the current host (`element:NAME` or
39/// `pattern:NAME`) and seeds the "Used by" graph.
40pub fn simplify(
41  rng: &mut Relaxng,
42  form: Pattern,
43  binding: &str,
44  parent: &str,
45  container: Option<&str>,
46) -> Vec<Pattern> {
47  match form {
48    Pattern::Grammar { name, body } => {
49      let new_body = simplify_args(rng, body, &name, binding, container);
50      vec![Pattern::Grammar { name, body: new_body }]
51    },
52
53    Pattern::Override { module, replacements } => {
54      simplify_override(rng, *module, replacements, binding, parent, container)
55    },
56
57    Pattern::Module { name, body } => {
58      // Push a placeholder FIRST so document-order in `rng.modules` is
59      // preserved (any nested modules surfaced during the body simplify
60      // appear after this one).
61      let idx = rng.modules.len();
62      rng.modules.push(Pattern::Module {
63        name: name.clone(),
64        body: Vec::new(),
65      });
66      let new_body: Vec<Pattern> = body
67        .into_iter()
68        .flat_map(|p| simplify(rng, p, binding, parent, container))
69        .collect();
70      if let Some(Pattern::Module { body: slot, .. }) = rng.modules.get_mut(idx) {
71        *slot = new_body.clone();
72      }
73      vec![Pattern::Module { name, body: new_body }]
74    },
75
76    Pattern::Element { name, body } => {
77      // Qualify the element container with the innermost enclosing
78      // pattern (`element:NAME@pattern:QNAME`) when one exists.
79      // Element names in HTML-shaped schemas are generic (`div`,
80      // `span`), so the bare element name doesn't identify a use
81      // site; the host pattern does. Nested elements inherit the
82      // same host. `tex::symbol_uses` splits the form back apart and
83      // picks whichever half identifies the definition uniquely.
84      let elem_container = match container {
85        Some(c) => {
86          let host = c.split_once('@').map_or(c, |(_, h)| h);
87          if host.starts_with("pattern:") {
88            format!("element:{}@{}", name, host)
89          } else {
90            format!("element:{}", name)
91          }
92        },
93        None => format!("element:{}", name),
94      };
95      let new_body: Vec<Pattern> = body
96        .into_iter()
97        .flat_map(|p| simplify(rng, p, binding, parent, Some(&elem_container)))
98        .collect();
99      rng
100        .elements
101        .entry(name.clone())
102        .or_default()
103        .extend(new_body.clone());
104      vec![Pattern::Element { name, body: new_body }]
105    },
106
107    Pattern::Ref { qname } => simplify_ref(rng, &qname, binding, container, false),
108    Pattern::ParentRef { qname } => simplify_ref(rng, &qname, parent, container, true),
109
110    Pattern::Def { combiner, name, body } => {
111      simplify_def(rng, combiner, name, body, binding, parent, container)
112    },
113
114    // Pass-through: simplify children, keep wrapper.
115    Pattern::Combination { op, body } => {
116      let new_body = simplify_args(rng, body, binding, parent, container);
117      vec![Pattern::Combination { op, body: new_body }]
118    },
119    Pattern::Start { body } => {
120      let new_body = simplify_args(rng, body, binding, parent, container);
121      vec![Pattern::Start { body: new_body }]
122    },
123    Pattern::Attribute { name, body } => {
124      let new_body = simplify_args(rng, body, binding, parent, container);
125      vec![Pattern::Attribute { name, body: new_body }]
126    },
127
128    // Leaves: pass through unchanged.
129    other @ (Pattern::Value(_)
130    | Pattern::Data(_)
131    | Pattern::Doc(_)
132    | Pattern::Text
133    | Pattern::ElementRef { .. }) => vec![other],
134  }
135}
136
137fn simplify_args(
138  rng: &mut Relaxng,
139  forms: Vec<Pattern>,
140  binding: &str,
141  parent: &str,
142  container: Option<&str>,
143) -> Vec<Pattern> {
144  forms
145    .into_iter()
146    .flat_map(|p| simplify(rng, p, binding, parent, container))
147    .collect()
148}
149
150fn simplify_ref(
151  rng: &mut Relaxng,
152  name: &str,
153  bind: &str,
154  container: Option<&str>,
155  _is_parent: bool,
156) -> Vec<Pattern> {
157  // ParentRef and Ref both return a `Ref` after qname-rewriting; the
158  // distinction was only in which binding to use, which the caller has
159  // already chosen by passing the right `bind`.
160  let qname = format!("{}:{}", bind, name);
161  if let Some(c) = container {
162    rng
163      .uses_name
164      .entry(qname.clone())
165      .or_default()
166      .insert(c.to_string());
167  }
168  vec![Pattern::Ref { qname }]
169}
170
171fn simplify_def(
172  rng: &mut Relaxng,
173  combiner: DefCombiner,
174  name: String,
175  body: Vec<Pattern>,
176  binding: &str,
177  parent: &str,
178  container: Option<&str>,
179) -> Vec<Pattern> {
180  let qname = format!("{}:{}", binding, name);
181  if let Some(c) = container {
182    rng
183      .uses_name
184      .entry(qname.clone())
185      .or_default()
186      .insert(c.to_string());
187  }
188  let pattern_container = format!("pattern:{}", qname);
189  let args = simplify_args(rng, body, binding, parent, Some(&pattern_container));
190
191  // Special case: a plain `<define>` with one Element body folds into
192  // `elementdefs[qname] -> tag`. The returned AST keeps the Def
193  // wrapper (the fold used to return the bare element): the emitter
194  // needs the define's name to document schemas where many defines
195  // share one element tag (HTML profiles, where every pattern renders
196  // a `div`/`span`). For uniquely-named tags `tex::to_tex_def`
197  // reproduces the folded `\elementdef` rendering, so XML-schema docs
198  // are unchanged.
199  if combiner == DefCombiner::Group
200    && args.len() == 1
201    && let Pattern::Element { name: el_name, .. } = &args[0]
202  {
203    rng.elementdefs.insert(qname.clone(), el_name.clone());
204    rng
205      .element_reverse_defs
206      .insert(el_name.clone(), qname.clone());
207    return vec![Pattern::Def {
208      combiner,
209      name: qname,
210      body: args,
211    }];
212  }
213
214  // Combine with any prior definition under the same qname.
215  let xargs: Vec<Pattern> = args
216    .iter()
217    .filter(|p| !matches!(p, Pattern::Doc(_)))
218    .cloned()
219    .collect();
220  let prev = rng.defs.get(&qname).cloned();
221  let prev_combiner = rng.def_combiner.get(&qname).copied();
222
223  let mut effective = combiner;
224  let mut prev_args: Vec<Pattern> = Vec::new();
225  let mut keep_prev = prev.is_some();
226  if let Some(prev_pat) = &prev {
227    if let Pattern::Combination { body, .. } = prev_pat {
228      prev_args = body.clone();
229    } else {
230      prev_args = vec![prev_pat.clone()];
231    }
232    match (combiner, prev_combiner) {
233      (DefCombiner::Group, Some(DefCombiner::Group)) => {
234        // Apparent re-definition — drop the previous value.
235        keep_prev = false;
236      },
237      (DefCombiner::Group, Some(other)) => {
238        // Inherit the previous combiner so nested Group definitions
239        // join under the previous combine="choice" / "interleave".
240        effective = other;
241      },
242      _ => {},
243    }
244  }
245  if !keep_prev {
246    prev_args.clear();
247  }
248
249  let combination_op = match effective {
250    DefCombiner::Group => CombineOp::Group,
251    DefCombiner::Choice => CombineOp::Choice,
252    DefCombiner::Interleave => CombineOp::Interleave,
253  };
254  let mut combined = prev_args;
255  combined.extend(xargs);
256  let combined_pat = simplify_combination(Pattern::Combination {
257    op:   combination_op,
258    body: combined,
259  });
260  rng.defs.insert(qname.clone(), combined_pat);
261  rng.def_combiner.insert(qname.clone(), effective);
262
263  // Returned pattern keeps the original combiner (matches Perl: stored
264  // op stays $op even when effective combiner shifts).
265  vec![Pattern::Def {
266    combiner,
267    name: qname,
268    body: args,
269  }]
270}
271
272/// Recursively flatten same-op `Group`/`Choice` nests and collapse a
273/// singleton `Group` to its only member. Port of `simplifyCombination`.
274pub fn simplify_combination(pat: Pattern) -> Pattern {
275  match pat {
276    Pattern::Combination { op, body } => {
277      let recursed: Vec<Pattern> = body.into_iter().map(simplify_combination).collect();
278      let flattened: Vec<Pattern> = if matches!(op, CombineOp::Group | CombineOp::Choice) {
279        let mut out = Vec::with_capacity(recursed.len());
280        for s in recursed {
281          match s {
282            Pattern::Combination { op: inner_op, body: inner_body } if inner_op == op => {
283              out.extend(inner_body);
284            },
285            other => out.push(other),
286          }
287        }
288        out
289      } else {
290        recursed
291      };
292      if op == CombineOp::Group && flattened.len() == 1 {
293        flattened.into_iter().next().unwrap()
294      } else {
295        Pattern::Combination { op, body: flattened }
296      }
297    },
298    other => other,
299  }
300}
301
302fn simplify_override(
303  rng: &mut Relaxng,
304  module: Pattern,
305  replacements: Vec<Pattern>,
306  binding: &str,
307  parent: &str,
308  container: Option<&str>,
309) -> Vec<Pattern> {
310  let (mod_name, mut patterns) = match module {
311    Pattern::Module { name, body } => (name, body),
312    other => {
313      // Defensive: shouldn't happen, but fall back to the inner item.
314      return simplify(rng, other, binding, parent, container);
315    },
316  };
317
318  // If replacements include a <start>, drop the module's <start>.
319  let has_replacement_start = replacements
320    .iter()
321    .any(|p| matches!(p, Pattern::Start { .. }));
322  if has_replacement_start {
323    patterns.retain(|p| !matches!(p, Pattern::Start { .. }));
324  }
325
326  // For each Def in replacements, remove the same-symbol Def from the
327  // module's patterns. (Any combine="..." Defs in replacements just
328  // accumulate — they don't strip the original.)
329  let replacement_defs: Vec<(DefCombiner, String)> = replacements
330    .iter()
331    .filter_map(|p| match p {
332      Pattern::Def { combiner, name, .. } => Some((*combiner, name.clone())),
333      _ => None,
334    })
335    .collect();
336  patterns.retain(|p| match p {
337    Pattern::Def { combiner: c, name: n, .. } => {
338      !replacement_defs.iter().any(|(rc, rn)| rc == c && rn == n)
339    },
340    _ => true,
341  });
342
343  let mut combined = patterns;
344  combined.extend(replacements);
345  let new_module = Pattern::Module {
346    name: format!("{} (overridden)", mod_name),
347    body: combined,
348  };
349  simplify(rng, new_module, binding, parent, container)
350}
351
352/// Recursively pull `Pattern::Start` bodies out of `Module` / `Grammar`
353/// wrappers. Mirrors `extractStart` and powers
354/// `Model::add_tag_content('#Document', ...)` later in the chain.
355pub fn extract_start(items: &[Pattern]) -> Vec<Pattern> {
356  let mut out = Vec::new();
357  for item in items {
358    match item {
359      Pattern::Start { body } => out.extend(body.iter().cloned()),
360      Pattern::Module { body, .. } | Pattern::Grammar { body, .. } => {
361        out.extend(extract_start(body));
362      },
363      _ => {},
364    }
365  }
366  out
367}
368
369// ----- unit tests ---------------------------------------------------------
370
371#[cfg(test)]
372mod tests {
373  use super::*;
374  use crate::common::relaxng::scan::scan_string;
375
376  fn simplify_xml(xml: &str) -> (Relaxng, Vec<Pattern>) {
377    let mut rng = Relaxng::default();
378    let raw = scan_string(&mut rng, xml).expect("scan");
379    let simp = simplify_top(&mut rng, raw);
380    (rng, simp)
381  }
382
383  #[test]
384  fn simplify_collapses_singleton_group() {
385    let inner = Pattern::Element { name: "x".into(), body: vec![] };
386    let combo = Pattern::Combination {
387      op:   CombineOp::Group,
388      body: vec![inner.clone()],
389    };
390    let result = simplify_combination(combo);
391    assert!(matches!(result, Pattern::Element { ref name, .. } if name == "x"));
392  }
393
394  #[test]
395  fn simplify_flattens_nested_choice() {
396    let inner_choice = Pattern::Combination {
397      op:   CombineOp::Choice,
398      body: vec![
399        Pattern::Element { name: "a".into(), body: vec![] },
400        Pattern::Element { name: "b".into(), body: vec![] },
401      ],
402    };
403    let outer = Pattern::Combination {
404      op:   CombineOp::Choice,
405      body: vec![inner_choice, Pattern::Element {
406        name: "c".into(),
407        body: vec![],
408      }],
409    };
410    let result = simplify_combination(outer);
411    let body = match &result {
412      Pattern::Combination { op: CombineOp::Choice, body } => body,
413      other => panic!("expected flat Choice, got {:?}", other),
414    };
415    assert_eq!(body.len(), 3);
416  }
417
418  #[test]
419  fn simplify_records_modules_in_document_order() {
420    // `scan_string` returns a flat Vec<Pattern> (no Module wrapper —
421    // that's what `scan_external` adds for files); to exercise the
422    // Module branch of simplify, we wrap manually here.
423    let xml = r#"
424      <grammar xmlns="http://relaxng.org/ns/structure/1.0">
425        <define name="A"><element name="a"><empty/></element></define>
426        <define name="B"><element name="b"><empty/></element></define>
427      </grammar>
428    "#;
429    let mut rng = Relaxng::default();
430    let raw = scan_string(&mut rng, xml).expect("scan");
431    let wrapped = vec![Pattern::Module {
432      name: "wrapper".into(),
433      body: raw,
434    }];
435    let _ = simplify_top(&mut rng, wrapped);
436    assert!(!rng.modules.is_empty(), "modules should be recorded");
437    let names: Vec<&str> = rng
438      .modules
439      .iter()
440      .filter_map(|m| match m {
441        Pattern::Module { name, .. } => Some(name.as_str()),
442        _ => None,
443      })
444      .collect();
445    assert_eq!(names, vec!["wrapper"]);
446  }
447
448  #[test]
449  fn simplify_singleton_element_def_records_elementdefs() {
450    let xml = r#"
451      <grammar xmlns="http://relaxng.org/ns/structure/1.0">
452        <define name="MY"><element name="my-el"><empty/></element></define>
453      </grammar>
454    "#;
455    let (rng, _) = simplify_xml(xml);
456    // Binding is the synthesized `grammar1`, so qname is `grammar1:MY`.
457    assert_eq!(
458      rng.elementdefs.get("grammar1:MY"),
459      Some(&"my-el".to_string())
460    );
461    assert_eq!(
462      rng.element_reverse_defs.get("my-el"),
463      Some(&"grammar1:MY".to_string())
464    );
465  }
466
467  #[test]
468  fn simplify_complex_def_recorded_in_defs() {
469    let xml = r#"
470      <grammar xmlns="http://relaxng.org/ns/structure/1.0">
471        <define name="X">
472          <choice>
473            <element name="a"><empty/></element>
474            <element name="b"><empty/></element>
475          </choice>
476        </define>
477      </grammar>
478    "#;
479    let (rng, _) = simplify_xml(xml);
480    assert!(rng.defs.contains_key("grammar1:X"));
481    assert_eq!(
482      rng.def_combiner.get("grammar1:X").copied(),
483      Some(DefCombiner::Group)
484    );
485  }
486
487  #[test]
488  fn simplify_combine_choice_accumulates() {
489    // Use `<ref>` bodies — single-element bodies of a plain `<define>`
490    // hit the elementdefs shortcut and bypass the defs table, so we'd
491    // never see them merged. `<ref>` bodies sidestep that path.
492    let xml = r#"
493      <grammar xmlns="http://relaxng.org/ns/structure/1.0">
494        <define name="X"><ref name="A"/></define>
495        <define name="X" combine="choice"><ref name="B"/></define>
496      </grammar>
497    "#;
498    let (rng, _) = simplify_xml(xml);
499    let combined = rng.defs.get("grammar1:X").expect("defs entry");
500    let body = match combined {
501      Pattern::Combination { op: CombineOp::Choice, body } => body,
502      other => panic!("expected Choice combination, got {:?}", other),
503    };
504    let qnames: Vec<&str> = body
505      .iter()
506      .filter_map(|p| match p {
507        Pattern::Ref { qname } => Some(qname.as_str()),
508        _ => None,
509      })
510      .collect();
511    assert!(
512      qnames.contains(&"grammar1:A"),
513      "A ref missing: {:?}",
514      qnames
515    );
516    assert!(
517      qnames.contains(&"grammar1:B"),
518      "B ref missing: {:?}",
519      qnames
520    );
521    assert_eq!(
522      rng.def_combiner.get("grammar1:X").copied(),
523      Some(DefCombiner::Choice)
524    );
525  }
526
527  #[test]
528  fn simplify_records_uses_name() {
529    let _xml = r#"
530      <grammar xmlns="http://relaxml.org/ns/structure/1.0"
531               xmlns:rng="http://relaxng.org/ns/structure/1.0">
532      </grammar>
533    "#;
534    // Using a real example that exercises ref tracking:
535    let xml = r#"
536      <grammar xmlns="http://relaxng.org/ns/structure/1.0">
537        <define name="P">
538          <element name="p"><ref name="Q"/></element>
539        </define>
540        <define name="Q"><text/></define>
541      </grammar>
542    "#;
543    let (rng, _) = simplify_xml(xml);
544    // P body recorded an Element whose body has a Ref to grammar1:Q.
545    let p_uses = rng.uses_name.get("grammar1:Q");
546    assert!(
547      p_uses.is_some(),
548      "uses_name for grammar1:Q should be populated, got {:?}",
549      rng.uses_name.keys().collect::<Vec<_>>()
550    );
551    // The Ref appears inside element:p, host-qualified with the define
552    // that hosts the element (pattern:grammar1:P) so the doc emitter
553    // can fall back to the pattern name for generic tags.
554    let containers = p_uses.unwrap();
555    assert!(
556      containers.contains("element:p@pattern:grammar1:P"),
557      "expected Q usage under element:p@pattern:grammar1:P, got {:?}",
558      containers
559    );
560  }
561
562  #[test]
563  fn simplify_extract_start_descends_grammar_and_module() {
564    let inner = Pattern::Start {
565      body: vec![Pattern::Element {
566        name: "root".into(),
567        body: vec![],
568      }],
569    };
570    let nested = vec![Pattern::Module {
571      name: "m".into(),
572      body: vec![Pattern::Grammar {
573        name: "g".into(),
574        body: vec![inner],
575      }],
576    }];
577    let starts = extract_start(&nested);
578    assert_eq!(starts.len(), 1);
579    assert!(matches!(starts[0], Pattern::Element { ref name, .. } if name == "root"));
580  }
581
582  #[test]
583  fn simplify_override_drops_overridden_def_and_keeps_replacement() {
584    // Build the AST manually since trang typically flattens includes,
585    // and `<include>` with overrides goes through scan_grammar_item ->
586    // Pattern::Override, which we want to verify here directly.
587    let module = Pattern::Module {
588      name: "m".into(),
589      body: vec![
590        Pattern::Def {
591          combiner: DefCombiner::Group,
592          name:     "X".into(),
593          body:     vec![Pattern::Element {
594            name: "original".into(),
595            body: vec![],
596          }],
597        },
598        Pattern::Def {
599          combiner: DefCombiner::Group,
600          name:     "Y".into(),
601          body:     vec![Pattern::Element {
602            name: "y-el".into(),
603            body: vec![],
604          }],
605        },
606      ],
607    };
608    let override_pat = Pattern::Override {
609      module:       Box::new(module),
610      replacements: vec![Pattern::Def {
611        combiner: DefCombiner::Group,
612        name:     "X".into(),
613        body:     vec![Pattern::Element {
614          name: "replacement".into(),
615          body: vec![],
616        }],
617      }],
618    };
619
620    let mut rng = Relaxng::default();
621    let _ = simplify(&mut rng, override_pat, "g", "", None);
622
623    // After simplification, only the "replacement" element wins for X.
624    assert_eq!(rng.elementdefs.get("g:X"), Some(&"replacement".to_string()));
625    // Y survives untouched.
626    assert_eq!(rng.elementdefs.get("g:Y"), Some(&"y-el".to_string()));
627    assert_eq!(rng.element_reverse_defs.get("original"), None);
628  }
629}