Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d285d4d
feat(xsd): implement element substitution groups
AlexanderWillner May 26, 2026
78f4b7b
feat(xsd): implement complexContent extension base merging
AlexanderWillner May 26, 2026
2733b7c
fix(xsd): resolve types/elements in own targetNamespace
AlexanderWillner May 26, 2026
9f15149
fix: cross-namespace substitution group validation
AlexanderWillner May 27, 2026
d453c29
fix: namespace-aware element ref matching for cross-namespace refs
AlexanderWillner May 27, 2026
7b791d7
test: add NAS substitution group regression test
AlexanderWillner May 27, 2026
921372c
feat: add sequence order validation in xs:sequence
AlexanderWillner May 27, 2026
75efca9
feat: merge extension bases for imported namespace types
AlexanderWillner May 27, 2026
640ece3
feat: implement xsd:any wildcard support
AlexanderWillner May 27, 2026
6571ef1
feat: public XSD schema fields + get_type_element_order API
AlexanderWillner May 27, 2026
e6bcd20
fix(xsd): resolve root elements from imported schemas
AlexanderWillner May 28, 2026
84af57a
Merge branch 'feat/xsd-complex-content-extension'
AlexanderWillner May 28, 2026
0c61303
feat(xsd): add validate_xsd_strict for strict XSD validation
AlexanderWillner May 29, 2026
bca7790
fix(xsd): propagate compositor minOccurs to child particles
AlexanderWillner May 29, 2026
07e6d88
fix(xsd): resolve attributeGroup refs and attribute ref declarations
AlexanderWillner May 29, 2026
5009e7e
Fix strict XSD validation: merge base attributes and handle choice gr…
AlexanderWillner May 29, 2026
8c00f3b
fix(strict): resolve cross-include prefixes, inherited simpleContent …
AlexanderWillner May 30, 2026
c21be46
validation/xsd: fix clippy warnings and doctest issues
May 30, 2026
14b8ac0
feat(xsd): enforce xs:sequence element ordering in strict validation
Jun 1, 2026
c13eeed
feat: add Element wrapper for ergonomic XML traversal
Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ pub mod xinclude;
pub mod xpath;

// Re-export primary types at the crate root for convenience.
pub use tree::{Attribute, Document, NodeId};
pub use tree::{Attribute, Document, Element, NodeId};
169 changes: 169 additions & 0 deletions src/tree/element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//! Convenience wrapper around [`Document`] + [`NodeId`] for ergonomic XML traversal.
//!
//! Provides an [`Element`] type with methods like [`child_by_name`](Element::child_by_name),
//! [`attribute`](Element::attribute), [`children`](Element::children), etc.

use crate::{Document, NodeId};

/// A borrowed reference to an XML element node within a [`Document`].
///
/// Lightweight handle — holds a [`NodeId`] and a reference to the parent
/// [`Document`]. All methods delegate to the underlying tree.
#[derive(Clone, Copy)]
pub struct Element<'a> {
pub(crate) doc: &'a Document,
pub(crate) id: NodeId,
}

impl<'a> Element<'a> {
/// Create a new `Element` wrapper.
///
/// Returns `None` if `id` is not an element node in the document.
#[must_use]
pub fn new(doc: &'a Document, id: NodeId) -> Option<Self> {
if doc.is_element(id) {
Some(Self { doc, id })
} else {
None
}
}

/// Returns the underlying [`NodeId`].
#[must_use]
#[inline]
pub fn id(&self) -> NodeId {
self.id
}

/// Returns the tag name of this element.
#[must_use]
#[inline]
pub fn tag_name(&self) -> TagName<'_> {
TagName {
local: self.doc.node_name(self.id).unwrap_or(""),
prefix: self.doc.node_prefix(self.id),
namespace: self.doc.node_namespace(self.id),
}
}

/// Returns the local name of this element (without namespace prefix).
#[must_use]
#[inline]
pub fn local_name(&self) -> &'a str {
self.doc.node_name(self.id).unwrap_or("")
}

/// Returns the text content of this element (concatenated text nodes).
#[must_use]
#[inline]
pub fn text(&self) -> Option<&'a str> {
self.doc.node_text(self.id)
}

/// Returns the first direct child element whose local name matches `name`.
#[must_use]
pub fn child_by_name(&self, name: &str) -> Option<Element<'a>> {
for child_id in self.doc.children(self.id) {
if self.doc.is_element(child_id) {
if self.doc.node_name(child_id).map_or(false, |n| n == name) {
return Some(Element {
doc: self.doc,
id: child_id,
});
}
}
}
None
}

/// Returns the value of an attribute by local name.
#[must_use]
#[inline]
pub fn attribute(&self, name: &str) -> Option<Attribute<'a>> {
self.doc.attribute(self.id, name).map(|value| Attribute { value })
}

/// Returns an iterator over direct child elements.
pub fn children(&self) -> ChildElements<'a> {
ChildElements {
inner: self.doc.children(self.id),
doc: self.doc,
}
}

/// Returns the parent element, if any.
#[must_use]
pub fn parent(&self) -> Option<Element<'a>> {
self.doc.parent(self.id).and_then(|pid| {
if self.doc.is_element(pid) {
Some(Element { doc: self.doc, id: pid })
} else {
None
}
})
}
}

/// The tag name of an element, split into local name, prefix, and namespace.
#[derive(Clone, Copy)]
pub struct TagName<'a> {
/// Local name (without prefix).
pub local: &'a str,
/// Namespace prefix (e.g. `"wfs"` in `wfs:Query`).
pub prefix: Option<&'a str>,
/// Namespace URI.
pub namespace: Option<&'a str>,
}

impl<'a> TagName<'a> {
/// Returns the local name.
#[must_use]
#[inline]
pub fn local(&self) -> &'a str {
self.local
}
}

/// An attribute value wrapper.
#[derive(Clone, Copy)]
pub struct Attribute<'a> {
/// The attribute value.
pub value: &'a str,
}

impl<'a> Attribute<'a> {
/// Returns the attribute value as a string.
#[must_use]
#[inline]
pub fn text(&self) -> &'a str {
self.value
}
}

/// Iterator over child element nodes.
pub struct ChildElements<'a> {
inner: crate::tree::Children<'a>,
doc: &'a Document,
}

impl<'a> Iterator for ChildElements<'a> {
type Item = Element<'a>;

fn next(&mut self) -> Option<Self::Item> {
for id in &mut self.inner {
if self.doc.is_element(id) {
return Some(Element { doc: self.doc, id });
}
}
None
}
}

// Extend Document with convenience methods.
impl Document {
/// Returns the root element as an [`Element`].
#[must_use]
pub fn root_element_ref(&self) -> Option<Element<'_>> {
self.root_element().map(|id| Element { doc: self, id })
}
}
2 changes: 2 additions & 0 deletions src/tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
//! prev\_sibling). This avoids borrow checker issues, reference cycles,
//! and per-node heap allocation.

mod element;
mod node;

pub use element::{Attribute as ElementAttribute, Element, TagName};
pub use node::NodeKind;

use crate::error::{ParseDiagnostic, ParseError};
Expand Down
Loading