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
202 .document_namespaces
203 .iter()
204 .find(|(p, u)| !p.is_empty() && u.as_str() == uri)
205 {
206 return prefix.clone();
207 }
208 let n = rng
209 .document_namespaces
210 .keys()
211 .filter(|p| p.starts_with("namespace"))
212 .count()
213 + 1;
214 let new_prefix = format!("namespace{}", n);
215 rng
216 .document_namespaces
217 .insert(new_prefix.clone(), uri.to_string());
218 new_prefix
219}
220
221fn scan_pattern(
225 rng: &mut Relaxng,
226 node: RoNode,
227 inherit_ns: Option<&str>,
228 ctx: &mut ScanContext<'_>,
229) -> Result<Vec<Pattern>, ScanError> {
230 let Some(op) = get_relax_op(node) else {
231 return Ok(Vec::new());
232 };
233 let ns = node
234 .get_attribute("ns")
235 .or_else(|| inherit_ns.map(String::from));
236 let ns_ref = ns.as_deref();
237
238 match op.as_str() {
239 "rng:element" => scan_pattern_element(rng, ns_ref, node, ctx),
240 "rng:attribute" => scan_pattern_attribute(rng, ns_ref, node, ctx),
241 "rng:mixed" => {
242 let mut body = vec![Pattern::Text];
243 body.extend(scan_children(rng, ns_ref, get_elements(node), ctx)?);
244 Ok(vec![Pattern::Combination {
245 op: CombineOp::Interleave,
246 body,
247 }])
248 },
249 "rng:ref" => Ok(vec![Pattern::Ref {
250 qname: node.get_attribute("name").unwrap_or_default(),
251 }]),
252 "rng:parentRef" => Ok(vec![Pattern::ParentRef {
253 qname: node.get_attribute("name").unwrap_or_default(),
254 }]),
255 "rng:empty" | "rng:notAllowed" => Ok(Vec::new()),
256 "rng:text" => Ok(vec![Pattern::Text]),
257 "rng:value" => Ok(vec![Pattern::Value(node.get_content())]),
258 "rng:data" => Ok(vec![Pattern::Data(
259 node.get_attribute("type").unwrap_or_default(),
260 )]),
261 "rng:externalRef" => {
262 let href = node.get_attribute("href").unwrap_or_default();
263 let paths: Vec<&Path> = ctx.search_paths.clone();
264 scan_external(rng, &href, ns_ref, &paths)
265 },
266 "rng:grammar" => {
267 rng.internal_grammars += 1;
268 let name = format!("grammar{}", rng.internal_grammars);
269 let body = scan_grammar_content(rng, ns_ref, get_elements(node), ctx)?;
270 Ok(vec![Pattern::Grammar { name, body }])
271 },
272 "rnga:documentation" => {
273 let text = node.get_content();
274 Ok(vec![Pattern::Doc(text)])
275 },
276 other => {
277 if let Some(stripped) = other.strip_prefix("rng:")
280 && let Some(cop) = combine_op_from_localname(stripped)
281 {
282 let body = scan_children(rng, ns_ref, get_elements(node), ctx)?;
283 return Ok(vec![Pattern::Combination { op: cop, body }]);
284 }
285 Ok(Vec::new())
287 },
288 }
289}
290
291fn scan_pattern_element(
292 rng: &mut Relaxng,
293 ns: Option<&str>,
294 node: RoNode,
295 ctx: &mut ScanContext<'_>,
296) -> Result<Vec<Pattern>, ScanError> {
297 let mut children = get_elements(node);
298 if let Some(name) = node.get_attribute("name") {
299 let body = scan_children(rng, ns, children, ctx)?;
300 Ok(vec![Pattern::Element {
301 name: encode_qname(rng, ns, &name),
302 body,
303 }])
304 } else if !children.is_empty() {
305 let name_node = children.remove(0);
306 let names = scan_name_class(rng, name_node, false, ns);
307 let body_proto = scan_children(rng, ns, children, ctx)?;
308 Ok(
309 names
310 .into_iter()
311 .map(|n| Pattern::Element {
312 name: n,
313 body: body_proto.clone(),
314 })
315 .collect(),
316 )
317 } else {
318 Ok(Vec::new())
319 }
320}
321
322fn scan_pattern_attribute(
323 rng: &mut Relaxng,
324 ns: Option<&str>,
325 node: RoNode,
326 ctx: &mut ScanContext<'_>,
327) -> Result<Vec<Pattern>, ScanError> {
328 let xns = node.get_attribute("ns"); let xns_ref = xns.as_deref();
330 let mut children = get_elements(node);
331 if let Some(name) = node.get_attribute("name") {
332 let body = scan_children(rng, ns, children, ctx)?;
333 Ok(vec![Pattern::Attribute {
334 name: encode_qname(rng, xns_ref, &name),
335 body,
336 }])
337 } else if !children.is_empty() {
338 let name_node = children.remove(0);
339 let names = scan_name_class(rng, name_node, true, ns);
340 let body_proto = scan_children(rng, ns, children, ctx)?;
341 Ok(
342 names
343 .into_iter()
344 .map(|n| Pattern::Attribute {
345 name: n,
346 body: body_proto.clone(),
347 })
348 .collect(),
349 )
350 } else {
351 Ok(Vec::new())
352 }
353}
354
355fn scan_children(
356 rng: &mut Relaxng,
357 ns: Option<&str>,
358 children: Vec<RoNode>,
359 ctx: &mut ScanContext<'_>,
360) -> Result<Vec<Pattern>, ScanError> {
361 let mut out = Vec::new();
362 for child in children {
363 out.extend(scan_pattern(rng, child, ns, ctx)?);
364 }
365 Ok(out)
366}
367
368fn scan_grammar_content(
369 rng: &mut Relaxng,
370 ns: Option<&str>,
371 content: Vec<RoNode>,
372 ctx: &mut ScanContext<'_>,
373) -> Result<Vec<Pattern>, ScanError> {
374 let mut out = Vec::new();
375 for node in content {
376 out.extend(scan_grammar_item(rng, node, ns, ctx)?);
377 }
378 Ok(out)
379}
380
381fn scan_grammar_item(
382 rng: &mut Relaxng,
383 node: RoNode,
384 inherit_ns: Option<&str>,
385 ctx: &mut ScanContext<'_>,
386) -> Result<Vec<Pattern>, ScanError> {
387 let Some(op) = get_relax_op(node) else {
388 return Ok(Vec::new());
389 };
390 let children = get_elements(node);
391 let ns = node
392 .get_attribute("ns")
393 .or_else(|| inherit_ns.map(String::from));
394 let ns_ref = ns.as_deref();
395
396 match op.as_str() {
397 "rng:start" => {
398 let body = scan_children(rng, ns_ref, children, ctx)?;
399 Ok(vec![Pattern::Start { body }])
400 },
401 "rng:define" => {
402 let name = node.get_attribute("name").unwrap_or_default();
403 let combiner = match node.get_attribute("combine").as_deref() {
404 Some("choice") => DefCombiner::Choice,
405 Some("interleave") => DefCombiner::Interleave,
406 _ => DefCombiner::Group,
407 };
408 let body = scan_children(rng, ns_ref, children, ctx)?;
409 Ok(vec![Pattern::Def { combiner, name, body }])
410 },
411 "rng:div" => scan_grammar_content(rng, ns_ref, children, ctx),
412 "rng:include" => {
413 let href = node.get_attribute("href").unwrap_or_default();
414 let paths: Vec<&Path> = ctx.search_paths.clone();
415 let (path, xml_doc) = resolve_schema_ref(&XmlParser::default(), &href, &paths)?;
419 let inner_root = xml_doc
420 .get_root_readonly()
421 .ok_or_else(|| ScanError::Parse("empty include".into()))?;
422 collect_namespaces(rng, inner_root);
423 let dir = path.parent().unwrap_or_else(|| Path::new("."));
426 let mut nested_paths: Vec<&Path> = Vec::with_capacity(ctx.search_paths.len() + 1);
427 nested_paths.push(dir);
428 nested_paths.extend(&ctx.search_paths);
429 let mut nested_ctx = ScanContext { search_paths: nested_paths };
430
431 let patterns = if get_relax_op(inner_root).as_deref() == Some("rng:grammar") {
434 let nns = inner_root
435 .get_attribute("ns")
436 .or_else(|| inherit_ns.map(String::from));
437 scan_grammar_content(
438 rng,
439 nns.as_deref(),
440 get_elements(inner_root),
441 &mut nested_ctx,
442 )?
443 } else {
444 scan_pattern(rng, inner_root, None, &mut nested_ctx)?
445 };
446
447 let modname = strip_rng_ext(&href);
448 let module = Pattern::Module { name: modname, body: patterns };
449 let replacements = scan_grammar_content(rng, ns_ref, children, ctx)?;
450 if replacements.is_empty() {
451 Ok(vec![module])
452 } else {
453 Ok(vec![Pattern::Override {
454 module: Box::new(module),
455 replacements,
456 }])
457 }
458 },
459 _ => Ok(Vec::new()),
460 }
461}
462
463fn scan_name_class(
467 rng: &mut Relaxng,
468 node: RoNode,
469 for_attr: bool,
470 ns: Option<&str>,
471) -> Vec<String> {
472 let Some(op) = get_relax_op(node) else {
473 return Vec::new();
474 };
475 match op.as_str() {
476 "rng:name" => {
477 let raw = node.get_content();
478 let (decns, local) = decode_qname(rng, &raw);
479 let effective_ns = decns.as_deref().or(ns);
480 let resolved_ns = if for_attr { None } else { effective_ns };
481 vec![encode_qname(rng, resolved_ns, &local)]
482 },
483 "rng:anyName" => {
484 let except: Vec<String> = get_elements(node)
485 .into_iter()
486 .flat_map(|c| scan_name_class(rng, c, for_attr, ns))
487 .collect();
488 let mut all = vec!["*".to_string(), "*:*".to_string()];
489 all.extend(except);
490 filter_names(all)
491 },
492 "rng:nsName" => {
493 let xns = node.get_attribute("ns").or_else(|| ns.map(String::from));
494 let star = encode_qname(rng, xns.as_deref(), "*");
495 let except: Vec<String> = get_elements(node)
496 .into_iter()
497 .flat_map(|c| scan_name_class(rng, c, for_attr, ns))
498 .collect();
499 let mut all = vec![star];
500 all.extend(except);
501 filter_names(all)
502 },
503 "rng:choice" => {
504 let mut names = std::collections::BTreeSet::new();
505 let mut child = node.get_first_child();
506 while let Some(c) = child {
507 for n in scan_name_class(rng, c, for_attr, ns) {
508 names.insert(n);
509 }
510 child = c.get_next_sibling();
511 }
512 names.into_iter().collect()
513 },
514 "rng:except" => {
515 let mut names = std::collections::BTreeSet::new();
516 for c in get_elements(node) {
517 for n in scan_name_class(rng, c, for_attr, ns) {
518 names.insert(n);
519 }
520 }
521 names.into_iter().map(|n| format!("!{}", n)).collect()
522 },
523 _ => Vec::new(),
524 }
525}
526
527fn filter_names(names: Vec<String>) -> Vec<String> {
530 use std::collections::BTreeMap;
531 let mut include: BTreeMap<String, String> = BTreeMap::new();
532 let mut exclude: BTreeMap<String, String> = BTreeMap::new();
533 for n in names {
534 if let Some(rest) = n.strip_prefix('!') {
535 exclude.insert(n.clone(), rest.to_string());
536 } else {
537 include.insert(n.clone(), n);
538 }
539 }
540 let drop_keys: Vec<String> = exclude
541 .iter()
542 .filter(|(_, target)| include.contains_key(target.as_str()))
543 .map(|(k, _)| k.clone())
544 .collect();
545 for k in drop_keys {
546 if let Some(target) = exclude.remove(&k) {
547 include.remove(&target);
548 }
549 }
550 include
551 .keys()
552 .cloned()
553 .chain(exclude.keys().cloned())
554 .collect()
555}
556
557fn decode_qname(rng: &Relaxng, raw: &str) -> (Option<String>, String) {
560 match raw.split_once(':') {
561 Some((prefix, local)) => match rng.document_namespaces.get(prefix) {
562 Some(uri) => (Some(uri.clone()), local.to_string()),
563 None => (None, raw.to_string()),
564 },
565 None => (None, raw.to_string()),
566 }
567}
568
569fn collect_namespaces(rng: &mut Relaxng, root: RoNode) {
572 for ns in root.get_namespace_declarations() {
573 let prefix = ns.get_prefix();
574 let href = ns.get_href();
575 if href.starts_with("http://relaxng.org") {
576 continue;
577 }
578 if !prefix.is_empty() {
581 rng
582 .document_namespaces
583 .retain(|p, u| p == &prefix || u != &href);
584 }
585 rng.document_namespaces.insert(prefix, href);
586 }
587}
588
589fn strip_rng_ext(name: &str) -> String {
590 name
591 .strip_suffix(".rng")
592 .or_else(|| name.strip_suffix(".rnc"))
593 .unwrap_or(name)
594 .to_string()
595}
596
597fn resolve_schema_ref(
615 parser: &XmlParser,
616 name: &str,
617 search_paths: &[&Path],
618) -> Result<(PathBuf, XmlDocument), ScanError> {
619 if let Some(p) = find_file(name, search_paths) {
620 let doc = parser
621 .parse_file(p.to_str().unwrap_or(""))
622 .map_err(|e| ScanError::Parse(format!("{:?}", e)))?;
623 return Ok((p, doc));
624 }
625 let mut embed_key = match name.strip_prefix("urn:x-LaTeXML:RelaxNG:") {
629 Some(rest) => rest.replace(':', "/"),
630 None => name.to_string(),
631 };
632 if !embed_key.ends_with(".rng") {
633 embed_key.push_str(".rng");
634 }
635 let bytes =
636 super::embedded::lookup(&embed_key).ok_or_else(|| ScanError::FileNotFound(name.to_string()))?;
637 let doc = parser
638 .parse_string(bytes)
639 .map_err(|e| ScanError::Parse(format!("{:?}", e)))?;
640 Ok((PathBuf::from(&embed_key), doc))
641}
642
643fn find_file(name: &str, search_paths: &[&Path]) -> Option<PathBuf> {
648 let bare = match name.strip_prefix("urn:x-LaTeXML:RelaxNG:") {
649 Some(rest) => rest.replace(':', "/"),
650 None => name.to_string(),
651 };
652 let asis = Path::new(&bare);
653 if asis.is_file() {
654 return Some(asis.to_path_buf());
655 }
656 for dir in search_paths {
657 let candidate = dir.join(&bare);
658 if candidate.is_file() {
659 return Some(candidate);
660 }
661 }
662 None
663}
664
665#[cfg(test)]
668mod tests {
669 use super::*;
670 use crate::common::relaxng::Pattern;
671
672 fn matches_combination(pat: &Pattern, op: CombineOp) -> bool {
673 matches!(pat, Pattern::Combination { op: o, .. } if *o == op)
674 }
675
676 #[test]
677 fn scan_empty_grammar() {
678 let xml = r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0"></grammar>"#;
679 let mut rng = Relaxng::default();
680 let patterns = scan_string(&mut rng, xml).expect("scan");
681 assert_eq!(patterns.len(), 1);
682 match &patterns[0] {
683 Pattern::Grammar { name, body } => {
684 assert_eq!(name, "grammar1");
685 assert!(body.is_empty());
686 },
687 other => panic!("expected Grammar, got {:?}", other),
688 }
689 }
690
691 #[test]
692 fn scan_simple_element() {
693 let xml = r#"
694 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
695 <start><element name="root"><empty/></element></start>
696 </grammar>
697 "#;
698 let mut rng = Relaxng::default();
699 let patterns = scan_string(&mut rng, xml).expect("scan");
700 let body = match &patterns[0] {
701 Pattern::Grammar { body, .. } => body,
702 other => panic!("expected Grammar, got {:?}", other),
703 };
704 let start_body = match &body[0] {
705 Pattern::Start { body } => body,
706 other => panic!("expected Start, got {:?}", other),
707 };
708 match &start_body[0] {
709 Pattern::Element { name, body: _ } => assert_eq!(name, "root"),
710 other => panic!("expected Element, got {:?}", other),
711 }
712 }
713
714 #[test]
715 fn scan_choice_combinator() {
716 let xml = r#"
717 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
718 <start>
719 <choice>
720 <element name="a"><empty/></element>
721 <element name="b"><empty/></element>
722 </choice>
723 </start>
724 </grammar>
725 "#;
726 let mut rng = Relaxng::default();
727 let patterns = scan_string(&mut rng, xml).expect("scan");
728 let body = match &patterns[0] {
729 Pattern::Grammar { body, .. } => body,
730 _ => unreachable!(),
731 };
732 let start_body = match &body[0] {
733 Pattern::Start { body } => body,
734 _ => unreachable!(),
735 };
736 assert!(matches_combination(&start_body[0], CombineOp::Choice));
737 }
738
739 #[test]
746 fn rng_include_resolves_urn_via_embedded() {
747 let xml = r#"
749 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
750 <include href="urn:x-LaTeXML:RelaxNG:LaTeXML-common.rng"/>
751 </grammar>
752 "#;
753 let mut rng = Relaxng::default();
754 scan_string(&mut rng, xml)
755 .expect("urn include (with .rng) should resolve from the embedded table");
756
757 let xml_noext = r#"
760 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
761 <include href="urn:x-LaTeXML:RelaxNG:LaTeXML-common"/>
762 </grammar>
763 "#;
764 let mut rng2 = Relaxng::default();
765 scan_string(&mut rng2, xml_noext)
766 .expect("no-extension urn include should resolve (.rng appended)");
767 }
768
769 #[test]
770 fn scan_define_with_combine_choice() {
771 let xml = r#"
772 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
773 <define name="X"><element name="x1"><empty/></element></define>
774 <define name="X" combine="choice"><element name="x2"><empty/></element></define>
775 </grammar>
776 "#;
777 let mut rng = Relaxng::default();
778 let patterns = scan_string(&mut rng, xml).expect("scan");
779 let body = match &patterns[0] {
780 Pattern::Grammar { body, .. } => body,
781 _ => unreachable!(),
782 };
783 assert_eq!(body.len(), 2);
784 match &body[0] {
785 Pattern::Def {
786 combiner: DefCombiner::Group,
787 name,
788 ..
789 } => assert_eq!(name, "X"),
790 other => panic!("expected Def(Group), got {:?}", other),
791 }
792 match &body[1] {
793 Pattern::Def {
794 combiner: DefCombiner::Choice,
795 name,
796 ..
797 } => assert_eq!(name, "X"),
798 other => panic!("expected Def(Choice), got {:?}", other),
799 }
800 }
801
802 #[test]
803 fn scan_attribute_with_value() {
804 let xml = r#"
805 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
806 <start>
807 <element name="root">
808 <attribute name="kind"><value>sample</value></attribute>
809 </element>
810 </start>
811 </grammar>
812 "#;
813 let mut rng = Relaxng::default();
814 let patterns = scan_string(&mut rng, xml).expect("scan");
815 let attr_body = match &patterns[0] {
817 Pattern::Grammar { body, .. } => match &body[0] {
818 Pattern::Start { body: sb } => match &sb[0] {
819 Pattern::Element { body: eb, .. } => match &eb[0] {
820 Pattern::Attribute { body: ab, .. } => ab.clone(),
821 _ => unreachable!(),
822 },
823 _ => unreachable!(),
824 },
825 _ => unreachable!(),
826 },
827 _ => unreachable!(),
828 };
829 match &attr_body[0] {
830 Pattern::Value(v) => assert_eq!(v, "sample"),
831 other => panic!("expected Value, got {:?}", other),
832 }
833 }
834
835 #[test]
836 fn scan_documentation_annotation() {
837 let xml = r#"
838 <grammar
839 xmlns="http://relaxng.org/ns/structure/1.0"
840 xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0">
841 <define name="X">
842 <a:documentation>An example pattern</a:documentation>
843 <element name="x"><empty/></element>
844 </define>
845 </grammar>
846 "#;
847 let mut rng = Relaxng::default();
848 let patterns = scan_string(&mut rng, xml).expect("scan");
849 let body = match &patterns[0] {
850 Pattern::Grammar { body, .. } => body,
851 _ => unreachable!(),
852 };
853 let def_body = match &body[0] {
854 Pattern::Def { body, .. } => body,
855 _ => unreachable!(),
856 };
857 match &def_body[0] {
858 Pattern::Doc(s) => assert_eq!(s, "An example pattern"),
859 other => panic!("expected Doc, got {:?}", other),
860 }
861 }
862
863 #[test]
864 fn scan_mixed_normalises_to_interleave_with_text() {
865 let xml = r#"
866 <grammar xmlns="http://relaxng.org/ns/structure/1.0">
867 <start>
868 <mixed><element name="b"><empty/></element></mixed>
869 </start>
870 </grammar>
871 "#;
872 let mut rng = Relaxng::default();
873 let patterns = scan_string(&mut rng, xml).expect("scan");
874 let inner = match &patterns[0] {
875 Pattern::Grammar { body, .. } => match &body[0] {
876 Pattern::Start { body } => match &body[0] {
877 Pattern::Combination { op, body } => (op, body.clone()),
878 _ => unreachable!(),
879 },
880 _ => unreachable!(),
881 },
882 _ => unreachable!(),
883 };
884 assert_eq!(*inner.0, CombineOp::Interleave);
885 assert!(matches!(inner.1[0], Pattern::Text));
886 }
887
888 #[test]
889 fn filter_names_drops_canceling_exclusion() {
890 let names = vec!["x".into(), "y".into(), "!y".into()];
891 let result = filter_names(names);
892 assert_eq!(result, vec!["x".to_string()]);
893 }
894
895 #[test]
896 fn filter_names_preserves_uncanceled_exclusion() {
897 let names = vec!["x".into(), "!z".into()];
898 let result = filter_names(names);
899 assert_eq!(result, vec!["x".to_string(), "!z".to_string()]);
900 }
901}