Commit Graph

246 Commits

Author SHA1 Message Date
07d3ddfa97 fix(regex): use the XSD definition of \w and \W
\w was the Perl word class (alphabetic, decimal, underscore). XSD 1.0 Datatypes
F.1 defines \w as [#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}], so symbols and marks are
word characters and the connector `_` (Pc) is not; \W is its complement and had
wrongly matched symbols. \w is now computed as "not punctuation, separator, or
other" via the category matcher.

XSTS: invalid instances accepted 231 to 210. The internal regex fixtures that
assumed `_` is a word character encoded Perl semantics and were corrected to the
XSD definition (symbols are word chars, `_` is not), confirmed against the XSTS
reV cases.

Refs #147, #129.
2026-06-13 18:31:07 +02:00
47e20ff90b fix(schema): enforce block="substitution" on substitution-group heads
The substitution filter dropped only members whose type derived from the head's
type by a blocked method (extension/restriction). The `substitution` keyword in
a head's block forbids substitution-group substitution outright, whatever the
member's type, and was ignored, so a same-type member still substituted for a
head declared block="substitution". Members are now dropped entirely when the
head blocks substitution.

XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74.
Baseline updated; regression tests cover the substitution block rejecting a
member, plain heads still validating, and the no-block control.

Refs #147, #129.
2026-06-13 18:31:07 +02:00
c9f41c387e fix(schema): honor empty-element default/fixed values and replace enumeration on restriction
An element with a default or fixed value takes that value when it appears empty,
so the empty content is valid even where the type would reject an empty string.
The constraint value, not the empty string, is validated against the type,
including any xsi:type override, so a fixed value invalid against the overriding
type is still rejected.

Validating the constraint value under xsi:type exposed a second defect: a
restriction's enumeration was unioned with the inherited enumeration, so a
derived type wrongly accepted the base's values. Enumeration now replaces on
restriction (the new set is the value space); pattern still accumulates and is
ANDed across derivation steps.

XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to
238. Baseline updated; regression tests cover empty default/fixed, present
content still validated, and the constraint value checked against an xsi:type.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
43456029ac fix(schema): enforce element-level block against xsi:type substitution
The block on an element declaration (distinct from block on its type) was
parsed into the derivation tables but never threaded to the instance validator,
so an xsi:type that substitutes a type derived by a blocked method was accepted.
elementBlock now flows through XSDCompiled, SchemaDocument, the validation
context, and both ComplexValidator construction sites to blockedSubstitutionError,
which checks the element's own block alongside the type's. It also resolves the
element-ref type indirection (a ref'd child carries an `element:foo` key that
resolves through `types` to the concrete declared type) so the derivation chain
and block checks fire for referenced children, not only the document root.

XSTS: invalid instances accepted 261 to 240. Baseline updated; regression tests
cover element block rejecting a restriction xsi:type, the no-block control, and
an unrelated block not over-rejecting.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
2ad15b774f style(schema): drop vestigial day clamp in DurationValue.instant
The reference dates are all day 1, so min(1, daysInMonth(...)) was always 1;
the clamp read as if the day could vary. Pass day 1 directly with a comment
explaining why no clamp is needed. No behavior change.

Found by the PR critic pass.
2026-06-13 18:04:25 +02:00
f94b2e43e0 style(schema): satisfy swiftlint on the XSTS conformance additions
The conformance fixes tripped swiftlint --strict (it had been run only as
swiftformat): the test helper binding `s` is too short (now `doc`), the
duration reference dateTimes were a three-member tuple (now (year, month) with
day fixed at 1), and DurationValue.Order nested two levels deep (now the
sibling DurationOrder). No behavior change; full suite and XSTS stay green.
2026-06-13 18:04:25 +02:00
a0ccd017f5 fix(regex): recognize the full XSD Unicode block set in \p{Is...}
Only about fifteen Unicode blocks were tabulated, so a pattern over any other
block (\p{IsArmenian}, \p{IsTamil}, \p{IsHangulJamo}, and many more) failed to
compile and rejected valid instances. The table now carries the Unicode 3.1
blocks the XSD regular-expression appendix fixes; surrogate blocks are omitted
since surrogates are not scalar values, and Specials uses the main FFF0..FFFD
range. Each range is verified bidirectionally by the XSTS corpus: valid
in-block instances pass and invalid out-of-block instances still fail, so no
range over-covers.

XSTS: valid instances rejected 502 to 395. Baseline updated; regression test
covers in-block matches, out-of-block rejection, and unknown block names.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
c6305c9d93 fix(schema): treat QName length facets as non-constraining
XSD 1.0 Datatypes 4.3.1 defines the unit of length only for string, anyURI,
binary, and list; for QName it is implementation-defined. We had counted
characters, rejecting valid QName values. Like Xerces and the XSTS NIST set we
now treat length/minLength/maxLength on QName as always satisfied. Lexical
QName validity is still enforced.

The suite's contributors disagree here (NIST: non-constraining; Microsoft: a
character count), which the spec does not resolve, so a single behavior cannot
satisfy both. Net XSTS: valid instances rejected 547 to 502; 4 Microsoft cases
move to invalid instances accepted (257 to 261), recorded as a deliberate
interpretation in docs/xsts-deviations.md.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
e6a0d19c9b fix(schema): enforce ordering facets on xs:duration
minInclusive, maxInclusive, minExclusive, and maxExclusive on xs:duration were
ignored: duration has no total order, so the generic comparator returned no
value and the bounds were skipped. A DurationValue now implements the XSD 1.0
Datatypes 3.2.6.2 partial order, comparing two durations by adding each to the
four reference dateTimes the spec fixes (a month is 28 to 31 days, so the order
is partial). A value incomparable to the bound, such as P1M against P30D, is
outside the ordered range and is rejected.

XSTS: invalid instances accepted 355 to 257. Baseline updated; regression tests
cover the total-order cases, the exclusive bounds, and the incomparable case.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
8f89acc354 fix(schema): enforce pattern and enumeration facets on union types
A union admits any value valid for one of its member types, but pattern and
enumeration (the only constraining facets XSD 1.0 Datatypes 4.1.5 allows on a
union) were ignored, so a member-valid value outside the enumeration or failing
the pattern was accepted. validateUnion now applies both on top of membership.
Enumeration compares in the value space of the admitting member, so 01 equals
an enumerated 1 for an integer member, consistent with the atomic enumeration
path, rather than a lexical compare.

XSTS #147: invalid instances accepted 555 to 355. Baseline updated; regression
tests cover enumeration (including value-space), pattern, and the no-facet case.

Refs #147, #129.
2026-06-13 18:04:25 +02:00
31e8c22e77 fix(schema): count list-datatype length facets by items, not characters
A restriction of a built-in list datatype (NMTOKENS, IDREFS, ENTITIES)
resolved its base through neither the BuiltinType table (only the singular
NMTOKEN exists there) nor the user-type table, so it fell through to an atomic
string and length/minLength/maxLength counted characters instead of list
items, rejecting valid instances. The restriction base now resolves through
simpleTypeReference, which keeps the list variety.

XSTS #146: valid instances rejected 604 to 547, invalid instances accepted
582 to 555. Baseline updated; regression test added.

Refs #146, #129.
2026-06-13 18:04:25 +02:00
85d435d035 fix(schema,regex): bound NFA state explosion and class-subtraction crash; land XSTS baseline
Running the full W3C XSTS archive to completion exposed three crash and hang
classes, now fixed with regression tests, which unblocked the #129 first
measurement.

- Content-model and regex NFA builders now cap total state count (2^20),
  degrading further occurrence/quantifier repetition to star, so nested
  high-maxOccurs schemas no longer OOM (was 8 GB on the MS Particles set).
- Content-model validation advances one active state-set per child instead of
  re-walking the prefix each time, removing the O(children^2) spin.
- Regex class subtraction [abc-[b]] no longer traps on a reversed range, and a
  reversed range such as [z-a] is rejected cleanly.

XSTS first measurement baselined: 75 valid schemas rejected, 2467 invalid
schemas accepted, 604 valid instances rejected, 582 invalid instances accepted
across 14383 groups. The suite runs green in 8s, opt-in via XSTS_ROOT.

Refs #129, #122.
2026-06-13 18:04:25 +02:00
4e0bd218db feat(schema): W3C XSTS runner and occurrence-unroll cap
The W3C XML Schema Test Suite (2006-11-06, ~13k groups; W3C Document
License, never vendored) runs opt-in via XSTS_ROOT: schemaTests must
compile or be rejected per their expected validity, instanceTests
validate against their group's schema, hrefs resolve against the
referencing file, and include/import locations from disk relative to
the schema. Disagreement counts are asserted exactly; baselines are
pending the first measurement (scheduled 2026-06-14), and failures
are written to /tmp/xsts-failures.txt for the burn-down. Building the
runner immediately caught a denial of service: maxOccurs in the
trillions (groupF009v) OOM-killed the process because the content
automaton unrolled occurrence counts literally; unrolling is capped
at 16384 with larger repetitions treated as unbounded, the libxml2
posture. Roadmap mermaid records the closed behavioral-parity set
(#141-#144, collation), the #139 performance epic, and XSTS moving
to runner-in. Advances #129.
2026-06-11 08:47:16 +02:00
ac85dd36de feat(xpath,xslt,wasm): evaluation budget, tailored collation, WASI runtime proof
PureXML.XPath.Budget is the configurable counterpart of libxml2's
fixed 10M node-set cap (libxml2Compatible preset included), thrown as
a located QueryError instead of a silent refusal, default unbounded.
xsl:sort honors lang with tailored alphabet orders for Polish and
Russian derived from the conformance golds, case-folded. The WASM
gate executes all 985 tests under wasmtime on an actual WASI runtime,
wired into CI; the first run caught a real wasm32 trap (Int is 32
bits there, so format-number's digit extraction trapped at 2^31
despite the 2^53 guard; internals are now Int64 and the xsl:number
bound is platform-correct). Xalan baseline 127 -> 125. Closes #143,
closes #144.
2026-06-11 06:26:13 +02:00
9e345ea0fe feat(xpath,parsing): public streaming surface, linear document-order at scale
EventReader.next(), both initializers, and the Event alias are public
(an internal typealias in a split file had been shadowing the public
Parsing.Event); a new PureXMLPublicAPITests target compiles without
testable import so facade gaps fail CI. The large-corpus benchmarks
exposed document-order sorting as quadratic on flat fan-outs; the fix's
first attempt was itself worse (an eager sibling table built per
predicate evaluation), caught by sampling the live run, and replaced
with a lazy table: single-node sets never touch order keys and a
parent's table builds on its second lookup. XPath at 42 MB drops
27.1 s to 1.57 s (190x to 11.7x); at 210 MB PureXML completes in 9.4 s
the query libxml2 refuses at its 10M node-set cap (#143 tracks our
configurable budget). Benchmark harness: size series to 210 MB, CORPUS
mode for real files (NASA, SwissProt measured), SAX stage,
refusal-aware table, NULL-safe C driver. All conformance suites,
Linux, and WASM verified. Closes #142.
2026-06-11 06:06:31 +02:00
72eaeeb439 perf(parsing): byte-backed Reader and bulk content runs
String input copies its UTF-8 once into owned storage (an unsafe
pointer encapsulated in a class lifetime, never escaping the parser)
and scalars decode straight off the pointer, eliminating the
per-character closure chain; ASCII literals match and consume at the
byte level; and plain-ASCII character data scans as a single byte run
with one String built per run. Parse drops 32 percent cumulative
(0.49 s to 0.34 s on the 4.2 MB corpus, libxml2 ratio 30x to 20x).
The W3C suite caught one boundary (a run starting with '>' could
swallow the ]]> check after the character path consumed ]]); the run
scanner leaves a leading '>' to the character path, pinned as a unit
regression. All conformance suites, Linux, and WASM verified.
2026-06-10 18:48:57 +02:00
0ce59c826b perf(parsing): allocation-free Reader.matches(), first measured pass
matches() ran on every scan decision and materialized an Array of the
literal per call, the hottest comparison in the parse profile. It now
walks the literal directly. Parse time on the 4.2 MB benchmark corpus
drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x); profile
findings and the optimization log recorded in docs/benchmarks.md.
2026-06-10 18:21:08 +02:00
6c0afa3749 fix(xslt): composition cycles terminate; id() gains a per-document index
An include or import cycle recursed without bound and crashed the
host with a stack overflow, reachable from stylesheet data. The
compile chain tracks the active load chain by resolved href and drops
re-entry, while diamond composition keeps loading both copies with
their own import precedences per 2.6.2. id() builds a per-document
index on first use instead of re-walking the document per call,
matching key(). Cycle and diamond regression tests added.
2026-06-10 18:07:02 +02:00
3ea4e6f37d feat(xslt): top-level stylesheet parameters
PureXML.XSLT.transform accepts parameters overriding xsl:param globals
by name; xsl:variable globals are never overridable, with parameter
names recorded across imports and includes. The conformance runner
honors the xalan harness contract, reading <case>.param files (CRLF
is one Character in Swift, so the runner splits on newline graphemes,
which the param files exposed). Closes #141. Xalan baseline
128 -> 127.
2026-06-10 17:35:11 +02:00
c96933df89 feat(xslt): space-ignorable sort collation
xsl:sort's default text order treats spaces as ignorable at the
primary level with a later-first-space tie-break, the Java Collator
semantics the conformance golds encode, derived from the gold
orderings. Xalan baseline 129 -> 128.
2026-06-10 17:24:06 +02:00
62db18f5f6 feat(xslt): DTD-typed id() and node-kind generate-id()
The XSLT layer's id() follows the XPath definition exactly: only
attributes the source's DTD declares as type ID identify elements
(internal or external subset, via the transform's resolver), and a
document without ID declarations yields the empty set. generate-id()
produces non-empty, distinct identifiers for attribute and namespace
nodes through their identity hash. Lookup implementations move to
XSLTLookups.swift for the file caps. Xalan baseline 131 -> 129.
2026-06-10 17:03:39 +02:00
06a7a9fcbd feat(html): HTML 4 void elements and the ampersand-brace convention
basefont and isindex join the void-element set, and the html output
method leaves an ampersand unescaped when a brace follows (the
&{...} attribute convention of XSLT 16.2). Xalan baseline 134 -> 131
(output 20 -> 17).
2026-06-10 16:41:54 +02:00
6174c7bde1 feat(xslt): global variable scope and precedence, NCName:* name tests
A top-level variable evaluates with every previously evaluated global
in scope, so referencing an earlier top-level param works, directly or
via call-template; same-name globals resolve by import precedence with
the last fold winning, so a later sibling import beats an earlier one;
the stylesheet fold uniformly favors later contributions, matching the
post-order precedence model; and the XPath compiler accepts NCName:*
name tests, matching any name in the prefix's namespace at the -0.25
default priority, so @ped:* outranks @*. Xalan baseline 144 -> 134
(variable 10 -> 5, conflictres 8 -> 5).
2026-06-10 16:38:03 +02:00
ef4e7afa7a feat(xslt): post-order import precedence, scoped apply-imports, relative hrefs
Import precedences are assigned in post-order over the import tree, so
sibling imports are distinct and every unit sits above its own
imports; apply-imports searches only the current template's
stylesheet's own import subtree per 5.6, reverting to the built-in
rule when that unit imports nothing; an included stylesheet's
declarations join the including unit at its precedence while its
imports join the import list; and include/import hrefs resolve against
the including stylesheet's URI. Included documents' roots stay
retained until their declarations are parsed (tree parents are weak,
so the in-scope xmlns chain could vanish mid-compile). Xalan baseline
159 -> 144 (reluri 11 -> 3, impincl 9 -> 2).
2026-06-10 16:27:06 +02:00
cff26b255d feat(xslt): attribute-set merging, copied namespaces, html booleans, loader entities
Same-name xsl:attribute-set declarations merge as ordered definitions
per 7.1.4, each expanding its used sets before its own attributes with
later definitions winning; xsl:copy carries the source element's
in-scope namespace nodes per 7.5; the namespace fixup lets a carried
attribute prefix shadow an inherited binding unless the element's own
name relies on it; the html method minimizes boolean attributes whose
value repeats their name; and external DTDs and entities in the
stylesheet or source resolve through the transform's document loader.
Four unit tests pin the behaviors. Xalan baseline 174 -> 159
(attribset 13 -> 4, copy 12 -> 7).
2026-06-10 16:11:02 +02:00
b84fdd00ef fix(xslt): no fatal traps on extreme format-number and xsl:number values
format-number of a value at or beyond 2^53 ran an unguarded
Double-to-Int conversion, a fatal trap reachable from user data; a
fraction picture beyond 15 places overflowed the scale conversion the
same way; and xsl:number value= beyond 2^53 trapped identically.
Digit extraction clamps fraction places to 15 (the 2^53 exactness
bound), pads back to the picture's minimum, renders out-of-range
integers through the exponent expander, and xsl:number falls back to
plain number rendering outside [1, 2^53). Sort-key rows always carry
one key per sort. Regression tests cover all three trap paths.
2026-06-10 15:35:36 +02:00
3d26db555f docs(xpath): note the strict unprefixed name-test rule on bindings 2026-06-10 15:03:19 +02:00
2b5080e30e fix(xslt): gate raw-output markers, cache pattern table, add CI tests
Raw-output marker resolution and 16.4 stripping run only when the
stylesheet mentions disable-output-escaping, so source text containing
U+E000/U+E001 survives every other transform untouched, and the text
method removes markers without unescaping content that was never
escaped. The pattern-matching function table is built once per
transform instead of per (template, node) membership test, and a dead
variable left in the numbering engine is gone. Eight focused unit
tests cover d-o-e (with the fragment rule and marker safety), CDATA
terminator splitting, simplified syntax, the html default method with
META injection, per-document key(), and attribute-start axes, so the
burn-down behaviors are CI-visible without the opt-in corpus.
2026-06-10 15:01:07 +02:00
a86a57eec4 feat(xpath): following and preceding axes from attribute nodes
The following and preceding axes work from attribute and namespace
starts, anchored at the owner element per XPath 1.0 document order:
an attribute follows its element, precedes the element's children,
and has no descendants of its own. Xalan baseline 182 -> 174
(axes 16 -> 8).
2026-06-10 14:05:28 +02:00
7b4887b7ad feat(xslt): per-document key indexes in document order
key() resolves its index against the current node's own document,
built lazily per document root, so keys work inside document() trees;
a node-set use expression indexes the matched node under every
member's string value; and key() results return in document order
regardless of lookup value order. Xalan baseline 187 -> 182.
2026-06-10 14:01:28 +02:00
40605ff6e5 feat(xslt): contextual sort keys and cross-document template matching
Sort keys evaluate once per node with the node's own position, size,
variables, and bindings, so position()-based keys work, and NaN keys
order before numbers under data-type=number. Match patterns evaluate
over the matched node's own document, so templates match nodes loaded
through document(); the match cache keys include the document
identity. Xalan baseline 204 -> 187 (sort 18 -> 11, mdocs 14 -> 6).
2026-06-10 13:54:37 +02:00
9945fb74a5 feat(xslt): prolog ordering, html doctype, simplified syntax, CDATA splitting
The XML declaration precedes the document type declaration; a
public-only doctype is emitted by the html method and ignored by the
xml method, which requires doctype-system; a literal result element
carrying xsl:version compiles as a single match="/" template (the 2.3
simplified syntax); and cdata-section text containing ]]> splits
across section boundaries. Parser entry and Parts move to
XSLTParserEntry.swift for the file caps. Xalan baseline 218 -> 204
(output 34 -> 21).
2026-06-10 13:48:03 +02:00
1646128b35 feat(xslt): stylesheet namespace resolution for patterns and expressions
Templates carry their in-scope xmlns bindings into match patterns
(match cache keyed on pattern plus bindings) and instantiated
expressions, resolving prefixed name tests by namespace URI; with
bindings supplied an unprefixed test selects only the null namespace,
the XPath 1.0 rule. Instruction-scope capture includes the default
declaration: xsl:element honors in-scope xmlns="uri" for unprefixed
names, xmlns="" keeps them namespace-free, and an unusable created
name drops the wrapper, keeping content. Xalan baseline 263 -> 218
(namespace 41 -> 13).
2026-06-10 13:36:59 +02:00
da8bb6b92a feat(xslt): key() over attribute nodes, key/id match patterns, doctyped sources
key() and document() read the string value of attribute and namespace
node arguments instead of dropping them, so the Muenchian grouping
idiom works; match patterns evaluate with the XSLT function table,
making key('k','v') and id('x') patterns match; and the XSLT entry
points parse sources, stylesheets, and document() loads with doctypes
allowed, as the conformance corpora require. Xalan baseline
292 -> 263 (idkey 39 -> 16).
2026-06-10 13:29:27 +02:00
b41678064e feat(xslt): templates over attribute and namespace nodes
apply-templates and for-each keep attribute and namespace nodes in
their selections; the context carries the non-tree current node with
its owner element backing the tree-only machinery, XPath evaluation
and current() start from it, match patterns select attribute nodes
through attribute identities in the match cache, the built-in rules
copy an attribute's or namespace's value as text, xsl:copy copies the
focused attribute or declaration itself, and an attribute result
arriving after element content is ignored (the XSLT recovery).
Sorting, for-each, and choose move to XSLTSorting.swift for the file
caps. Xalan baseline 352 -> 292 (axes 35 -> 16, copy 29 -> 16).
2026-06-10 13:24:31 +02:00
5a0c2ec7e4 feat(xslt): output methods in depth
disable-output-escaping on xsl:value-of and xsl:text (sentinel-marked
raw text, unescaped after serialization; string extraction from a
fragment drops the markers per 16.4), the html default method for a
null-namespace html root (16.1), the spec-default XML declaration for
the xml method, META Content-Type injection into head (16.2),
case-insensitive raw-text and void element classes, and HTML 4.01
Latin-1 named entities on html output. Whitespace-only stripping now
means XML whitespace, so NBSP-only text survives. Conf comparison
treats tag-adjacent whitespace as layout outside the canonical path.
Xalan baseline 371 -> 352 (output 49 -> 30).
2026-06-10 13:10:50 +02:00
255f7508fd feat(xslt): literal result element namespace copying
A literal result element copies its in-scope namespace declarations to
the result per 7.1.1, minus the XSLT namespace and the namespaces of
prefixes listed in exclude-result-prefixes and
extension-element-prefixes in scope (#default included); an aliased
stylesheet namespace declares its result prefix and URI instead. The
fixup pass drops declarations that repeat an inherited binding so the
copied nodes do not redeclare per descendant, and created names reuse
stylesheet prefixes. xsl:output indent=yes switches to the Xalan
indentation form (own lines, no leading spaces). Xalan baseline
446 -> 371 (namespace 89 -> 42).
2026-06-10 12:55:48 +02:00
38bbb349c5 feat(xslt): full format-number picture parsing
The picture parses into affixes, number part, and the negative
subpattern: literal prefixes and suffixes pass through, the negative
subpattern's affixes apply (with the default minus when it has no
distinguishing ones), per-mille and percent multiply and stay literal,
the grouping size derives from the pattern's last grouping separator,
colliding decimal/grouping separators read as grouping, and U+00A4
renders as the en-US currency symbol. Digit extraction now uses one
scaled rounding and integer arithmetic, eliminating per-digit float
drift. Xalan numberformat 30 -> 1; baseline 475 -> 446.
2026-06-10 12:45:57 +02:00
2134a23c41 feat(xslt): full xsl:number engine and stable document() identity
xsl:number implements all of XSLT 1.0 section 7.7: level
single/multiple/any with count and from matched as patterns through the
match cache, the value expression with the below-one bypass, and the
format-token engine (punctuation, separator and token reuse, zero
padding, alphabetic/roman/Greek sequences, digit grouping). An
unmatched single/multiple count renders empty; level=any always yields
a number. document() now caches per transform so repeated loads of the
same URI return the identical document and generate-id() stays unique
against live nodes (the idkey49 flake). Xalan baseline 557 -> 475
(numbering 84 -> 7).
2026-06-10 12:37:03 +02:00
e5402f8bf5 feat(xslt): namespaced name creation, copy attribute sets, result fixup
xsl:attribute/xsl:element honor the namespace attribute and resolve
prefixed names against the instruction's in-scope stylesheet
declarations (captured at parse); xsl:copy applies use-attribute-sets;
and a namespace-fixup pass before serialization declares every
namespace a created name carries, reusing same-URI bindings or
generating ns0.. prefixes, and undeclares an inherited default that
would capture an unqualified name. Xalan baseline 604 to 557.
2026-06-10 12:10:48 +02:00
3415ff3d9a feat(relaxng): schema-correctness validation, spec suite complete
A RELAX NG schema document is validated before pattern interpretation
in three layers: the section-3 grammar check (vocabulary, attributes,
arity, text, QNames, datatypeLibrary URIs, hrefs, 4.16 excepts, xmlns
prohibitions), compiler checks (4.17 combine rules, unresolved refs,
parentRef scope, startless grammars, include/externalRef cycles and
overrides, grammar-checked loaded documents, unknown datatypes,
built-in-library params), and the 4.19/7.1-7.4 restrictions over the
compiled algebra after 4.20/4.21 normalization (prohibited paths,
content types, duplicate attributes via representative-name overlap,
infinite-class attributes outside oneOrMore, interleave constraints),
with recursion detection deliberately before normalization. Incorrect
schemas: 213 compiling down to 7, all the pre-Fifth-Edition name-class
class (count asserted). Two leniency tests updated to spec behavior.

Closes #131
2026-06-10 11:38:45 +02:00
c628682086 feat(relaxng): QName values with dual-context resolution, instance level clean
A value of type QName compiles to its (namespace, local) pair via the
schema's xmlns/ns scope; the derivative engine threads the instance
element's in-scope bindings to value comparison in both the tree walk
and the streaming state (frames carry their bindings), so instance
prefixes and the default namespace resolve against the instance
document and unbound prefixes match nothing. Streaming also gains the
tree walk's empty-element rule. The spec suite's instance level is
fully clean: valid 289/289, invalid 291/291; only the asserted
schema-correctness class remains.
2026-06-10 10:57:14 +02:00
d0175d5ca0 refactor(validation): located structured validity findings
DocumentType.validityFindings carries ValidityFinding values (reason
plus subject) instead of strings, DTDSchema.declarationErrors holds
ready ValidationErrors, and every declaration-level finding renders at
its declaration's coding path (x, r/@a) instead of at the document
root, matching the XSD consistency-errors precedent. Texts unchanged.
2026-06-10 10:45:17 +02:00
5e7cc292f9 feat(relaxng): grammar scoping, parentRef, include overrides, div flattening
Each grammar element opens a define scope (scope-mangled names in the
flat table) with parentRef reaching the enclosing scope; includes are
transitive and include-level defines replace the included grammar's
same-name defines (4.7 override); div wrappers flatten while keeping
ns/datatypeLibrary inheritance (4.11); value text stays untrimmed so
whitespace-preserving datatypes compare exactly (6.2.9); dataExcept
recurses through choices. Spec suite: valid 286/289, invalid 287/291;
the six remaining instance cases are all the QName datatype.
2026-06-10 10:34:44 +02:00
b8fa90ccf5 feat(relaxng): ns inheritance, QNames, include-start, combine, data-except
The compiler implements simplification 4.9-4.10 (ns inheritance across
ancestors and document boundaries; QName resolution against in-scope
xmlns declarations; the attribute name= shorthand stays namespace-less),
include brings in the included grammar's start with overrides (4.7),
multiple starts and defines combine with a single omitted combine
merging by the named method (4.17), data/except lands as a dataExcept
pattern in the derivative engine (4.12), an empty element matches data
whose lexical space admits the empty string, and whitespace-only
attribute values match empty-matching patterns. Spec suite: valid
274/289, invalid 279/291.
2026-06-10 10:24:46 +02:00
2eab91bf2b feat(relaxng): simplification 4.1, 4.2, and xml:base href resolution
Foreign-namespace elements are stripped before pattern interpretation
(namespace-less elements count only inside fully unqualified schemas),
name/type/combine values are whitespace-trimmed, and include and
externalRef hrefs resolve against in-scope xml:base attributes and the
loaded document's URI, threaded through nested loads with visited
tracking on resolved hrefs. Spec-suite baselines tighten to valid
242/289 and invalid 278/291.
2026-06-10 09:18:34 +02:00
ddfe14370c feat(canonical): C14N 1.0 spec-example vectors, three conformance fixes
The six worked examples of the C14N 1.0 Recommendation's section 3 run
as input/expected-output tests. They exposed and fixed: top-level
siblings now separated by single line feeds in the canonical form;
superfluous empty default-namespace declarations dropped in inclusive
mode (xmlns="" renders only when undeclaring a rendered non-empty
default); and scalar-level escape loops, so a CR clustered with a
following LF still escapes to &#xD;. Escaping split to its own file
for the length caps.

Closes #132
2026-06-10 08:51:47 +02:00
2f1b936e36 feat(parsing): scalar-level lexing, xmlconf archive fully conformant
The reader buffers one Unicode scalar per element, splitting
multi-scalar graphemes from any source, so combining marks cannot merge
with delimiters and the Fifth Edition's scalar-defined name productions
classify correctly; the replacement-text, content-model, and ATTLIST
sub-scanners and the entity-literal validator follow, while grapheme
clusters reassemble wherever scanned text accumulates. Clears the last
eduni baseline, grows the asserted IBM pre-5e class to 300, and with
the xmltest manifest's own EDITION exclusions for 140/141 empties the
xmltest baseline: every xmlconf case applicable to a Fifth Edition,
namespace-aware processor passes.

Closes #135
2026-06-10 08:40:26 +02:00
98792a1715 feat(parsing): strict internal-subset profile, xmltest 185 of 186
Limits(strictInternalSubset: true) rejects conditional sections and
parameter-entity references inside markup declarations in the internal
subset, the two feature classes XML 1.0 reserves for the external
subset. DeclSep references and quoted attribute defaults (AttValue)
stay legal; defaults unchanged. The xmltest runner opts in, leaving
case 141 (pre-Fifth-Edition name classes) as its only deviation.

Closes #128
2026-06-10 08:29:54 +02:00
3d56d19cbd feat(parsing): per-entity base-URI tracking for nested external entities
A relative system identifier declared inside an external entity
resolves against that entity's own URI (RFC 3986, the C14N resolver):
ExternalID gains base and resolvedSystemID, the scanner threads the
current entity's URI through external-subset and PE scans, and
resolvers receive the merged identifier. Clears eduni E18; the eduni
baseline now holds only the grapheme-lexing class (#135).

Closes #138
2026-06-10 08:24:17 +02:00