1use std::path::{Path, PathBuf};
21
22use libxml::{
23 parser::Parser as XmlParser,
24 readonly::RoNode,
25 tree::{Document as XmlDocument, NodeType},
26};
27
28use super::{CombineOp, DefCombiner, Pattern, Relaxng};
29
30const RNG_NS: &str = "http://relaxng.org/ns/structure/1.0";
32const RNGA_NS: &str = "http://relaxng.org/ns/compatibility/annotations/1.0";
34
35#[derive(Debug)]
37pub enum ScanError {
38 FileNotFound(String),
40 Parse(String),
42 UnknownOp { op: String, file: PathBuf },
46}
47
48impl std::fmt::Display for ScanError {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 ScanError::FileNotFound(name) => write!(f, "RelaxNG file not found: {}", name),
52 ScanError::Parse(msg) => write!(f, "RelaxNG parse error: {}", msg),
53 ScanError::UnknownOp { op, file } => {
54 write!(f, "Unknown RelaxNG op '{}' in {}", op, file.display())
55 },
56 }
57 }
58}
59
60impl std::error::Error for ScanError {}
61
62pub fn scan_external(
76 rng: &mut Relaxng,
77 name: &str,
78 inherit_ns: Option<&str>,
79 search_paths: &[&Path],
80) -> Result<Vec<Pattern>, ScanError> {
81 let parser = XmlParser::default();
82 let (path, xml_doc): (PathBuf, XmlDocument) = resolve_schema_ref(&parser, name, search_paths)?;
84 let root = xml_doc
85 .get_root_readonly()
86 .ok_or_else(|| ScanError::Parse("empty document".into()))?;
87
88 collect_namespaces(rng, root);
92
93 if rng.primary_namespace.is_none()
98 && let Some(uri) = root.get_attribute("ns")
99 && !uri.is_empty()
100 {
101 rng.primary_namespace = Some(uri);
102 }
103
104 let modname = strip_rng_ext(name);
105 let mut new_paths: Vec<&Path> = Vec::with_capacity(search_paths.len() + 1);
106 let dir = path.parent().unwrap_or_else(|| Path::new("."));
107 new_paths.push(dir);
108 new_paths.extend(search_paths);
109
110 let mut ctx = ScanContext { search_paths: new_paths };
113 let body = scan_pattern(rng, root, inherit_ns, &mut ctx)?;
114 Ok(vec![Pattern::Module { name: modname, body }])
115}
116
117pub fn scan_string(rng: &mut Relaxng, xml: &str) -> Result<Vec<Pattern>, ScanError> {
120 let parser = XmlParser::default();
121 let xml_doc = parser
122 .parse_string(xml)
123 .map_err(|e| ScanError::Parse(format!("{:?}", e)))?;
124 let root = xml_doc
125 .get_root_readonly()
126 .ok_or_else(|| ScanError::Parse("empty document".into()))?;
127 collect_namespaces(rng, root);
128 let mut ctx = ScanContext { search_paths: Vec::new() };
129 scan_pattern(rng, root, None, &mut ctx)
130}
131
132struct ScanContext<'p> {
135 search_paths: Vec<&'p Path>,
136}
137
138fn get_relax_op(node: RoNode) -> Option<String> {
142 if node.get_type() != Some(NodeType::ElementNode) {
143 return None;
144 }
145 let local = node.get_name();
146 let ns_uri = node
147 .get_namespace()
148 .map(|ns| ns.get_href())
149 .unwrap_or_default();
150 let prefix = match ns_uri.as_str() {
151 RNG_NS => "rng",
152 RNGA_NS => "rnga",
153 "" => return None,
154 other => return Some(format!("{{{}}}:{}", other, local)),
155 };
156 Some(format!("{}:{}", prefix, local))
157}
158
159fn get_elements(node: RoNode) -> Vec<RoNode> {
162 let mut out = Vec::new();
163 let mut child = node.get_first_child();
164 while let Some(c) = child {
165 if c.get_type() == Some(NodeType::ElementNode) {
166 out.push(c);
167 }
168 child = c.get_next_sibling();
169 }
170 out
171}
172
173fn combine_op_from_localname(name: &str) -> Option<CombineOp> {
176 Some(match name {
177 "group" => CombineOp::Group,
178 "interleave" => CombineOp::Interleave,
179 "choice" => CombineOp::Choice,
180 "optional" => CombineOp::Optional,
181 "zeroOrMore" => CombineOp::ZeroOrMore,
182 "oneOrMore" => CombineOp::OneOrMore,
183 "list" => CombineOp::List,
184 _ => return None,
185 })
186}
187
188fn encode_qname(rng: &mut Relaxng, ns: Option<&str>, local: &str) -> String {
194 match ns {
195 None | Some("") => local.to_string(),
196 Some(uri) => format!("{}:{}", ensure_prefix(rng, uri), local),
197 }
198}
199
200fn ensure_prefix(rng: &mut Relaxng, uri: &str) -> String {
201 if let Some(prefix) = rng.code_namespace_prefixes.get(uri) {
208 return prefix.clone();
209 }
210 if let Some((prefix, _)) = rng
212 .document_namespaces
213 .iter()
214 .find(|(p, u)| !p.is_empty() && u.as_str() == uri)
215 {
216 return prefix.clone();
217 }
218 let n = rng
219 .document_namespaces
220 .keys()
221 .filter(|p| p.starts_with("namespace"))
222 .count()
223 + 1;
224 let new_prefix = format!("namespace{}", n);
225 rng
226 .document_namespaces
227 .insert(new_prefix.clone(), uri.to_string());
228 new_prefix
229}
230
231fn scan_pattern(
235 rng: &mut Relaxng,
236 node: RoNode,
237 inherit_ns: Option<&str>,
238 ctx: &mut ScanContext<'_>,
239) -> Result<Vec<Pattern>, ScanError> {
240 let Some(op) = get_relax_op(node) else {
241 return Ok(Vec::new());
242 };
243 let ns = node
244 .get_attribute("ns")
245 .or_else(|| inherit_ns.map(String::from));
246 let ns_ref = ns.as_deref();
247
248 match op.as_str() {
249 "rng:element" => scan_pattern_element(rng, ns_ref, node, ctx),
250 "rng:attribute" => scan_pattern_attribute(rng, ns_ref, node, ctx),
251 "rng:mixed" => {
252 let mut body = vec![Pattern::Text];
253 body.extend(scan_children(rng, ns_ref, get_elements(node), ctx)?);
254 Ok(vec![Pattern::Combination {
255 op: CombineOp::Interleave,
256 body,
257 }])
258 },
259 "rng:ref" => Ok(vec![Pattern::Ref {
260 qname: node.get_attribute("name").unwrap_or_default(),
261 }]),
262 "rng:parentRef" => Ok(vec![Pattern::ParentRef {
263 qname: node.get_attribute("name").unwrap_or_default(),
264 }]),
265 "rng:empty" | "rng:notAllowed" => Ok(Vec::new()),
266 "rng:text" => Ok(vec![Pattern::Text]),
267 "rng:value" => Ok(vec![Pattern::Value(node.get_content())]),
268 "rng:data" => Ok(vec![Pattern::Data(
269 node.get_attribute("type").unwrap_or_default(),
270 )]),
271 "rng:externalRef" => {
272 let href = node.get_attribute("href").unwrap_or_default();
273 let paths: Vec<&Path> = ctx.search_paths.clone();
274 scan_external(rng, &href, ns_ref, &paths)
275 },
276 "rng:grammar" => {
277 rng.internal_grammars += 1;
278 let name = format!("grammar{}", rng.internal_grammars);
279 let body = scan_grammar_content(rng, ns_ref, get_elements(node), ctx)?;
280 Ok(vec![Pattern::Grammar { name, body }])
281 },
282 "rnga:documentation" => {
283 let text = node.get_content();
284 Ok(vec![Pattern::Doc(text)])
285 },
286 other => {
287 if let Some(stripped) = other.strip_prefix("rng:")
290 && let Some(cop) = combine_op_from_localname(stripped)
291 {
292 let body = scan_children(rng, ns_ref, get_elements(node), ctx)?;
293 return Ok(vec![Pattern::Combination { op: cop, body }]);
294 }
295 Ok(Vec::new())
297 },
298 }
299}
300
301fn scan_pattern_element(
302 rng: &mut Relaxng,
303 ns: Option<&str>,
304 node: RoNode,
305 ctx: &mut ScanContext<'_>,
306) -> Result<Vec<Pattern>, ScanError> {
307 let mut children = get_elements(node);
308 if let Some(name) = node.get_attribute("name") {
309 let body = scan_children(rng, ns, children, ctx)?;
310 Ok(vec![Pattern::Element {
311 name: encode_qname(rng, ns, &name),
312 body,
313 }])
314 } else if !children.is_empty() {
315 let name_node = children.remove(0);
316 let names = scan_name_class(rng, name_node, false, ns);
317 let body_proto = scan_children(rng, ns, children, ctx)?;
318 Ok(
319 names
320 .into_iter()
321 .map(|n| Pattern::Element {
322 name: n,
323 body: body_proto.clone(),
324 })
325 .collect(),
326 )
327 } else {
328 Ok(Vec::new())
329 }
330}
331
332fn scan_pattern_attribute(
333 rng: &mut Relaxng,
334 ns: Option<&str>,
335 node: RoNode,
336 ctx: &mut ScanContext<'_>,
337) -> Result<Vec<Pattern>, ScanError> {
338 let xns = node.get_attribute("ns"); let xns_ref = xns.as_deref();
340 let mut children = get_elements(node);
341 if let Some(name) = node.get_attribute("name") {
342 let body = scan_children(rng, ns, children, ctx)?;
343 Ok(vec![Pattern::Attribute {
344 name: encode_qname(rng, xns_ref, &name),
345 body,
346 }])
347 } else if !children.is_empty() {
348 let name_node = children.remove(0);
349 let names = scan_name_class(rng, name_node, true, ns);
350 let body_proto = scan_children(rng, ns, children, ctx)?;
351 Ok(
352 names
353 .into_iter()
354 .map(|n| Pattern::Attribute {
355 name: n,
356 body: body_proto.clone(),
357 })
358 .collect(),
359 )
360 } else {
361 Ok(Vec::new())
362 }
363}
364
365fn scan_children(
366 rng: &mut Relaxng,
367 ns: Option<&str>,
368 children: Vec<RoNode>,
369 ctx: &mut ScanContext<'_>,
370) -> Result<Vec<Pattern>, ScanError> {
371 let mut out = Vec::new();
372 for child in children {
373 out.extend(scan_pattern(rng, child, ns, ctx)?);
374 }
375 Ok(out)
376}
377
378fn scan_grammar_content(
379 rng: &mut Relaxng,
380 ns: Option<&str>,
381 content: Vec<RoNode>,
382 ctx: &mut ScanContext<'_>,
383) -> Result<Vec<Pattern>, ScanError> {
384 let mut out = Vec::new();
385 for node in content {
386 out.extend(scan_grammar_item(rng, node, ns, ctx)?);
387 }
388 Ok(out)
389}
390
391fn scan_grammar_item(
392 rng: &mut Relaxng,
393 node: RoNode,
394 inherit_ns: Option<&str>,
395 ctx: &mut ScanContext<'_>,
396) -> Result<Vec<Pattern>, ScanError> {
397 let Some(op) = get_relax_op(node) else {
398 return Ok(Vec::new());
399 };
400 let children = get_elements(node);
401 let ns = node
402 .get_attribute("ns")
403 .or_else(|| inherit_ns.map(String::from));
404 let ns_ref = ns.as_deref();
405
406 match op.as_str() {
407 "rng:start" => {
408 let body = scan_children(rng, ns_ref, children, ctx)?;
409 Ok(vec![Pattern::Start { body }])
410 },
411 "rng:define" => {
412 let name = node.get_attribute("name").unwrap_or_default();
413 let combiner = match node.get_attribute("combine").as_deref() {
414 Some("choice") => DefCombiner::Choice,
415 Some("interleave") => DefCombiner::Interleave,
416 _ => DefCombiner::Group,
417 };
418 let body = scan_children(rng, ns_ref, children, ctx)?;
419 Ok(vec![Pattern::Def { combiner, name, body }])
420 },
421 "rng:div" => scan_grammar_content(rng, ns_ref, children, ctx),
422 "rng:include" => {
423 let href = node.get_attribute("href").unwrap_or_default();
424 let paths: Vec<&Path> = ctx.search_paths.clone();
425 let (path, xml_doc) = resolve_schema_ref(&XmlParser::default(), &href, &paths)?;
429 let inner_root = xml_doc
430 .get_root_readonly()
431 .ok_or_else(|| ScanError::Parse("empty include".into()))?;
432 collect_namespaces(rng, inner_root);
433 let dir = path.parent().unwrap_or_else(|| Path::new("."));
436 let mut nested_paths: Vec<&Path> = Vec::with_capacity(ctx.search_paths.len() + 1);
437 nested_paths.push(dir);
438 nested_paths.extend(&ctx.search_paths);
439 let mut nested_ctx = ScanContext { search_paths: nested_paths };
440
441 let patterns = if get_relax_op(inner_root).as_deref() == Some("rng:grammar") {
444 let nns = inner_root
445 .get_attribute("ns")
446 .or_else(|| inherit_ns.map(String::from));
447 scan_grammar_content(
448 rng,
449 nns.as_deref(),
450 get_elements(inner_root),
451 &mut nested_ctx,
452 )?
453 } else {
454 scan_pattern(rng, inner_root, None, &mut nested_ctx)?
455 };
456
457 let modname = strip_rng_ext(&href);
458 let module = Pattern::Module { name: modname, body: patterns };
459 let replacements = scan_grammar_content(rng, ns_ref, children, ctx)?;
460 if replacements.is_empty() {
461 Ok(vec![module])
462 } else {
463 Ok(vec![Pattern::Override {
464 module: Box::new(module),
465 replacements,
466 }])
467 }
468 },
469 _ => Ok(Vec::new()),
470 }
471}
472
473fn scan_name_class(
477 rng: &mut Relaxng,
478 node: RoNode,
479 for_attr: bool,
480 ns: Option<&str>,
481) -> Vec<String> {
482 let Some(op) = get_relax_op(node) else {
483 return Vec::new();
484 };
485 match op.as_str() {
486 "rng:name" => {
487 let raw = node.get_content();
488 let (decns, local) = decode_qname(rng, &raw);
489 let effective_ns = decns.as_deref().or(ns);
490 let resolved_ns = if for_attr { None } else { effective_ns };
491 vec![encode_qname(rng, resolved_ns, &local)]
492 },
493 "rng:anyName" => {
494 let except: Vec<String> = get_elements(node)
495 .into_iter()
496 .flat_map(|c| scan_name_class(rng, c, for_attr, ns))
497 .collect();
498 let mut all = vec!["*".to_string(), "*:*".to_string()];
499 all.extend(except);
500 filter_names(all)
501 },
502 "rng:nsName" => {
503 let xns = node.get_attribute("ns").or_else(|| ns.map(String::from));
504 let star = encode_qname(rng, xns.as_deref(), "*");
505 let except: Vec<String> = get_elements(node)
506 .into_iter()
507 .flat_map(|c| scan_name_class(rng, c, for_attr, ns))
508 .collect();
509 let mut all = vec![star];
510 all.extend(except);
511 filter_names(all)
512 },
513 "rng:choice" => {
514 let mut names = std::collections::BTreeSet::new();
515 let mut child = node.get_first_child();
516 while let Some(c) = child {
517 for n in scan_name_class(rng, c, for_attr, ns) {
518 names.insert(n);
519 }
520 child = c.get_next_sibling();
521 }
522 names.into_iter().collect()
523 },
524 "rng:except" => {
525 let mut names = std::collections::BTreeSet::new();
526 for c in get_elements(node) {
527 for n in scan_name_class(rng, c, for_attr, ns) {
528 names.insert(n);
529 }
530 }
531 names.into_iter().map(|n| format!("!{}", n)).collect()
532 },
533 _ => Vec::new(),
534 }
535}
536
537fn filter_names(names: Vec<String>) -> Vec<String> {
540 use std::collections::BTreeMap;
541 let mut include: BTreeMap<String, String> = BTreeMap::new();
542 let mut exclude: BTreeMap<String, String> = BTreeMap::new();
543 for n in names {
544 if let Some(rest) = n.strip_prefix('!') {
545 exclude.insert(n.clone(), rest.to_string());
546 } else {
547 include.insert(n.clone(), n);
548 }
549 }
550 let drop_keys: Vec<String> = exclude
551 .iter()
552 .filter(|(_, target)| include.contains_key(target.as_str()))
553 .map(|(k, _)| k.clone())
554 .collect();
555 for k in drop_keys {
556 if let Some(target) = exclude.remove(&k) {
557 include.remove(&target);
558 }
559 }
560 include
561 .keys()
562 .cloned()
563 .chain(exclude.keys().cloned())
564 .collect()
565}
566
567fn decode_qname(rng: &Relaxng, raw: &str) -> (Option<String>, String) {
570 match raw.split_once(':') {
571 Some((prefix, local)) => match rng.document_namespaces.get(prefix) {
572 Some(uri) => (Some(uri.clone()), local.to_string()),
573 None => (None, raw.to_string()),
574 },
575 None => (None, raw.to_string()),
576 }
577}
578
579fn collect_namespaces(rng: &mut Relaxng, root: RoNode) {
582 for ns in root.get_namespace_declarations() {
583 let prefix = ns.get_prefix();
584 let href = ns.get_href();
585 if href.starts_with("http://relaxng.org") {
586 continue;
587 }
588 if !prefix.is_empty() {
591 rng
592 .document_namespaces
593 .retain(|p, u| p == &prefix || u != &href);
594 }
595 rng.document_namespaces.insert(prefix, href);
596 }
597}
598
599fn strip_rng_ext(name: &str) -> String {
600 name
601 .strip_suffix(".rng")
602 .or_else(|| name.strip_suffix(".rnc"))
603 .unwrap_or(name)
604 .to_string()
605}
606
607fn resolve_schema_ref(
625 parser: &XmlParser,
626 name: &str,
627 search_paths: &[&Path],
628) -> Result<(PathBuf, XmlDocument), ScanError> {
629 if let Some(p) = find_file(name, search_paths) {
630 let doc = parser
631 .parse_file(p.to_str().unwrap_or(""))
632 .map_err(|e| ScanError::Parse(format!("{:?}", e)))?;
633 return Ok((p, doc));
634 }
635 let mut embed_key = match name.strip_prefix("urn:x-LaTeXML:RelaxNG:") {
639 Some(rest) => rest.replace(':', "/"),
640 None => name.to_string(),
641 };
642 if !embed_key.ends_with(".rng") {
643 embed_key.push_str(".rng");
644 }
645 let bytes =
646 super::embedded::lookup(&embed_key).ok_or_else(|| ScanError::FileNotFound(name.to_string()))?;
647 let doc = parser
648 .parse_string(bytes)
649 .map_err(|e| ScanError::Parse(format!("{:?}", e)))?;
650 Ok((PathBuf::from(&embed_key), doc))
651}
652
653fn find_file(name: &str, search_paths: &[&Path]) -> Option<PathBuf> {
658 let bare = match name.strip_prefix("urn:x-LaTeXML:RelaxNG:") {
659 Some(rest) => rest.replace(':', "/"),
660 None => name.to_string(),
661 };
662 let mut candidates: Vec<String> = vec![bare.clone()];
668 if !bare.ends_with(".rng") {
669 candidates.push(format!("{bare}.rng"));
670 }
671 for candidate in &candidates {
672 let asis = Path::new(candidate);
673 if asis.is_file() {
674 return Some(asis.to_path_buf());
675 }
676 for dir in search_paths {
678 let joined = dir.join(candidate);
679 if joined.is_file() {
680 return Some(joined);
681 }
682 }
683 }
684 None
685}
686
687#[cfg(test)]
690mod tests {
691 use super::*;
692 use crate::common::relaxng::Pattern;
693
694 fn matches_combination(pat: &Pattern, op: CombineOp) -> bool {
695 matches!(pat, Pattern::Combination { op: o, .. } if *o == op)
696 }
697
698 #[test]
699 fn scan_empty_grammar() {
700 let xml = r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0"></grammar>"#;
701 let mut rng = Relaxng::default();
702 let patterns = scan_string(&mut rng, xml).expect("scan");
703 assert_eq!(patterns.len(), 1);
704 match &patterns[0] {
705 Pattern::Grammar { name, body } => {
706 assert_eq!(name, "grammar1");
707 assert!(body.is_empty());
708 },
709 other => panic!("expected Grammar, got {:?}", other),
710 }
711 }
712
713 #[test]
714 fn scan_simple_element() {
715 let xml = r#"
716 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
717 <start><element name="root"><empty/></element></start>
718 </grammar>
719 "#;
720 let mut rng = Relaxng::default();
721 let patterns = scan_string(&mut rng, xml).expect("scan");
722 let body = match &patterns[0] {
723 Pattern::Grammar { body, .. } => body,
724 other => panic!("expected Grammar, got {:?}", other),
725 };
726 let start_body = match &body[0] {
727 Pattern::Start { body } => body,
728 other => panic!("expected Start, got {:?}", other),
729 };
730 match &start_body[0] {
731 Pattern::Element { name, body: _ } => assert_eq!(name, "root"),
732 other => panic!("expected Element, got {:?}", other),
733 }
734 }
735
736 #[test]
737 fn scan_choice_combinator() {
738 let xml = r#"
739 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
740 <start>
741 <choice>
742 <element name="a"><empty/></element>
743 <element name="b"><empty/></element>
744 </choice>
745 </start>
746 </grammar>
747 "#;
748 let mut rng = Relaxng::default();
749 let patterns = scan_string(&mut rng, xml).expect("scan");
750 let body = match &patterns[0] {
751 Pattern::Grammar { body, .. } => body,
752 _ => unreachable!(),
753 };
754 let start_body = match &body[0] {
755 Pattern::Start { body } => body,
756 _ => unreachable!(),
757 };
758 assert!(matches_combination(&start_body[0], CombineOp::Choice));
759 }
760
761 #[test]
768 fn rng_include_resolves_urn_via_embedded() {
769 let xml = r#"
771 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
772 <include href="urn:x-LaTeXML:RelaxNG:LaTeXML-common.rng"/>
773 </grammar>
774 "#;
775 let mut rng = Relaxng::default();
776 scan_string(&mut rng, xml)
777 .expect("urn include (with .rng) should resolve from the embedded table");
778
779 let xml_noext = r#"
782 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
783 <include href="urn:x-LaTeXML:RelaxNG:LaTeXML-common"/>
784 </grammar>
785 "#;
786 let mut rng2 = Relaxng::default();
787 scan_string(&mut rng2, xml_noext)
788 .expect("no-extension urn include should resolve (.rng appended)");
789 }
790
791 #[test]
792 fn scan_define_with_combine_choice() {
793 let xml = r#"
794 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
795 <define name="X"><element name="x1"><empty/></element></define>
796 <define name="X" combine="choice"><element name="x2"><empty/></element></define>
797 </grammar>
798 "#;
799 let mut rng = Relaxng::default();
800 let patterns = scan_string(&mut rng, xml).expect("scan");
801 let body = match &patterns[0] {
802 Pattern::Grammar { body, .. } => body,
803 _ => unreachable!(),
804 };
805 assert_eq!(body.len(), 2);
806 match &body[0] {
807 Pattern::Def {
808 combiner: DefCombiner::Group,
809 name,
810 ..
811 } => assert_eq!(name, "X"),
812 other => panic!("expected Def(Group), got {:?}", other),
813 }
814 match &body[1] {
815 Pattern::Def {
816 combiner: DefCombiner::Choice,
817 name,
818 ..
819 } => assert_eq!(name, "X"),
820 other => panic!("expected Def(Choice), got {:?}", other),
821 }
822 }
823
824 #[test]
825 fn scan_attribute_with_value() {
826 let xml = r#"
827 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
828 <start>
829 <element name="root">
830 <attribute name="kind"><value>sample</value></attribute>
831 </element>
832 </start>
833 </grammar>
834 "#;
835 let mut rng = Relaxng::default();
836 let patterns = scan_string(&mut rng, xml).expect("scan");
837 let attr_body = match &patterns[0] {
839 Pattern::Grammar { body, .. } => match &body[0] {
840 Pattern::Start { body: sb } => match &sb[0] {
841 Pattern::Element { body: eb, .. } => match &eb[0] {
842 Pattern::Attribute { body: ab, .. } => ab.clone(),
843 _ => unreachable!(),
844 },
845 _ => unreachable!(),
846 },
847 _ => unreachable!(),
848 },
849 _ => unreachable!(),
850 };
851 match &attr_body[0] {
852 Pattern::Value(v) => assert_eq!(v, "sample"),
853 other => panic!("expected Value, got {:?}", other),
854 }
855 }
856
857 #[test]
858 fn scan_documentation_annotation() {
859 let xml = r#"
860 <grammar
861 xmlns="http://relaxng.org/ns/structure/1.0"
862 xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0">
863 <define name="X">
864 <a:documentation>An example pattern</a:documentation>
865 <element name="x"><empty/></element>
866 </define>
867 </grammar>
868 "#;
869 let mut rng = Relaxng::default();
870 let patterns = scan_string(&mut rng, xml).expect("scan");
871 let body = match &patterns[0] {
872 Pattern::Grammar { body, .. } => body,
873 _ => unreachable!(),
874 };
875 let def_body = match &body[0] {
876 Pattern::Def { body, .. } => body,
877 _ => unreachable!(),
878 };
879 match &def_body[0] {
880 Pattern::Doc(s) => assert_eq!(s, "An example pattern"),
881 other => panic!("expected Doc, got {:?}", other),
882 }
883 }
884
885 #[test]
886 fn scan_mixed_normalises_to_interleave_with_text() {
887 let xml = r#"
888 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
889 <start>
890 <mixed><element name="b"><empty/></element></mixed>
891 </start>
892 </grammar>
893 "#;
894 let mut rng = Relaxng::default();
895 let patterns = scan_string(&mut rng, xml).expect("scan");
896 let inner = match &patterns[0] {
897 Pattern::Grammar { body, .. } => match &body[0] {
898 Pattern::Start { body } => match &body[0] {
899 Pattern::Combination { op, body } => (op, body.clone()),
900 _ => unreachable!(),
901 },
902 _ => unreachable!(),
903 },
904 _ => unreachable!(),
905 };
906 assert_eq!(*inner.0, CombineOp::Interleave);
907 assert!(matches!(inner.1[0], Pattern::Text));
908 }
909
910 #[test]
911 fn filter_names_drops_canceling_exclusion() {
912 let names = vec!["x".into(), "y".into(), "!y".into()];
913 let result = filter_names(names);
914 assert_eq!(result, vec!["x".to_string()]);
915 }
916
917 #[test]
918 fn filter_names_preserves_uncanceled_exclusion() {
919 let names = vec!["x".into(), "!z".into()];
920 let result = filter_names(names);
921 assert_eq!(result, vec!["x".to_string(), "!z".to_string()]);
922 }
923}