Skip to main content

sup_xml_core/
relaxng.rs

1#![forbid(unsafe_code)]
2
3//! RelaxNG validation against the arena DOM.
4//!
5//! Arena-backed counterpart to [`crate::relaxng`].  Same semantics — Brzozowski
6//! derivative algorithm, same supported pattern subset, same datatype handling —
7//! but operates on [`sup_xml_tree::dom::Document`] / [`Node`].
8//!
9//! Public surface:
10//!
11//! - [`parse_schema`] — compiles an XML-syntax RelaxNG schema into an
12//!   [`RngSchema`].  Same return type as the legacy [`crate::relaxng::parse_schema`],
13//!   so callers can mix-and-match the compiler with either validator.
14//! - [`validate`] — validates an arena [`Document`] against a compiled
15//!   [`RngSchema`].
16//!
17//! `RngSchema`, [`Pattern`], [`NameClass`], and [`RELAXNG_NS`] live in this
18//! module — the pattern AST is DOM-agnostic.
19
20use std::collections::HashMap;
21use std::sync::Arc;
22
23use sup_xml_tree::dom::{Document, Node, NodeKind};
24
25use crate::parser::parse_str;
26use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
27use crate::options::ParseOptions;
28
29/// RelaxNG namespace URI.  Constant per spec § 1.
30pub const RELAXNG_NS: &str = "http://relaxng.org/ns/structure/1.0";
31
32// ── pattern AST ──────────────────────────────────────────────────────────────
33
34/// A RelaxNG pattern.  See module docs for what's supported.
35///
36/// Patterns are wrapped in `Arc` so derivative-style transformations
37/// can share subtrees cheaply across alternatives.
38#[derive(Debug, Clone)]
39pub enum Pattern {
40    /// `<empty/>` — matches the empty content sequence.
41    Empty,
42    /// `<notAllowed/>` — matches nothing.
43    NotAllowed,
44    /// `<text/>` — matches any sequence of text.
45    Text,
46    /// `<value type=...>X</value>` — text content must equal `X`
47    /// after whitespace normalisation per the datatype.
48    Value {
49        datatype: String,
50        text: String,
51    },
52    /// `<data type="..."><param name=...>...</param>...</data>` —
53    /// text content must validate against the named datatype.
54    Data {
55        datatype: String,
56        params: Vec<(String, String)>,
57    },
58    /// `<list>` — whitespace-tokenize the text, match each token
59    /// against the inner pattern.
60    List(Arc<Pattern>),
61    /// `<element name="X">child</element>`.
62    Element {
63        name: NameClass,
64        child: Arc<Pattern>,
65    },
66    /// `<attribute name="X">child</attribute>`.
67    Attribute {
68        name: NameClass,
69        child: Arc<Pattern>,
70    },
71    /// `<group>` — sequential composition.
72    Group(Arc<Pattern>, Arc<Pattern>),
73    /// `<interleave>` — both patterns must match, in any order.
74    Interleave(Arc<Pattern>, Arc<Pattern>),
75    /// `<choice>` — match either alternative.
76    Choice(Arc<Pattern>, Arc<Pattern>),
77    /// `<oneOrMore>` — match `inner` one or more times.
78    OneOrMore(Arc<Pattern>),
79    /// `<ref name="X"/>`.
80    Ref(String),
81    /// Internal: a pattern that matches a fixed remaining set of
82    /// children, produced by derivatives.  Equivalent to a
83    /// successfully-matched-so-far state.
84    After(Arc<Pattern>, Arc<Pattern>),
85}
86
87/// A RelaxNG name class — a predicate over `(namespace_uri,
88/// local_name)` pairs.
89#[derive(Debug, Clone)]
90pub enum NameClass {
91    /// `<name ns="X">local</name>` — exactly this name.
92    Name {
93        namespace: String,
94        local: String,
95    },
96    /// `<anyName/>` (optionally with `<except>...</except>`).
97    AnyName(Option<Box<NameClass>>),
98    /// `<nsName ns="X"/>` — any local name in the given namespace.
99    NsName {
100        namespace: String,
101        except: Option<Box<NameClass>>,
102    },
103    /// `<choice>` of name classes.
104    Choice(Box<NameClass>, Box<NameClass>),
105    /// Internal: matches nothing.
106    Nothing,
107}
108
109impl NameClass {
110    #[allow(dead_code)]
111    fn describe(&self) -> String {
112        match self {
113            NameClass::Name { namespace, local } => {
114                if namespace.is_empty() {
115                    local.clone()
116                } else {
117                    format!("{{{namespace}}}{local}")
118                }
119            }
120            NameClass::AnyName(_) => "*".into(),
121            NameClass::NsName { namespace, .. } => format!("{{{namespace}}}*"),
122            NameClass::Choice(a, b) => format!("{} | {}", a.describe(), b.describe()),
123            NameClass::Nothing => "<nothing>".into(),
124        }
125    }
126}
127
128/// A compiled RelaxNG schema.
129#[derive(Debug, Clone)]
130pub struct RngSchema {
131    pub start: Arc<Pattern>,
132    pub defines: HashMap<String, Arc<Pattern>>,
133}
134
135// ── smart constructors (keep derivatives compact) ────────────────────────────
136//
137// These mirror the private helpers in `crate::relaxng`.  Duplicated rather
138// than re-exported because the legacy module keeps them private; behaviour is
139// byte-identical.
140
141fn p_empty() -> Arc<Pattern> { Arc::new(Pattern::Empty) }
142fn p_not_allowed() -> Arc<Pattern> { Arc::new(Pattern::NotAllowed) }
143
144fn choice(a: Arc<Pattern>, b: Arc<Pattern>) -> Arc<Pattern> {
145    match (&*a, &*b) {
146        (Pattern::NotAllowed, _) => b,
147        (_, Pattern::NotAllowed) => a,
148        _ => Arc::new(Pattern::Choice(a, b)),
149    }
150}
151
152fn group(a: Arc<Pattern>, b: Arc<Pattern>) -> Arc<Pattern> {
153    match (&*a, &*b) {
154        (Pattern::NotAllowed, _) | (_, Pattern::NotAllowed) => p_not_allowed(),
155        (Pattern::Empty, _) => b,
156        (_, Pattern::Empty) => a,
157        _ => Arc::new(Pattern::Group(a, b)),
158    }
159}
160
161fn interleave(a: Arc<Pattern>, b: Arc<Pattern>) -> Arc<Pattern> {
162    match (&*a, &*b) {
163        (Pattern::NotAllowed, _) | (_, Pattern::NotAllowed) => p_not_allowed(),
164        (Pattern::Empty, _) => b,
165        (_, Pattern::Empty) => a,
166        _ => Arc::new(Pattern::Interleave(a, b)),
167    }
168}
169
170fn one_or_more(p: Arc<Pattern>) -> Arc<Pattern> {
171    match &*p {
172        Pattern::NotAllowed => p_not_allowed(),
173        Pattern::Empty => p_empty(),
174        _ => Arc::new(Pattern::OneOrMore(p)),
175    }
176}
177
178fn after(a: Arc<Pattern>, b: Arc<Pattern>) -> Arc<Pattern> {
179    match &*a {
180        Pattern::NotAllowed => p_not_allowed(),
181        _ => Arc::new(Pattern::After(a, b)),
182    }
183}
184
185// ── nullability ──────────────────────────────────────────────────────────────
186
187/// True iff `p` can match the empty content sequence.
188fn nullable(p: &Pattern, defs: &HashMap<String, Arc<Pattern>>) -> bool {
189    match p {
190        Pattern::Empty | Pattern::Text => true,
191        Pattern::NotAllowed
192        | Pattern::Value { .. }
193        | Pattern::Data { .. }
194        | Pattern::List(_)
195        | Pattern::Element { .. }
196        | Pattern::Attribute { .. }
197        | Pattern::After(_, _) => false,
198        Pattern::Group(a, b) | Pattern::Interleave(a, b) => {
199            nullable(a, defs) && nullable(b, defs)
200        }
201        Pattern::Choice(a, b) => nullable(a, defs) || nullable(b, defs),
202        Pattern::OneOrMore(inner) => nullable(inner, defs),
203        Pattern::Ref(name) => match defs.get(name) {
204            Some(target) => nullable(target, defs),
205            None => false,
206        },
207    }
208}
209
210/// Helper to discriminate name-class matches; `NameClass::matches` is private
211/// in the legacy module, so we reimplement it here against the same enum.
212fn name_class_matches(nc: &NameClass, ns: &str, local: &str) -> bool {
213    match nc {
214        NameClass::Name { namespace, local: l } => namespace == ns && l == local,
215        NameClass::AnyName(except) => {
216            except.as_ref().is_none_or(|e| !name_class_matches(e, ns, local))
217        }
218        NameClass::NsName { namespace, except } => {
219            namespace == ns
220                && except.as_ref().is_none_or(|e| !name_class_matches(e, ns, local))
221        }
222        NameClass::Choice(a, b) => name_class_matches(a, ns, local)
223            || name_class_matches(b, ns, local),
224        NameClass::Nothing => false,
225    }
226}
227
228// ── derivatives ──────────────────────────────────────────────────────────────
229
230/// Derivative of `p` with respect to a child element with name `(ns, local)`,
231/// attributes `atts`, and content `children`.  Returns the residual pattern
232/// that the rest of the parent's content must match.
233fn child_deriv<'a>(
234    p: &Pattern,
235    ns: &str,
236    local: &str,
237    atts: &[(String, String, String)],
238    children: &[&'a Node<'a>],
239    defs: &HashMap<String, Arc<Pattern>>,
240) -> Arc<Pattern> {
241    match p {
242        Pattern::Element { name, child } => {
243            if !name_class_matches(name, ns, local) {
244                return p_not_allowed();
245            }
246            // First validate attributes against the inner pattern.
247            let after_atts = consume_attributes(child.clone(), atts, defs);
248            if matches!(&*after_atts, Pattern::NotAllowed) {
249                return p_not_allowed();
250            }
251            // Then validate child content (text + elements) against the residual.
252            let after_content = consume_content(after_atts, children, defs);
253            if nullable(&after_content, defs) {
254                p_empty()
255            } else {
256                p_not_allowed()
257            }
258        }
259        Pattern::Choice(a, b) => choice(
260            child_deriv(a, ns, local, atts, children, defs),
261            child_deriv(b, ns, local, atts, children, defs),
262        ),
263        Pattern::Group(a, b) => {
264            let d_a = child_deriv(a, ns, local, atts, children, defs);
265            let in_a = group(d_a, b.clone());
266            if nullable(a, defs) {
267                let d_b = child_deriv(b, ns, local, atts, children, defs);
268                choice(in_a, d_b)
269            } else {
270                in_a
271            }
272        }
273        Pattern::Interleave(a, b) => {
274            let d_a = child_deriv(a, ns, local, atts, children, defs);
275            let d_b = child_deriv(b, ns, local, atts, children, defs);
276            choice(interleave(d_a, b.clone()), interleave(a.clone(), d_b))
277        }
278        Pattern::OneOrMore(inner) => {
279            let d = child_deriv(inner, ns, local, atts, children, defs);
280            group(
281                d,
282                choice(p_empty(), Arc::new(Pattern::OneOrMore(inner.clone()))),
283            )
284        }
285        Pattern::After(a, b) => after(
286            child_deriv(a, ns, local, atts, children, defs),
287            b.clone(),
288        ),
289        Pattern::Ref(name) => match defs.get(name) {
290            Some(target) => child_deriv(target, ns, local, atts, children, defs),
291            None => p_not_allowed(),
292        },
293        _ => p_not_allowed(),
294    }
295}
296
297/// Derivative of `p` with respect to a text run.
298fn text_deriv(p: &Pattern, text: &str, defs: &HashMap<String, Arc<Pattern>>) -> Arc<Pattern> {
299    match p {
300        Pattern::Text => p_empty(),
301        Pattern::Value { datatype, text: expected } => {
302            if value_matches(datatype, text, expected) {
303                p_empty()
304            } else {
305                p_not_allowed()
306            }
307        }
308        Pattern::Data { datatype, params } => {
309            if data_matches(datatype, params, text) {
310                #[cfg(feature = "xsd")]
311                record_id_value(datatype, text);
312                p_empty()
313            } else {
314                p_not_allowed()
315            }
316        }
317        Pattern::List(inner) => {
318            let tokens: Vec<&str> = text.split_whitespace().collect();
319            let mut current = inner.clone();
320            for t in &tokens {
321                current = text_deriv(&current, t, defs);
322                if matches!(&*current, Pattern::NotAllowed) {
323                    return p_not_allowed();
324                }
325            }
326            if nullable(&current, defs) {
327                p_empty()
328            } else {
329                p_not_allowed()
330            }
331        }
332        Pattern::Choice(a, b) => choice(
333            text_deriv(a, text, defs),
334            text_deriv(b, text, defs),
335        ),
336        Pattern::Group(a, b) => {
337            let d_a = text_deriv(a, text, defs);
338            let in_a = group(d_a, b.clone());
339            if nullable(a, defs) {
340                let d_b = text_deriv(b, text, defs);
341                choice(in_a, d_b)
342            } else {
343                in_a
344            }
345        }
346        Pattern::Interleave(a, b) => {
347            let d_a = text_deriv(a, text, defs);
348            let d_b = text_deriv(b, text, defs);
349            choice(interleave(d_a, b.clone()), interleave(a.clone(), d_b))
350        }
351        Pattern::OneOrMore(inner) => {
352            let d = text_deriv(inner, text, defs);
353            group(
354                d,
355                choice(p_empty(), Arc::new(Pattern::OneOrMore(inner.clone()))),
356            )
357        }
358        Pattern::After(a, b) => after(text_deriv(a, text, defs), b.clone()),
359        Pattern::Ref(name) => match defs.get(name) {
360            Some(target) => text_deriv(target, text, defs),
361            None => p_not_allowed(),
362        },
363        Pattern::Empty => {
364            if text.trim().is_empty() {
365                p_empty()
366            } else {
367                p_not_allowed()
368            }
369        }
370        _ => p_not_allowed(),
371    }
372}
373
374/// Consume an attribute against `p`.  Returns the residual pattern.
375fn att_deriv(
376    p: &Pattern,
377    ns: &str,
378    local: &str,
379    value: &str,
380    defs: &HashMap<String, Arc<Pattern>>,
381) -> Arc<Pattern> {
382    match p {
383        Pattern::Attribute { name, child } => {
384            if !name_class_matches(name, ns, local) {
385                return p_not_allowed();
386            }
387            let after_text = text_deriv(child, value, defs);
388            if nullable(&after_text, defs) {
389                p_empty()
390            } else {
391                p_not_allowed()
392            }
393        }
394        Pattern::Choice(a, b) => choice(
395            att_deriv(a, ns, local, value, defs),
396            att_deriv(b, ns, local, value, defs),
397        ),
398        Pattern::Group(a, b) => {
399            let d_a = att_deriv(a, ns, local, value, defs);
400            let in_a = group(d_a, b.clone());
401            let d_b = att_deriv(b, ns, local, value, defs);
402            let in_b = group(a.clone(), d_b);
403            choice(in_a, in_b)
404        }
405        Pattern::Interleave(a, b) => {
406            let d_a = att_deriv(a, ns, local, value, defs);
407            let d_b = att_deriv(b, ns, local, value, defs);
408            choice(interleave(d_a, b.clone()), interleave(a.clone(), d_b))
409        }
410        Pattern::OneOrMore(inner) => {
411            let d = att_deriv(inner, ns, local, value, defs);
412            group(
413                d,
414                choice(p_empty(), Arc::new(Pattern::OneOrMore(inner.clone()))),
415            )
416        }
417        Pattern::After(a, b) => after(att_deriv(a, ns, local, value, defs), b.clone()),
418        Pattern::Ref(name) => match defs.get(name) {
419            Some(target) => att_deriv(target, ns, local, value, defs),
420            None => p_not_allowed(),
421        },
422        _ => p_not_allowed(),
423    }
424}
425
426/// Validate the content (text + child elements) of an element against the
427/// residual pattern returned after attribute matching.
428fn consume_content<'a>(
429    p: Arc<Pattern>,
430    children: &[&'a Node<'a>],
431    defs: &HashMap<String, Arc<Pattern>>,
432) -> Arc<Pattern> {
433    let mut current = p;
434    for c in children {
435        if matches!(&*current, Pattern::NotAllowed) {
436            return p_not_allowed();
437        }
438        current = match c.kind {
439            NodeKind::Element => {
440                let (ns, local) = name_split(c);
441                let atts = collect_attributes(c);
442                let child_refs: Vec<&Node<'_>> = c
443                    .children()
444                    .filter(|n| {
445                        !(n.kind == NodeKind::Text
446                            && n.content().trim().is_empty())
447                    })
448                    .collect();
449                child_deriv(&current, &ns, local, &atts, &child_refs, defs)
450            }
451            NodeKind::Text => {
452                let txt = c.content();
453                if txt.trim().is_empty() {
454                    // Whitespace-only text between elements is tolerated
455                    // whether the pattern accepts text or not.
456                    current
457                } else {
458                    text_deriv(&current, txt, defs)
459                }
460            }
461            NodeKind::CData => text_deriv(&current, c.content(), defs),
462            // Comments and PIs are ignored by RelaxNG validation.
463            _ => current,
464        };
465    }
466    current
467}
468
469/// Validate an element's attributes against `p`.  Loops over the element's
470/// attributes (skipping `xmlns*`) and applies `att_deriv` for each.
471fn consume_attributes(
472    p: Arc<Pattern>,
473    atts: &[(String, String, String)],
474    defs: &HashMap<String, Arc<Pattern>>,
475) -> Arc<Pattern> {
476    let mut current = p;
477    for (ns, local, value) in atts {
478        if matches!(&*current, Pattern::NotAllowed) {
479            return p_not_allowed();
480        }
481        current = att_deriv(&current, ns, local, value, defs);
482    }
483    current
484}
485
486// ── helpers for navigating the arena tree ────────────────────────────────────
487
488/// Split an element's qualified name into `(namespace_uri, local)`.  The
489/// namespace URI comes from the element's resolved `namespace` binding (when
490/// the parser was run with `namespace_aware: true`); otherwise it's the empty
491/// string and we fall back to local-name-only matching.
492fn name_split<'a>(elem: &'a Node<'a>) -> (String, &'a str) {
493    debug_assert!(elem.kind == NodeKind::Element, "name_split on non-element");
494    let name: &str = elem.name();
495    let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
496    let ns = elem
497        .namespace
498        .get()
499        .map(|n| n.href().to_string())
500        .unwrap_or_default();
501    (ns, local)
502}
503
504/// RelaxNG simplification (§ 4.1, "Annotations"): an element from a
505/// namespace other than the RelaxNG namespace is a foreign annotation
506/// and is removed before grammar processing — a RelaxNG schema "can be
507/// mixed freely with stuff from other namespaces" (e.g. embedded ISO
508/// Schematron `<sch:*>` rules).  An element with no resolved namespace
509/// is treated as RelaxNG so the non-namespace-aware fallback still
510/// parses bare schemas.
511fn is_foreign_element<'a>(elem: &'a Node<'a>) -> bool {
512    let (ns, _) = name_split(elem);
513    !ns.is_empty() && ns != RELAXNG_NS
514}
515
516/// Collect `(namespace, local, value)` for each non-`xmlns*` attribute.
517fn collect_attributes<'a>(elem: &'a Node<'a>) -> Vec<(String, String, String)> {
518    debug_assert!(elem.kind == NodeKind::Element, "collect_attributes on non-element");
519    elem.attributes()
520        .filter_map(|a| {
521            let aname: &str = a.name();
522            if aname == "xmlns" || aname.starts_with("xmlns:") {
523                return None;
524            }
525            let local = aname.rsplit_once(':').map(|(_, l)| l).unwrap_or(aname);
526            let ns = a.namespace.get().map(|n| n.href().to_string()).unwrap_or_default();
527            Some((ns, local.to_string(), a.value().to_string()))
528        })
529        .collect()
530}
531
532// ── value / data validation ──────────────────────────────────────────────────
533
534fn value_matches(datatype: &str, actual: &str, expected: &str) -> bool {
535    if datatype == "token" || datatype == "NMTOKEN" {
536        let a_norm = actual.split_whitespace().collect::<Vec<_>>().join(" ");
537        let e_norm = expected.split_whitespace().collect::<Vec<_>>().join(" ");
538        a_norm == e_norm
539    } else {
540        actual == expected
541    }
542}
543
544fn data_matches(datatype: &str, _params: &[(String, String)], text: &str) -> bool {
545    #[cfg(feature = "xsd")]
546    {
547        if let Some(checker) = xsd_datatype_checker(datatype) {
548            return checker(text);
549        }
550    }
551    let _ = datatype;
552    !text.is_empty() || datatype == "string"
553}
554
555// ── ID / IDREF semantic cross-reference checking ─────────────────────────────
556//
557// RELAX NG's structural (derivative) validation only checks that an
558// `<data type="ID"/>` / `IDREF` value is a syntactically valid NCName; the
559// *semantic* rule — every IDREF must reference a declared ID — is a
560// separate pass.  The derivative engine is pure, so we accumulate the
561// values into thread-local state during the walk and resolve them at the
562// end, exactly as libxml2 does.  The collector is only armed when the
563// schema actually uses IDREF/IDREFS, so every other schema is untouched.
564
565#[cfg(feature = "xsd")]
566#[derive(Default)]
567struct IdRefState {
568    ids:    std::collections::HashSet<String>,
569    idrefs: Vec<String>,
570}
571
572#[cfg(feature = "xsd")]
573thread_local! {
574    static ID_REFS: std::cell::RefCell<Option<IdRefState>> =
575        const { std::cell::RefCell::new(None) };
576}
577
578/// Record a value matched against an `ID`/`IDREF`/`IDREFS` datatype, when
579/// the collector is armed.  No-op otherwise.  Over-collection from a failed
580/// `<choice>` branch is possible (and matches libxml2) but rare.
581#[cfg(feature = "xsd")]
582fn record_id_value(datatype: &str, text: &str) {
583    ID_REFS.with(|c| {
584        if let Some(st) = c.borrow_mut().as_mut() {
585            match datatype {
586                "ID"     => { st.ids.insert(text.trim().to_string()); }
587                "IDREF"  => st.idrefs.push(text.trim().to_string()),
588                "IDREFS" => st.idrefs.extend(text.split_whitespace().map(str::to_string)),
589                _ => {}
590            }
591        }
592    });
593}
594
595/// Whether any pattern in the schema declares an `IDREF`/`IDREFS` datatype
596/// (so resolution must be checked).  `ID`-only schemas need no cross-check.
597#[cfg(feature = "xsd")]
598fn schema_uses_idref(schema: &RngSchema) -> bool {
599    fn walk(p: &Pattern) -> bool {
600        match p {
601            Pattern::Data { datatype, .. } => matches!(datatype.as_str(), "IDREF" | "IDREFS"),
602            Pattern::List(a) | Pattern::OneOrMore(a) => walk(a),
603            Pattern::Element { child, .. } | Pattern::Attribute { child, .. } => walk(child),
604            Pattern::Group(a, b) | Pattern::Interleave(a, b)
605            | Pattern::Choice(a, b) | Pattern::After(a, b) => walk(a) || walk(b),
606            _ => false,
607        }
608    }
609    walk(schema.start.as_ref()) || schema.defines.values().any(|p| walk(p.as_ref()))
610}
611
612/// Arm or disarm the collector for a validation run.
613#[cfg(feature = "xsd")]
614fn set_idref_tracking(on: bool) {
615    ID_REFS.with(|c| *c.borrow_mut() = on.then(IdRefState::default));
616}
617
618/// Take the collector and return the first IDREF that resolves to no ID,
619/// or `None` when every reference resolves (or tracking was off).
620#[cfg(feature = "xsd")]
621fn first_unresolved_idref() -> Option<String> {
622    ID_REFS.with(|c| {
623        c.borrow_mut().take().and_then(|st| {
624            st.idrefs.iter().find(|r| !st.ids.contains(*r)).cloned()
625        })
626    })
627}
628
629#[cfg(feature = "xsd")]
630fn xsd_datatype_checker(datatype: &str) -> Option<fn(&str) -> bool> {
631    match datatype {
632        "string" | "token" | "normalizedString" | "Name" | "NCName" | "NMTOKEN" | "QName"
633        | "ID" | "IDREF" | "IDREFS" | "ENTITY" | "ENTITIES" | "language" => {
634            Some(|s| !s.is_empty())
635        }
636        "boolean" => Some(|s| matches!(s.trim(), "true" | "false" | "1" | "0")),
637        "decimal" | "double" | "float" => Some(|s| s.trim().parse::<f64>().is_ok()),
638        "integer" | "int" | "long" | "short" | "byte" => {
639            Some(|s| s.trim().parse::<i64>().is_ok())
640        }
641        "positiveInteger" => Some(|s| s.trim().parse::<i64>().is_ok_and(|n| n > 0)),
642        "nonNegativeInteger" | "unsignedInt" | "unsignedLong" | "unsignedShort"
643        | "unsignedByte" => Some(|s| s.trim().parse::<u64>().is_ok()),
644        "negativeInteger" => Some(|s| s.trim().parse::<i64>().is_ok_and(|n| n < 0)),
645        "nonPositiveInteger" => Some(|s| s.trim().parse::<i64>().is_ok_and(|n| n <= 0)),
646        "anyURI" => Some(|s| !s.is_empty()),
647        "date" => Some(|s| s.len() >= 10 && s[..10].chars().filter(|c| *c == '-').count() == 2),
648        _ => None,
649    }
650}
651
652// ── schema parser ────────────────────────────────────────────────────────────
653
654/// Parse a RelaxNG XML schema into an [`RngSchema`].  Schemas are parsed in
655/// namespace-aware mode so name-class `ns` attribute inheritance works
656/// correctly.
657pub fn parse_schema(source: &str) -> Result<RngSchema> {
658    parse_schema_with_base(source, None)
659}
660
661/// Parse a RELAX NG schema, resolving `<include href="…">` against
662/// `base` (the schema document's URL).  `parse_schema` is the
663/// no-base form.
664pub fn parse_schema_with_base(source: &str, base: Option<&str>) -> Result<RngSchema> {
665    let opts = ParseOptions {
666        namespace_aware: true,
667        ..ParseOptions::default()
668    };
669    let doc = parse_str(source, &opts)?;
670    parse_schema_doc(&doc, base)
671}
672
673fn parse_schema_doc(doc: &Document, base: Option<&str>) -> Result<RngSchema> {
674    let root = doc.root();
675    if root.kind != NodeKind::Element {
676        return Err(schema_err("schema root must be an element"));
677    }
678    let mut ctx = SchemaCtx {
679        defines: HashMap::new(),
680        define_combines: HashMap::new(),
681        start_combine: None,
682        default_ns: String::new(),
683        base: base.map(str::to_string),
684    };
685    if let Some(ns) = attr(root, "ns") {
686        ctx.default_ns = ns.to_string();
687    }
688
689    // The schema's root must be in the RELAX NG namespace (§ 4.1); a
690    // root from another namespace isn't a RELAX NG pattern at all.  An
691    // unresolved (empty) namespace is tolerated for the
692    // non-namespace-aware fallback path.
693    let (root_ns, local) = name_split(root);
694    if !root_ns.is_empty() && root_ns != RELAXNG_NS {
695        return Err(schema_err(format!(
696            "schema root <{}> is not in the RELAX NG namespace",
697            root.name()
698        )));
699    }
700    let start = match local {
701        "grammar" => parse_grammar(root, &mut ctx)?,
702        _ => parse_pattern_element(root, &ctx)?,
703    };
704
705    Ok(RngSchema { start, defines: ctx.defines })
706}
707
708struct SchemaCtx {
709    defines: HashMap<String, Arc<Pattern>>,
710    define_combines: HashMap<String, Option<CombineKind>>,
711    start_combine: Option<CombineKind>,
712    default_ns: String,
713    /// Base URI for resolving `<include href="…">`, threaded from the
714    /// schema document's URL (or the including file when nested).
715    base: Option<String>,
716}
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
719enum CombineKind {
720    Choice,
721    Interleave,
722}
723
724fn parse_combine_attr<'a>(elem: &'a Node<'a>, ctx_label: &str) -> Result<Option<CombineKind>> {
725    match attr(elem, "combine") {
726        None => Ok(None),
727        Some("choice") => Ok(Some(CombineKind::Choice)),
728        Some("interleave") => Ok(Some(CombineKind::Interleave)),
729        Some(other) => Err(schema_err(format!(
730            "invalid combine={other:?} on {ctx_label} — must be \"choice\" or \"interleave\""
731        ))),
732    }
733}
734
735fn merge_with_combine(
736    name: &str,
737    existing: Arc<Pattern>,
738    new_pat: Arc<Pattern>,
739    prev_combine: Option<CombineKind>,
740    new_combine: Option<CombineKind>,
741    label: &str,
742) -> Result<(Arc<Pattern>, CombineKind)> {
743    let effective = match (prev_combine, new_combine) {
744        (Some(a), Some(b)) if a == b => a,
745        (Some(a), Some(b)) => {
746            return Err(schema_err(format!(
747                "inconsistent combine values for {label} {name:?}: previously {a:?}, now {b:?}"
748            )));
749        }
750        (None, Some(b)) => b,
751        (Some(a), None) => a,
752        (None, None) => {
753            return Err(schema_err(format!(
754                "duplicate {label} {name:?} requires a combine attribute (\"choice\" or \"interleave\") \
755                 on at least the second occurrence — RelaxNG spec § 4.17"
756            )));
757        }
758    };
759    let merged = match effective {
760        CombineKind::Choice => choice(existing, new_pat),
761        CombineKind::Interleave => interleave(existing, new_pat),
762    };
763    Ok((merged, effective))
764}
765
766fn parse_grammar<'a>(elem: &'a Node<'a>, ctx: &mut SchemaCtx) -> Result<Arc<Pattern>> {
767    if let Some(ns) = attr(elem, "ns") {
768        ctx.default_ns = ns.to_string();
769    }
770    let mut start: Option<Arc<Pattern>> = None;
771    for child in elem.children() {
772        if child.kind != NodeKind::Element || is_foreign_element(child) {
773            continue;
774        }
775        match local_name(child.name()) {
776            "start" => handle_start(child, ctx, &mut start)?,
777            "define" => handle_define(child, ctx)?,
778            "div" => parse_grammar_div(child, ctx, &mut start)?,
779            "include" => handle_include(child, ctx, &mut start)?,
780            other => {
781                return Err(schema_err(format!(
782                    "unsupported grammar child <{other}>"
783                )));
784            }
785        }
786    }
787    start.ok_or_else(|| schema_err("<grammar> requires a <start>"))
788}
789
790/// Process `<include href="…">` (RELAX NG § 4.7): load the referenced
791/// grammar and fold its `<start>`/`<define>`/`<div>` into the including
792/// grammar.  A `<define>`/`<start>` *inside* the `<include>` overrides
793/// (replaces) the same-named component from the included grammar.
794fn handle_include<'a>(
795    elem:  &'a Node<'a>,
796    ctx:   &mut SchemaCtx,
797    start: &mut Option<Arc<Pattern>>,
798) -> Result<()> {
799    let href = attr(elem, "href")
800        .ok_or_else(|| schema_err("<include> requires an href attribute"))?;
801    let resolved = crate::resolve_uri(href, ctx.base.as_deref());
802    let path = resolved.strip_prefix("file://").unwrap_or(&resolved);
803    let src = std::fs::read_to_string(path)
804        .map_err(|e| schema_err(format!("<include href=\"{href}\">: {e}")))?;
805    let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
806    let inc_doc = parse_str(&src, &opts)?;
807    let inc_root = inc_doc.root();
808    if local_name(inc_root.name()) != "grammar" {
809        return Err(schema_err("<include> href must point to a <grammar>"));
810    }
811
812    // Components redefined inside the <include> override the included
813    // ones; apply them first and remember their names so the included
814    // versions are skipped.
815    let mut overridden: std::collections::HashSet<String> = std::collections::HashSet::new();
816    let mut override_start = false;
817    for ovc in elem.children() {
818        if ovc.kind != NodeKind::Element || is_foreign_element(ovc) {
819            continue;
820        }
821        match local_name(ovc.name()) {
822            "define" => {
823                if let Some(n) = attr(ovc, "name") {
824                    overridden.insert(n.to_string());
825                }
826                handle_define(ovc, ctx)?;
827            }
828            "start" => {
829                override_start = true;
830                handle_start(ovc, ctx, start)?;
831            }
832            _ => {}
833        }
834    }
835
836    // The included grammar resolves its own nested includes against its
837    // own location.
838    let saved_base = ctx.base.take();
839    ctx.base = Some(resolved.clone());
840    if let Some(ns) = attr(inc_root, "ns") {
841        // A `ns` on the included grammar applies only within it; we don't
842        // currently track per-grammar default namespaces, so leave the
843        // ambient one in place (the common case has matching/no `ns`).
844        let _ = ns;
845    }
846    for child in inc_root.children() {
847        if child.kind != NodeKind::Element || is_foreign_element(child) {
848            continue;
849        }
850        match local_name(child.name()) {
851            "start" if !override_start => handle_start(child, ctx, start)?,
852            "start" => {}
853            "define" => {
854                if attr(child, "name").map(|n| !overridden.contains(n)).unwrap_or(true) {
855                    handle_define(child, ctx)?;
856                }
857            }
858            "div" => parse_grammar_div(child, ctx, start)?,
859            "include" => handle_include(child, ctx, start)?,
860            _ => {}
861        }
862    }
863    ctx.base = saved_base;
864    Ok(())
865}
866
867fn parse_grammar_div<'a>(
868    elem: &'a Node<'a>,
869    ctx: &mut SchemaCtx,
870    start: &mut Option<Arc<Pattern>>,
871) -> Result<()> {
872    for child in elem.children() {
873        if child.kind != NodeKind::Element || is_foreign_element(child) {
874            continue;
875        }
876        match local_name(child.name()) {
877            "start" => handle_start(child, ctx, start)?,
878            "define" => handle_define(child, ctx)?,
879            "div" => parse_grammar_div(child, ctx, start)?,
880            _ => {}
881        }
882    }
883    Ok(())
884}
885
886fn handle_start<'a>(
887    e: &'a Node<'a>,
888    ctx: &mut SchemaCtx,
889    start: &mut Option<Arc<Pattern>>,
890) -> Result<()> {
891    let new_combine = parse_combine_attr(e, "<start>")?;
892    let new_pat = combine_seq(parse_pattern_children(e, ctx)?);
893    match start.take() {
894        None => {
895            *start = Some(new_pat);
896            ctx.start_combine = new_combine;
897        }
898        Some(existing) => {
899            let prev = ctx.start_combine;
900            let (merged, effective) =
901                merge_with_combine("<start>", existing, new_pat, prev, new_combine, "<start>")?;
902            *start = Some(merged);
903            ctx.start_combine = Some(effective);
904        }
905    }
906    Ok(())
907}
908
909fn handle_define<'a>(e: &'a Node<'a>, ctx: &mut SchemaCtx) -> Result<()> {
910    let name = attr(e, "name")
911        .ok_or_else(|| schema_err("<define> missing name attribute"))?
912        .to_string();
913    let new_combine = parse_combine_attr(e, &format!("<define name={name:?}/>"))?;
914    let new_pat = combine_seq(parse_pattern_children(e, ctx)?);
915    match ctx.defines.get(&name).cloned() {
916        None => {
917            ctx.defines.insert(name.clone(), new_pat);
918            ctx.define_combines.insert(name, new_combine);
919        }
920        Some(existing) => {
921            let prev_combine = ctx.define_combines.get(&name).copied().flatten();
922            let (merged, effective) = merge_with_combine(
923                &name,
924                existing,
925                new_pat,
926                prev_combine,
927                new_combine,
928                "<define>",
929            )?;
930            ctx.defines.insert(name.clone(), merged);
931            ctx.define_combines.insert(name, Some(effective));
932        }
933    }
934    Ok(())
935}
936
937fn parse_pattern_element<'a>(elem: &'a Node<'a>, ctx: &SchemaCtx) -> Result<Arc<Pattern>> {
938    debug_assert!(elem.kind == NodeKind::Element, "parse_pattern_element on non-element");
939    match local_name(elem.name()) {
940        "element" => {
941            let nc = parse_name_class(elem, ctx, false)?;
942            let skip_first_child = attr(elem, "name").is_none();
943            let pats = parse_content_patterns(elem, ctx, skip_first_child)?;
944            // An `<element>` pattern requires a content pattern (§ 4.13);
945            // `<element name="b"/>` with nothing inside is a malformed
946            // schema, not an empty-content element.
947            if pats.is_empty() {
948                return Err(schema_err("<element> pattern has no content pattern"));
949            }
950            let child = combine_seq(pats);
951            Ok(Arc::new(Pattern::Element { name: nc, child }))
952        }
953        "attribute" => {
954            let nc = parse_name_class(elem, ctx, true)?;
955            let skip_first_child = attr(elem, "name").is_none();
956            let pats = parse_content_patterns(elem, ctx, skip_first_child)?;
957            let child = if pats.is_empty() {
958                Arc::new(Pattern::Text)
959            } else {
960                combine_seq(pats)
961            };
962            Ok(Arc::new(Pattern::Attribute { name: nc, child }))
963        }
964        "text" => Ok(Arc::new(Pattern::Text)),
965        "empty" => Ok(p_empty()),
966        "notAllowed" => Ok(p_not_allowed()),
967        "value" => {
968            let datatype = attr(elem, "type").unwrap_or("token").to_string();
969            let text = elem.text_content().unwrap_or("").to_string();
970            Ok(Arc::new(Pattern::Value { datatype, text }))
971        }
972        "data" => {
973            let datatype = attr(elem, "type").unwrap_or("string").to_string();
974            let params: Vec<(String, String)> = elem
975                .children()
976                .filter_map(|c| {
977                    if c.kind == NodeKind::Element && local_name(c.name()) == "param" {
978                        let name = attr(c, "name")?.to_string();
979                        let value = c.text_content().unwrap_or("").to_string();
980                        return Some((name, value));
981                    }
982                    None
983                })
984                .collect();
985            Ok(Arc::new(Pattern::Data { datatype, params }))
986        }
987        "list" => {
988            let inner = combine_seq(parse_pattern_children(elem, ctx)?);
989            Ok(Arc::new(Pattern::List(inner)))
990        }
991        "group" => Ok(combine_seq(parse_pattern_children(elem, ctx)?)),
992        "choice" => {
993            let pats = parse_pattern_children(elem, ctx)?;
994            Ok(combine_choice(pats))
995        }
996        "interleave" => {
997            let pats = parse_pattern_children(elem, ctx)?;
998            Ok(combine_interleave(pats))
999        }
1000        "mixed" => {
1001            let inner = combine_seq(parse_pattern_children(elem, ctx)?);
1002            Ok(interleave(Arc::new(Pattern::Text), inner))
1003        }
1004        "optional" => {
1005            let p = combine_seq(parse_pattern_children(elem, ctx)?);
1006            Ok(choice(p_empty(), p))
1007        }
1008        "zeroOrMore" => {
1009            let p = combine_seq(parse_pattern_children(elem, ctx)?);
1010            Ok(choice(p_empty(), one_or_more(p)))
1011        }
1012        "oneOrMore" => {
1013            let p = combine_seq(parse_pattern_children(elem, ctx)?);
1014            Ok(one_or_more(p))
1015        }
1016        "ref" => {
1017            let name = attr(elem, "name")
1018                .ok_or_else(|| schema_err("<ref> missing name attribute"))?
1019                .to_string();
1020            Ok(Arc::new(Pattern::Ref(name)))
1021        }
1022        other => Err(schema_err(format!(
1023            "unsupported pattern <{other}> — see module docs for the v1 subset"
1024        ))),
1025    }
1026}
1027
1028fn parse_pattern_children<'a>(elem: &'a Node<'a>, ctx: &SchemaCtx) -> Result<Vec<Arc<Pattern>>> {
1029    parse_content_patterns(elem, ctx, false)
1030}
1031
1032/// Walk an element's children, parsing each as a content pattern.  When
1033/// `skip_first_child` is true the first element child is assumed to be a name
1034/// class (already consumed by the caller) and is skipped.
1035fn parse_content_patterns<'a>(
1036    elem: &'a Node<'a>,
1037    ctx: &SchemaCtx,
1038    skip_first_child: bool,
1039) -> Result<Vec<Arc<Pattern>>> {
1040    let mut out = Vec::new();
1041    let mut skipped = !skip_first_child;
1042    for child in elem.children() {
1043        if child.kind != NodeKind::Element || is_foreign_element(child) {
1044            continue;
1045        }
1046        if !skipped {
1047            skipped = true;
1048            continue;
1049        }
1050        let local = local_name(child.name());
1051        if matches!(local, "name" | "anyName" | "nsName") {
1052            continue;
1053        }
1054        out.push(parse_pattern_element(child, ctx)?);
1055    }
1056    Ok(out)
1057}
1058
1059/// Resolve a RELAX NG name-class QName to `(namespace, local)`.
1060///
1061/// Per the RELAX NG spec § 4.10 / § 7: a prefixed name resolves the
1062/// prefix against the in-scope namespace declarations of the schema
1063/// document; an *unprefixed* name uses the inherited `ns` value for an
1064/// **element**, but the **empty** namespace for an **attribute** (XML
1065/// attributes are not in the default namespace).  Getting the
1066/// attribute case wrong makes every named attribute in an `ns`-scoped
1067/// grammar (e.g. ISO Schematron's `test` / `context`) fail to match.
1068fn resolve_name_class_qname(
1069    elem: &Node<'_>,
1070    name: &str,
1071    default_ns: &str,
1072    is_attribute: bool,
1073) -> (String, String) {
1074    match name.split_once(':') {
1075        Some((prefix, local)) => {
1076            let ns = resolve_schema_prefix(elem, prefix)
1077                .unwrap_or_else(|| default_ns.to_string());
1078            (ns, local.to_string())
1079        }
1080        None => {
1081            let ns = if is_attribute { String::new() } else { default_ns.to_string() };
1082            (ns, name.to_string())
1083        }
1084    }
1085}
1086
1087/// Resolve a namespace prefix used in a name-class QName, walking the
1088/// schema element and its ancestors for the matching `xmlns:` decl.
1089/// `xml` is predefined (XML 1.0 § 3.7).
1090fn resolve_schema_prefix(elem: &Node<'_>, prefix: &str) -> Option<String> {
1091    if prefix == "xml" {
1092        return Some("http://www.w3.org/XML/1998/namespace".to_string());
1093    }
1094    let mut cur = Some(elem);
1095    while let Some(e) = cur {
1096        for (p, href) in e.ns_declarations() {
1097            if p == Some(prefix) {
1098                return Some(href.to_string());
1099            }
1100        }
1101        cur = e.parent.get();
1102    }
1103    None
1104}
1105
1106/// Parse the name class of an `<element>` or `<attribute>`.
1107fn parse_name_class<'a>(elem: &'a Node<'a>, ctx: &SchemaCtx, is_attribute: bool) -> Result<NameClass> {
1108    let elem_default_ns = attr(elem, "ns")
1109        .map(|s| s.to_string())
1110        .unwrap_or_else(|| ctx.default_ns.clone());
1111
1112    if let Some(name) = attr(elem, "name") {
1113        let (namespace, local) =
1114            resolve_name_class_qname(elem, name, &elem_default_ns, is_attribute);
1115        return Ok(NameClass::Name { namespace, local });
1116    }
1117
1118    for child in elem.children() {
1119        if child.kind != NodeKind::Element {
1120            continue;
1121        }
1122        if let Some(nc) = parse_name_class_element(child, &elem_default_ns, is_attribute)? {
1123            return Ok(nc);
1124        }
1125    }
1126    Err(schema_err(format!(
1127        "<{}> needs a name attribute or child <name>/<anyName>/<nsName>/<choice>",
1128        local_name(elem.name())
1129    )))
1130}
1131
1132fn parse_name_class_element<'a>(
1133    elem: &'a Node<'a>,
1134    default_ns: &str,
1135    is_attribute: bool,
1136) -> Result<Option<NameClass>> {
1137    match local_name(elem.name()) {
1138        "name" => {
1139            let raw = elem.text_content().unwrap_or("").trim().to_string();
1140            // An explicit `ns` on the <name> wins; otherwise resolve the
1141            // QName the same way as the `name` attribute form.
1142            let (namespace, local) = match attr(elem, "ns") {
1143                Some(ns) => (ns.to_string(), raw),
1144                None => resolve_name_class_qname(elem, &raw, default_ns, is_attribute),
1145            };
1146            Ok(Some(NameClass::Name { namespace, local }))
1147        }
1148        "anyName" => {
1149            let except = parse_except_name_class(elem, default_ns, is_attribute)?;
1150            Ok(Some(NameClass::AnyName(except.map(Box::new))))
1151        }
1152        "nsName" => {
1153            let ns = attr(elem, "ns")
1154                .map(|s| s.to_string())
1155                .unwrap_or_else(|| default_ns.to_string());
1156            let except = parse_except_name_class(elem, default_ns, is_attribute)?;
1157            Ok(Some(NameClass::NsName {
1158                namespace: ns,
1159                except: except.map(Box::new),
1160            }))
1161        }
1162        "choice" => {
1163            let mut alts = Vec::new();
1164            for child in elem.children() {
1165                if child.kind != NodeKind::Element {
1166                    continue;
1167                }
1168                if let Some(nc) = parse_name_class_element(child, default_ns, is_attribute)? {
1169                    alts.push(nc);
1170                }
1171            }
1172            Ok(Some(combine_name_choice(alts)))
1173        }
1174        _ => Ok(None),
1175    }
1176}
1177
1178fn parse_except_name_class<'a>(
1179    elem: &'a Node<'a>,
1180    default_ns: &str,
1181    is_attribute: bool,
1182) -> Result<Option<NameClass>> {
1183    for child in elem.children() {
1184        if child.kind != NodeKind::Element {
1185            continue;
1186        }
1187        if local_name(child.name()) != "except" {
1188            continue;
1189        }
1190        let mut alts = Vec::new();
1191        for ec in child.children() {
1192            if ec.kind != NodeKind::Element {
1193                continue;
1194            }
1195            if let Some(nc) = parse_name_class_element(ec, default_ns, is_attribute)? {
1196                alts.push(nc);
1197            }
1198        }
1199        if alts.is_empty() {
1200            return Err(schema_err("<except> needs at least one name class"));
1201        }
1202        return Ok(Some(combine_name_choice(alts)));
1203    }
1204    Ok(None)
1205}
1206
1207fn combine_name_choice(alts: Vec<NameClass>) -> NameClass {
1208    if alts.is_empty() {
1209        return NameClass::Nothing;
1210    }
1211    let mut iter = alts.into_iter();
1212    let mut acc = iter.next().unwrap();
1213    for a in iter {
1214        acc = NameClass::Choice(Box::new(acc), Box::new(a));
1215    }
1216    acc
1217}
1218
1219fn combine_seq(mut pats: Vec<Arc<Pattern>>) -> Arc<Pattern> {
1220    match pats.len() {
1221        0 => p_empty(),
1222        1 => pats.remove(0),
1223        _ => {
1224            let mut iter = pats.into_iter();
1225            let mut acc = iter.next().unwrap();
1226            for p in iter {
1227                acc = group(acc, p);
1228            }
1229            acc
1230        }
1231    }
1232}
1233
1234fn combine_choice(mut pats: Vec<Arc<Pattern>>) -> Arc<Pattern> {
1235    match pats.len() {
1236        0 => p_not_allowed(),
1237        1 => pats.remove(0),
1238        _ => {
1239            let mut iter = pats.into_iter();
1240            let mut acc = iter.next().unwrap();
1241            for p in iter {
1242                acc = choice(acc, p);
1243            }
1244            acc
1245        }
1246    }
1247}
1248
1249fn combine_interleave(mut pats: Vec<Arc<Pattern>>) -> Arc<Pattern> {
1250    match pats.len() {
1251        0 => p_empty(),
1252        1 => pats.remove(0),
1253        _ => {
1254            let mut iter = pats.into_iter();
1255            let mut acc = iter.next().unwrap();
1256            for p in iter {
1257                acc = interleave(acc, p);
1258            }
1259            acc
1260        }
1261    }
1262}
1263
1264fn attr<'a>(elem: &'a Node<'a>, name: &str) -> Option<&'a str> {
1265    elem.attributes().find_map(|a| {
1266        if a.name() == name {
1267            Some(a.value())
1268        } else {
1269            None
1270        }
1271    })
1272}
1273
1274fn local_name(name: &str) -> &str {
1275    name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name)
1276}
1277
1278// ── public validation ────────────────────────────────────────────────────────
1279
1280/// Validate `doc` against `schema`.
1281pub fn validate(schema: &RngSchema, doc: &Document) -> Result<()> {
1282    let root = doc.root();
1283    if root.kind != NodeKind::Element {
1284        return Err(validation_err("document root is not an element"));
1285    }
1286    let (ns, local) = name_split(root);
1287    let atts = collect_attributes(root);
1288    let children: Vec<&Node<'_>> = root
1289        .children()
1290        .filter(|n| {
1291            !(n.kind == NodeKind::Text && n.content().trim().is_empty())
1292        })
1293        .collect();
1294    // Arm ID/IDREF collection for the walk when the schema uses references.
1295    #[cfg(feature = "xsd")]
1296    let track_idrefs = schema_uses_idref(schema);
1297    #[cfg(feature = "xsd")]
1298    set_idref_tracking(track_idrefs);
1299    let result = child_deriv(
1300        &schema.start,
1301        &ns,
1302        local,
1303        &atts,
1304        &children,
1305        &schema.defines,
1306    );
1307    if nullable(&result, &schema.defines) {
1308        // Structural validation passed — now resolve IDREFs against the
1309        // IDs collected during the walk (RELAX NG's semantic constraint).
1310        #[cfg(feature = "xsd")]
1311        if track_idrefs {
1312            if let Some(missing) = first_unresolved_idref() {
1313                return Err(validation_err(format!(
1314                    "IDREF value \"{missing}\" does not reference a declared ID"
1315                )));
1316            }
1317        }
1318        Ok(())
1319    } else {
1320        #[cfg(feature = "xsd")]
1321        set_idref_tracking(false);
1322        // Produce libxml2's "Did not expect element X there"
1323        // (`RELAXNG_ERR_ELEMWRONG`, with the element's line) when the
1324        // failure is an unexpected child element; fall back to a generic
1325        // message otherwise.
1326        let located = element_content_for(&schema.start, &ns, local, &schema.defines)
1327            .map(|content| consume_attributes(content, &atts, &schema.defines))
1328            .and_then(|after_atts| locate_unexpected_child(after_atts, &children, &schema.defines));
1329        match located {
1330            Some((name, line)) => {
1331                let mut e = validation_err(format!("Did not expect element {name} there"));
1332                e.code = crate::error::ErrorCode::RelaxngErrElemwrong;
1333                e.line = Some(line);
1334                Err(e)
1335            }
1336            None => Err(validation_err(format!(
1337                "document root <{}> did not match the schema",
1338                root.name()
1339            ))),
1340        }
1341    }
1342}
1343
1344/// Resolve the content pattern of the `<element>` pattern that matches
1345/// `(ns, local)`, looking through `ref`/`choice`.  Used only to build a
1346/// located validation error message.
1347fn element_content_for(
1348    p:     &Pattern,
1349    ns:    &str,
1350    local: &str,
1351    defs:  &HashMap<String, Arc<Pattern>>,
1352) -> Option<Arc<Pattern>> {
1353    match p {
1354        Pattern::Element { name, child } if name_class_matches(name, ns, local) => {
1355            Some(child.clone())
1356        }
1357        Pattern::Ref(n) => defs.get(n).and_then(|d| element_content_for(d, ns, local, defs)),
1358        Pattern::Choice(a, b) => element_content_for(a, ns, local, defs)
1359            .or_else(|| element_content_for(b, ns, local, defs)),
1360        _ => None,
1361    }
1362}
1363
1364/// Walk an element's content pattern against its children (as the
1365/// validator does) and return the local name of the first child element
1366/// the pattern rejects — the one libxml2 reports as "not expected".
1367fn locate_unexpected_child<'a>(
1368    content:  Arc<Pattern>,
1369    children: &[&'a Node<'a>],
1370    defs:     &HashMap<String, Arc<Pattern>>,
1371) -> Option<(String, u32)> {
1372    let mut current = content;
1373    for c in children {
1374        match c.kind {
1375            NodeKind::Element => {
1376                let (cns, cl) = name_split(c);
1377                let catts = collect_attributes(c);
1378                let cchildren: Vec<&Node<'_>> = c
1379                    .children()
1380                    .filter(|n| !(n.kind == NodeKind::Text && n.content().trim().is_empty()))
1381                    .collect();
1382                let next = child_deriv(&current, &cns, cl, &catts, &cchildren, defs);
1383                if matches!(&*next, Pattern::NotAllowed) {
1384                    return Some((cl.to_string(), c.line_no()));
1385                }
1386                current = next;
1387            }
1388            NodeKind::Text if !c.content().trim().is_empty() => {
1389                current = text_deriv(&current, c.content(), defs);
1390            }
1391            NodeKind::CData => current = text_deriv(&current, c.content(), defs),
1392            _ => {}
1393        }
1394    }
1395    None
1396}
1397
1398// ── error helpers ────────────────────────────────────────────────────────────
1399
1400fn schema_err(msg: impl Into<String>) -> XmlError {
1401    XmlError::new(ErrorDomain::Validation, ErrorLevel::Fatal, msg)
1402}
1403
1404fn validation_err(msg: impl Into<String>) -> XmlError {
1405    // libxml2 reports RELAX NG *validation* failures in the `RELAXNGV`
1406    // domain at ERROR level (schema *parse* failures are fatal); lxml
1407    // filters `error_log` by `level_name == "ERROR"` and `domain_name`.
1408    XmlError::new(ErrorDomain::RelaxNGValidate, ErrorLevel::Error, msg)
1409}
1410
1411// ── tests ────────────────────────────────────────────────────────────────────
1412
1413#[cfg(test)]
1414mod tests {
1415    use super::*;
1416
1417    fn schema(s: &str) -> RngSchema {
1418        parse_schema(s).expect("schema parses")
1419    }
1420
1421    fn doc(s: &str) -> Document {
1422        // Validators use namespace-aware parsing so that `<nsName>` filtering
1423        // can compare resolved namespace URIs, not raw qualified names.
1424        let opts = ParseOptions {
1425            namespace_aware: true,
1426            ..ParseOptions::default()
1427        };
1428        parse_str(s, &opts).expect("doc parses")
1429    }
1430
1431    #[test]
1432    fn simple_element_with_text_validates() {
1433        let s = schema(
1434            r#"<element name="greeting" xmlns="http://relaxng.org/ns/structure/1.0">
1435                 <text/>
1436               </element>"#,
1437        );
1438        validate(&s, &doc("<greeting>hello</greeting>")).unwrap();
1439    }
1440
1441    #[test]
1442    fn wrong_element_name_rejected() {
1443        let s = schema(
1444            r#"<element name="greeting" xmlns="http://relaxng.org/ns/structure/1.0"><text/></element>"#,
1445        );
1446        let err = validate(&s, &doc("<farewell>bye</farewell>")).expect_err("wrong name");
1447        assert!(err.message.contains("did not match"), "got: {}", err.message);
1448    }
1449
1450    #[test]
1451    fn attribute_with_value_validates() {
1452        let s = schema(
1453            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1454                 <attribute name="kind"><value>small</value></attribute>
1455               </element>"#,
1456        );
1457        validate(&s, &doc(r#"<r kind="small"/>"#)).unwrap();
1458        assert!(validate(&s, &doc(r#"<r kind="huge"/>"#)).is_err());
1459    }
1460
1461    #[test]
1462    fn missing_required_attribute_rejected() {
1463        let s = schema(
1464            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1465                 <attribute name="id"><text/></attribute>
1466               </element>"#,
1467        );
1468        assert!(validate(&s, &doc("<r/>")).is_err());
1469    }
1470
1471    #[test]
1472    fn group_of_elements_validates_in_order() {
1473        let s = schema(
1474            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1475                 <group>
1476                   <element name="a"><text/></element>
1477                   <element name="b"><text/></element>
1478                 </group>
1479               </element>"#,
1480        );
1481        validate(&s, &doc("<r><a>1</a><b>2</b></r>")).unwrap();
1482        assert!(
1483            validate(&s, &doc("<r><b>2</b><a>1</a></r>")).is_err(),
1484            "wrong order should fail"
1485        );
1486    }
1487
1488    #[test]
1489    fn choice_picks_matching_alternative() {
1490        let s = schema(
1491            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1492                 <choice>
1493                   <element name="a"><text/></element>
1494                   <element name="b"><text/></element>
1495                 </choice>
1496               </element>"#,
1497        );
1498        validate(&s, &doc("<r><a>1</a></r>")).unwrap();
1499        validate(&s, &doc("<r><b>2</b></r>")).unwrap();
1500        assert!(validate(&s, &doc("<r><c>3</c></r>")).is_err());
1501    }
1502
1503    #[test]
1504    fn zero_or_more_matches_any_count() {
1505        let s = schema(
1506            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1507                 <zeroOrMore><element name="item"><text/></element></zeroOrMore>
1508               </element>"#,
1509        );
1510        validate(&s, &doc("<r/>")).unwrap();
1511        validate(&s, &doc("<r><item>1</item></r>")).unwrap();
1512        validate(
1513            &s,
1514            &doc("<r><item>1</item><item>2</item><item>3</item></r>"),
1515        )
1516        .unwrap();
1517    }
1518
1519    #[test]
1520    fn one_or_more_requires_at_least_one() {
1521        let s = schema(
1522            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1523                 <oneOrMore><element name="item"><text/></element></oneOrMore>
1524               </element>"#,
1525        );
1526        validate(&s, &doc("<r><item>1</item></r>")).unwrap();
1527        assert!(validate(&s, &doc("<r/>")).is_err());
1528    }
1529
1530    #[test]
1531    fn optional_is_zero_or_one() {
1532        let s = schema(
1533            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1534                 <group>
1535                   <element name="req"><text/></element>
1536                   <optional><element name="opt"><text/></element></optional>
1537                 </group>
1538               </element>"#,
1539        );
1540        validate(&s, &doc("<r><req>x</req></r>")).unwrap();
1541        validate(&s, &doc("<r><req>x</req><opt>y</opt></r>")).unwrap();
1542    }
1543
1544    #[test]
1545    fn grammar_with_define_and_ref() {
1546        let s = schema(
1547            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1548                 <start>
1549                   <element name="root"><ref name="payload"/></element>
1550                 </start>
1551                 <define name="payload">
1552                   <oneOrMore><element name="item"><text/></element></oneOrMore>
1553                 </define>
1554               </grammar>"#,
1555        );
1556        validate(&s, &doc("<root><item>a</item><item>b</item></root>")).unwrap();
1557    }
1558
1559    #[test]
1560    fn empty_pattern_requires_no_content() {
1561        let s = schema(
1562            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0"><empty/></element>"#,
1563        );
1564        validate(&s, &doc("<r/>")).unwrap();
1565        assert!(validate(&s, &doc("<r><x/></r>")).is_err());
1566    }
1567
1568    #[test]
1569    fn interleave_accepts_any_order() {
1570        let s = schema(
1571            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1572                 <interleave>
1573                   <element name="a"><text/></element>
1574                   <element name="b"><text/></element>
1575                 </interleave>
1576               </element>"#,
1577        );
1578        validate(&s, &doc("<r><a>1</a><b>2</b></r>")).unwrap();
1579        validate(&s, &doc("<r><b>2</b><a>1</a></r>")).unwrap();
1580        assert!(validate(&s, &doc("<r><a>1</a></r>")).is_err());
1581        assert!(validate(&s, &doc("<r><a>1</a><a>2</a></r>")).is_err());
1582    }
1583
1584    #[test]
1585    fn interleave_with_zero_or_more() {
1586        let s = schema(
1587            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1588                 <interleave>
1589                   <element name="head"><text/></element>
1590                   <zeroOrMore><element name="item"><text/></element></zeroOrMore>
1591                 </interleave>
1592               </element>"#,
1593        );
1594        validate(&s, &doc("<r><head>h</head></r>")).unwrap();
1595        validate(
1596            &s,
1597            &doc("<r><item>1</item><head>h</head><item>2</item></r>"),
1598        )
1599        .unwrap();
1600        validate(
1601            &s,
1602            &doc("<r><head>h</head><item>1</item><item>2</item></r>"),
1603        )
1604        .unwrap();
1605    }
1606
1607    #[test]
1608    fn mixed_allows_text_interleaved_with_elements() {
1609        let s = schema(
1610            r#"<element name="p" xmlns="http://relaxng.org/ns/structure/1.0">
1611                 <mixed>
1612                   <zeroOrMore><element name="b"><text/></element></zeroOrMore>
1613                 </mixed>
1614               </element>"#,
1615        );
1616        validate(&s, &doc("<p>hello <b>world</b></p>")).unwrap();
1617        validate(&s, &doc("<p>plain text</p>")).unwrap();
1618        validate(&s, &doc("<p><b>x</b></p>")).unwrap();
1619    }
1620
1621    #[test]
1622    fn data_string_accepts_any_text() {
1623        let s = schema(
1624            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1625                 <data type="string"/>
1626               </element>"#,
1627        );
1628        validate(&s, &doc("<r>any text</r>")).unwrap();
1629    }
1630
1631    #[cfg(feature = "xsd")]
1632    #[test]
1633    fn data_positive_integer_validates() {
1634        let s = schema(
1635            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1636                 <data type="positiveInteger"/>
1637               </element>"#,
1638        );
1639        validate(&s, &doc("<r>42</r>")).unwrap();
1640        validate(&s, &doc("<r>1</r>")).unwrap();
1641        assert!(validate(&s, &doc("<r>0</r>")).is_err(), "0 is not positive");
1642        assert!(validate(&s, &doc("<r>-5</r>")).is_err(), "negative");
1643        assert!(validate(&s, &doc("<r>abc</r>")).is_err(), "not integer");
1644    }
1645
1646    #[cfg(feature = "xsd")]
1647    #[test]
1648    fn data_boolean_in_attribute() {
1649        let s = schema(
1650            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1651                 <attribute name="enabled"><data type="boolean"/></attribute>
1652               </element>"#,
1653        );
1654        validate(&s, &doc(r#"<r enabled="true"/>"#)).unwrap();
1655        validate(&s, &doc(r#"<r enabled="false"/>"#)).unwrap();
1656        assert!(validate(&s, &doc(r#"<r enabled="maybe"/>"#)).is_err());
1657    }
1658
1659    #[test]
1660    fn list_of_tokens() {
1661        let s = schema(
1662            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1663                 <list>
1664                   <oneOrMore><value>x</value></oneOrMore>
1665                 </list>
1666               </element>"#,
1667        );
1668        validate(&s, &doc("<r>x x x</r>")).unwrap();
1669        validate(&s, &doc("<r>x</r>")).unwrap();
1670        assert!(validate(&s, &doc("<r>x y</r>")).is_err(), "y is not x");
1671    }
1672
1673    #[test]
1674    fn any_name_matches_any_element() {
1675        let s = schema(
1676            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="root">
1677                 <oneOrMore>
1678                   <element><anyName/><text/></element>
1679                 </oneOrMore>
1680               </element>"#,
1681        );
1682        validate(&s, &doc("<root><a>1</a><b>2</b><z>3</z></root>")).unwrap();
1683    }
1684
1685    #[test]
1686    fn ns_name_filters_by_namespace() {
1687        let s = schema(
1688            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="root">
1689                 <oneOrMore>
1690                   <element><nsName ns="urn:custom"/><text/></element>
1691                 </oneOrMore>
1692               </element>"#,
1693        );
1694        // The arena parser resolves namespaces automatically when invoked with
1695        // `namespace_aware: true` — no separate resolve pass needed.
1696        validate(&s, &doc(r#"<root><x xmlns="urn:custom">1</x></root>"#)).unwrap();
1697        assert!(validate(&s, &doc(r#"<root><x xmlns="urn:other">1</x></root>"#)).is_err());
1698    }
1699
1700    #[test]
1701    fn name_class_choice() {
1702        let s = schema(
1703            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="root">
1704                 <oneOrMore>
1705                   <element>
1706                     <choice><name>a</name><name>b</name></choice>
1707                     <text/>
1708                   </element>
1709                 </oneOrMore>
1710               </element>"#,
1711        );
1712        validate(&s, &doc("<root><a>1</a><b>2</b></root>")).unwrap();
1713        assert!(validate(&s, &doc("<root><c>1</c></root>")).is_err());
1714    }
1715
1716    #[test]
1717    fn nested_choice_resolves_via_derivative() {
1718        let s = schema(
1719            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="r">
1720                 <choice>
1721                   <group>
1722                     <element name="a"><text/></element>
1723                     <element name="b"><text/></element>
1724                   </group>
1725                   <group>
1726                     <element name="a"><text/></element>
1727                     <element name="c"><text/></element>
1728                   </group>
1729                 </choice>
1730               </element>"#,
1731        );
1732        validate(&s, &doc("<r><a>1</a><b>2</b></r>")).unwrap();
1733        validate(&s, &doc("<r><a>1</a><c>3</c></r>")).unwrap();
1734        assert!(validate(&s, &doc("<r><a>1</a><d>4</d></r>")).is_err());
1735    }
1736
1737    #[test]
1738    fn deeply_recursive_grammar() {
1739        let s = schema(
1740            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1741                 <start>
1742                   <element name="tree"><ref name="branches"/></element>
1743                 </start>
1744                 <define name="branches">
1745                   <zeroOrMore>
1746                     <element name="branch"><ref name="branches"/></element>
1747                   </zeroOrMore>
1748                 </define>
1749               </grammar>"#,
1750        );
1751        validate(&s, &doc("<tree/>")).unwrap();
1752        validate(&s, &doc("<tree><branch/></tree>")).unwrap();
1753        validate(
1754            &s,
1755            &doc("<tree><branch><branch><branch/></branch></branch></tree>"),
1756        )
1757        .unwrap();
1758    }
1759
1760    #[test]
1761    fn unsupported_pattern_errors_at_schema_parse() {
1762        let result = parse_schema(
1763            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="r">
1764                 <unknownPattern/>
1765               </element>"#,
1766        );
1767        assert!(result.is_err());
1768    }
1769
1770    #[test]
1771    fn empty_attribute_value_via_data() {
1772        let s = schema(
1773            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
1774                 <attribute name="x"><data type="string"/></attribute>
1775               </element>"#,
1776        );
1777        validate(&s, &doc(r#"<r x="value"/>"#)).unwrap();
1778    }
1779
1780    // ── combine attribute (modular schema composition) ───────────────────────
1781
1782    #[test]
1783    fn combine_choice_merges_two_defines() {
1784        let s = schema(
1785            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1786                 <start>
1787                   <element name="root"><ref name="content"/></element>
1788                 </start>
1789                 <define name="content" combine="choice">
1790                   <element name="a"><text/></element>
1791                 </define>
1792                 <define name="content" combine="choice">
1793                   <element name="b"><text/></element>
1794                 </define>
1795               </grammar>"#,
1796        );
1797        validate(&s, &doc("<root><a>1</a></root>")).unwrap();
1798        validate(&s, &doc("<root><b>2</b></root>")).unwrap();
1799        assert!(
1800            validate(&s, &doc("<root><c>3</c></root>")).is_err(),
1801            "<c> not in either define"
1802        );
1803    }
1804
1805    #[test]
1806    fn combine_choice_merges_three_defines() {
1807        let s = schema(
1808            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1809                 <start>
1810                   <element name="root"><ref name="content"/></element>
1811                 </start>
1812                 <define name="content" combine="choice">
1813                   <element name="a"><text/></element>
1814                 </define>
1815                 <define name="content" combine="choice">
1816                   <element name="b"><text/></element>
1817                 </define>
1818                 <define name="content" combine="choice">
1819                   <element name="c"><text/></element>
1820                 </define>
1821               </grammar>"#,
1822        );
1823        validate(&s, &doc("<root><a>1</a></root>")).unwrap();
1824        validate(&s, &doc("<root><b>2</b></root>")).unwrap();
1825        validate(&s, &doc("<root><c>3</c></root>")).unwrap();
1826        assert!(validate(&s, &doc("<root><d>4</d></root>")).is_err());
1827    }
1828
1829    #[test]
1830    fn combine_interleave_merges_two_defines() {
1831        let s = schema(
1832            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1833                 <start>
1834                   <element name="root"><ref name="content"/></element>
1835                 </start>
1836                 <define name="content" combine="interleave">
1837                   <element name="head"><text/></element>
1838                 </define>
1839                 <define name="content" combine="interleave">
1840                   <element name="body"><text/></element>
1841                 </define>
1842               </grammar>"#,
1843        );
1844        validate(&s, &doc("<root><head>h</head><body>b</body></root>")).unwrap();
1845        validate(&s, &doc("<root><body>b</body><head>h</head></root>")).unwrap();
1846        assert!(validate(&s, &doc("<root><head>h</head></root>")).is_err());
1847        assert!(validate(&s, &doc("<root><body>b</body></root>")).is_err());
1848    }
1849
1850    #[test]
1851    fn combine_one_bare_define_plus_one_with_combine() {
1852        let s = schema(
1853            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1854                 <start>
1855                   <element name="root"><ref name="content"/></element>
1856                 </start>
1857                 <define name="content">
1858                   <element name="a"><text/></element>
1859                 </define>
1860                 <define name="content" combine="choice">
1861                   <element name="b"><text/></element>
1862                 </define>
1863               </grammar>"#,
1864        );
1865        validate(&s, &doc("<root><a>1</a></root>")).unwrap();
1866        validate(&s, &doc("<root><b>2</b></root>")).unwrap();
1867    }
1868
1869    #[test]
1870    fn combine_on_start_element() {
1871        let s = schema(
1872            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1873                 <start combine="choice">
1874                   <element name="a"><text/></element>
1875                 </start>
1876                 <start combine="choice">
1877                   <element name="b"><text/></element>
1878                 </start>
1879               </grammar>"#,
1880        );
1881        validate(&s, &doc("<a>1</a>")).unwrap();
1882        validate(&s, &doc("<b>2</b>")).unwrap();
1883    }
1884
1885    #[test]
1886    fn combine_duplicate_define_without_combine_errors() {
1887        let result = parse_schema(
1888            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1889                 <start><element name="r"><ref name="x"/></element></start>
1890                 <define name="x"><element name="a"><text/></element></define>
1891                 <define name="x"><element name="b"><text/></element></define>
1892               </grammar>"#,
1893        );
1894        assert!(
1895            result.is_err(),
1896            "duplicate define with no combine should be rejected"
1897        );
1898        let err = result.unwrap_err();
1899        assert!(
1900            err.message.contains("combine"),
1901            "error should mention combine; got: {}",
1902            err.message
1903        );
1904    }
1905
1906    #[test]
1907    fn combine_inconsistent_values_error() {
1908        let result = parse_schema(
1909            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1910                 <start><element name="r"><ref name="x"/></element></start>
1911                 <define name="x" combine="choice">
1912                   <element name="a"><text/></element>
1913                 </define>
1914                 <define name="x" combine="interleave">
1915                   <element name="b"><text/></element>
1916                 </define>
1917               </grammar>"#,
1918        );
1919        assert!(
1920            result.is_err(),
1921            "inconsistent combine values must be rejected"
1922        );
1923        let err = result.unwrap_err();
1924        assert!(
1925            err.message.contains("combine") || err.message.contains("inconsistent"),
1926            "error should mention combine; got: {}",
1927            err.message
1928        );
1929    }
1930
1931    #[test]
1932    fn combine_invalid_value_errors() {
1933        let result = parse_schema(
1934            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1935                 <start><element name="r"><ref name="x"/></element></start>
1936                 <define name="x" combine="merge">
1937                   <element name="a"><text/></element>
1938                 </define>
1939                 <define name="x" combine="merge">
1940                   <element name="b"><text/></element>
1941                 </define>
1942               </grammar>"#,
1943        );
1944        assert!(result.is_err(), "invalid combine value must be rejected");
1945    }
1946
1947    #[test]
1948    fn combine_single_define_with_combine_attribute() {
1949        let s = schema(
1950            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
1951                 <start>
1952                   <element name="root"><ref name="content"/></element>
1953                 </start>
1954                 <define name="content" combine="choice">
1955                   <element name="a"><text/></element>
1956                 </define>
1957               </grammar>"#,
1958        );
1959        validate(&s, &doc("<root><a>1</a></root>")).unwrap();
1960    }
1961
1962    // ── NameClass::describe ─────────────────────────────────────
1963
1964    #[test]
1965    fn name_class_describe_all_variants() {
1966        let n1 = NameClass::Name {
1967            namespace: String::new(),
1968            local: "elt".into(),
1969        };
1970        assert_eq!(n1.describe(), "elt");
1971
1972        let n2 = NameClass::Name {
1973            namespace: "urn:x".into(),
1974            local: "elt".into(),
1975        };
1976        assert_eq!(n2.describe(), "{urn:x}elt");
1977
1978        assert_eq!(NameClass::AnyName(None).describe(), "*");
1979
1980        let n3 = NameClass::NsName {
1981            namespace: "urn:y".into(),
1982            except: None,
1983        };
1984        assert_eq!(n3.describe(), "{urn:y}*");
1985
1986        let n4 = NameClass::Choice(
1987            Box::new(NameClass::Name { namespace: String::new(), local: "a".into() }),
1988            Box::new(NameClass::Name { namespace: String::new(), local: "b".into() }),
1989        );
1990        assert_eq!(n4.describe(), "a | b");
1991
1992        assert_eq!(NameClass::Nothing.describe(), "<nothing>");
1993    }
1994
1995    // ── attribute derivative paths: Choice / Group / Interleave / OneOrMore ──
1996
1997    #[test]
1998    fn attribute_choice_of_two_attrs() {
1999        let s = schema(
2000            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2001                 <choice>
2002                   <attribute name="x"><text/></attribute>
2003                   <attribute name="y"><text/></attribute>
2004                 </choice>
2005               </element>"#,
2006        );
2007        validate(&s, &doc(r#"<r x="1"/>"#)).unwrap();
2008        validate(&s, &doc(r#"<r y="2"/>"#)).unwrap();
2009        assert!(validate(&s, &doc(r#"<r z="3"/>"#)).is_err());
2010    }
2011
2012    #[test]
2013    fn attribute_group_requires_both() {
2014        let s = schema(
2015            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2016                 <group>
2017                   <attribute name="x"><text/></attribute>
2018                   <attribute name="y"><text/></attribute>
2019                 </group>
2020               </element>"#,
2021        );
2022        validate(&s, &doc(r#"<r x="1" y="2"/>"#)).unwrap();
2023        validate(&s, &doc(r#"<r y="2" x="1"/>"#)).unwrap();
2024        assert!(validate(&s, &doc(r#"<r x="1"/>"#)).is_err());
2025    }
2026
2027    #[test]
2028    fn attribute_interleave_accepts_any_order() {
2029        let s = schema(
2030            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2031                 <interleave>
2032                   <attribute name="x"><text/></attribute>
2033                   <attribute name="y"><text/></attribute>
2034                 </interleave>
2035               </element>"#,
2036        );
2037        validate(&s, &doc(r#"<r x="1" y="2"/>"#)).unwrap();
2038        validate(&s, &doc(r#"<r y="2" x="1"/>"#)).unwrap();
2039    }
2040
2041    #[test]
2042    fn attribute_one_or_more_with_named_attrs() {
2043        // <oneOrMore><attribute><anyName/><text/></attribute></oneOrMore>
2044        // matches any number of attributes.
2045        let s = schema(
2046            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2047                 <oneOrMore>
2048                   <attribute><anyName/><text/></attribute>
2049                 </oneOrMore>
2050               </element>"#,
2051        );
2052        validate(&s, &doc(r#"<r a="1"/>"#)).unwrap();
2053        validate(&s, &doc(r#"<r a="1" b="2" c="3"/>"#)).unwrap();
2054        assert!(validate(&s, &doc("<r/>")).is_err(), "oneOrMore requires ≥1");
2055    }
2056
2057    // ── value{} datatype on an attribute ─────────────────────────
2058
2059    #[test]
2060    fn attribute_value_constraint() {
2061        let s = schema(
2062            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2063                 <attribute name="kind">
2064                   <choice>
2065                     <value>a</value>
2066                     <value>b</value>
2067                   </choice>
2068                 </attribute>
2069               </element>"#,
2070        );
2071        validate(&s, &doc(r#"<r kind="a"/>"#)).unwrap();
2072        validate(&s, &doc(r#"<r kind="b"/>"#)).unwrap();
2073        assert!(validate(&s, &doc(r#"<r kind="c"/>"#)).is_err());
2074    }
2075
2076    // ── nsName / anyName with <except> ──────────────────────────
2077
2078    #[test]
2079    fn ns_name_with_except() {
2080        // RelaxNG spec § 7: <nsName> with <except> excludes specific
2081        // names within the namespace.
2082        let s = schema(
2083            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="root">
2084                 <oneOrMore>
2085                   <element>
2086                     <nsName ns="urn:n">
2087                       <except><name ns="urn:n">forbidden</name></except>
2088                     </nsName>
2089                     <text/>
2090                   </element>
2091                 </oneOrMore>
2092               </element>"#,
2093        );
2094        validate(&s, &doc(r#"<root><a xmlns="urn:n">1</a></root>"#)).unwrap();
2095        assert!(validate(&s,
2096            &doc(r#"<root><forbidden xmlns="urn:n">1</forbidden></root>"#))
2097            .is_err());
2098    }
2099
2100    #[test]
2101    fn any_name_with_except() {
2102        // <anyName> with <except> excludes specific names from the
2103        // "match anything" set.
2104        let s = schema(
2105            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="root">
2106                 <oneOrMore>
2107                   <element>
2108                     <anyName>
2109                       <except><name>banned</name></except>
2110                     </anyName>
2111                     <text/>
2112                   </element>
2113                 </oneOrMore>
2114               </element>"#,
2115        );
2116        validate(&s, &doc("<root><a>1</a><b>2</b></root>")).unwrap();
2117        assert!(validate(&s, &doc("<root><banned>1</banned></root>")).is_err());
2118    }
2119
2120    // ── choice with text+element (mixed-like) ────────────────────
2121
2122    #[test]
2123    fn nested_group_with_optional_middle() {
2124        let s = schema(
2125            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2126                 <group>
2127                   <element name="head"><text/></element>
2128                   <optional><element name="body"><text/></element></optional>
2129                   <element name="foot"><text/></element>
2130                 </group>
2131               </element>"#,
2132        );
2133        validate(&s, &doc("<r><head>h</head><foot>f</foot></r>")).unwrap();
2134        validate(&s,
2135            &doc("<r><head>h</head><body>b</body><foot>f</foot></r>")).unwrap();
2136        assert!(validate(&s, &doc("<r><head>h</head></r>")).is_err());
2137    }
2138
2139    // ── grammar with broken ref → error at parse or validate ────
2140
2141    #[test]
2142    fn ref_to_undefined_name_errors_at_validate() {
2143        // Schema parses (ref captured) but validating against it must fail
2144        // when the ref is looked up.
2145        let result = parse_schema(
2146            r#"<grammar xmlns="http://relaxng.org/ns/structure/1.0">
2147                 <start><element name="r"><ref name="undefined"/></element></start>
2148               </grammar>"#,
2149        );
2150        // Implementation may catch this at parse OR at validate; we
2151        // accept either (the line we want to cover is the
2152        // `None => p_not_allowed()` arm in deriv).
2153        if let Ok(s) = result {
2154            assert!(validate(&s, &doc("<r>x</r>")).is_err());
2155        }
2156    }
2157
2158    // ── list_of_tokens variations ───────────────────────────────
2159
2160    #[test]
2161    fn list_of_data_typed_tokens() {
2162        let s = schema(
2163            r#"<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
2164                 <list>
2165                   <oneOrMore><data type="string"/></oneOrMore>
2166                 </list>
2167               </element>"#,
2168        );
2169        validate(&s, &doc("<r>a b c</r>")).unwrap();
2170        validate(&s, &doc("<r>single</r>")).unwrap();
2171    }
2172}