All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- XPath attribute results are first-class attribute nodes (#47).
Node-sets (
XPathValue::NodeSet) now holdXPathNodeentries — either a tree node or an attribute identified by owner element and index — instead of bareNodeIds. This fixes a family of wrong-answer bugs rooted in the old one-value-per-element override map://a/@x != //a/@ycomparisons no longer clobber each other's values,@*yields one node per attribute (previously only the first per element),count((//a)[1]/@href)returns a number instead of a type error,name()/local-name()/namespace-uri()work on attribute nodes,@attr/..navigates to the owner element, predicates evaluate with the attribute as context node, and namespace declarations (xmlns,xmlns:*) are no longer visible as attributes per the XPath 1.0 data model. Mixed node-sets (//a | //a/@x) sort in document order with attributes directly after their owner element. Breaking: code matchingXPathValue::NodeSetmust handleXPathNode; use.anchor()for the owning tree node or.as_tree_node()to filter attributes out. The single-match collapse (attribute paths returningXPathValue::String) is gone — convert withstring()where a string is wanted.xmllint --xpathprints attribute results asname="value"lines, matching libxml2. The C API keepsxmloxide_xpath_nodeset_item()(returns the owner element id for attributes) and addsxmloxide_xpath_nodeset_item_is_attribute(),..._attr_name(), and..._attr_value(). - XPath step predicates apply per context node per XPath 1.0 §2.4:
//a/b[1]now selects the firstbof everya(previously the first of the merged set), andposition()/last()in step predicates are relative to each context node's own node-set, matching libxml2. The parenthesized form(//a/b)[1]keeps global-position semantics. - Schematron rules with attribute contexts (
context="//@id") now fire with the attribute itself as the context node —.is the attribute value and<value-of select="."/>reads it, per ISO Schematron. Previously such rules either never fired (single match) or misfired against the owner element.
- Fix exponential-time entity recursion check (#42, thanks @hey-jj). The WFC: No Recursion walks in the DTD validator re-visited entities once per path, so a 474-byte document with chained entity declarations took 8+ seconds to parse and a 694-byte one about 22 hours. The walks (including parameter entities and ATTLIST-default validation) now memoize entities proven acyclic, making the check linear in the size of the DTD. Cycle detection is unaffected.
- Bound element nesting across entity expansions. Entity replacement text
is parsed by nested sub-parsers, which now inherit the outer parser's
nesting depth so total element depth stays bounded by
ParseOptions::max_depthinstead ofmax_depthper expansion level.
- General entity replacement text is parsed as content per XML 1.0 §4.4
(#43, thanks @hey-jj).
EntityRefnodes now carry their parsed expansion as children: character references in declarations are expanded when replacement text is built (§4.5), nested entity references are included, and markup-bearing entities produce real element children instead of escaped text — while serialization still emits&name;, keeping round-trips lossless. Replacement text must match the content production (§4.3.2); unbalanced or split tags are rejected. Entity expansion is subject to the expansion counter, a nesting-depth cap, and the 5x amplification guard, matching libxml2. DTD content-model validation sees through entity references (§4.4.3), so entity-supplied elements are validated too. - XPath
!=uses its own existential semantics for node-sets per XPath 1.0 §3.4 (#44, thanks @hey-jj).!=was evaluated asnot(=), inverting empty-node-set comparisons and breaking multi-node sets (both=and!=can hold at once). Node-set vs boolean keeps boolean-conversion semantics; scalar comparisons are unchanged. An absent attribute step now also yields an empty node-set (false under both=and!=) instead of an empty-string sentinel. - XPath filter-path continuations navigate instead of filtering (#20,
thanks @ancientcatz).
(//a)[1]/@hrefparsed to the same AST as(//a)[@href], so the trailing path acted as a predicate andstring((//a)[1]/@href)returned the anchor text. A newExpr::FilterPathAST variant evaluates the continuation steps against the filter's node-set. Breaking: downstream exhaustive matches onxpath::ast::Exprmust handle the new variant.
- Fix stack exhaustion when parsing deeply-nested DTD content models
(CVE-2026-61727 / GHSA-7jmw-29gc-ffx4, thanks @williamareynolds). The DTD
parser recursed without a depth bound when parsing an
<!ELEMENT>content model, so a small untrusted document with many nested(in a content model could overflow the stack and abort the process — an uncatchable denial of service (CWE-674). This is reachable on the default parse path because the internal DTD subset is parsed during ordinary parsing, and the existingParseOptions::max_depthdid not cover it (it bounds element nesting, not the DTD content-model grammar). The content-model recursion is now bounded at 256 levels and returns a normalParseErrorpast the limit; well-formed DTDs are unaffected.
- Exclusive C14N: emit
xmlns=""default-namespace undeclaration when an element with no default namespace is canonicalized inside scope of an inherited default namespace (#16, thanks @williamareynolds). Previously the undeclaration was only emitted in inclusive C14N. Per Canonical XML 1.0 §2.3 (inherited by Exclusive C14N §3), the empty default namespace must be made explicit so the canonical form correctly reflects the element's namespace context.
- Bump
actions/checkout4.3.1 → 6.0.2 across all workflows (#14) - Bump
actions/download-artifactv4 → v8.0.1 in release workflow (#13) - Bump
actions/upload-artifactv4 → v7.0.1 in release workflow (#15) - Bump
softprops/action-gh-releasev2 → v3.0.0 in release workflow (#12) - Bump
taiki-e/install-action2.75.19 → 2.75.26 in release workflow (#17) - Remove
bench.ymlworkflow — the regression check required agh-pagesbranch that was never created, so it had been failing on every PR. Run benchmarks locally withcargo bench.
- Schematron C FFI —
xmloxide_parse_schematron,xmloxide_free_schematron,xmloxide_validate_schematron, andxmloxide_validate_schematron_with_phasefor C/C++ consumers; Schematron was previously only available in Rust, Python, and WASM - CSS selector C FFI —
xmloxide_css_select,xmloxide_css_select_first, andxmloxide_free_nodeid_arrayfor querying elements with CSS selectors from C/C++ - CSS selectors in Python —
css_select()andcss_select_first()methods onDocumentin pyxmloxide - WASM tree mutation APIs —
createElement,createText,createComment,appendChild,removeNode,setAttribute,removeAttribute,setTextContent,insertBefore,cloneNodeonWasmDocument - Validation benchmarks — criterion benchmarks for DTD, RelaxNG, XSD, and Schematron validation
- Expanded XPath benchmarks — count, string function, position predicate, ancestor axis, and union expression benchmarks
- CSS selector benchmarks — class selector and complex combinator benchmarks
- 58 CSS evaluator inline tests covering tag, class, ID, attribute, pseudo-class, combinator, and universal selector matching
- 10 new FFI tests (5 Schematron + 5 CSS) bringing FFI test total to 138
- README incorrectly listed Schematron as unsupported — the Limitations section claimed "No Schematron" despite Schematron being added in 0.4.0
- README listed XPath as "1.0 only" — updated to "XPath 1.0+" reflecting the 17+ XPath 2.0 functions added in prior releases
- Outdated test counts in README — updated from 936 to 1078 unit tests
- Unit tests expanded from 1010 to 1078
- FFI tests expanded from 128 to 138
- README now documents serde, async, and Schematron features
- MIGRATION.md expanded with HTML5 parsing, HTML5 streaming, Schematron validation, and CSS selector migration examples
- CLAUDE.md module map updated with css/, serde_xml/, async_xml, and full ffi/ listing
xmllint --schematronadded to CLI documentation in README- Schematron added to migration table in README
xmloxide.hheader updated with Schematron and CSS selector declarations
0.4.0 - 2026-03-14
- ISO Schematron validation (
validation::schematronmodule) — rule-based XML validation per ISO/IEC 19757-3, complementing DTD, RelaxNG, and XSDparse_schematron()/validate_schematron()/validate_schematron_with_phase()API- Assert/report checks with
XPath-driven test expressions - Firing rule semantics (first matching rule wins per pattern)
- Three-level
<sch:let>variables (schema, pattern, rule scope) - Message interpolation via
<sch:value-of select="..."/> - Phase-based selective validation (
<sch:phase>/<sch:active>) - Dual namespace support: ISO (
http://purl.oclc.org/dml/schematron) and classic 1.5 (http://www.ascc.net/xml/schematron), plussch:prefix - 31 unit tests + 11 integration tests with realistic purchase order schema
xmllint --schematron— CLI validation against Schematron schemas, following the existing--relaxngand--schemapatterns- XPath
matches()function — regex matching for Schematron pattern validation, with a hand-rolled engine (noregexcrate dependency) supporting character classes, quantifiers, shorthand (\d,\s,\w), alternation, grouping, counted quantifiers{n,m}, and flags (i,s) - XPath namespace-aware name matching —
XPathContext::set_namespace()registers prefix→URI bindings so that prefixed name tests like//inv:invoiceresolve via namespace URI comparison instead of string matching; Schematron<sch:ns>bindings are automatically threaded through - XSD
elementFormDefaultsupport — when set to"qualified", child elements in instance documents must carry the schema's target namespace; fixes namespace validation for UBL 2.4 and similar schemas - WASM validation APIs —
validateRelaxng(),validateXsd(),validateSchematron()onWasmDocument, returningWasmValidationResultwithisValid,errors, andwarnings - Python validation APIs —
validate_relaxng(),validate_xsd(),validate_schematron()onDocument, returningValidationResultwithis_valid,errors,warnings, and__bool__()support fuzz_schematronfuzz target for schema parsing and validation (11 total)
- XPath attribute path returning String instead of NodeSet — multi-step paths
ending with an attribute axis (e.g.,
item/@amount) now correctly return aNodeSet, fixingsum(),count(), and comparison operations on attribute collections - XPath
prefix:*tokenization — the lexer now correctly tokenizes namespace wildcard expressions likeinv:*as a single token instead of failing with a parse error - Schematron message interpolation for NodeSets —
<sch:value-of>expressions that return element NodeSets now correctly compute string values using the document context instead of returning empty strings
- Unit tests expanded from 988 to 1010
- Fuzz targets expanded from 10 to 11
0.3.3 - 2026-03-13
xsd:importandxsd:includesupport (#3) — multi-file XSD schema composition for real-world schemas like UBL 2.4SchemaResolvertrait for pluggable schema loading (filesystem, HTTP, embedded, etc.)parse_xsd_with_options()— new entry point that followsxsd:include(same-namespace merging, chameleon includes) andxsd:import(cross-namespace type resolution)XsdParseOptionsstruct with optional resolver and base URI- Cycle detection prevents infinite loops in circular import/include chains
- Namespace-aware type resolution via
QNameprefix maps — imported types liketns:AddressTyperesolve correctly throughimported_namespaces - Existing
parse_xsd()unchanged (backward compatible, silently ignores import/include)
xsd:element refsupport — element references (ref="cbc:ID") resolve to global element declarations in local or imported schemas, enabling real-world UBL 2.4 validation- 26 new tests (24 unit + 2 integration) covering include merging, chameleon includes, import cross-namespace resolution, cycle detection, transitive includes, namespace mismatch errors, element refs, and UBL-like multi-schema validation patterns
- UBL 2.4
BusinessCardschema integration test: parses 15-file schema graph and validates the official OASIS example document with zero errors
0.3.1 - 2026-03-06
- Pin
tempfiledev-dependency to<3.20andproptestto<1.7to avoid transitive dependencies requiring Rust 1.84+/1.85+, breaking the MSRV of 1.81
- Pre-commit hook now includes an MSRV check: runs
cargo checkwith the 1.81 toolchain (if installed) or scansCargo.lockfor edition2024 dependencies
0.3.0 - 2026-03-06
- CSS selector engine (
cssmodule) — query document trees with familiar CSS syntax including tag, class, ID, attribute, descendant, child, adjacent sibling, general sibling combinators,:first-child,:last-child,:only-child,:empty,:not(),:nth-child(),:nth-last-child(), and selector groups - Streaming HTML5 SAX API (
html5::saxmodule) — callback-driven API that wraps the WHATWG HTML5 tokenizer directly without building a DOM tree, with automatic character coalescing for efficient text handling - Auto-populated
id_map—element_by_id()now works out of the box for XML, HTML 4, and HTML5 documents without requiring DTD validation; the parser automatically indexesidattributes during tree construction - Fast
#idCSS selector path — pure#idselectors use O(1) hash lookup viaelement_by_id()instead of tree traversal - Tree mutation API —
Document::create_element(),create_text(),create_comment(),append_child(),insert_before(),remove_node(),clone_node(),set_text_content(),set_attribute(),remove_attribute() Document::with_capacity(n)— pre-size the arena when expected node count is knownDocument::is_element(id)— convenience method for checking node type- Serde XML support (
serdefeature) — serialize/deserialize Rust types to/from XML viaserde_xmlmodule - Async XML parsing (
asyncfeature) —parse_async()for parsing fromtokio::io::AsyncReadsources - WebAssembly bindings (
xmloxide-wasmsubcrate) — parse, query, and serialize XML/HTML from JavaScript viawasm-bindgen - Python bindings (
pyxmloxidesubcrate) — parse, query, and serialize XML/HTML from Python via PyO3 - Property-based testing — 20 proptest properties covering roundtrip parsing, serialization invariants, and edge cases
- Ecosystem benchmarks — head-to-head benchmarks against
roxmltreeandquick-xml
- HTML 4 parser infinite loop on bare
<not followed by a valid tag start - HTML5 tokenizer panic on multi-byte characters in the ambiguous ampersand state
- Parser performance —
#[inline]annotations on hot-path tree accessors (node_name,attributes,attribute,NodeId::as_index/from_index), direct node field access inDescendantsandChildreniterators (avoiding method-call indirection), arena pre-sizing from estimated input node count - Unit tests expanded from 848 to 936
- FFI tests expanded from 112 to 128
0.2.0 - 2026-03-05
- WHATWG HTML5 parser — full implementation of the HTML Living Standard
parsing algorithm (§13.2.5 tokenizer, §13.2.6 tree construction, §13.5
named character references)
- 7032/7032 html5lib tokenizer tests passing (100%)
- 1778/1778 html5lib tree construction tests passing (100%)
- Fragment parsing (the
innerHTMLalgorithm) viaHtml5ParseOptions::fragment_context - Scripting flag support (
<noscript>raw text vs normal parsing) parse_html5(),parse_html5_with_options(),parse_html5_full()API
- HTML5 serializer —
serialize_html5()with WHATWG-compliant output: void elements without closing tags, raw text elements without escaping, foreign content (SVG/MathML) self-closing tags - HTML5 error reporting —
parse_html5_full()returnsHtml5ParseResultwith all parse errors asParseDiagnosticvalues with source locations - HTML5 FFI bindings —
xmloxide_parse_html5(),xmloxide_parse_html5_fragment(),xmloxide_serialize_html5()C functions xmllint --html5— CLI support for HTML5 parsing- HTML5 fuzz targets —
fuzz_html5_parseandfuzz_html5_fragmentfor security testing across 24 different fragment contexts - HTML5 benchmarks — full document and fragment parsing benchmarks
- html5lib-tests CI — tokenizer and tree construction conformance suites run on every push/PR and weekly
- HTML5 parser performance — 24% faster than initial implementation via bulk text scanning in Data state, fast-path tag name/attribute scanning, and character batching in the tree builder (~197µs for a 50-section document)
- Fuzz targets expanded from 4 to 10 (added SAX, reader, push, validation, HTML5 parse, HTML5 fragment)
- FFI tests expanded from 112 to 118 (6 new HTML5 tests)
- Unit tests expanded from 785 to 848
0.1.1 - 2026-03-02
- Fix docs.rs build failure caused by
all-features = truepulling in thebench-libxml2feature, which requires system libxml2 headers unavailable in the docs.rs sandbox. Now explicitly listscliandffifeatures.
- Expanded doc comments on
Documentnavigation, iteration, and mutation methods,HtmlParseOptionsbuilder methods,XmlReaderaccessors, andSerializeOptionsbuilder methods.
0.1.0 - 2026-03-01
Initial release of xmloxide — a pure Rust reimplementation of libxml2.
- XML 1.0 parser — hand-rolled recursive descent parser with full W3C XML 1.0 (Fifth Edition) conformance (1727/1727 applicable tests passing)
- Error recovery — parse malformed XML and produce a usable tree, matching libxml2's recovery behavior (119/119 libxml2 compatibility tests passing)
- Arena-based DOM tree —
DocumentwithNodeIdindices for O(1) access, cache-friendly layout, and safe bulk deallocation - HTML parser — error-tolerant HTML 4.01 parsing with auto-closing tags, implicit elements, and void element handling
- SAX2 streaming parser — event-driven API via
SaxHandlertrait - XmlReader — pull-based parsing API
- Push/incremental parser — feed chunks of data as they arrive
- XPath 1.0 — full expression parser and evaluator with all core functions
and axes, including
namespace::axis support - DTD validation — parse and validate against Document Type Definitions
- RelaxNG validation — parse and validate against RelaxNG schemas
- XML Schema (XSD) validation — parse and validate against XML Schema definitions
- Canonical XML — C14N 1.0 and Exclusive C14N serialization
- XInclude — document inclusion processing
- XML Catalogs — OASIS XML Catalogs for URI resolution
- XML serialization — 1.5-2.4x faster than libxml2
- HTML serialization — void elements, attribute rules
- C/C++ FFI — full C API with header file (
include/xmloxide.h) covering document parsing, tree navigation and mutation, serialization, XPath, SAX2 streaming, push parser, XmlReader, validation, C14N, XInclude, and catalogs xmllintCLI — command-line tool for parsing, validating, and querying XML/HTML (behindclifeature flag)- Character encoding — automatic detection and transcoding via
encoding_rs - Namespace support — full Namespaces in XML 1.0 implementation
- String interning — dictionary-based interning for fast comparisons
- Fuzz targets — XML, HTML, XPath, and roundtrip fuzz testing
- Benchmark suite — criterion benchmarks for parsing, serialization, SAX, XmlReader, XPath, push parsing, and head-to-head comparison with libxml2
- Parsing within 3-4% of libxml2 on most documents, 12% faster on SVG
- Serialization is 1.5-2.4x faster than libxml2
- XPath is 1.1-2.7x faster than libxml2 across all benchmarks
- Key optimizations: O(1) character peek, bulk text scanning, ASCII fast paths,
zero-copy element name splitting, inline entity resolution, XPath
//step fusion with fused axis expansion, inlined tree accessors, and name-test fast paths for child/descendant axes
- 785 unit tests across all modules
- 112 FFI integration tests covering the full C API surface
- 1727/1727 W3C XML Conformance Test Suite tests (100%)
- 119/119 libxml2 compatibility tests (100%)
- Real-world XML, security/DoS, and entity resolver integration tests