Skip to main content

relax_ng/
validator.rs

1//! Validates a document (via the generic [`Element`] trait) against a
2//! [`CompiledSchema`](crate::CompiledSchema) using Brzozowski-derivative
3//! pattern matching — the standard approach for RELAX NG validators,
4//! derived independently from the pattern algebra in the spec (not ported
5//! from any other implementation; see `plan/DECISIONS.md`).
6//!
7//! # The derivative idea
8//!
9//! A pattern describes the set of "things" (child sequences, attribute
10//! sets, text values) it accepts. Given one such thing has just been seen
11//! (an attribute, a child element, a run of text), the *derivative* of the
12//! pattern with respect to it is a new pattern describing what must still
13//! follow. A pattern is *nullable* if it accepts having nothing more
14//! follow at all. Validating a whole element reduces to: derive its
15//! content pattern through every attribute (unordered) and then every
16//! child in order, and check the result is nullable.
17//!
18//! Every derivative function here transparently follows `ref` — including
19//! through `interleave` — which is deliberate: the most serious bug found
20//! in the `xmloxide` RELAX NG parser (the reason this crate exists at all,
21//! see `plan/DECISIONS.md`) was exactly a `ref` inside `interleave` not
22//! being resolved for attribute matching, rejecting almost every attribute
23//! in a modular schema. [`attribute_matches_through_ref_and_interleave`]
24//! (in the test module) is a standing regression test for that failure
25//! mode.
26//!
27//! # Datatypes
28//!
29//! `data`/`value` matching is delegated to a [`DatatypeRegistry`] the
30//! caller supplies to [`validate`] — this module never hardcodes datatype
31//! semantics itself (see `crate::datatypes`). Every `data`/`value`
32//! reachable in the schema is checked against the registry once, upfront,
33//! before any document is touched, so an unregistered `datatypeLibrary` or
34//! an invalid facet fails fast with a single, clear [`DatatypeError`]
35//! rather than partway through validation.
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::fmt;
39
40use crate::CompiledSchema;
41use crate::datatypes::{DatatypeContext, DatatypeError, DatatypeRegistry};
42use crate::grammar::{Context, Param, Pattern};
43use crate::simplify::{choice, group, interleave, name_class_matches};
44
45type Definitions = BTreeMap<String, Pattern>;
46
47/// An expanded (namespace-resolved) name: local name plus an optional
48/// namespace URI. `None` means "no namespace" — the caller's document
49/// implementation is responsible for resolving any prefixes itself (this
50/// crate has no XML/namespace parser of its own).
51#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
52pub struct ExpandedName {
53    /// The namespace URI, or `None` for "no namespace".
54    pub namespace: Option<String>,
55    /// The local (unprefixed) name.
56    pub local: String,
57}
58
59impl ExpandedName {
60    /// Builds an expanded name from a namespace (`None` for "no
61    /// namespace") and a local name.
62    pub fn new(namespace: Option<impl Into<String>>, local: impl Into<String>) -> Self {
63        Self {
64            namespace: namespace.map(Into::into),
65            local: local.into(),
66        }
67    }
68}
69
70impl fmt::Display for ExpandedName {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match &self.namespace {
73            Some(namespace) => write!(formatter, "{{{namespace}}}{}", self.local),
74            None => formatter.write_str(&self.local),
75        }
76    }
77}
78
79/// One child of an element, in document order. Comments and processing
80/// instructions are not part of this model — a correct `Element`
81/// implementation simply never yields them here.
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub enum Content<E> {
84    /// A child element.
85    Element(E),
86    /// A single, non-empty, already-merged run of text — if two text
87    /// nodes are adjacent in the underlying document (however "adjacent"
88    /// is defined there, e.g. around a comment), the implementation must
89    /// merge them into one `Text` before they reach here.
90    Text(String),
91}
92
93/// The minimal view of a document element this crate needs to validate it.
94/// Implement this over whatever tree type the caller already has — no
95/// document is parsed by this crate itself.
96pub trait Element: Sized {
97    /// Whatever shape of location this implementation can report per
98    /// element — a formatted `String` (`"line:column"` or an XPath-like
99    /// pointer), a structured `{line, column, byte_offset}` struct the
100    /// caller defines, or `()` for callers with no location tracking at
101    /// all. Carried through unchanged into any [`ValidationError`] raised
102    /// against the element, so callers with real position data don't
103    /// have to re-parse a formatted string to get it back out — see
104    /// [`ValidationError::location`].
105    type Location;
106    /// This element's expanded name.
107    fn name(&self) -> ExpandedName;
108    /// This element's attributes, in any order (attribute order is never
109    /// significant in XML, and RELAX NG never treats it as such either).
110    /// `xmlns`/`xmlns:*` namespace declarations are not attributes for
111    /// RELAX NG's purposes and must not be yielded here.
112    fn attributes(&self) -> impl Iterator<Item = (ExpandedName, String)>;
113    /// This element's children, in document order.
114    fn children(&self) -> impl Iterator<Item = Content<Self>>;
115    /// An optional location, included in any [`ValidationError`] raised
116    /// against this element when available.
117    fn location(&self) -> Option<Self::Location> {
118        None
119    }
120    /// The XML namespace prefix bindings in scope at this element
121    /// (`("", uri)` for the default namespace), including any inherited
122    /// from ancestors — needed only to resolve `QName`/`NOTATION`-typed
123    /// attribute/text *values* (not element/attribute names, which arrive
124    /// already-expanded via [`Self::name`]). Default: no bindings, so
125    /// `QName`/`NOTATION` values only match when unprefixed and the
126    /// default namespace (if any) is what the schema expects.
127    fn namespace_bindings(&self) -> impl Iterator<Item = (String, String)> {
128        std::iter::empty()
129    }
130}
131
132/// Validates `root` against `schema` using `registry` to check `data`/
133/// `value` patterns. Returns the list of problems found (empty means
134/// valid) — or, if the schema uses a `datatypeLibrary`/type/parameter
135/// `registry` doesn't recognize, a [`DatatypeError`] *instead of*
136/// attempting validation at all, since we can't know whether the document
137/// would pass a check we can't perform.
138pub fn validate<E: Element>(
139    schema: &CompiledSchema,
140    registry: &DatatypeRegistry,
141    root: &E,
142) -> Result<Vec<ValidationError<E::Location>>, DatatypeError> {
143    let mut checked_refs = BTreeSet::new();
144    check_datatypes_supported(
145        &schema.start,
146        &schema.definitions,
147        registry,
148        &mut checked_refs,
149    )?;
150    for pattern in schema.definitions.values() {
151        check_datatypes_supported(pattern, &schema.definitions, registry, &mut checked_refs)?;
152    }
153
154    let mut errors = Vec::new();
155    let mut path = Vec::new();
156    match find_element_contents(&schema.start, &root.name(), &schema.definitions) {
157        Some(content) => validate_element_content(
158            &content,
159            root,
160            &schema.definitions,
161            registry,
162            &mut path,
163            &mut errors,
164        ),
165        None => errors.push(ValidationError {
166            kind: ValidationErrorKind::UnexpectedElement(root.name()),
167            path: Vec::new(),
168            location: root.location(),
169        }),
170    }
171    Ok(errors)
172}
173
174/// A single validation problem, with the path of element names from the
175/// document root down to (and including) the element it occurred on, and
176/// an optional location if [`Element::location`] provided one.
177///
178/// Generic over `L` (== the reporting [`Element`] implementation's own
179/// [`Element::Location`]) rather than hardcoding a formatted `String`, so
180/// a caller with real per-node position data (line/column/byte offset,
181/// or anything else) gets it back out structured, not as text to
182/// re-parse.
183#[derive(Clone, Debug, Eq, PartialEq)]
184pub struct ValidationError<L> {
185    kind: ValidationErrorKind,
186    path: Vec<ExpandedName>,
187    location: Option<L>,
188}
189
190impl<L> ValidationError<L> {
191    /// What kind of problem this is.
192    pub fn kind(&self) -> &ValidationErrorKind {
193        &self.kind
194    }
195    /// The element names from the document root down to (and including)
196    /// the element this problem occurred on.
197    pub fn path(&self) -> &[ExpandedName] {
198        &self.path
199    }
200    /// The location [`Element::location`] provided for the affected
201    /// element, if any.
202    pub fn location(&self) -> Option<&L> {
203        self.location.as_ref()
204    }
205}
206
207impl<L: fmt::Display> fmt::Display for ValidationError<L> {
208    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209        write!(formatter, "{}", self.kind)?;
210        if let Some(location) = &self.location {
211            write!(formatter, " at {location}")?;
212        }
213        Ok(())
214    }
215}
216
217impl<L: fmt::Debug + fmt::Display> std::error::Error for ValidationError<L> {}
218
219/// What kind of problem a [`ValidationError`] describes.
220#[derive(Clone, Debug, Eq, PartialEq)]
221pub enum ValidationErrorKind {
222    /// An element with this name isn't allowed at this position.
223    UnexpectedElement(ExpandedName),
224    /// A text child isn't allowed at this position (and isn't part of a
225    /// `data`/`value`/`list` that could accept it).
226    UnexpectedText,
227    /// This attribute name isn't allowed on this element at all.
228    UnexpectedAttribute(ExpandedName),
229    /// This attribute is allowed on this element, but its value doesn't
230    /// match what the schema requires.
231    InvalidAttributeValue(ExpandedName, String),
232    /// The element ended while the schema still required more content
233    /// (a missing child element, missing text, or a missing attribute).
234    MissingContent,
235}
236
237impl fmt::Display for ValidationErrorKind {
238    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match self {
240            Self::UnexpectedElement(name) => write!(formatter, "unexpected element `{name}`"),
241            Self::UnexpectedText => formatter.write_str("unexpected text"),
242            Self::UnexpectedAttribute(name) => write!(formatter, "unexpected attribute `{name}`"),
243            Self::InvalidAttributeValue(name, value) => {
244                write!(formatter, "invalid value `{value}` for attribute `{name}`")
245            }
246            Self::MissingContent => formatter.write_str("missing required content"),
247        }
248    }
249}
250
251fn validate_element_content<E: Element>(
252    content: &Pattern,
253    element: &E,
254    definitions: &Definitions,
255    registry: &DatatypeRegistry,
256    path: &mut Vec<ExpandedName>,
257    errors: &mut Vec<ValidationError<E::Location>>,
258) {
259    path.push(element.name());
260    let mut pattern = content.clone();
261    let context = DatatypeContext::from_bindings(element.namespace_bindings().collect());
262
263    // Attributes are unordered — fold the derivative over them in
264    // whatever order the caller's iterator happens to yield.
265    for (name, value) in element.attributes() {
266        if !attribute_name_known(&pattern, &name, definitions) {
267            errors.push(ValidationError {
268                kind: ValidationErrorKind::UnexpectedAttribute(name),
269                path: path.clone(),
270                location: element.location(),
271            });
272            continue;
273        }
274        let derived = deriv_attribute(&pattern, &name, &value, definitions, registry, &context);
275        if matches!(derived, Pattern::NotAllowed) {
276            errors.push(ValidationError {
277                kind: ValidationErrorKind::InvalidAttributeValue(name, value),
278                path: path.clone(),
279                location: element.location(),
280            });
281        } else {
282            pattern = derived;
283        }
284    }
285
286    // Children are ordered.
287    let children: Vec<_> = element.children().collect();
288    // §6.2.7 weak match: next to other children, whitespace-only text is
289    // stripped before matching, not offered to the pattern. Offering it
290    // lets a `text` further on consume it and so skip an optional element
291    // that has to come first — `<fieldset> <legend>` would lose its legend.
292    let strip_whitespace = children.len() > 1;
293    for child in children {
294        match child {
295            Content::Text(text) if strip_whitespace && text.trim().is_empty() => {}
296            Content::Text(text) => {
297                let derived = deriv_text(&pattern, &text, definitions, registry, &context);
298                if matches!(derived, Pattern::NotAllowed) {
299                    // §6.2.7: a text node that consists entirely of
300                    // whitespace is treated as if it weren't there at all
301                    // when it doesn't otherwise fit the content model —
302                    // but *only* if nothing reachable there actually wants
303                    // text (`wants_text`); a bare `data`/`value` (or a
304                    // `choice` where every branch wants text) still must
305                    // see a real, matching event. Non-whitespace text that
306                    // doesn't fit is always an error.
307                    if !text.trim().is_empty() || wants_text(&pattern, definitions) {
308                        errors.push(ValidationError {
309                            kind: ValidationErrorKind::UnexpectedText,
310                            path: path.clone(),
311                            location: element.location(),
312                        });
313                    }
314                } else {
315                    pattern = derived;
316                }
317            }
318            Content::Element(child_element) => {
319                let name = child_element.name();
320                match find_element_contents(&pattern, &name, definitions) {
321                    Some(child_content) => {
322                        validate_element_content(
323                            &child_content,
324                            &child_element,
325                            definitions,
326                            registry,
327                            path,
328                            errors,
329                        );
330                        pattern = deriv_element_event(&pattern, &name, definitions);
331                    }
332                    None => errors.push(ValidationError {
333                        kind: ValidationErrorKind::UnexpectedElement(name),
334                        path: path.clone(),
335                        location: child_element.location(),
336                    }),
337                }
338            }
339        }
340    }
341
342    if !nullable(&pattern, definitions, registry) {
343        errors.push(ValidationError {
344            kind: ValidationErrorKind::MissingContent,
345            path: path.clone(),
346            location: element.location(),
347        });
348    }
349    path.pop();
350}
351
352fn param_pairs(params: &[Param]) -> Vec<(&str, &str)> {
353    params
354        .iter()
355        .map(|param| (param.name.as_str(), param.value.as_str()))
356        .collect()
357}
358
359/// The namespace-resolution context for a `value` pattern's *own* literal
360/// text, as written in the schema — used as the "expected" side of
361/// [`DatatypeLibrary::values_equal`] for `QName`/`NOTATION`-like types.
362/// Deliberately built from the schema, never the document: `namespace` is
363/// §4.9's (already-inherited) `ns` attribute, used to resolve the literal
364/// if it's unprefixed; `schema_context.namespaces` are the schema XML's
365/// own in-scope `xmlns:` bindings, used to resolve it if it has a prefix.
366fn schema_value_context(namespace: &Option<String>, schema_context: &Context) -> DatatypeContext {
367    let mut bindings: Vec<(String, String)> = schema_context
368        .namespaces
369        .iter()
370        .map(|(prefix, uri)| (prefix.clone(), uri.clone()))
371        .collect();
372    bindings.push((String::new(), namespace.clone().unwrap_or_default()));
373    DatatypeContext::from_bindings(bindings)
374}
375
376/// Every `data`/`value` reachable in the schema must use a `datatypeLibrary`
377/// `registry` has an implementation for, with a known type name and
378/// well-formed, applicable parameters — checked once, upfront, over the
379/// *whole* schema (not lazily as validation happens to reach one), so the
380/// caller gets a clear, consistent error regardless of which document path
381/// would have triggered it.
382fn check_datatypes_supported(
383    pattern: &Pattern,
384    definitions: &Definitions,
385    registry: &DatatypeRegistry,
386    checked_refs: &mut BTreeSet<String>,
387) -> Result<(), DatatypeError> {
388    match pattern {
389        Pattern::Data {
390            datatype,
391            datatype_library,
392            params,
393            except,
394            ..
395        } => {
396            let library = datatype_library.as_deref().unwrap_or("");
397            let implementation = registry
398                .get(library)
399                .ok_or_else(|| DatatypeError::unsupported_library(library))?;
400            implementation
401                .validate_params(datatype, &param_pairs(params))
402                .map_err(|reason| DatatypeError::type_problem(library, datatype, &reason))?;
403            for child in except {
404                check_datatypes_supported(child, definitions, registry, checked_refs)?;
405            }
406            Ok(())
407        }
408        Pattern::Value {
409            datatype,
410            datatype_library,
411            ..
412        } => {
413            let library = datatype_library.as_deref().unwrap_or("");
414            let type_name = datatype.as_deref().unwrap_or("token");
415            let implementation = registry
416                .get(library)
417                .ok_or_else(|| DatatypeError::unsupported_library(library))?;
418            implementation
419                .validate_params(type_name, &[])
420                .map_err(|reason| DatatypeError::type_problem(library, type_name, &reason))
421        }
422        Pattern::Element { body, .. } | Pattern::Attribute { body, .. } => {
423            body.iter().try_for_each(|child| {
424                check_datatypes_supported(child, definitions, registry, checked_refs)
425            })
426        }
427        Pattern::Group(values)
428        | Pattern::Interleave(values)
429        | Pattern::Choice(values)
430        | Pattern::OneOrMore(values)
431        | Pattern::List(values) => values.iter().try_for_each(|child| {
432            check_datatypes_supported(child, definitions, registry, checked_refs)
433        }),
434        Pattern::Ref(name) => {
435            if checked_refs.insert(name.clone())
436                && let Some(target) = definitions.get(name)
437            {
438                check_datatypes_supported(target, definitions, registry, checked_refs)?;
439            }
440            Ok(())
441        }
442        Pattern::Empty | Pattern::NotAllowed | Pattern::Text => Ok(()),
443        Pattern::Optional(_)
444        | Pattern::ZeroOrMore(_)
445        | Pattern::Mixed(_)
446        | Pattern::ExternalRef { .. }
447        | Pattern::ParentRef(_)
448        | Pattern::Grammar(_) => {
449            unreachable!("resolved into other patterns by simplify_pattern")
450        }
451    }
452}
453
454/// Looks up a `ref`'s target definition — every `deriv_*`/`nullable`-family
455/// function below follows `ref` transparently and unconditionally, which is
456/// safe (never infinitely recurses) only because RELAX NG §4.19 is checked
457/// at schema-compile time, before any document is ever validated, and
458/// guarantees both that `name` exists and that no such chain of `ref`s can
459/// reach itself again without first crossing an `element`.
460fn resolve_ref<'a>(name: &str, definitions: &'a Definitions) -> &'a Pattern {
461    definitions
462        .get(name)
463        .expect("RELAX NG §4.18 guarantees every ref target exists")
464}
465
466/// Whether `pattern` accepts having nothing more follow — i.e. whether the
467/// content/attributes/text matched *so far* (already folded into
468/// `pattern` via the various `deriv_*` functions below) are already
469/// enough. `ref` is always resolved: recursion through it is guaranteed to
470/// terminate because RELAX NG §4.19 (checked at schema-compile time,
471/// before any document is ever validated) already rules out a `ref`
472/// reaching itself again without crossing an `element` — and none of the
473/// constructs `nullable` recurses through (`group`/`choice`/`interleave`/
474/// `oneOrMore`/`ref`/`list`) is an `element`. `data`/`value` nullability
475/// asks whether `registry` would accept the implicit empty string (e.g.
476/// the built-in `string`/`token` do; most XSD types don't).
477pub(crate) fn nullable(
478    pattern: &Pattern,
479    definitions: &Definitions,
480    registry: &DatatypeRegistry,
481) -> bool {
482    match pattern {
483        Pattern::Empty => true,
484        Pattern::NotAllowed => false,
485        Pattern::Text => true,
486        Pattern::Element { .. } | Pattern::Attribute { .. } => false,
487        Pattern::Data {
488            datatype,
489            datatype_library,
490            params,
491            except,
492            ..
493        } => {
494            let library = datatype_library.as_deref().unwrap_or("");
495            let implementation = registry
496                .get(library)
497                .expect("checked upfront by check_datatypes_supported");
498            implementation.matches(
499                datatype,
500                &param_pairs(params),
501                "",
502                &DatatypeContext::empty(),
503            ) && !text_matches_except(except, "", definitions, registry, &DatatypeContext::empty())
504        }
505        Pattern::Value {
506            value,
507            datatype,
508            datatype_library,
509            namespace,
510            context: schema_context,
511        } => {
512            let library = datatype_library.as_deref().unwrap_or("");
513            let type_name = datatype.as_deref().unwrap_or("token");
514            let implementation = registry
515                .get(library)
516                .expect("checked upfront by check_datatypes_supported");
517            implementation.values_equal(
518                type_name,
519                value,
520                &schema_value_context(namespace, schema_context),
521                "",
522                &DatatypeContext::empty(),
523            )
524        }
525        // A `list` with no text child at all behaves like its inner
526        // pattern seeing zero tokens — same as `list_matches` below would
527        // compute for empty/whitespace-only text, without needing an
528        // actual `Content::Text` event to run that logic. Not `nullable`
529        // (rule-3-inclusive): a `list` around a bare `data`/`value` slot
530        // still needs an actual token, even though that datatype alone
531        // would also tolerate the literal empty string — see
532        // `structurally_nullable`'s docs.
533        Pattern::List(values) => structurally_nullable(&values[0], definitions),
534        Pattern::Choice(values) => values
535            .iter()
536            .any(|value| nullable(value, definitions, registry)),
537        Pattern::Group(values) | Pattern::Interleave(values) => values
538            .iter()
539            .all(|value| nullable(value, definitions, registry)),
540        Pattern::OneOrMore(values) => nullable(&values[0], definitions, registry),
541        Pattern::Ref(name) => nullable(resolve_ref(name, definitions), definitions, registry),
542        Pattern::Optional(_)
543        | Pattern::ZeroOrMore(_)
544        | Pattern::Mixed(_)
545        | Pattern::ExternalRef { .. }
546        | Pattern::ParentRef(_)
547        | Pattern::Grammar(_) => {
548            unreachable!("resolved into other patterns by simplify_pattern")
549        }
550    }
551}
552
553/// Derives `values` (the operands of a `group`/`interleave`/attribute-
554/// matching-`group`) with respect to one event, trying every position
555/// `deriv_one` succeeds at and rebuilding the container (`wrap`) around
556/// whichever one operand changed, leaving the rest untouched. If
557/// `sequential`, a position is only reachable once every earlier operand's
558/// *child content* is satisfied (`group`'s ordered semantics for
559/// element/text content, gated by [`child_reachable`] rather than plain
560/// [`nullable`] — see there for why); if not, every position is always
561/// reachable (`interleave`'s semantics — and, deliberately, `group`'s
562/// *attribute*-matching semantics too, since unlike child content,
563/// attributes have no order in XML at all).
564fn deriv_positions(
565    values: &[Pattern],
566    sequential: bool,
567    wrap: fn(Vec<Pattern>) -> Pattern,
568    definitions: &Definitions,
569    deriv_one: &dyn Fn(&Pattern) -> Pattern,
570) -> Pattern {
571    let mut alternatives = Vec::new();
572    let mut reachable = true;
573    for (index, value) in values.iter().enumerate() {
574        if reachable {
575            let derived = deriv_one(value);
576            if !matches!(derived, Pattern::NotAllowed) {
577                let mut branch = values.to_vec();
578                if sequential {
579                    // Standard Brzozowski concatenation-derivative rule:
580                    // D_c(A·B)'s "skip A" branch (taken because A is
581                    // already nullable) is exactly D_c(B) — A itself is
582                    // *closed*, fixed to `Empty`, not left as A. Left
583                    // unchanged, A would wrongly stay available to match
584                    // *more* events later, after B has already started
585                    // (e.g. `group(text, element bar)` would let text
586                    // reappear after `bar`, which isn't sequential at
587                    // all). Interleave/attribute-matching (`!sequential`)
588                    // has no such "prefix must close" concept — every
589                    // other position, before or after `index`, genuinely
590                    // does stay open regardless of order.
591                    for earlier in branch.iter_mut().take(index) {
592                        *earlier = close_child_position(earlier, definitions);
593                    }
594                }
595                branch[index] = derived;
596                alternatives.push(wrap(branch));
597            }
598        }
599        if sequential {
600            reachable = reachable && structurally_nullable(value, definitions);
601        }
602    }
603    choice(alternatives)
604}
605
606/// Whether `pattern` is satisfied by *zero* events, in the strict
607/// structural sense — unlike [`nullable`], a bare `data`/`value`/`list`
608/// (which structurally requires exactly one text event to occur at all)
609/// is never considered satisfied here just because it happens to accept
610/// the *literal empty string* as a value (that narrower equivalence is
611/// §6.2.7 weak-match rule 3, and belongs only at the true top level —
612/// deciding whether a whole element/attribute that saw *no* relevant
613/// event at all is still valid, which [`nullable`] already handles).
614///
615/// This stricter notion is what §6.2.7 weak-match rule 2 actually needs
616/// (dropping a whitespace-only text/attribute event requires the pattern
617/// to *already*, strictly, accept zero events — not "would accept the
618/// empty string if offered one"), what deciding whether a `list` has
619/// enough tokens needs (a `list` with a `data`/`value` slot no token ever
620/// reached is not satisfied just because that slot's datatype tolerates
621/// `""`), and what gates `group`'s sequential "skip a satisfied-so-far
622/// prefix" logic in [`deriv_positions`] (an `attribute` operand is the one
623/// exception: attributes are orthogonal to child/token sequencing
624/// entirely, matched separately by [`deriv_attribute`]).
625fn structurally_nullable(pattern: &Pattern, definitions: &Definitions) -> bool {
626    match pattern {
627        Pattern::Attribute { .. } | Pattern::Empty | Pattern::Text => true,
628        Pattern::NotAllowed
629        | Pattern::Element { .. }
630        | Pattern::Data { .. }
631        | Pattern::Value { .. } => false,
632        Pattern::List(values) => structurally_nullable(&values[0], definitions),
633        Pattern::Choice(values) => values
634            .iter()
635            .any(|value| structurally_nullable(value, definitions)),
636        Pattern::Group(values) | Pattern::Interleave(values) => values
637            .iter()
638            .all(|value| structurally_nullable(value, definitions)),
639        Pattern::OneOrMore(values) => structurally_nullable(&values[0], definitions),
640        Pattern::Ref(name) => structurally_nullable(resolve_ref(name, definitions), definitions),
641        Pattern::Optional(_)
642        | Pattern::ZeroOrMore(_)
643        | Pattern::Mixed(_)
644        | Pattern::ExternalRef { .. }
645        | Pattern::ParentRef(_)
646        | Pattern::Grammar(_) => {
647            unreachable!("resolved into other patterns by simplify_pattern")
648        }
649    }
650}
651
652/// Whether some *reachable* position in `pattern` is a text-desiring leaf
653/// (`text`/`data`/`value`/`list`) that a whitespace-only text/attribute
654/// event, once it fails to match normally, should be treated as a real
655/// (failed) candidate for — rather than silently dropped as
656/// formatting/insignificant whitespace (§6.2.7). "Reachable" mirrors
657/// `deriv_positions`'s own sequential-prefix gating for `group`
658/// ([`structurally_nullable`] decides what counts as "already past"); for
659/// `choice`, a branch not taken is never a candidate for *this* event, so
660/// only refuse to drop when *every* branch wants text — if even one
661/// doesn't, that's the branch this whitespace can harmlessly fall through
662/// to (e.g. `choice(value "x", element bar {empty})`: leading whitespace
663/// before `<bar/>` is fine, even though the *other* branch wants text).
664/// `interleave`/`group`, by contrast, require *every* operand to
665/// eventually be satisfied (unordered or ordered respectively), so any
666/// one of them still wanting text is enough to not drop.
667fn wants_text(pattern: &Pattern, definitions: &Definitions) -> bool {
668    match pattern {
669        Pattern::Text | Pattern::Data { .. } | Pattern::Value { .. } | Pattern::List(_) => true,
670        Pattern::Attribute { .. }
671        | Pattern::Empty
672        | Pattern::NotAllowed
673        | Pattern::Element { .. } => false,
674        Pattern::Choice(values) => values.iter().all(|value| wants_text(value, definitions)),
675        Pattern::Interleave(values) => values.iter().any(|value| wants_text(value, definitions)),
676        Pattern::Group(values) => {
677            let mut reachable = true;
678            let mut found = false;
679            for value in values {
680                if reachable {
681                    found = found || wants_text(value, definitions);
682                }
683                reachable = reachable && structurally_nullable(value, definitions);
684            }
685            found
686        }
687        Pattern::OneOrMore(values) => wants_text(&values[0], definitions),
688        Pattern::Ref(name) => wants_text(resolve_ref(name, definitions), definitions),
689        Pattern::Optional(_)
690        | Pattern::ZeroOrMore(_)
691        | Pattern::Mixed(_)
692        | Pattern::ExternalRef { .. }
693        | Pattern::ParentRef(_)
694        | Pattern::Grammar(_) => {
695            unreachable!("resolved into other patterns by simplify_pattern")
696        }
697    }
698}
699
700/// The "closed" form of a `group` position that a later position's event
701/// was matched *past* (see [`deriv_positions`]'s `sequential` case):
702/// standard Brzozowski concatenation-derivative rule, D_c(A·B)'s "skip A"
703/// branch is exactly D_c(B), not A·D_c(B) — so A is fixed to `Empty`
704/// (done, contributing nothing further to *child* matching), not left
705/// open to match more child events later. `attribute` sub-patterns are the
706/// one exception: they're orthogonal to child sequencing (matched
707/// entirely separately, see [`deriv_attribute`]) and must stay exactly as
708/// they were — closing one here would wrongly mark a not-yet-matched
709/// attribute as satisfied.
710fn close_child_position(pattern: &Pattern, definitions: &Definitions) -> Pattern {
711    match pattern {
712        Pattern::Attribute { .. } => pattern.clone(),
713        Pattern::Group(values) => group(
714            values
715                .iter()
716                .map(|value| close_child_position(value, definitions))
717                .collect(),
718        ),
719        Pattern::Interleave(values) => interleave(
720            values
721                .iter()
722                .map(|value| close_child_position(value, definitions))
723                .collect(),
724        ),
725        Pattern::Choice(values) => choice(
726            values
727                .iter()
728                .map(|value| close_child_position(value, definitions))
729                .collect(),
730        ),
731        Pattern::OneOrMore(values) => close_child_position(&values[0], definitions),
732        Pattern::Ref(name) => close_child_position(resolve_ref(name, definitions), definitions),
733        _ => Pattern::Empty,
734    }
735}
736
737/// A `Choice`'s derivative: every operand derived independently, discarding
738/// (via [`choice`]) whichever branches turned `notAllowed` — shared by
739/// [`deriv_attribute`]/[`deriv_element_event`]/[`deriv_text`], which only
740/// differ in what `deriv_one` does.
741fn deriv_choice(values: &[Pattern], deriv_one: impl Fn(&Pattern) -> Pattern) -> Pattern {
742    choice(values.iter().map(deriv_one).collect())
743}
744
745/// The derivative structural step shared by every `deriv_*` function below:
746/// `group`/`interleave`/`choice`/`oneOrMore`/`ref` all recurse exactly the
747/// same way no matter which kind of event (`attribute`/element/text) is
748/// being derived — only `deriv_one` (what to do with one operand/the `ref`
749/// target) and, for `group`, `sequential` (see [`deriv_positions`]) differ
750/// per event kind. `interleave` is always non-sequential regardless of
751/// event kind — unlike `group`'s child-content ordering, attributes have no
752/// order in XML at all, and neither does `interleave`'s own definition.
753/// Returns `None` for every other pattern — its own matching leaf variant,
754/// or one that's always `notAllowed` for this event kind — which the caller
755/// handles itself.
756fn deriv_structural(
757    pattern: &Pattern,
758    group_sequential: bool,
759    definitions: &Definitions,
760    deriv_one: &dyn Fn(&Pattern) -> Pattern,
761) -> Option<Pattern> {
762    Some(match pattern {
763        Pattern::Group(values) => {
764            deriv_positions(values, group_sequential, group, definitions, deriv_one)
765        }
766        Pattern::Interleave(values) => {
767            deriv_positions(values, false, interleave, definitions, deriv_one)
768        }
769        Pattern::Choice(values) => deriv_choice(values, deriv_one),
770        Pattern::OneOrMore(values) => one_or_more_deriv(&values[0], pattern, deriv_one),
771        Pattern::Ref(name) => deriv_one(resolve_ref(name, definitions)),
772        _ => return None,
773    })
774}
775
776/// The derivative of `pattern` with respect to one *attribute* named
777/// `name` with string value `value`. Transparently follows `ref` — see
778/// the module docs — and, critically, treats `group` the same as
779/// `interleave` (attributes have no order), unlike [`deriv_element_event`]
780/// and [`deriv_text`], where `group` really is sequential.
781fn deriv_attribute(
782    pattern: &Pattern,
783    name: &ExpandedName,
784    value: &str,
785    definitions: &Definitions,
786    registry: &DatatypeRegistry,
787    context: &DatatypeContext,
788) -> Pattern {
789    let deriv_one = |v: &Pattern| deriv_attribute(v, name, value, definitions, registry, context);
790    if let Some(result) = deriv_structural(pattern, false, definitions, &deriv_one) {
791        return result;
792    }
793    match pattern {
794        Pattern::Attribute {
795            name: name_class,
796            body,
797            ..
798        } => {
799            if name_class_matches(name_class, &name.namespace, &name.local)
800                && value_matches(&body[0], value, definitions, registry, context)
801            {
802                Pattern::Empty
803            } else {
804                Pattern::NotAllowed
805            }
806        }
807        Pattern::Element { .. }
808        | Pattern::Text
809        | Pattern::Data { .. }
810        | Pattern::Value { .. }
811        | Pattern::List(_)
812        | Pattern::Empty
813        | Pattern::NotAllowed => Pattern::NotAllowed,
814        Pattern::Optional(_)
815        | Pattern::ZeroOrMore(_)
816        | Pattern::Mixed(_)
817        | Pattern::ExternalRef { .. }
818        | Pattern::ParentRef(_)
819        | Pattern::Grammar(_) => {
820            unreachable!("resolved into other patterns by simplify_pattern")
821        }
822        Pattern::Group(_)
823        | Pattern::Interleave(_)
824        | Pattern::Choice(_)
825        | Pattern::OneOrMore(_)
826        | Pattern::Ref(_) => unreachable!("handled by deriv_structural above"),
827    }
828}
829
830/// The derivative of `pattern` with respect to a single child *element*
831/// event (just its name — the element's own content is validated
832/// separately and recursively, see [`find_element_contents`]).
833fn deriv_element_event(
834    pattern: &Pattern,
835    name: &ExpandedName,
836    definitions: &Definitions,
837) -> Pattern {
838    let deriv_one = |v: &Pattern| deriv_element_event(v, name, definitions);
839    if let Some(result) = deriv_structural(pattern, true, definitions, &deriv_one) {
840        return result;
841    }
842    match pattern {
843        Pattern::Element {
844            name: name_class, ..
845        } => {
846            if name_class_matches(name_class, &name.namespace, &name.local) {
847                Pattern::Empty
848            } else {
849                Pattern::NotAllowed
850            }
851        }
852        Pattern::Attribute { .. }
853        | Pattern::Text
854        | Pattern::Data { .. }
855        | Pattern::Value { .. }
856        | Pattern::List(_)
857        | Pattern::Empty
858        | Pattern::NotAllowed => Pattern::NotAllowed,
859        Pattern::Optional(_)
860        | Pattern::ZeroOrMore(_)
861        | Pattern::Mixed(_)
862        | Pattern::ExternalRef { .. }
863        | Pattern::ParentRef(_)
864        | Pattern::Grammar(_) => {
865            unreachable!("resolved into other patterns by simplify_pattern")
866        }
867        Pattern::Group(_)
868        | Pattern::Interleave(_)
869        | Pattern::Choice(_)
870        | Pattern::OneOrMore(_)
871        | Pattern::Ref(_) => unreachable!("handled by deriv_structural above"),
872    }
873}
874
875/// The derivative of `pattern` with respect to a text *value* — used both
876/// for a child `Content::Text` event (the whole merged run of text handed
877/// over as one value) and, from [`list_matches`], for each individual
878/// whitespace-separated token inside a `list`.
879fn deriv_text(
880    pattern: &Pattern,
881    text: &str,
882    definitions: &Definitions,
883    registry: &DatatypeRegistry,
884    context: &DatatypeContext,
885) -> Pattern {
886    let deriv_one = |v: &Pattern| deriv_text(v, text, definitions, registry, context);
887    if let Some(result) = deriv_structural(pattern, true, definitions, &deriv_one) {
888        return result;
889    }
890    match pattern {
891        // `text` absorbs any amount of text, including across separate
892        // (non-adjacent, interleaved-with-elements) runs, so it stays
893        // `text` rather than being "used up".
894        Pattern::Text => Pattern::Text,
895        Pattern::Data {
896            datatype,
897            datatype_library,
898            params,
899            except,
900            ..
901        } => {
902            let library = datatype_library.as_deref().unwrap_or("");
903            let implementation = registry
904                .get(library)
905                .expect("checked upfront by check_datatypes_supported");
906            if implementation.matches(datatype, &param_pairs(params), text, context)
907                && !text_matches_except(except, text, definitions, registry, context)
908            {
909                Pattern::Empty
910            } else {
911                Pattern::NotAllowed
912            }
913        }
914        Pattern::Value {
915            value,
916            datatype,
917            datatype_library,
918            namespace,
919            context: schema_context,
920        } => {
921            let library = datatype_library.as_deref().unwrap_or("");
922            let type_name = datatype.as_deref().unwrap_or("token");
923            let implementation = registry
924                .get(library)
925                .expect("checked upfront by check_datatypes_supported");
926            if implementation.values_equal(
927                type_name,
928                value,
929                &schema_value_context(namespace, schema_context),
930                text,
931                context,
932            ) {
933                Pattern::Empty
934            } else {
935                Pattern::NotAllowed
936            }
937        }
938        Pattern::List(values) => {
939            if list_matches(&values[0], text, definitions, registry, context) {
940                Pattern::Empty
941            } else {
942                Pattern::NotAllowed
943            }
944        }
945        Pattern::Element { .. }
946        | Pattern::Attribute { .. }
947        | Pattern::Empty
948        | Pattern::NotAllowed => Pattern::NotAllowed,
949        Pattern::Optional(_)
950        | Pattern::ZeroOrMore(_)
951        | Pattern::Mixed(_)
952        | Pattern::ExternalRef { .. }
953        | Pattern::ParentRef(_)
954        | Pattern::Grammar(_) => {
955            unreachable!("resolved into other patterns by simplify_pattern")
956        }
957        Pattern::Group(_)
958        | Pattern::Interleave(_)
959        | Pattern::Choice(_)
960        | Pattern::OneOrMore(_)
961        | Pattern::Ref(_) => unreachable!("handled by deriv_structural above"),
962    }
963}
964
965/// `oneOrMore(p)`'s derivative w.r.t. any event: consume one occurrence of
966/// `p` now, then either the `oneOrMore` continues or (having satisfied the
967/// "one") it may now stop.
968fn one_or_more_deriv(
969    inner: &Pattern,
970    whole: &Pattern,
971    deriv_one: &dyn Fn(&Pattern) -> Pattern,
972) -> Pattern {
973    group(vec![
974        deriv_one(inner),
975        choice(vec![whole.clone(), Pattern::Empty]),
976    ])
977}
978
979fn text_matches_except(
980    except: &[Pattern],
981    text: &str,
982    definitions: &Definitions,
983    registry: &DatatypeRegistry,
984    context: &DatatypeContext,
985) -> bool {
986    except.iter().any(|item| {
987        nullable(
988            &deriv_text(item, text, definitions, registry, context),
989            definitions,
990            registry,
991        )
992    })
993}
994
995/// Whether `text`, split on XML whitespace into tokens, matches `pattern`
996/// (a `list`'s inner pattern) when each token is fed through in sequence.
997fn list_matches(
998    pattern: &Pattern,
999    text: &str,
1000    definitions: &Definitions,
1001    registry: &DatatypeRegistry,
1002    context: &DatatypeContext,
1003) -> bool {
1004    let mut current = pattern.clone();
1005    for token in text.split_ascii_whitespace() {
1006        current = deriv_text(&current, token, definitions, registry, context);
1007        if matches!(current, Pattern::NotAllowed) {
1008            return false;
1009        }
1010    }
1011    // Not `nullable`: a slot no token ever reached (e.g. the second
1012    // `data` in `list { data type="token", data type="token" }` given
1013    // only one token) must not be excused just because its datatype
1014    // would also tolerate the literal empty string — see
1015    // `structurally_nullable`'s docs.
1016    structurally_nullable(&current, definitions)
1017}
1018
1019/// Whether `value` (an attribute's string value, or — recursively — a
1020/// `list` element's whole text) satisfies `pattern` on its own — including
1021/// §6.2.7: a value that's whitespace-only (or empty) and doesn't otherwise
1022/// fit still matches if nothing in `pattern` actually wants text (e.g.
1023/// `attribute id { empty }` accepts `id=" "`) — see [`wants_text`].
1024fn value_matches(
1025    pattern: &Pattern,
1026    value: &str,
1027    definitions: &Definitions,
1028    registry: &DatatypeRegistry,
1029    context: &DatatypeContext,
1030) -> bool {
1031    let derived = deriv_text(pattern, value, definitions, registry, context);
1032    if !matches!(derived, Pattern::NotAllowed) && structurally_nullable(&derived, definitions) {
1033        return true;
1034    }
1035    value.trim().is_empty() && !wants_text(pattern, definitions)
1036}
1037
1038/// Whether *some* `attribute` pattern reachable in `pattern` (transparent
1039/// through `ref`, `group`, `interleave`, `choice`, `oneOrMore` — the exact
1040/// same positions [`deriv_attribute`] would look at) has a name class
1041/// matching `name`, regardless of value. Used to tell "this attribute
1042/// isn't allowed at all" apart from "this attribute is allowed, but its
1043/// value is wrong".
1044fn attribute_name_known(pattern: &Pattern, name: &ExpandedName, definitions: &Definitions) -> bool {
1045    match pattern {
1046        Pattern::Attribute {
1047            name: name_class, ..
1048        } => name_class_matches(name_class, &name.namespace, &name.local),
1049        Pattern::Group(values) | Pattern::Interleave(values) | Pattern::Choice(values) => values
1050            .iter()
1051            .any(|value| attribute_name_known(value, name, definitions)),
1052        Pattern::OneOrMore(values) => attribute_name_known(&values[0], name, definitions),
1053        Pattern::Ref(ref_name) => {
1054            attribute_name_known(resolve_ref(ref_name, definitions), name, definitions)
1055        }
1056        _ => false,
1057    }
1058}
1059
1060/// The union (as a `choice`) of every `element` pattern's content reachable
1061/// in `pattern` (transparently through `ref`/`group`(sequential, like
1062/// [`deriv_element_event`])/`interleave`/`choice`/`oneOrMore`) whose name
1063/// class matches `name` — `None` if none do. Used both to find what a
1064/// child element's own content must be validated against, and (via
1065/// [`nullable`]/emptiness) to tell whether this element name is expected
1066/// here at all.
1067fn find_element_contents(
1068    pattern: &Pattern,
1069    name: &ExpandedName,
1070    definitions: &Definitions,
1071) -> Option<Pattern> {
1072    let mut found = Vec::new();
1073    collect_element_contents(pattern, name, definitions, &mut found);
1074    if found.is_empty() {
1075        None
1076    } else {
1077        Some(choice(found))
1078    }
1079}
1080
1081fn collect_element_contents(
1082    pattern: &Pattern,
1083    name: &ExpandedName,
1084    definitions: &Definitions,
1085    out: &mut Vec<Pattern>,
1086) {
1087    match pattern {
1088        Pattern::Element {
1089            name: name_class,
1090            body,
1091            ..
1092        } if name_class_matches(name_class, &name.namespace, &name.local) => {
1093            out.push(body[0].clone());
1094        }
1095        Pattern::Choice(values) | Pattern::Interleave(values) => {
1096            for value in values {
1097                collect_element_contents(value, name, definitions, out);
1098            }
1099        }
1100        Pattern::Group(values) => {
1101            let mut reachable = true;
1102            for value in values {
1103                if !reachable {
1104                    break;
1105                }
1106                collect_element_contents(value, name, definitions, out);
1107                reachable = structurally_nullable(value, definitions);
1108            }
1109        }
1110        Pattern::OneOrMore(values) => collect_element_contents(&values[0], name, definitions, out),
1111        Pattern::Ref(ref_name) => {
1112            collect_element_contents(resolve_ref(ref_name, definitions), name, definitions, out)
1113        }
1114        _ => {}
1115    }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121    use crate::datatypes::DatatypeLibrary;
1122    use crate::{ResolveError, SchemaResolver, SchemaSource, SchemaSyntax};
1123
1124    struct NoResolver;
1125
1126    impl SchemaResolver for NoResolver {
1127        fn resolve(&self, href: &str, _base_uri: &str) -> Result<SchemaSource, ResolveError> {
1128            Err(ResolveError::new(format!(
1129                "validator tests don't use includes (requested `{href}`)"
1130            )))
1131        }
1132    }
1133
1134    fn schema(text: &str) -> CompiledSchema {
1135        let document = crate::parse(&SchemaSource::new(
1136            text,
1137            "memory:/main.rnc",
1138            SchemaSyntax::Compact,
1139        ))
1140        .expect("schema parses");
1141        crate::simplify(&document, &NoResolver).expect("schema simplifies")
1142    }
1143
1144    fn xml_schema(text: &str) -> CompiledSchema {
1145        let document = crate::parse(&SchemaSource::new(
1146            text,
1147            "memory:/main.rng",
1148            SchemaSyntax::Xml,
1149        ))
1150        .expect("schema parses");
1151        crate::simplify(&document, &NoResolver).expect("schema simplifies")
1152    }
1153
1154    fn registry() -> DatatypeRegistry {
1155        DatatypeRegistry::new()
1156    }
1157
1158    /// A minimal, structured (not pre-formatted) location — exercises
1159    /// [`Element::Location`] as something other than `String`/`()`, the
1160    /// whole point of that associated type being generic rather than
1161    /// hardcoded.
1162    #[derive(Clone, Debug, Eq, PartialEq)]
1163    struct TestLocation {
1164        line: u32,
1165        column: u32,
1166    }
1167
1168    impl fmt::Display for TestLocation {
1169        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1170            write!(formatter, "{}:{}", self.line, self.column)
1171        }
1172    }
1173
1174    #[derive(Clone, Debug)]
1175    struct TestElement {
1176        name: ExpandedName,
1177        attributes: Vec<(ExpandedName, String)>,
1178        children: Vec<Content<TestElement>>,
1179        namespace_bindings: Vec<(String, String)>,
1180        location: Option<TestLocation>,
1181    }
1182
1183    impl TestElement {
1184        fn new(local: &str) -> Self {
1185            Self {
1186                name: ExpandedName::new(None::<String>, local),
1187                attributes: Vec::new(),
1188                children: Vec::new(),
1189                namespace_bindings: Vec::new(),
1190                location: None,
1191            }
1192        }
1193
1194        fn new_namespaced(namespace: &str, local: &str) -> Self {
1195            Self {
1196                name: ExpandedName::new(Some(namespace), local),
1197                attributes: Vec::new(),
1198                children: Vec::new(),
1199                namespace_bindings: Vec::new(),
1200                location: None,
1201            }
1202        }
1203
1204        fn at(mut self, line: u32, column: u32) -> Self {
1205            self.location = Some(TestLocation { line, column });
1206            self
1207        }
1208
1209        fn attr(mut self, local: &str, value: &str) -> Self {
1210            self.attributes
1211                .push((ExpandedName::new(None::<String>, local), value.to_owned()));
1212            self
1213        }
1214
1215        fn text(mut self, text: &str) -> Self {
1216            self.children.push(Content::Text(text.to_owned()));
1217            self
1218        }
1219
1220        fn child(mut self, element: TestElement) -> Self {
1221            self.children.push(Content::Element(element));
1222            self
1223        }
1224
1225        fn xmlns(mut self, prefix: &str, uri: &str) -> Self {
1226            self.namespace_bindings
1227                .push((prefix.to_owned(), uri.to_owned()));
1228            self
1229        }
1230    }
1231
1232    impl Element for TestElement {
1233        type Location = TestLocation;
1234        fn name(&self) -> ExpandedName {
1235            self.name.clone()
1236        }
1237        fn attributes(&self) -> impl Iterator<Item = (ExpandedName, String)> {
1238            self.attributes.clone().into_iter()
1239        }
1240        fn children(&self) -> impl Iterator<Item = Content<Self>> {
1241            self.children.clone().into_iter()
1242        }
1243        fn location(&self) -> Option<Self::Location> {
1244            self.location.clone()
1245        }
1246        fn namespace_bindings(&self) -> impl Iterator<Item = (String, String)> {
1247            self.namespace_bindings.clone().into_iter()
1248        }
1249    }
1250
1251    fn kinds(errors: &[ValidationError<TestLocation>]) -> Vec<ValidationErrorKind> {
1252        errors.iter().map(|error| error.kind().clone()).collect()
1253    }
1254
1255    fn name(local: &str) -> ExpandedName {
1256        ExpandedName::new(None::<String>, local)
1257    }
1258
1259    // --- nullable ---
1260
1261    #[test]
1262    fn nullable_base_cases() {
1263        let definitions = Definitions::new();
1264        let registry = registry();
1265        assert!(nullable(&Pattern::Empty, &definitions, &registry));
1266        assert!(!nullable(&Pattern::NotAllowed, &definitions, &registry));
1267        assert!(nullable(&Pattern::Text, &definitions, &registry));
1268    }
1269
1270    #[test]
1271    fn nullable_group_requires_every_operand_nullable() {
1272        let definitions = Definitions::new();
1273        let registry = registry();
1274        assert!(nullable(
1275            &Pattern::Group(vec![Pattern::Empty, Pattern::Text]),
1276            &definitions,
1277            &registry
1278        ));
1279        assert!(!nullable(
1280            &Pattern::Group(vec![Pattern::Empty, Pattern::NotAllowed]),
1281            &definitions,
1282            &registry
1283        ));
1284    }
1285
1286    #[test]
1287    fn nullable_choice_requires_any_operand_nullable() {
1288        let definitions = Definitions::new();
1289        let registry = registry();
1290        assert!(nullable(
1291            &Pattern::Choice(vec![Pattern::NotAllowed, Pattern::Empty]),
1292            &definitions,
1293            &registry
1294        ));
1295        assert!(!nullable(
1296            &Pattern::Choice(vec![Pattern::NotAllowed, Pattern::NotAllowed]),
1297            &definitions,
1298            &registry
1299        ));
1300    }
1301
1302    #[test]
1303    fn nullable_one_or_more_delegates_to_inner() {
1304        let definitions = Definitions::new();
1305        let registry = registry();
1306        assert!(nullable(
1307            &Pattern::OneOrMore(vec![Pattern::Empty]),
1308            &definitions,
1309            &registry
1310        ));
1311        assert!(!nullable(
1312            &Pattern::OneOrMore(vec![Pattern::NotAllowed]),
1313            &definitions,
1314            &registry
1315        ));
1316    }
1317
1318    #[test]
1319    fn nullable_list_delegates_to_inner_same_as_zero_tokens_would() {
1320        let definitions = Definitions::new();
1321        let registry = registry();
1322        // `list { empty }` accepts having no text at all.
1323        assert!(nullable(
1324            &Pattern::List(vec![Pattern::Empty]),
1325            &definitions,
1326            &registry
1327        ));
1328        assert!(!nullable(
1329            &Pattern::List(vec![Pattern::NotAllowed]),
1330            &definitions,
1331            &registry
1332        ));
1333    }
1334
1335    #[test]
1336    fn nullable_ref_resolves_through_definitions() {
1337        let definitions = Definitions::from([("x".to_owned(), Pattern::Empty)]);
1338        let registry = registry();
1339        assert!(nullable(&Pattern::Ref("x".into()), &definitions, &registry));
1340    }
1341
1342    // --- individual derivative functions: element / attribute / text ---
1343
1344    #[test]
1345    fn deriv_element_event_matches_by_name_and_rejects_others() {
1346        let compiled = schema("start = element foo { empty }");
1347        let registry = registry();
1348        assert!(nullable(
1349            &deriv_element_event(&compiled.start, &name("foo"), &compiled.definitions),
1350            &compiled.definitions,
1351            &registry
1352        ));
1353        assert!(matches!(
1354            deriv_element_event(&compiled.start, &name("bar"), &compiled.definitions),
1355            Pattern::NotAllowed
1356        ));
1357    }
1358
1359    #[test]
1360    fn deriv_attribute_checks_name_and_value() {
1361        let compiled = schema(r#"start = element foo { attribute id { "x" } }"#);
1362        let registry = registry();
1363        let context = DatatypeContext::empty();
1364        let Pattern::Element { body, .. } = &compiled.start else {
1365            panic!("expected an element pattern");
1366        };
1367        assert!(nullable(
1368            &deriv_attribute(
1369                &body[0],
1370                &name("id"),
1371                "x",
1372                &compiled.definitions,
1373                &registry,
1374                &context
1375            ),
1376            &compiled.definitions,
1377            &registry
1378        ));
1379        assert!(matches!(
1380            deriv_attribute(
1381                &body[0],
1382                &name("id"),
1383                "y",
1384                &compiled.definitions,
1385                &registry,
1386                &context
1387            ),
1388            Pattern::NotAllowed
1389        ));
1390        assert!(matches!(
1391            deriv_attribute(
1392                &body[0],
1393                &name("other"),
1394                "x",
1395                &compiled.definitions,
1396                &registry,
1397                &context
1398            ),
1399            Pattern::NotAllowed
1400        ));
1401    }
1402
1403    #[test]
1404    fn deriv_text_data_accepts_any_string_value_requires_exact_match() {
1405        let registry = registry();
1406        let context = DatatypeContext::empty();
1407        let compiled = schema("start = element foo { token }");
1408        let Pattern::Element { body, .. } = &compiled.start else {
1409            panic!("expected an element pattern");
1410        };
1411        assert!(nullable(
1412            &deriv_text(
1413                &body[0],
1414                "whatever text",
1415                &compiled.definitions,
1416                &registry,
1417                &context
1418            ),
1419            &compiled.definitions,
1420            &registry
1421        ));
1422
1423        let compiled = schema(r#"start = element foo { "exact" }"#);
1424        let Pattern::Element { body, .. } = &compiled.start else {
1425            panic!("expected an element pattern");
1426        };
1427        assert!(nullable(
1428            &deriv_text(
1429                &body[0],
1430                "exact",
1431                &compiled.definitions,
1432                &registry,
1433                &context
1434            ),
1435            &compiled.definitions,
1436            &registry
1437        ));
1438        assert!(matches!(
1439            deriv_text(
1440                &body[0],
1441                "different",
1442                &compiled.definitions,
1443                &registry,
1444                &context
1445            ),
1446            Pattern::NotAllowed
1447        ));
1448    }
1449
1450    #[test]
1451    fn deriv_text_token_equality_collapses_whitespace() {
1452        let registry = registry();
1453        let context = DatatypeContext::empty();
1454        let compiled = schema(r#"start = element foo { token "a b" }"#);
1455        let Pattern::Element { body, .. } = &compiled.start else {
1456            panic!("expected an element pattern");
1457        };
1458        assert!(nullable(
1459            &deriv_text(
1460                &body[0],
1461                "a   b",
1462                &compiled.definitions,
1463                &registry,
1464                &context
1465            ),
1466            &compiled.definitions,
1467            &registry
1468        ));
1469    }
1470
1471    // --- `group` (sequential/"after") vs `interleave` (unordered) ---
1472
1473    #[test]
1474    fn group_derivative_is_sequential() {
1475        let registry = registry();
1476        let compiled = schema("start = element root { element a { empty }, element b { empty } }");
1477        let Pattern::Element { body, .. } = &compiled.start else {
1478            panic!("expected an element pattern");
1479        };
1480        let content = &body[0];
1481        // "b" cannot be matched before "a" — group is ordered.
1482        assert!(matches!(
1483            deriv_element_event(content, &name("b"), &compiled.definitions),
1484            Pattern::NotAllowed
1485        ));
1486        let after_a = deriv_element_event(content, &name("a"), &compiled.definitions);
1487        let after_a_b = deriv_element_event(&after_a, &name("b"), &compiled.definitions);
1488        assert!(nullable(&after_a_b, &compiled.definitions, &registry));
1489    }
1490
1491    #[test]
1492    fn interleave_derivative_allows_either_order() {
1493        let registry = registry();
1494        let compiled = schema("start = element root { element a { empty } & element b { empty } }");
1495        let Pattern::Element { body, .. } = &compiled.start else {
1496            panic!("expected an element pattern");
1497        };
1498        let content = &body[0];
1499        // Unlike `group`, "b" is allowed to come before "a".
1500        let after_b = deriv_element_event(content, &name("b"), &compiled.definitions);
1501        assert!(!matches!(after_b, Pattern::NotAllowed));
1502        let after_b_a = deriv_element_event(&after_b, &name("a"), &compiled.definitions);
1503        assert!(nullable(&after_b_a, &compiled.definitions, &registry));
1504    }
1505
1506    // --- the xmloxide regression this crate exists to fix (issue #56):
1507    // `ref` inside `interleave` must resolve for *attribute* matching. ---
1508
1509    #[test]
1510    fn attribute_matches_through_ref_and_interleave_xmloxide_issue_56() {
1511        let compiled = schema(
1512            "start = element foo { attrs & text }\n\
1513             attrs = attribute id { text }",
1514        );
1515        let document = TestElement::new("foo").attr("id", "x").text("hello");
1516        let errors = validate(&compiled, &registry(), &document).expect("builtin datatypes only");
1517        assert_eq!(kinds(&errors), Vec::<ValidationErrorKind>::new());
1518    }
1519
1520    #[test]
1521    fn attribute_name_known_resolves_ref_through_group_and_choice_too() {
1522        // The same transparency, exercised for `group` (attribute-matching
1523        // semantics, unordered) and `choice`.
1524        let compiled = schema(
1525            "start = element foo { a, b }\n\
1526             a = attribute x { text } | attribute y { text }\n\
1527             b = empty",
1528        );
1529        let Pattern::Element { body, .. } = &compiled.start else {
1530            panic!("expected an element pattern");
1531        };
1532        assert!(attribute_name_known(
1533            &body[0],
1534            &name("x"),
1535            &compiled.definitions
1536        ));
1537        assert!(attribute_name_known(
1538            &body[0],
1539            &name("y"),
1540            &compiled.definitions
1541        ));
1542        assert!(!attribute_name_known(
1543            &body[0],
1544            &name("z"),
1545            &compiled.definitions
1546        ));
1547    }
1548
1549    #[test]
1550    fn ref_is_transparent_for_child_elements_through_choice_and_group() {
1551        let compiled = schema(
1552            "start = element foo { greeting, farewell }\n\
1553             greeting = element hello { empty } | element hi { empty }\n\
1554             farewell = element bye { empty }",
1555        );
1556        let document = TestElement::new("foo")
1557            .child(TestElement::new("hi"))
1558            .child(TestElement::new("bye"));
1559        let errors = validate(&compiled, &registry(), &document).expect("builtin datatypes only");
1560        assert_eq!(kinds(&errors), Vec::<ValidationErrorKind>::new());
1561    }
1562
1563    // --- end-to-end `validate()`: hand-picked valid/invalid document pairs ---
1564
1565    #[test]
1566    fn valid_document_with_required_attribute_and_text() {
1567        let compiled = schema("start = element foo { attribute id { text }, text }");
1568        let document = TestElement::new("foo").attr("id", "1").text("hello");
1569        let errors = validate(&compiled, &registry(), &document).unwrap();
1570        assert!(errors.is_empty());
1571    }
1572
1573    #[test]
1574    fn missing_required_attribute_is_reported() {
1575        let compiled = schema("start = element foo { attribute id { text }, text }");
1576        let document = TestElement::new("foo").text("hello");
1577        let errors = validate(&compiled, &registry(), &document).unwrap();
1578        assert_eq!(kinds(&errors), vec![ValidationErrorKind::MissingContent]);
1579    }
1580
1581    #[test]
1582    fn unexpected_attribute_is_reported() {
1583        let compiled = schema("start = element foo { empty }");
1584        let document = TestElement::new("foo").attr("id", "1");
1585        let errors = validate(&compiled, &registry(), &document).unwrap();
1586        assert_eq!(
1587            kinds(&errors),
1588            vec![ValidationErrorKind::UnexpectedAttribute(name("id"))]
1589        );
1590    }
1591
1592    /// `Element::Location` is generic — a caller with real structured
1593    /// position data (not just a pre-formatted string) gets it back out
1594    /// of `ValidationError::location()` unchanged, not re-parsed from
1595    /// text. `TestLocation` (`{line, column}`) stands in for whatever
1596    /// shape a real caller's own position type has.
1597    #[test]
1598    fn structured_location_is_reported_on_the_validation_error() {
1599        let compiled = schema("start = element foo { empty }");
1600        let document = TestElement::new("foo").attr("id", "1").at(3, 7);
1601        let errors = validate(&compiled, &registry(), &document).unwrap();
1602        assert_eq!(errors.len(), 1);
1603        assert_eq!(
1604            errors[0].location(),
1605            Some(&TestLocation { line: 3, column: 7 })
1606        );
1607        // `Display` still renders it as formatted text for messages that
1608        // want that.
1609        assert_eq!(errors[0].to_string(), "unexpected attribute `id` at 3:7");
1610    }
1611
1612    #[test]
1613    fn invalid_attribute_value_is_reported_distinctly_from_unexpected_attribute() {
1614        let compiled = schema(r#"start = element foo { attribute id { "x" } }"#);
1615        let document = TestElement::new("foo").attr("id", "y");
1616        let errors = validate(&compiled, &registry(), &document).unwrap();
1617        // The mismatched attribute is left unconsumed (so later attributes
1618        // keep being checked against the schema rather than being drowned
1619        // out by one bad value), which is also, truthfully, "required
1620        // content still missing" once the element ends.
1621        assert_eq!(
1622            kinds(&errors),
1623            vec![
1624                ValidationErrorKind::InvalidAttributeValue(name("id"), "y".to_owned()),
1625                ValidationErrorKind::MissingContent,
1626            ]
1627        );
1628    }
1629
1630    #[test]
1631    fn unexpected_child_element_is_reported() {
1632        let compiled = schema("start = element foo { element bar { empty } }");
1633        let document = TestElement::new("foo").child(TestElement::new("baz"));
1634        let errors = validate(&compiled, &registry(), &document).unwrap();
1635        assert_eq!(
1636            kinds(&errors),
1637            vec![
1638                ValidationErrorKind::UnexpectedElement(name("baz")),
1639                ValidationErrorKind::MissingContent,
1640            ]
1641        );
1642    }
1643
1644    #[test]
1645    fn unexpected_root_element_is_reported() {
1646        let compiled = schema("start = element foo { empty }");
1647        let document = TestElement::new("bar");
1648        let errors = validate(&compiled, &registry(), &document).unwrap();
1649        assert_eq!(
1650            kinds(&errors),
1651            vec![ValidationErrorKind::UnexpectedElement(name("bar"))]
1652        );
1653    }
1654
1655    #[test]
1656    fn unexpected_text_is_reported() {
1657        let compiled = schema("start = element foo { empty }");
1658        let document = TestElement::new("foo").text("surprise");
1659        let errors = validate(&compiled, &registry(), &document).unwrap();
1660        assert_eq!(kinds(&errors), vec![ValidationErrorKind::UnexpectedText]);
1661    }
1662
1663    #[test]
1664    fn one_or_more_children_are_validated_recursively_and_repeatedly() {
1665        let compiled = schema("start = element foo { element item { text }+ }");
1666        let document = TestElement::new("foo")
1667            .child(TestElement::new("item").text("a"))
1668            .child(TestElement::new("item").text("b"));
1669        let errors = validate(&compiled, &registry(), &document).unwrap();
1670        assert!(errors.is_empty());
1671    }
1672
1673    #[test]
1674    fn one_or_more_requires_at_least_one_occurrence() {
1675        let compiled = schema("start = element foo { element item { empty }+ }");
1676        let document = TestElement::new("foo");
1677        let errors = validate(&compiled, &registry(), &document).unwrap();
1678        assert_eq!(kinds(&errors), vec![ValidationErrorKind::MissingContent]);
1679    }
1680
1681    #[test]
1682    fn interleaved_text_and_element_children_validate_regardless_of_order() {
1683        let compiled = schema("start = element foo { mixed { element bar { empty } } }");
1684        let document = TestElement::new("foo")
1685            .text("before ")
1686            .child(TestElement::new("bar"))
1687            .text(" after");
1688        let errors = validate(&compiled, &registry(), &document).unwrap();
1689        assert!(errors.is_empty());
1690    }
1691
1692    #[test]
1693    fn list_datatype_matches_whitespace_separated_tokens() {
1694        let compiled = schema("start = element foo { list { token+ } }");
1695        let document = TestElement::new("foo").text("a b   c");
1696        let errors = validate(&compiled, &registry(), &document).unwrap();
1697        assert!(errors.is_empty());
1698    }
1699
1700    #[test]
1701    fn list_datatype_rejects_a_token_that_does_not_match() {
1702        let compiled = schema(r#"start = element foo { list { "a", "b" } }"#);
1703        let document = TestElement::new("foo").text("a c");
1704        let errors = validate(&compiled, &registry(), &document).unwrap();
1705        assert_eq!(
1706            kinds(&errors),
1707            vec![
1708                ValidationErrorKind::UnexpectedText,
1709                ValidationErrorKind::MissingContent,
1710            ]
1711        );
1712    }
1713
1714    #[test]
1715    fn unsupported_datatype_library_fails_fast_before_validating() {
1716        // Deliberately the whattf library, not XSD — `DatatypeRegistry::new`
1717        // now supports XSD out of the box (see `plan/DECISIONS.md`), so
1718        // this needs a genuinely unregistered library to still exercise
1719        // the fail-fast path; the whattf library is exactly that (a
1720        // caller like html-conform must register its own implementation).
1721        // XML syntax, not compact: compact-syntax identifiers don't accept
1722        // `-`, and `non-empty-string` is vnu's real type name.
1723        let compiled = xml_schema(
1724            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo">
1725                 <data type="non-empty-string" datatypeLibrary="http://whattf.org/datatype-draft"/>
1726               </element>"#,
1727        );
1728        let document = TestElement::new("foo").text("hello");
1729        let error = validate(&compiled, &registry(), &document).unwrap_err();
1730        assert_eq!(error.library(), "http://whattf.org/datatype-draft");
1731    }
1732
1733    #[test]
1734    fn builtin_only_registry_rejects_xsd_too() {
1735        let compiled = schema(
1736            "datatypes xsd = \"http://www.w3.org/2001/XMLSchema-datatypes\"\n\
1737             start = element foo { xsd:string }",
1738        );
1739        let document = TestElement::new("foo").text("hello");
1740        let error = validate(&compiled, &DatatypeRegistry::builtin_only(), &document).unwrap_err();
1741        assert_eq!(
1742            error.library(),
1743            "http://www.w3.org/2001/XMLSchema-datatypes"
1744        );
1745    }
1746
1747    // --- XSD, via the default registry ---
1748
1749    #[test]
1750    fn xsd_string_and_integer_types_match() {
1751        // Note: RNG XML's `type` attribute is always a bare string, never a
1752        // namespace-prefixed QName — the library comes from the separate
1753        // `datatypeLibrary` attribute (or §4.3 inheritance from an
1754        // ancestor), unlike compact syntax's `prefix:typeName` sugar.
1755        let compiled = xml_schema(
1756            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
1757                 <group>
1758                   <attribute name="count"><data type="positiveInteger"/></attribute>
1759                   <data type="string"/>
1760                 </group>
1761               </element>"#,
1762        );
1763        let valid = TestElement::new("foo").attr("count", "3").text("hello");
1764        assert!(validate(&compiled, &registry(), &valid).unwrap().is_empty());
1765
1766        let invalid = TestElement::new("foo").attr("count", "0").text("hello");
1767        let errors = validate(&compiled, &registry(), &invalid).unwrap();
1768        assert_eq!(
1769            kinds(&errors),
1770            vec![
1771                ValidationErrorKind::InvalidAttributeValue(name("count"), "0".to_owned()),
1772                ValidationErrorKind::MissingContent,
1773            ]
1774        );
1775    }
1776
1777    #[test]
1778    fn xsd_pattern_facet_is_enforced() {
1779        let compiled = xml_schema(
1780            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
1781                 <data type="string"><param name="pattern">[a-z]+</param></data>
1782               </element>"#,
1783        );
1784        let valid = TestElement::new("foo").text("abc");
1785        assert!(validate(&compiled, &registry(), &valid).unwrap().is_empty());
1786        let invalid = TestElement::new("foo").text("ABC");
1787        assert_eq!(
1788            kinds(&validate(&compiled, &registry(), &invalid).unwrap()),
1789            vec![
1790                ValidationErrorKind::UnexpectedText,
1791                ValidationErrorKind::MissingContent,
1792            ]
1793        );
1794    }
1795
1796    #[test]
1797    fn xsd_decimal_value_equality_ignores_lexical_differences() {
1798        let compiled = xml_schema(
1799            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema-datatypes" name="foo">
1800                 <value type="decimal" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">1.50</value>
1801               </element>"#,
1802        );
1803        // `1.5` and `01.50` are the same `decimal` value, just spelled
1804        // differently — real bignum-style equality, not string equality.
1805        let document = TestElement::new("foo").text("01.500");
1806        assert!(
1807            validate(&compiled, &registry(), &document)
1808                .unwrap()
1809                .is_empty()
1810        );
1811    }
1812
1813    #[test]
1814    fn xsd_qname_resolves_prefix_via_document_namespace_bindings() {
1815        let compiled = xml_schema(
1816            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo">
1817                 <data type="QName" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"/>
1818               </element>"#,
1819        );
1820        let document = TestElement::new("foo")
1821            .xmlns("h", "urn:example:h")
1822            .text("h:widget");
1823        assert!(
1824            validate(&compiled, &registry(), &document)
1825                .unwrap()
1826                .is_empty()
1827        );
1828
1829        // No `h` binding in scope at all — the QName can't be resolved, so
1830        // it isn't a legal QName *value* here even though it's lexically
1831        // shaped like one.
1832        let unresolved = TestElement::new("foo").text("h:widget");
1833        assert_eq!(
1834            kinds(&validate(&compiled, &registry(), &unresolved).unwrap()),
1835            vec![
1836                ValidationErrorKind::UnexpectedText,
1837                ValidationErrorKind::MissingContent,
1838            ]
1839        );
1840    }
1841
1842    #[test]
1843    fn xsd_unknown_type_name_is_a_datatype_error() {
1844        let compiled = xml_schema(
1845            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
1846                 <data type="not-a-real-type"/>
1847               </element>"#,
1848        );
1849        let document = TestElement::new("foo").text("hello");
1850        let error = validate(&compiled, &registry(), &document).unwrap_err();
1851        assert_eq!(
1852            error.library(),
1853            "http://www.w3.org/2001/XMLSchema-datatypes"
1854        );
1855    }
1856
1857    // --- external plugin: a dummy library, demonstrating the trait
1858    // contract a caller like html-conform would implement for vnu's
1859    // `http://whattf.org/datatype-draft` (not shipped by this crate). ---
1860
1861    struct DummyNonEmptyStringLibrary;
1862
1863    impl DatatypeLibrary for DummyNonEmptyStringLibrary {
1864        fn validate_params(&self, type_name: &str, params: &[(&str, &str)]) -> Result<(), String> {
1865            if type_name != "non-empty-string" {
1866                return Err(format!("unknown dummy type `{type_name}`"));
1867            }
1868            if let Some((param, _)) = params.first() {
1869                return Err(format!(
1870                    "`non-empty-string` takes no parameters, found `{param}`"
1871                ));
1872            }
1873            Ok(())
1874        }
1875
1876        fn matches(
1877            &self,
1878            type_name: &str,
1879            _params: &[(&str, &str)],
1880            value: &str,
1881            _context: &DatatypeContext,
1882        ) -> bool {
1883            type_name == "non-empty-string" && !value.is_empty()
1884        }
1885
1886        fn values_equal(
1887            &self,
1888            type_name: &str,
1889            expected: &str,
1890            _expected_context: &DatatypeContext,
1891            actual: &str,
1892            _actual_context: &DatatypeContext,
1893        ) -> bool {
1894            type_name == "non-empty-string" && expected == actual
1895        }
1896    }
1897
1898    #[test]
1899    fn external_plugin_library_can_be_registered_and_used() {
1900        let compiled = xml_schema(
1901            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" name="foo">
1902                 <data type="non-empty-string" datatypeLibrary="http://whattf.org/datatype-draft"/>
1903               </element>"#,
1904        );
1905        let mut registry = DatatypeRegistry::new();
1906        registry.register(
1907            "http://whattf.org/datatype-draft",
1908            DummyNonEmptyStringLibrary,
1909        );
1910
1911        let valid = TestElement::new("foo").text("hello");
1912        assert!(validate(&compiled, &registry, &valid).unwrap().is_empty());
1913
1914        // An empty text *event* against a bare `data` isn't excused by
1915        // §6.2.7 weak-match rule 2: that rule only drops a whitespace-only
1916        // event when the content seen *so far* already, strictly,
1917        // satisfies the pattern without it — a bare `data`/`value` never
1918        // does (it structurally requires a real event), so this is
1919        // reported directly, same as a genuinely non-empty mismatch would
1920        // be.
1921        let invalid = TestElement::new("foo").text("");
1922        assert_eq!(
1923            kinds(&validate(&compiled, &registry, &invalid).unwrap()),
1924            vec![
1925                ValidationErrorKind::UnexpectedText,
1926                ValidationErrorKind::MissingContent,
1927            ]
1928        );
1929    }
1930
1931    // --- adapter smoke test: a realistic html-conform/xmloxide-shaped tree
1932    // (namespace-qualified elements, an unprefixed attribute, nested
1933    // children, text) — this crate has no dependency on html-conform or
1934    // xmloxide (no HTML/XML-parser dependency in the core, see
1935    // `plan/DECISIONS.md`), so `TestElement` stands in for its sibling
1936    // project's `NormalizedNode` adapter output, confirming the `Element`
1937    // trait contract works for that shape before the Public-API phase. ---
1938
1939    #[test]
1940    fn adapter_smoke_test_with_a_realistic_xhtml_shaped_tree() {
1941        const XHTML: &str = "http://www.w3.org/1999/xhtml";
1942        // Compact-syntax (`.rnc`) prefixed element names are a known,
1943        // separate gap outside Phase 05's scope (see `plan/00-STATUS.md`)
1944        // — the XML (`.rng`) syntax resolves `xmlns:`-qualified names
1945        // correctly, so this realistic, namespace-qualified smoke test
1946        // uses that syntax instead.
1947        let compiled = xml_schema(
1948            r#"<element xmlns="http://relaxng.org/ns/structure/1.0" xmlns:xhtml="http://www.w3.org/1999/xhtml" name="xhtml:html">
1949                 <group>
1950                   <element name="xhtml:head"><empty/></element>
1951                   <element name="xhtml:body">
1952                     <element name="xhtml:p">
1953                       <group>
1954                         <attribute name="id"><text/></attribute>
1955                         <text/>
1956                       </group>
1957                     </element>
1958                   </element>
1959                 </group>
1960               </element>"#,
1961        );
1962        let document = TestElement::new_namespaced(XHTML, "html")
1963            .child(TestElement::new_namespaced(XHTML, "head"))
1964            .child(
1965                TestElement::new_namespaced(XHTML, "body").child(
1966                    TestElement::new_namespaced(XHTML, "p")
1967                        .attr("id", "intro")
1968                        .text("hello"),
1969                ),
1970            );
1971        let errors = validate(&compiled, &registry(), &document).expect("builtin datatypes only");
1972        assert_eq!(kinds(&errors), Vec::<ValidationErrorKind>::new());
1973    }
1974}