1#![allow(clippy::wrong_self_convention)]
21
22use std::collections::BTreeMap;
23
24use rustc_hash::FxHashMap as HashMap;
25
26use super::{CombineOp, DefCombiner, Pattern, Relaxng};
27
28type ElementChoice<'a> = (CombineOp, Vec<(String, &'a [Pattern])>);
32
33#[derive(Debug, Clone, Copy)]
36pub struct Options {
37 pub skip_svg: bool,
38 pub skip_aria: bool,
39 pub skip_xhtml: bool,
40}
41
42impl Default for Options {
43 fn default() -> Self {
44 Options {
45 skip_svg: true,
46 skip_aria: true,
47 skip_xhtml: true,
48 }
49 }
50}
51
52struct EmitState<'a> {
54 rng: &'a Relaxng,
55 opts: Options,
56 defined_patterns: HashMap<String, i8>,
61 ambiguous_elements: rustc_hash::FxHashSet<String>,
64}
65
66fn ambiguous_element_tags(rng: &Relaxng) -> rustc_hash::FxHashSet<String> {
87 let mut hosts: HashMap<&str, rustc_hash::FxHashSet<&str>> = HashMap::default();
88 for (qname, tag) in &rng.elementdefs {
89 hosts
90 .entry(tag.as_str())
91 .or_default()
92 .insert(qname.as_str());
93 }
94 for (qname, pat) in &rng.defs {
95 if let Pattern::Element { name, .. } = pat {
96 hosts
97 .entry(name.as_str())
98 .or_default()
99 .insert(qname.as_str());
100 }
101 }
102 for uses in rng.uses_name.values() {
103 for u in uses {
104 if let Some(rest) = u.strip_prefix("element:")
105 && let Some((tag, host)) = rest.split_once('@')
106 {
107 let host = host.strip_prefix("pattern:").unwrap_or(host);
111 hosts.entry(tag).or_default().insert(host);
112 }
113 }
114 }
115 hosts
116 .into_iter()
117 .filter(|(_, h)| h.len() > 1)
118 .map(|(tag, _)| tag.to_string())
119 .collect()
120}
121
122pub fn document_modules(rng: &Relaxng, opts: Options) -> String {
124 let mut emit = EmitState {
125 rng,
126 opts,
127 defined_patterns: HashMap::default(),
128 ambiguous_elements: ambiguous_element_tags(rng),
129 };
130 let mut docs = String::new();
131 for module in &rng.modules {
137 let (op, name, content) = match module {
138 Pattern::Module { name, body } => ("module", name.clone(), body),
139 _ => continue,
140 };
141 let _ = op;
142 if emit.opts.skip_svg && is_svg_module(&name) {
143 continue;
144 }
145 let mod_name = strip_urn_prefix(&name);
146
147 let to_emit: Vec<&Pattern> = content
151 .iter()
152 .flat_map(|item| match item {
153 Pattern::Grammar { body, .. } => body.iter().collect::<Vec<_>>(),
154 other => vec![other],
155 })
156 .collect();
157
158 let mut preamble = String::new();
159 for item in content {
162 if let Pattern::Grammar { body, .. } = item {
163 let mods: Vec<String> = body
164 .iter()
165 .filter_map(|d| match d {
166 Pattern::Module { name, .. } => Some(name.clone()),
167 _ => None,
168 })
169 .collect();
170 if !mods.is_empty() {
171 let refs: Vec<String> = mods
172 .iter()
173 .map(|m| format!("\\moduleref{{{}}}", clean_tex(m)))
174 .collect();
175 if !preamble.is_empty() {
176 preamble.push('\n');
177 }
178 preamble.push_str(&format!(
179 "\\par\\noindent\\textit{{Includes:}} {}.",
180 refs.join(", ")
181 ));
182 }
183 }
184 }
185
186 let mut defs: Vec<String> = Vec::new();
188 for item in &to_emit {
189 if matches!(item, Pattern::Module { .. }) {
191 continue;
192 }
193 let rendered = emit.to_tex(item);
194 if rendered.is_empty() {
195 continue;
196 }
197 match item {
198 Pattern::Doc(_) | Pattern::Start { .. } => {
199 if !preamble.is_empty() {
200 preamble.push('\n');
201 }
202 preamble.push_str(&rendered);
203 },
204 _ => defs.push(rendered),
205 }
206 }
207
208 docs.push_str(&format!("\n\\begin{{schemamodule}}{{{}}}", mod_name));
209 if !preamble.is_empty() {
210 docs.push('\n');
211 docs.push_str(&preamble);
212 }
213 let body = defs.join("\n");
214 if !body.is_empty() {
215 docs.push_str(&format!(
216 "\n\\begin{{description}}\n{}\n\\end{{description}}",
217 body
218 ));
219 }
220 docs.push_str("\n\\end{schemamodule}");
221 }
222 let mut keys: Vec<String> = emit
226 .defined_patterns
227 .iter()
228 .filter(|(_, v)| **v < 0)
229 .map(|(k, _)| k.clone())
230 .collect();
231 keys.sort();
232 for name in keys {
233 let from = format!("\\patternadd{{{}}}", name);
234 let to = format!("\\patterndefadd{{{}}}", name);
235 if let Some(idx) = docs.find(&from) {
236 docs.replace_range(idx..idx + from.len(), &to);
237 }
238 }
239 docs
240}
241
242pub fn clean_tex(s: &str) -> String {
247 if s == "#PCDATA" {
248 return String::from(r"\typename{text}");
249 }
250 let mut out = strip_urn_prefix(s);
251 out = out.replace('#', "\\#");
255 out = wrap_angle_text(&out);
256 out = out.replace('_', "\\_");
257 out
258}
259
260fn clean_tex_name(s: &str, strip_prefixes: &[String]) -> String {
269 let cleaned = clean_tex(s);
270 for prefix in strip_prefixes {
271 let with_colon = format!("{}:", prefix);
272 if let Some(rest) = cleaned.strip_prefix(&with_colon) {
273 return rest.to_string();
274 }
275 }
276 cleaned
277}
278
279fn strip_urn_prefix(s: &str) -> String {
280 s.strip_prefix("urn:x-LaTeXML:RelaxNG:")
281 .unwrap_or(s)
282 .to_string()
283}
284
285fn wrap_angle_text(s: &str) -> String {
287 let mut out = String::with_capacity(s.len());
288 let bytes = s.as_bytes();
289 let mut i = 0;
290 while i < bytes.len() {
291 if bytes[i] == b'<'
292 && let Some(end) = s[i + 1..].find('>')
293 {
294 let inner = &s[i + 1..i + 1 + end];
295 out.push_str("\\texttt{");
296 out.push_str(inner);
297 out.push('}');
298 i += 1 + end + 1;
299 continue;
300 }
301 out.push(bytes[i] as char);
302 i += 1;
303 }
304 out
305}
306
307impl EmitState<'_> {
310 fn to_tex(&mut self, p: &Pattern) -> String {
311 match p {
312 Pattern::Doc(s) => format!("{}\n\n", clean_tex(s)),
317 Pattern::Ref { qname } => self.to_tex_ref(qname),
318 Pattern::Def { combiner, name, body } => {
319 let combiner_label = match combiner {
320 DefCombiner::Group => "",
321 DefCombiner::Choice => "choice",
322 DefCombiner::Interleave => "interleave",
323 };
324 self.to_tex_def(combiner_label, name, body)
325 },
326 Pattern::Element { name, body } => self.to_tex_element(name, body),
327 Pattern::Attribute { name, body } => self.to_tex_attribute(name, body),
328 Pattern::Combination { op, body } => self.to_tex_combination(*op, body),
329 Pattern::Data(t) => format!("\\typename{{{}}}", clean_tex(t)),
330 Pattern::Value(v) => format!("\\attrval{{{}}}", clean_tex(v)),
331 Pattern::Start { body } => {
332 let (docs, spec) = self.extract_docs(body);
340 let content = spec
341 .iter()
342 .map(|p| self.to_tex(p))
343 .collect::<Vec<_>>()
344 .join(" ");
345 let mut s = format!("\\par\\noindent\\textit{{Start symbol:}} {}", content);
346 if !docs.is_empty() {
347 s.push_str(&format!(" \\par{}", docs));
348 }
349 s
350 },
351 Pattern::Grammar { body, .. } => {
352 let mut mods: Vec<String> = Vec::new();
357 let mut rest: Vec<&Pattern> = Vec::new();
358 for d in body {
359 match d {
360 Pattern::Module { name, .. } => mods.push(name.clone()),
361 other => rest.push(other),
362 }
363 }
364 let mut out = String::new();
365 if !mods.is_empty() {
366 let refs: Vec<String> = mods
367 .iter()
368 .map(|m| format!("\\moduleref{{{}}}", clean_tex(m)))
369 .collect();
370 out.push_str(&format!(
371 "\\par\\noindent\\textit{{Includes:}} {}.\n",
372 refs.join(", ")
373 ));
374 }
375 for r in rest {
376 out.push_str(&self.to_tex(r));
377 out.push('\n');
378 }
379 out
380 },
381 Pattern::Module { name, .. } => {
382 if self.opts.skip_svg && is_svg_module(name) {
386 format!(
387 "\\par\\noindent\\textit{{Module}} \\texttt{{{}}} \\textit{{included}}.",
388 clean_tex(name)
389 )
390 } else {
391 format!(
392 "\\par\\noindent\\textit{{Module}} \\moduleref{{{}}} \\textit{{included}}.",
393 clean_tex(name)
394 )
395 }
396 },
397 Pattern::ParentRef { qname } => self.to_tex_ref(qname),
398 Pattern::ElementRef { qname } => format!(
399 "\\elementref{{{}}}",
400 clean_tex_name(qname, &self.rng.display_strip_prefixes)
401 ),
402 Pattern::Override { module, .. } => self.to_tex(module),
403 Pattern::Text => clean_tex("#PCDATA"),
404 }
405 }
406
407 fn to_tex_ref(&self, name: &str) -> String {
408 if let Some(el) = self.rng.elementdefs.get(name) {
409 if !self.ambiguous_elements.contains(el) {
414 let cleaned = clean_tex_name(el, &self.rng.display_strip_prefixes);
415 if self.opts.skip_xhtml && cleaned == "xhtml:*" {
416 return String::from("\\texttt{xhtml:*}");
417 }
418 return format!("\\elementref{{{}}}", cleaned);
419 }
420 }
421 if (name.ends_with("_attributes") || name.ends_with("_model"))
422 && let Some(def) = self.rng.defs.get(name)
423 {
424 let cloned = def.clone();
427 let mut tmp = EmitState {
428 rng: self.rng,
429 opts: self.opts,
430 defined_patterns: HashMap::default(),
431 ambiguous_elements: self.ambiguous_elements.clone(),
432 };
433 return tmp.to_tex(&cloned);
434 }
435 let stripped = strip_first_qualifier(name);
436 if self.opts.skip_svg && stripped == "svg" {
437 return String::from("\\texttt{svg:svg}");
438 }
439 format!("\\patternref{{{}}}", clean_tex(&stripped))
440 }
441
442 fn to_tex_def(&mut self, combiner: &str, qname: &str, data: &[Pattern]) -> String {
443 if combiner.is_empty()
453 && self.rng.elementdefs.contains_key(qname)
454 && data.len() == 1
455 && let Pattern::Element { name, body } = &data[0]
456 && !self.ambiguous_elements.contains(name)
457 {
458 return self.to_tex_element(name, body);
459 }
460 if self.opts.skip_aria && qname.contains("aria") {
461 return String::new();
462 }
463 if qname.ends_with("_attributes") || qname.ends_with("_model") {
464 return String::new();
465 }
466 let stripped = strip_first_qualifier(qname);
467 if self.opts.skip_svg && stripped.starts_with("svg") {
468 return String::new();
469 }
470 let cleaned_name = clean_tex(&stripped);
471 let (docs, spec) = self.extract_docs(data);
472
473 if combiner.is_empty()
483 && spec.len() == 1
484 && let Pattern::Element { name, body } = &spec[0]
485 && self.ambiguous_elements.contains(name)
486 && !is_wildcard_name(name)
487 {
488 if matches!(self.defined_patterns.get(&cleaned_name), Some(v) if *v > 0) {
489 return String::new();
490 }
491 self.defined_patterns.insert(cleaned_name.clone(), 1);
492 let (el_docs, el_spec) = self.extract_docs(body);
493 let merged_docs = format!("{}{}", docs, el_docs);
494 let (attr, content) = self.to_tex_body(&el_spec);
495 let mut card = format!(
496 "\\item[\\textit{{Element}}:] \\texttt{{{}}}",
497 clean_tex_name(name, &self.rng.display_strip_prefixes)
498 );
499 card.push_str(&attr);
500 if !content.is_empty() {
501 card.push_str(&format!("\\item[\\textit{{Content}}:] {}", content));
502 }
503 if let Some(uses) = self.symbol_uses(qname) {
504 card.push_str(&format!("\\item[\\textit{{Used by}}:] {}", uses));
505 }
506 let mut out = format!(
507 "\\patterndef{{{}}}{{{}}}{{{}}}\n",
508 cleaned_name, merged_docs, card
509 );
510 for (el_name, el_body) in &collect_element_descendants(&el_spec) {
515 let cleaned_el = clean_tex_name(el_name, &self.rng.display_strip_prefixes);
516 let (el_attr, el_content) = self.to_tex_body(el_body);
517 let mut eb = el_attr;
518 if !el_content.is_empty() {
519 eb.push_str(&format!("\\item[\\textit{{Content}}:] {}", el_content));
520 }
521 eb.push_str(&format!(
522 "\\item[\\textit{{Used by}}:] \\patternref{{{}}}",
523 cleaned_name
524 ));
525 out.push_str(&format!("\\elementdef{{{}}}{{}}{{{}}}\n", cleaned_el, eb));
526 }
527 return out;
528 }
529
530 if combiner.is_empty()
538 && let Some((op, elements)) = self.detect_element_choice(&spec)
539 {
540 let (pattern_body, element_defs) =
541 self.render_element_choice(qname, &cleaned_name, op, &elements);
542 if matches!(self.defined_patterns.get(&cleaned_name), Some(v) if *v > 0) {
543 return String::new();
544 }
545 self.defined_patterns.insert(cleaned_name.clone(), 1);
546 let mut out = format!(
547 "\\patterndef{{{}}}{{{}}}{{{}}}\n",
548 cleaned_name, docs, pattern_body
549 );
550 for ed in element_defs {
551 out.push_str(&ed);
552 }
553 return out;
554 }
555
556 let (attr, content) = self.to_tex_body(&spec);
557
558 if !combiner.is_empty() {
559 let mut body = attr;
560 if !content.is_empty() {
561 let sep = if combiner == "choice" {
562 "\\textbar="
563 } else {
564 "\\&="
565 };
566 body.push_str(&format!("\\item[{}] {}", sep, content));
567 }
568 self
569 .defined_patterns
570 .entry(cleaned_name.clone())
571 .or_insert(-1);
572 return format!("\\patternadd{{{}}}{{{}}}{{{}}}\n", cleaned_name, docs, body);
573 }
574
575 let mut attr = attr;
577 let mut content = content;
578 if attr.is_empty() && cleaned_name.contains("\\_attributes") {
579 attr = String::from("\\item[\\textit{Attributes:}] \\textit{empty}");
580 }
581 if content.is_empty() && cleaned_name.contains("\\_model") {
582 content = String::from("\\textit{empty}");
583 }
584 let mut body = attr;
585 if !content.is_empty() {
586 body.push_str(&format!("\\item[\\textit{{Content}}:] {}", content));
587 }
588 if !cleaned_name.contains("\\_attributes")
590 && let Some(stored) = self.rng.defs.get(qname)
591 && self.is_content(stored)
592 && !self.is_attributes(stored)
593 {
594 let (xattr, xcontent) = self.to_tex_body(std::slice::from_ref(stored));
595 let unwrapped = strip_outer_parens(&xcontent);
601 if xattr.is_empty() && !xcontent.is_empty() && xcontent != content && unwrapped != content {
602 body.push_str(&format!("\\item[\\textit{{Expansion}}:] {}", xcontent));
603 }
604 }
605 if let Some(uses) = self.symbol_uses(qname) {
606 body.push_str(&format!("\\item[\\textit{{Used by}}:] {}", uses));
607 }
608 if matches!(self.defined_patterns.get(&cleaned_name), Some(v) if *v > 0) {
609 return String::new();
610 }
611 self.defined_patterns.insert(cleaned_name.clone(), 1);
612 let extras = collect_element_descendants(&spec);
617 let mut out = format!("\\patterndef{{{}}}{{{}}}{{{}}}\n", cleaned_name, docs, body);
618 for (el_name, el_body) in &extras {
619 let cleaned_el = clean_tex_name(el_name, &self.rng.display_strip_prefixes);
620 let (el_attr, el_content) = self.to_tex_body(el_body);
621 let mut eb = el_attr;
622 if !el_content.is_empty() {
623 eb.push_str(&format!("\\item[\\textit{{Content}}:] {}", el_content));
624 }
625 eb.push_str(&format!(
626 "\\item[\\textit{{Used by}}:] \\patternref{{{}}}",
627 cleaned_name
628 ));
629 out.push_str(&format!("\\elementdef{{{}}}{{}}{{{}}}\n", cleaned_el, eb));
630 }
631 out
632 }
633
634 fn to_tex_element(&mut self, qname: &str, data: &[Pattern]) -> String {
635 let local = qname.strip_prefix("ltx:").unwrap_or(qname);
636 if self.opts.skip_xhtml && local == "xhtml:*" {
637 return String::new();
638 }
639 if is_wildcard_name(qname) {
648 return self.render_inline_element(qname, data);
649 }
650 let cleaned = clean_tex_name(qname, &self.rng.display_strip_prefixes);
651 let (docs, spec) = self.extract_docs(data);
652 let (attr, content) = self.to_tex_body(&spec);
653 let content = if content.is_empty() {
654 String::from("\\typename{empty}")
655 } else {
656 content
657 };
658 let mut body = attr;
659 body.push_str(&format!("\\item[\\textit{{Content}}:] {}", content));
660 if let Some(ename) = self.rng.element_reverse_defs.get(qname)
661 && let Some(uses) = self.symbol_uses(ename)
662 {
663 body.push_str(&format!("\\item[\\textit{{Used by}}:] {}", uses));
664 }
665 format!("\\elementdef{{{}}}{{{}}}{{{}}}\n", cleaned, docs, body)
666 }
667
668 fn to_tex_attribute(&mut self, name: &str, data: &[Pattern]) -> String {
669 let cleaned = clean_tex_name(name, &self.rng.display_strip_prefixes);
670 if let Some(rest) = cleaned.strip_prefix('!') {
671 return format!(
672 "\\item[\\textit{{Excluding attribute }}]\\texttt{{{}}}",
673 rest
674 );
675 }
676 if is_wildcard_name(&cleaned) {
680 return self.render_inline_attribute(&cleaned, data);
681 }
682 let (docs, spec) = self.extract_docs(data);
683 let content = if spec.is_empty() {
684 String::from("\\typename{text}")
685 } else {
686 spec
687 .iter()
688 .map(|p| self.to_tex(p))
689 .collect::<Vec<_>>()
690 .join(" ")
691 };
692 format!("\\attrdef{{{}}}{{{}}}{{{}}}", cleaned, docs, content)
693 }
694
695 fn render_inline_element(&mut self, qname: &str, data: &[Pattern]) -> String {
709 if !is_wildcard_name(qname) {
710 return format!(
711 "\\elementref{{{}}}",
712 clean_tex_name(qname, &self.rng.display_strip_prefixes)
713 );
714 }
715 let cleaned = clean_tex_name(qname, &self.rng.display_strip_prefixes);
716 let (_docs, spec) = self.extract_docs(data);
717 let parts: Vec<String> = spec.iter().map(|p| self.to_tex(p)).collect();
718 let parts: Vec<String> = parts.into_iter().filter(|s| !s.is_empty()).collect();
719 if parts.is_empty() {
720 format!("\\textit{{element}}~\\texttt{{{}}}", cleaned)
721 } else {
722 format!(
723 "\\textit{{element}}~\\texttt{{{}}}~\\{{{}\\}}",
724 cleaned,
725 parts.join(", ")
726 )
727 }
728 }
729
730 fn detect_element_choice<'a>(&self, spec: &'a [Pattern]) -> Option<ElementChoice<'a>> {
744 if spec.len() != 1 {
745 return None;
746 }
747 if let Pattern::Element { name, body } = &spec[0] {
751 if is_wildcard_name(name) {
752 return None;
753 }
754 return Some((CombineOp::Choice, vec![(name.clone(), body.as_slice())]));
755 }
756 let Pattern::Combination { op, body } = &spec[0] else {
757 return None;
758 };
759 if !matches!(
760 op,
761 CombineOp::Choice | CombineOp::Group | CombineOp::Interleave
762 ) {
763 return None;
764 }
765 if body.is_empty() {
766 return None;
767 }
768 let mut elements: Vec<(String, &'a [Pattern])> = Vec::with_capacity(body.len());
769 for child in body {
770 let Pattern::Element { name, body: el_body } = child else {
771 return None;
772 };
773 if is_wildcard_name(name) {
774 return None;
775 }
776 elements.push((name.clone(), el_body.as_slice()));
777 }
778 Some((*op, elements))
779 }
780
781 fn render_element_choice(
791 &mut self,
792 qname: &str,
793 parent_clean: &str,
794 op: CombineOp,
795 elements: &[(String, &[Pattern])],
796 ) -> (String, Vec<String>) {
797 let mut sorted_names: Vec<String> = elements.iter().map(|(n, _)| n.clone()).collect();
802 sorted_names.sort();
803 sorted_names.dedup();
804 let names_tex: Vec<String> = sorted_names
805 .iter()
806 .map(|n| {
807 format!(
808 "\\elementref{{{}}}",
809 clean_tex_name(n, &self.rng.display_strip_prefixes)
810 )
811 })
812 .collect();
813 let sep = match op {
814 CombineOp::Choice => " ~\\textbar~ ",
815 CombineOp::Interleave => " ~\\&~ ",
816 CombineOp::Group => ", ",
817 _ => " ~\\textbar~ ",
818 };
819 let content = if names_tex.len() == 1 {
820 names_tex[0].clone()
821 } else {
822 format!("({})", names_tex.join(sep))
823 };
824 let mut body = format!("\\item[\\textit{{Content}}:] {}", content);
825 if let Some(uses) = self.symbol_uses(qname) {
826 body.push_str(&format!("\\item[\\textit{{Used by}}:] {}", uses));
827 }
828
829 let mut seen: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
834 let mut element_defs: Vec<String> = Vec::new();
835 for (name, el_body) in elements {
836 if !seen.insert(name.clone()) {
837 continue;
838 }
839 let cleaned = clean_tex_name(name, &self.rng.display_strip_prefixes);
840 let (el_attr, el_content) = self.to_tex_body(el_body);
841 let mut eb = el_attr;
842 if !el_content.is_empty() {
843 eb.push_str(&format!("\\item[\\textit{{Content}}:] {}", el_content));
844 }
845 eb.push_str(&format!(
846 "\\item[\\textit{{Used by}}:] \\patternref{{{}}}",
847 parent_clean
848 ));
849 element_defs.push(format!("\\elementdef{{{}}}{{}}{{{}}}\n", cleaned, eb));
850 }
851 (body, element_defs)
852 }
853
854 fn render_inline_attribute(&mut self, cleaned: &str, data: &[Pattern]) -> String {
857 let (_docs, spec) = self.extract_docs(data);
858 let content = if spec.is_empty() {
859 String::from("\\typename{text}")
860 } else {
861 spec
862 .iter()
863 .map(|p| self.to_tex(p))
864 .collect::<Vec<_>>()
865 .join(" ")
866 };
867 format!("\\textit{{attribute}}~\\texttt{{{}}}={}", cleaned, content)
868 }
869
870 fn to_tex_combination(&mut self, op: CombineOp, data: &[Pattern]) -> String {
871 let dedup_owned: Vec<Pattern>;
874 let data: &[Pattern] = if has_wildcard_pair(data) {
875 dedup_owned = dedupe_wildcard_pairs(data);
876 &dedup_owned
877 } else {
878 data
879 };
880 let inner: Vec<String> = data
889 .iter()
890 .map(|p| match p {
891 Pattern::Element { name, body } if !is_wildcard_name(name) => {
892 self.render_inline_element(name, body)
893 },
894 Pattern::Attribute { name, body } if !is_wildcard_name(name) => {
895 let cleaned = clean_tex_name(name, &self.rng.display_strip_prefixes);
896 self.render_inline_attribute(&cleaned, body)
897 },
898 _ => self.to_tex(p),
899 })
900 .collect();
901 match op {
902 CombineOp::Group => {
903 if inner.len() == 1 {
904 inner.into_iter().next().unwrap()
905 } else {
906 format!("({})", inner.join(", "))
907 }
908 },
909 CombineOp::Interleave => format!("({})", inner.join(" ~\\&~ ")),
910 CombineOp::Choice => format!("({})", inner.join(" ~\\textbar~ ")),
911 CombineOp::Optional => {
912 if inner.len() == 1 && matches!(data[0], Pattern::Attribute { .. }) {
914 inner.into_iter().next().unwrap()
915 } else {
916 format!(
917 "{}\\textsuperscript{{?}}",
918 inner.first().cloned().unwrap_or_default()
919 )
920 }
921 },
922 CombineOp::ZeroOrMore | CombineOp::OneOrMore => {
923 format!(
925 "{}\\textsuperscript{{*}}",
926 inner.first().cloned().unwrap_or_default()
927 )
928 },
929 CombineOp::List => format!("({})", inner.join(", ")),
930 }
931 }
932
933 fn extract_docs(&mut self, data: &[Pattern]) -> (String, Vec<Pattern>) {
938 let mut docs = String::new();
939 let mut rest = Vec::with_capacity(data.len());
940 for item in data {
941 if let Pattern::Doc(_) = item {
942 docs.push_str(&self.to_tex(item));
943 } else {
944 rest.push(item.clone());
945 }
946 }
947 (docs, rest)
948 }
949
950 fn to_tex_body(&mut self, data: &[Pattern]) -> (String, String) {
955 let mut attributes: Vec<String> = Vec::new();
956 let mut content: Vec<String> = Vec::new();
957 let mut attr_patterns: Vec<String> = Vec::new();
958 let dedup_owned: Vec<Pattern>;
965 let data: &[Pattern] = if has_wildcard_pair(data) {
966 dedup_owned = dedupe_wildcard_pairs(data);
967 &dedup_owned
968 } else {
969 data
970 };
971 let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
979 let mut deque: std::collections::VecDeque<Pattern> = data.iter().cloned().collect();
980 while let Some(item) = deque.pop_front() {
981 match &item {
982 Pattern::Attribute { name, body } => {
983 if let Some(t) = simple_attr_type(body)
984 && !is_wildcard_name(name)
985 {
986 grouped
987 .entry(t)
988 .or_default()
989 .push(clean_tex_name(name, &self.rng.display_strip_prefixes));
990 continue;
991 }
992 attributes.push(self.to_tex(&item));
993 },
994 Pattern::Combination { body, .. } if self.is_attributes(&item) => {
995 for c in body.iter().cloned().rev() {
996 deque.push_front(c);
997 }
998 },
999 Pattern::Ref { qname } if qname.ends_with("_attributes") || qname.ends_with("_model") => {
1000 if let Some(def) = self.rng.defs.get(qname).cloned() {
1001 deque.push_front(def);
1002 }
1003 },
1004 Pattern::Ref { qname } if qname_ends_with_attributes(qname) => {
1005 attr_patterns.push(self.to_tex(&item));
1006 },
1007 Pattern::Element { name, body } if !is_wildcard_name(name) => {
1013 content.push(self.render_inline_element(name, body));
1014 },
1015 _ => content.push(self.to_tex(&item)),
1016 }
1017 }
1018
1019 let mut attr_str = String::new();
1020 if !attr_patterns.is_empty() {
1021 attr_str.push_str("\\item[\\textit{Attributes}:] ");
1022 attr_str.push_str(&attr_patterns.join(", "));
1023 }
1024 for (type_name, mut names) in grouped {
1031 names.sort();
1032 let monospaced: Vec<String> = names.iter().map(|n| format!("\\texttt{{{}}}", n)).collect();
1033 attr_str.push_str(&format!(
1034 "\\item[\\textit{{{}}}:] {}",
1035 attr_group_label(&type_name),
1036 monospaced.join(", "),
1037 ));
1038 }
1039 for a in attributes {
1040 attr_str.push_str(&a);
1041 }
1042 let content_str = content.join(", ");
1043 (attr_str, content_str)
1044 }
1045
1046 fn is_attributes(&self, item: &Pattern) -> bool {
1048 match item {
1049 Pattern::Attribute { .. } => true,
1050 Pattern::Ref { qname } => self
1051 .rng
1052 .defs
1053 .get(qname)
1054 .map(|p| self.is_attributes(p))
1055 .unwrap_or(false),
1056 Pattern::Combination {
1057 op:
1058 CombineOp::Optional
1059 | CombineOp::Choice
1060 | CombineOp::Group
1061 | CombineOp::ZeroOrMore
1062 | CombineOp::OneOrMore,
1063 body,
1064 } => body.iter().all(|p| self.is_attributes(p)),
1065 _ => false,
1066 }
1067 }
1068
1069 fn is_content(&self, item: &Pattern) -> bool {
1071 match item {
1072 Pattern::Element { .. } | Pattern::Grammar { .. } => true,
1073 Pattern::Ref { qname } => {
1074 if self.rng.elementdefs.contains_key(qname) {
1075 return true;
1076 }
1077 self
1078 .rng
1079 .defs
1080 .get(qname)
1081 .map(|p| self.is_content(p))
1082 .unwrap_or(false)
1083 },
1084 Pattern::Combination {
1085 op:
1086 CombineOp::Optional
1087 | CombineOp::Choice
1088 | CombineOp::Group
1089 | CombineOp::ZeroOrMore
1090 | CombineOp::OneOrMore,
1091 body,
1092 } => body.iter().all(|p| self.is_content(p)),
1093 Pattern::Text => true,
1094 _ => false,
1095 }
1096 }
1097
1098 fn symbol_uses(&self, qname: &str) -> Option<String> {
1101 let uses = self.rng.uses_name.get(qname)?;
1102 let mut sorted: Vec<&String> = uses.iter().collect();
1103 sorted.sort_by(|a, b| {
1108 let ka = a.split('@').next().unwrap_or(a);
1109 let kb = b.split('@').next().unwrap_or(b);
1110 ka.cmp(kb).then_with(|| a.cmp(b))
1111 });
1112 let mut pattern_parts: Vec<String> = Vec::new();
1125 let mut element_parts: Vec<String> = Vec::new();
1126 for u in sorted {
1127 if self.opts.skip_svg && u.contains("SVG.") {
1128 continue;
1129 }
1130 if let Some(rest) = u.strip_prefix("pattern:") {
1131 if let Some(idx) = rest.find(':') {
1132 let after = &rest[idx + 1..];
1133 if let Some(name) = after
1134 .strip_suffix("_attributes")
1135 .or_else(|| after.strip_suffix("_model"))
1136 {
1137 element_parts.push(format!(
1138 "\\elementref{{{}}}",
1139 clean_tex_name(name, &self.rng.display_strip_prefixes)
1140 ));
1141 } else {
1142 pattern_parts.push(format!("\\patternref{{{}}}", clean_tex(after)));
1143 }
1144 }
1145 continue;
1146 }
1147 if let Some(rest) = u.strip_prefix("element:") {
1148 let (tag, host) = match rest.split_once('@') {
1149 Some((t, h)) => (t, Some(h)),
1150 None => (rest, None),
1151 };
1152 if self.ambiguous_elements.contains(tag)
1153 && let Some(hrest) = host.and_then(|h| h.strip_prefix("pattern:"))
1154 && let Some(idx) = hrest.find(':')
1155 {
1156 pattern_parts.push(format!("\\patternref{{{}}}", clean_tex(&hrest[idx + 1..])));
1157 continue;
1158 }
1159 element_parts.push(format!(
1160 "\\elementref{{{}}}",
1161 clean_tex_name(tag, &self.rng.display_strip_prefixes)
1162 ));
1163 }
1164 }
1165 let mut parts: Vec<String> = Vec::new();
1166 for p in pattern_parts.into_iter().chain(element_parts) {
1167 if !parts.contains(&p) {
1168 parts.push(p);
1169 }
1170 }
1171 if parts.is_empty() {
1172 None
1173 } else {
1174 Some(parts.join(", "))
1175 }
1176 }
1177}
1178
1179fn is_svg_module(name: &str) -> bool { name.contains(":svg:") || name.starts_with("svg") }
1186
1187fn strip_first_qualifier(s: &str) -> String {
1188 if let Some(idx) = s.find(':') {
1191 let prefix = &s[..idx];
1192 if !prefix.is_empty() && prefix.chars().all(|c| c.is_alphanumeric() || c == '_') {
1193 return s[idx + 1..].to_string();
1194 }
1195 }
1196 s.to_string()
1197}
1198
1199fn collect_element_descendants(spec: &[Pattern]) -> Vec<(String, &[Pattern])> {
1211 let mut out = Vec::new();
1212 let mut seen: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
1213 for p in spec {
1214 walk_for_elements(p, &mut out, &mut seen);
1215 }
1216 out
1217}
1218
1219fn walk_for_elements<'a>(
1220 p: &'a Pattern,
1221 out: &mut Vec<(String, &'a [Pattern])>,
1222 seen: &mut rustc_hash::FxHashSet<String>,
1223) {
1224 match p {
1225 Pattern::Element { name, body } => {
1226 if !is_wildcard_name(name) && seen.insert(name.clone()) {
1227 out.push((name.clone(), body.as_slice()));
1228 }
1229 },
1233 Pattern::Combination { body, .. } => {
1234 for c in body {
1235 walk_for_elements(c, out, seen);
1236 }
1237 },
1238 _ => {},
1239 }
1240}
1241
1242fn simple_attr_type(body: &[Pattern]) -> Option<String> {
1255 if body.iter().any(|p| matches!(p, Pattern::Doc(_))) {
1256 return None;
1257 }
1258 match body {
1259 [] => Some("text".into()),
1260 [Pattern::Text] => Some("text".into()),
1261 [Pattern::Data(t)] => Some(t.clone()),
1262 _ => None,
1263 }
1264}
1265
1266fn attr_group_label(type_name: &str) -> String {
1271 let cleaned = clean_tex(type_name);
1272 let mut chars = cleaned.chars();
1273 match chars.next() {
1274 None => "Attributes".into(),
1275 Some(c) => format!(
1276 "{}{} attributes",
1277 c.to_uppercase().collect::<String>(),
1278 chars.as_str()
1279 ),
1280 }
1281}
1282
1283fn is_wildcard_name(name: &str) -> bool { name == "*" || name == "*:*" || name.ends_with(":*") }
1289
1290fn has_wildcard_pair(data: &[Pattern]) -> bool {
1294 data.windows(2).any(|w| is_wildcard_pair(&w[0], &w[1]))
1295}
1296
1297fn is_wildcard_pair(a: &Pattern, b: &Pattern) -> bool {
1298 match (a, b) {
1299 (Pattern::Element { name: n1, .. }, Pattern::Element { name: n2, .. })
1300 | (Pattern::Attribute { name: n1, .. }, Pattern::Attribute { name: n2, .. }) => {
1301 n1 == "*" && n2 == "*:*"
1302 },
1303 _ => false,
1304 }
1305}
1306
1307fn dedupe_wildcard_pairs(data: &[Pattern]) -> Vec<Pattern> {
1314 let mut out = Vec::with_capacity(data.len());
1315 let mut i = 0;
1316 while i < data.len() {
1317 if i + 1 < data.len() && is_wildcard_pair(&data[i], &data[i + 1]) {
1318 out.push(data[i + 1].clone());
1321 i += 2;
1322 } else {
1323 out.push(data[i].clone());
1324 i += 1;
1325 }
1326 }
1327 out
1328}
1329
1330fn strip_outer_parens(s: &str) -> &str {
1336 let bytes = s.as_bytes();
1337 if bytes.first() != Some(&b'(') || bytes.last() != Some(&b')') {
1338 return s;
1339 }
1340 let mut depth = 0i32;
1342 for (i, b) in bytes.iter().enumerate() {
1343 match b {
1344 b'(' => depth += 1,
1345 b')' => {
1346 depth -= 1;
1347 if depth == 0 && i + 1 != bytes.len() {
1348 return s;
1349 }
1350 },
1351 _ => {},
1352 }
1353 }
1354 if depth != 0 {
1355 return s;
1356 }
1357 &s[1..s.len() - 1]
1358}
1359
1360fn qname_ends_with_attributes(qname: &str) -> bool {
1361 let rest = qname.strip_suffix("attributes").unwrap_or("");
1363 if qname == "attributes" {
1364 return false;
1365 }
1366 match rest.chars().last() {
1367 Some(c) => !c.is_ascii_alphabetic() && qname.ends_with("attributes"),
1368 None => false,
1369 }
1370}
1371
1372#[cfg(test)]
1375mod tests {
1376 use super::*;
1377
1378 #[test]
1379 fn clean_tex_pcdata() {
1380 assert_eq!(clean_tex("#PCDATA"), r"\typename{text}");
1381 }
1382
1383 #[test]
1384 fn clean_tex_underscore() {
1385 assert_eq!(clean_tex("foo_bar"), r"foo\_bar");
1386 }
1387
1388 #[test]
1389 fn clean_tex_strips_urn() {
1390 assert_eq!(clean_tex("urn:x-LaTeXML:RelaxNG:foo"), "foo");
1391 }
1392
1393 #[test]
1394 fn clean_tex_wraps_angles() {
1395 assert_eq!(clean_tex("a<b>c"), r"a\texttt{b}c");
1397 }
1398
1399 #[test]
1400 fn clean_tex_escapes_hash() {
1401 assert_eq!(clean_tex("foo#bar"), r"foo\#bar");
1402 }
1403
1404 #[test]
1405 fn clean_tex_name_consults_strip_list() {
1406 let strip_ltx = vec!["ltx".to_string()];
1407 assert_eq!(clean_tex_name("ltx:para", &strip_ltx), "para");
1408 assert_eq!(clean_tex_name("xhtml:div", &strip_ltx), "xhtml:div");
1410 let strip_both = vec!["xhtml".to_string(), "ltx".to_string()];
1412 assert_eq!(clean_tex_name("xhtml:div", &strip_both), "div");
1413 assert_eq!(clean_tex_name("ltx:para", &strip_both), "para");
1414 assert_eq!(clean_tex_name("ltx:para", &[]), "ltx:para");
1416 }
1417
1418 #[test]
1419 fn document_modules_emits_schemamodule() {
1420 let mut rng = Relaxng::default();
1421 rng.modules.push(Pattern::Module {
1422 name: "test".into(),
1423 body: vec![],
1424 });
1425 let out = document_modules(&rng, Options::default());
1426 assert!(out.contains("\\begin{schemamodule}{test}"));
1427 assert!(out.contains("\\end{schemamodule}"));
1428 }
1429
1430 #[test]
1431 fn document_modules_skips_svg_module_when_skip_svg() {
1432 let mut rng = Relaxng::default();
1433 rng.modules.push(Pattern::Module {
1434 name: "x:svg:foo".into(),
1435 body: vec![],
1436 });
1437 let out = document_modules(&rng, Options::default());
1438 assert!(!out.contains("schemamodule"));
1439 }
1440
1441 #[test]
1442 fn combination_rendering() {
1443 let rng = Relaxng::default();
1444 let mut emit = EmitState {
1445 rng: &rng,
1446 opts: Options::default(),
1447 defined_patterns: HashMap::default(),
1448 ambiguous_elements: ambiguous_element_tags(&rng),
1449 };
1450 let body = vec![Pattern::Ref { qname: "g:A".into() }, Pattern::Ref {
1451 qname: "g:B".into(),
1452 }];
1453 let group = emit.to_tex_combination(CombineOp::Group, &body);
1454 assert_eq!(group, "(\\patternref{A}, \\patternref{B})");
1455 let choice = emit.to_tex_combination(CombineOp::Choice, &body);
1456 assert_eq!(choice, "(\\patternref{A} ~\\textbar~ \\patternref{B})");
1457 let inter = emit.to_tex_combination(CombineOp::Interleave, &body);
1458 assert_eq!(inter, "(\\patternref{A} ~\\&~ \\patternref{B})");
1459 }
1460
1461 #[test]
1462 fn singleton_group_collapses_in_combination() {
1463 let rng = Relaxng::default();
1464 let mut emit = EmitState {
1465 rng: &rng,
1466 opts: Options::default(),
1467 defined_patterns: HashMap::default(),
1468 ambiguous_elements: ambiguous_element_tags(&rng),
1469 };
1470 let body = vec![Pattern::Ref { qname: "g:Only".into() }];
1471 let result = emit.to_tex_combination(CombineOp::Group, &body);
1472 assert_eq!(result, "\\patternref{Only}");
1473 }
1474
1475 #[test]
1476 fn element_renders_with_content_and_used_by() {
1477 let mut rng = Relaxng::default();
1478 rng
1479 .element_reverse_defs
1480 .insert("foo".into(), "g:Foo".into());
1481 rng
1482 .uses_name
1483 .entry("g:Foo".into())
1484 .or_default()
1485 .insert("element:bar".into());
1486 let mut emit = EmitState {
1487 rng: &rng,
1488 opts: Options::default(),
1489 defined_patterns: HashMap::default(),
1490 ambiguous_elements: ambiguous_element_tags(&rng),
1491 };
1492 let out = emit.to_tex_element("foo", &[Pattern::Text]);
1493 assert!(out.contains("\\elementdef{foo}"));
1494 assert!(out.contains("\\item[\\textit{Content}:]"));
1495 assert!(out.contains("\\elementref{bar}"));
1496 }
1497
1498 #[test]
1499 fn def_emits_patterndef_then_skips_duplicates() {
1500 let rng = Relaxng::default();
1501 let mut emit = EmitState {
1502 rng: &rng,
1503 opts: Options::default(),
1504 defined_patterns: HashMap::default(),
1505 ambiguous_elements: ambiguous_element_tags(&rng),
1506 };
1507 let body = vec![Pattern::Text];
1508 let first = emit.to_tex_def("", "g:X", &body);
1509 let second = emit.to_tex_def("", "g:X", &body);
1510 assert!(first.contains("\\patterndef{X}"));
1511 assert_eq!(second, "");
1512 }
1513
1514 #[test]
1515 fn def_combine_choice_emits_patternadd() {
1516 let rng = Relaxng::default();
1517 let mut emit = EmitState {
1518 rng: &rng,
1519 opts: Options::default(),
1520 defined_patterns: HashMap::default(),
1521 ambiguous_elements: ambiguous_element_tags(&rng),
1522 };
1523 let out = emit.to_tex_def("choice", "g:X", &[Pattern::Text]);
1524 assert!(out.contains("\\patternadd{X}"));
1525 assert_eq!(emit.defined_patterns.get("X").copied(), Some(-1));
1527 }
1528
1529 #[test]
1530 fn trivial_text_attributes_collapse_into_grouped_line() {
1531 let xml = r##"
1536 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
1537 <define name="OnEvent">
1538 <group>
1539 <optional><attribute name="onclick"><text/></attribute></optional>
1540 <optional><attribute name="onabort"><text/></attribute></optional>
1541 <optional><attribute name="onblur"><text/></attribute></optional>
1542 </group>
1543 </define>
1544 </grammar>
1545 "##;
1546 use crate::common::relaxng::scan::scan_string;
1547 let mut rng = Relaxng::default();
1548 let raw = scan_string(&mut rng, xml).expect("scan");
1549 let wrapped = vec![Pattern::Module { name: "m".into(), body: raw }];
1550 let _ = crate::common::relaxng::simplify::simplify_top(&mut rng, wrapped);
1551 let out = document_modules(&rng, Options::default());
1552
1553 assert!(
1554 out.contains(
1555 "\\item[\\textit{Text attributes}:] \\texttt{onabort}, \\texttt{onblur}, \\texttt{onclick}"
1556 ),
1557 "expected sorted Text attributes line with monospaced names, got:\n{}",
1558 out
1559 );
1560 assert!(
1561 !out.contains("\\attrdef{onclick}"),
1562 "trivial text attribute should not render as a per-attribute card:\n{}",
1563 out
1564 );
1565 }
1566
1567 #[test]
1568 fn typed_attributes_grouped_per_type_label() {
1569 let xml = r##"
1572 <grammar xmlns="http://relaxng.org/ns/structure/1.0"
1573 datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
1574 <define name="P">
1575 <group>
1576 <attribute name="a"><text/></attribute>
1577 <attribute name="b"><data type="string"/></attribute>
1578 <attribute name="c"><data type="integer"/></attribute>
1579 <attribute name="d">
1580 <choice><value>x</value><value>y</value></choice>
1581 </attribute>
1582 </group>
1583 </define>
1584 </grammar>
1585 "##;
1586 use crate::common::relaxng::scan::scan_string;
1587 let mut rng = Relaxng::default();
1588 let raw = scan_string(&mut rng, xml).expect("scan");
1589 let wrapped = vec![Pattern::Module { name: "m".into(), body: raw }];
1590 let _ = crate::common::relaxng::simplify::simplify_top(&mut rng, wrapped);
1591 let out = document_modules(&rng, Options::default());
1592
1593 assert!(
1594 out.contains("\\item[\\textit{Text attributes}:] \\texttt{a}"),
1595 "{}",
1596 out
1597 );
1598 assert!(
1599 out.contains("\\item[\\textit{String attributes}:] \\texttt{b}"),
1600 "{}",
1601 out
1602 );
1603 assert!(
1604 out.contains("\\item[\\textit{Integer attributes}:] \\texttt{c}"),
1605 "{}",
1606 out
1607 );
1608 assert!(out.contains("\\attrdef{d}"), "{}", out);
1610 }
1611
1612 #[test]
1613 fn anyelement_renders_inline_without_nested_cards() {
1614 let xml = r##"
1619 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
1620 <define name="anyElement">
1621 <element>
1622 <anyName/>
1623 <zeroOrMore>
1624 <choice>
1625 <attribute><anyName/><text/></attribute>
1626 <text/>
1627 <ref name="anyElement"/>
1628 </choice>
1629 </zeroOrMore>
1630 </element>
1631 </define>
1632 </grammar>
1633 "##;
1634 use crate::common::relaxng::scan::scan_string;
1635 let mut rng = Relaxng::default();
1636 let raw = scan_string(&mut rng, xml).expect("scan");
1637 let wrapped = vec![Pattern::Module { name: "m".into(), body: raw }];
1638 let _ = crate::common::relaxng::simplify::simplify_top(&mut rng, wrapped);
1639 let out = document_modules(&rng, Options::default());
1640
1641 assert!(
1642 out.contains("\\patterndef{anyElement}"),
1643 "expected anyElement patterndef, got:\n{}",
1644 out
1645 );
1646 assert!(
1647 !out.contains("\\elementdef{*}"),
1648 "wildcard element rendered as nested elementdef card:\n{}",
1649 out
1650 );
1651 assert!(
1652 !out.contains("\\elementdef{*:*}"),
1653 "wildcard element rendered as nested elementdef card:\n{}",
1654 out
1655 );
1656 assert!(
1657 !out.contains("\\attrdef{*}"),
1658 "wildcard attribute rendered as nested attrdef card:\n{}",
1659 out
1660 );
1661 assert!(
1662 !out.contains("\\attrdef{*:*}"),
1663 "wildcard attribute rendered as nested attrdef card:\n{}",
1664 out
1665 );
1666 assert!(
1667 out.contains("\\textit{element}~\\texttt{*:*}"),
1668 "expected inline element render for wildcard:\n{}",
1669 out
1670 );
1671 assert!(
1672 out.contains("\\textit{attribute}~\\texttt{*:*}"),
1673 "expected inline attribute render for wildcard:\n{}",
1674 out
1675 );
1676 assert!(
1678 !out.contains("\\textit{Expansion}"),
1679 "Expansion duplicates Content for anyElement; should be suppressed:\n{}",
1680 out
1681 );
1682 }
1683
1684 #[test]
1685 fn dedupe_wildcard_pairs_collapses_adjacent() {
1686 let body = vec![
1687 Pattern::Element {
1688 name: "*".into(),
1689 body: vec![Pattern::Text],
1690 },
1691 Pattern::Element {
1692 name: "*:*".into(),
1693 body: vec![Pattern::Text],
1694 },
1695 Pattern::Ref { qname: "x".into() },
1696 ];
1697 let folded = dedupe_wildcard_pairs(&body);
1698 assert_eq!(folded.len(), 2);
1699 match &folded[0] {
1700 Pattern::Element { name, .. } => assert_eq!(name, "*:*"),
1701 other => panic!("expected Element *:*, got {:?}", other),
1702 }
1703 }
1704
1705 #[test]
1706 fn strip_outer_parens_only_when_outer_match() {
1707 assert_eq!(strip_outer_parens("(abc)"), "abc");
1708 assert_eq!(strip_outer_parens("(a)(b)"), "(a)(b)");
1709 assert_eq!(strip_outer_parens("abc"), "abc");
1710 assert_eq!(strip_outer_parens("(a"), "(a");
1711 }
1712
1713 #[test]
1714 fn element_choice_renders_as_namelinks_plus_sibling_cards() {
1715 let xml = r##"
1721 <grammar xmlns="http://relaxng.org/ns/structure/1.0"
1722 ns="http://example.org/ns">
1723 <define name="X">
1724 <choice>
1725 <element name="a"><ref name="B"/></element>
1726 <element name="b"><ref name="B"/></element>
1727 <element name="c"><ref name="B"/></element>
1728 </choice>
1729 </define>
1730 <define name="B"><text/></define>
1731 </grammar>
1732 "##;
1733 use crate::common::relaxng::scan::scan_string;
1734 let mut rng = Relaxng::default();
1735 let raw = scan_string(&mut rng, xml).expect("scan");
1736 let wrapped = vec![Pattern::Module { name: "m".into(), body: raw }];
1737 let _ = crate::common::relaxng::simplify::simplify_top(&mut rng, wrapped);
1738 let out = document_modules(&rng, Options::default());
1739
1740 let patterndef = out
1743 .find("\\patterndef{X}")
1744 .map(|i| {
1745 &out[i..out[i..]
1746 .find("\\elementdef")
1747 .map(|j| i + j)
1748 .unwrap_or(out.len())]
1749 })
1750 .expect("patterndef X present");
1751 assert!(
1752 patterndef.contains("\\item[\\textit{Content}:]"),
1753 "patterndef should expose Content line, got:\n{}",
1754 patterndef
1755 );
1756 let pos_a = patterndef
1757 .find("\\elementref{namespace1:a}")
1758 .expect("a present");
1759 let pos_b = patterndef
1760 .find("\\elementref{namespace1:b}")
1761 .expect("b present");
1762 let pos_c = patterndef
1763 .find("\\elementref{namespace1:c}")
1764 .expect("c present");
1765 assert!(
1766 pos_a < pos_b && pos_b < pos_c,
1767 "expected alphabetised order: {}",
1768 patterndef
1769 );
1770 assert!(
1771 patterndef.contains("\\textbar"),
1772 "expected | separator: {}",
1773 patterndef
1774 );
1775 assert!(
1776 !patterndef.contains("\\elementdef{namespace1:"),
1777 "patterndef body should NOT carry nested elementdef cards:\n{}",
1778 patterndef
1779 );
1780
1781 assert!(out.contains("\\elementdef{namespace1:a}"), "{}", out);
1784 assert!(out.contains("\\elementdef{namespace1:b}"), "{}", out);
1785 assert!(out.contains("\\elementdef{namespace1:c}"), "{}", out);
1786 assert!(
1787 out.contains("\\patternref{X}"),
1788 "per-element card should cite parent in Used by:\n{}",
1789 out
1790 );
1791 }
1792
1793 #[test]
1794 fn differing_bodies_keep_first_occurrence_per_name() {
1795 let xml = r##"
1800 <grammar xmlns="http://relaxng.org/ns/structure/1.0"
1801 ns="http://example.org/ns">
1802 <define name="X">
1803 <choice>
1804 <element name="a"><text/></element>
1805 <element name="a"><ref name="B"/></element>
1806 <element name="b"><text/></element>
1807 </choice>
1808 </define>
1809 <define name="B"><text/></define>
1810 </grammar>
1811 "##;
1812 use crate::common::relaxng::scan::scan_string;
1813 let mut rng = Relaxng::default();
1814 let raw = scan_string(&mut rng, xml).expect("scan");
1815 let wrapped = vec![Pattern::Module { name: "m".into(), body: raw }];
1816 let _ = crate::common::relaxng::simplify::simplify_top(&mut rng, wrapped);
1817 let out = document_modules(&rng, Options::default());
1818
1819 let patterndef = out
1821 .find("\\patterndef{X}")
1822 .map(|i| {
1823 &out[i..out[i..]
1824 .find("\\elementdef")
1825 .map(|j| i + j)
1826 .unwrap_or(out.len())]
1827 })
1828 .unwrap();
1829 assert_eq!(
1830 patterndef.matches("\\elementref{namespace1:a}").count(),
1831 1,
1832 "duplicate 'a' should appear once in body: {}",
1833 patterndef
1834 );
1835 assert_eq!(
1837 out.matches("\\elementdef{namespace1:a}").count(),
1838 1,
1839 "{}",
1840 out
1841 );
1842 assert_eq!(
1843 out.matches("\\elementdef{namespace1:b}").count(),
1844 1,
1845 "{}",
1846 out
1847 );
1848 }
1849
1850 #[test]
1851 fn unmatched_patternadd_upgrades_to_patterndefadd() {
1852 let mut rng = Relaxng::default();
1853 rng.modules.push(Pattern::Module {
1854 name: "m".into(),
1855 body: vec![Pattern::Def {
1856 combiner: DefCombiner::Choice,
1857 name: "g:Lonely".into(),
1858 body: vec![Pattern::Text],
1859 }],
1860 });
1861 let out = document_modules(&rng, Options::default());
1862 assert!(
1863 out.contains("\\patterndefadd{Lonely}"),
1864 "expected upgrade, got:\n{}",
1865 out
1866 );
1867 assert!(
1868 !out.contains("\\patternadd{Lonely}"),
1869 "patternadd should have been replaced"
1870 );
1871 }
1872}