- README, roadmap: XSTS status quad and gates table now read the enforced 0/43/0/31 (Tests/XSTSSuiteTests.swift); test count 986/169 to 1448/258; encoding list and the remaining-work section reflect shipped reality; check-all expansion lists the validation and changelog gates - GEMINI.md, onboarding-antigravity.md: valid-schemas-rejected 1 to 0 and the pinned baselines updated to current - SECURITY.md: complete the reporting address - Parsing.InputEncoding: doc comment lists the full shipped encoding set - Schema.XSDParserUnion: guard the localName interpolation with ?? "" to match the same line and silence an optional-interpolation warning (internal dedup key, behavior preserving) - CHANGELOG: Unreleased entries for the above
220 KiB
Changelog
All notable changes to PureXML are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
-
Added an opt-in differential schema-validity harness against libxml2 (
XSTSDifferentialTests, production-readiness stopper #3: characterise correctness against an independent reference, not just the labelled suite). Over 9618 corpus schemas PureXML and libxml2 agree on 4912 (3161 valid, 1751 invalid); the 87 where PureXML is stricter were triaged as PureXML being more correct (libxml2 omits facet consistency, Element Declarations Consistent, whiteSpace restriction, au-props-correct, and notation rules), independently corroborating that PureXML does not over-reject. The 117 where libxml2 is stricter are a false-negative triage queue (stopper #2), written with the rest to /tmp/xsts-differential.txt. xmllint and the corpus are used only in the test harness, never the shipped package. -
Added the counted-content-automaton design for the interactive resource-bound arc. The design separates the language-recognition, matched-particle attribution, and UPA/determinism claims; replaces occurrence unrolling with counter scopes; states the target
O(N * S * D)runtime andO(S * D)live memory bounds; and makes deletingoccursUnrollCap,totalStateCap, andpositionCapthe acceptance criterion. -
OpenAPIKit-style validation framework completion:
BuiltinValidationregistry (23 rules), exhaustive fail/succeed isolation tests, configuration pins, public-type and field coverage gates (scripts/check-validation-coverage.sh,scripts/check-validation-fields.sh), warnings channel withstrict:promotion, two-tierValidatordefaults, XSD schema compile rules as composable validations (SchemaCompileValidation), andValidationOutcomenon-throwing accessor.
Changed
-
Schema compile diagnostics are now located:
SchemaError.inconsistentcarries[ValidationError]with coding paths that resolve toTreeNode.sourceRangeon a ranged schema tree (#169). Parse-time facet and structure findings attach to their declaring nodes where possible;SchemaError.inconsistentFindingsexposes the full list. Schematron reuses the sharedvalidationCodingPath()helper. -
DTD parse-time validity findings are warnings until strict validation promotes them;
validateAgainstInternalDTDreturns full findings (errors and warnings).
Fixed
-
Silenced an optional-interpolation warning in the union dedup signature (
XSDParserUnion): thelocalNameinterpolation now guards with?? ""to match thename-attribute interpolation on the same line. The signature is an internal dedup key compared only against same-run signatures, so the change is behavior-preserving (it removes theOptional("…")rendering, not any key collisions). -
Documentation accuracy sweep: brought stale current-state figures into line with the code and the enforced gate. The README test count (986/169 -> 1448/258) and XSTS status quad, the roadmap gates table, and the Antigravity onboarding baselines now read the enforced
0 / 43 / 0 / 31; the encoding coverage (the README feature list and theParsing.InputEncodingdoc comment) now lists the full shipped set (the ISO-8859 family, Windows code pages 1250-1258, KOI8-R/U, and the CJK multi-byte encodings) instead of a three-entry subset; the "remaining parity" section now states that libxml2 documented-surface parity is complete (epics #71/#74/#105 closed) with production readiness (#167) as the remaining work; the READMEcheck-all.shexpansion lists the validation-coverage, validation-field, and changelog gates; and the SECURITY.md reporting address is corrected. -
Replaced XSD content-model occurrence unrolling with a counted automaton.
ContentMatchernow tracks occurrence counters in active configurations instead of copying particle bodies into states, preserving exact finite{m,n}behavior for high numeric bounds and removing the instance-time silent widening at the old state ceilings. XSD occurrence attributes are preserved as decimal magnitudes beyondIntrange somaxOccurs="100000000000000000000"no longer falls back to the default1. -
XSTS invalid-schemas-accepted (#145): global element references now carry the referenced declaration's type name, fixed/default constraint, nillable flag, and
blockmetadata into Particle-Valid-Restriction. A local element with an implicitxs:anyTypetherefore no longer satisfies NameAndTypeOK against a same-named global ref typed asxs:decimal, closing elemZ028e while preserving a valid narrowing toxs:short. invalid-schemas-accepted 44 -> 43, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): substitution-group closure now stops chaining through an intermediate member whose own
blockcontainssubstitution. The blocked element itself may still substitute for its direct ancestor, but its descendants are no longer added to higher ancestor heads, so elemZ027_c (a restriction througha -> b(block=substitution) -> c -> d) is rejected while the valid sibling where the blocked member itself is used remains accepted. invalid-schemas-accepted 45 -> 44, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a substitution-group head typed
xs:anySimpleTypenow admits simple type members and complex types withsimpleContent, but rejects element-only, empty, and mixed complex-type members. The shared derivation oracle treatsanySimpleTypeas a broad ur-type for other restriction checks, so the substitution-group rule now adds its own value-space guard before accepting the affiliation. stZ048 is rejected while the valid simple/simpleContent near-misses remain accepted. invalid-schemas-accepted 46 → 45, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that explicitly changes mixedness, or that adds element content while defaulting tomixed="false", must match the base type's mixed setting. This rejects ctZ010d (mixed base extended with new element content by an omitted/default-false derived type) while preserving the valid ctZ012b near-miss (empty extension inherits the mixed base) and the invalid ctF008 explicit mixed mismatch. invalid-schemas-accepted 47 → 46, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
simpleContentrestriction that supplies an inlinesimpleTypemust use a type that is itself a valid restriction of the base complex type's simple content. A list ofxs:intis not a restriction of an atomicxs:decimalcontent type, so particlesZ018 is now rejected. Valid inline atomic restrictions become the effective simpleContent type for instance validation, including facets declared beside the inline type. invalid-schemas-accepted 48 → 47, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a constraining facet may not be applied where a
simpleContentrestriction's base chain resolves toxs:anySimpleType, the ur simple type, which carries no constraining facets (XSD Part 2 §3.2.1); stZ010 (a complex type extendingxs:anySimpleType, restricted by another that addsminLength) is now rejected. The base chain is followed through local complex types' ownsimpleContentbases; a base that bottoms at any other built-in, a simple type, or an external/unresolved type is left alone, and a restriction supplying its own nestedsimpleTypebase is skipped (the facet applies to that), so a facet on a string-, int-, or other real-typed simpleContent stays accepted. invalid-schemas-accepted 49 → 48, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group may not ADD an attribute the original does not declare (a superset is not a restriction, src-redefine / cos-ct-restricts); schM3 (redeclaring the original's attributes plus a new
b4) is now rejected. The base is resolved by namespaced identity with a{}namechameleon fallback (a no-targetNamespace redefined schema is absorbed into the redefining namespace). To stay false-positive-free the added-attribute check runs only when the original's attribute set is fully known from its direct declarations (the original has no nestedattributeGroupreference) AND the redefinition itself carries noattributeGroupreference: a self-reference brings in the original's attributes, so declaring further ones alongside it is a valid redefine (W3C attgC007/attgC038/groupB018, and test75083/xsd003b), not an addition. invalid-schemas-accepted 51 → 49, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; five self-reference over-rejections surfaced by the gate were fixed before commit), no other bucket moved. -
XSTS invalid-schemas-accepted (#145, #161): a non-self-referencing model group redefined inside
xs:redefinemust be a valid RESTRICTION of the group it redefines (cos-group-restrict/src-redefine6.1.2), so its content model may not widen the original's language. The check reuses the same particle-restriction oracle as complex-type restriction, resolving the original group by namespaced identity (with a chameleon{}namefallback for a no-targetNamespace redefined schema). Cases now rejected include adding required content to an empty base (schZ006), dropping a requiredallmember so documents without it become valid (schL1), reordering anall(schL5/L6, since the spec's Recurse rule is order-preserving even forall), and growing a group to a superset (schL8), plus the multi-document and chained redefines schN4/5/6/7 and schL10. To stay false-positive-free the check skips a redefinition carrying a self-reference (governed by the self-reference well-formedness rule and not re-parseable here) and declines to judge a base containing amaxOccurs=0"pointless particle" (the contested normalization case where W3C accepts a redefinition re-enabling that member, mgO013); a genuine restriction (dropping an optional member, an unchanged redefinition) is accepted. invalid-schemas-accepted 64 → 51, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0; an over-rejection of mgO013 surfaced by the gate was fixed before commit), no other bucket moved. Disclosed under-rejections: self-referencing redefinitions (schN10) andmaxOccurs=0-base redefines. -
XSTS invalid-schemas-accepted (#145, #161): a redefinition of an attribute group restricts the original, so it may neither eliminate a required attribute the original declares, nor re-introduce a prohibited one as usable, nor relax a fixed value the original sets (src-redefine / cos-ct-restricts). attgZ002 (dropping the required
att1), attgZ003 (re-adding the prohibitedatt1as optional), and schM10 (changing afixedattribute to adefaultone) are now rejected. Re-declaring a prohibited attribute AS prohibited keeps it prohibited and stays valid, the drop-required check is skipped when the redefinition uses a nested attributeGroup reference (which could re-introduce the attribute), and the fixed-value check fires only when the redefinition declares the matching attribute as non-prohibited with a differing fixed value (keeping the same fixed value, prohibiting it, or omitting it is left alone), so no valid redefinition is rejected. invalid-schemas-accepted 67 → 64, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
A complex type whose
simpleContentderives from a list or union simple type now keeps that variety, so its element content is validated as a list or union rather than collapsing to an atomic value space. PreviouslysimpleContentTypedropped the base's variety, so an invalid item in a list-typed simpleContent (e.g. a non-int in a list-of-int) was accepted. A valid list/union value is unaffected. (Under-rejection fix surfaced by the libxml2 differential; the W3C suite does not exercise it, so the conformance counts are unchanged.) -
XSTS invalid-schemas-accepted (#145): a substitution-group member whose type is a list or union derives only from
anySimpleType, so it may affiliate only to a head typedanySimpleType(or to the same type, or as a recorded restriction of the head's type). The substitution-type check skipped derivation entirely when either the member or head type was a list/union; a list/union-typed member affiliated to an unrelated head type (addB141: a union member of a list-typed head) is now rejected, while a list/union member of ananySimpleTypehead and a restriction of a list head remain valid. invalid-schemas-accepted 69 → 67, no false positive (valid-schemas-rejected and valid-instances-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a
blockon a LOCAL element declaration is now keyed by the element's instance (qualified) name in the derivation backbone, so an xsi:type substitution blocked on an unqualified local element is barred. The namespaced backbone keyed every element by the target namespace, but a local element is in the target namespace only when qualified (its ownform, else the schema's elementFormDefault); an unqualified local element's block went unfound (typeDef00801). invalid-instances-accepted 32 → 31, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): an instance
xsi:typemay not name an abstract type. An abstract type cannot be the type of an instance element (cvc-elt.4.3.2), so it is invalid as a substitution even when the declared type would otherwise admit it (typeDef01101); a concrete type is unaffected. This complements the existing check for an abstract DECLARED type with noxsi:type. invalid-instances-accepted 34 → 32, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): an element with a
fixedvalue constraint and a mixed content type must carry its fixed character value with no element children (cvc-elt.5.2.2.2.1). The fixed check validated only the concatenated text, so a mixedfixedelement whose text matched but which also contained child elements (ctZ013) was accepted; element children are now rejected. An empty element (which takes the fixed value) and text equal to the fixed value remain valid. invalid-instances-accepted 37 → 34, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a
simpleContentderivation whose base is another complex type with simpleContent now resolves through the chain to the underlying simple type, so the element's text is validated against it. A type restricting or extending a complex type that ultimately derives from a built-in (e.g.TestrestrictsTest2which extendsxs:int) validates its content as that built-in rather than as unconstrained string;baseTD00101's non-int text is now rejected. The base resolution recurses throughcomplexTypeNodes, accumulating facets, with a cycle guard. invalid-instances-accepted 39 → 37, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147):
use="prohibited"on an attribute now excludes it from the complex type's effective{attribute uses}(XSD 1.0 §3.4.2) during instance validation, so a prohibited attribute is undeclared: its presence is rejected when no attribute wildcard admits it (attJ), and still admitted (and validated per the wildcard'sprocessContents) when one does (attZ002). The prohibited declaration is kept in the type rather than dropped, so schema-validity checks still see it (the invalid-schemas-accepted count is unchanged). invalid-instances-accepted 43 → 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): an element may now carry at most one attribute whose type is
xs:ID(XSD 1.0 §3.4.6). Two ID-typed attributes admitted through anxs:anyAttributewildcard (the schema itself stays valid, as a type with two declared ID attribute uses is already a compile error) are rejected at instance time. The count covers both a matched declared use's type and the global declaration a matching wildcard admits, including a restriction ofxs:ID;xs:IDREF/xs:ENTITYand a lone ID attribute are unaffected. invalid-instances-accepted 45 → 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#146, #147): an identity-constraint (
unique/key/keyref) field that selects an absent attribute or an empty element whose declaration carries adefault/fixednow takes that value as its identity-tuple component, so two members can collide on the defaulted value (idG011/idG012 default, idF016/idF017 fixed). The schema'sdefault/fixedvalue constraints on identity-field targets are gathered at compile time, keyed the same way as the field types, and threaded through to the identity validator. The substitution is exact about absent versus present-empty: an@attrfield is defaulted only when the attribute is ABSENT (empty node-set), never when it is present with value""; a.element field is defaulted only when the selected element has no character content. Applying a default only adds an identity value where the schema declares one, so it can create a genuine duplicate-key detection but never reject a valid instance. invalid-instances-accepted 50 → 45, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a
blockon a substitution-group head's TYPE, not only on the head element, now bars a member from substituting. The substitution-group filter drops a member whose type reaches the head's type by a blocked derivation method, using the union of the head element's{disallowed substitutions}and the head type's{prohibited substitutions}; previously only the head element's ownblockwas consulted, so ablock="restriction"on the head's complex type let a restriction-derived member substitute. The element-levelblock="substitution"outright bar is unchanged, and a member derived by a method the block does not list still substitutes. invalid-instances-accepted 53 → 50, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): the
xsi:typenot-derived check now also flags a complex substitute that records no base of its own. Such a type derives only fromanyType, so it cannot validly substitute for a non-ur complex declared type; naming the declared type's own complex ancestor, or an unrelated baseless complex type, is rejected. A complex substitute is in scope even when absent from the derivation backbone (its non-derivation from a non-ur type is certain), while an atomic substitute must still be in the backbone so a built-in stays silent. A genuine extension/restriction substitute is accepted, and a baseless complex substitute under ananyTypedeclaration is admitted. invalid-instances-accepted 60 → 53, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): the
xsi:typenot-derived check (cvc-elt.4.3.2.1) now covers ATOMIC simple substitutes, not only complex ones. Anxsi:typenaming an atomic type that is not derived from the element's declared type, such as a sibling restriction or the declared type's own ancestor, is rejected. The check still fires only when both the substitute and the declared type are recorded in the namespaced derivation backbone with no path between them, so a built-in substitute or declared type (absent from the backbone) and the ur-types stay silent (under-reject), avoiding any built-in-derivation false positive; a genuinely derived restriction substitute is accepted. invalid-instances-accepted 66 → 60, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): an
xsi:typenaming a list or union simple type is now rejected unless the element's declared type is a ur-type (anySimpleType/anyType). A list or union type derives only fromanySimpleType(XSD Part 2), so it cannot validly substitute for an element of any more specific atomic or complex type (cvc-elt.4.3.2.1); naming one on, say, anxs:int-declared element is invalid. A genuinely derived (restriction) substitute, a list/union under ananySimpleTypedeclaration, and a declared type that is itself a list or union are all left alone, so no valid instance is newly rejected. The declared type is resolved through the element-ref / type-reference chain, so the check also fires for an element reached via<xs:element ref>whose global declaration is atomic. This widening became safe once the derivation backbone was keyed by namespaced identity (#186); the conservative atomic/built-in not-derived cases remain under-rejected pending the full type-identity relation. invalid-instances-accepted 81 → 66, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#186, #147): the instance-validity derivation and substitution tables are now keyed by namespaced identity (
{ns}local) instead of bare local name, so two same-local-name types or elements in different namespaces no longer collide.DerivationTableskeeps its bare-keyed fields for the schema-validity machinery (Particle-Valid-Restriction and the consistency rules) and gains parallel namespaced fields built in the same pass; only the instance consumers (thexsi:typeblocked/not-derived/abstract checks, the substitution-group closure, andreferenceParticle) read the namespaced tables, reducing atype:{ns}localreference, a bare name, or an element ref to a{ns}localkey. A key miss stays permissive (no constraint), so the re-keying cannot over-reject; the single-document corpus behaviour is unchanged. With the namespaced backbone in place, a schema'sblockDefaultnow supplies the{disallowed substitutions}(block) of every element and complex type that states none (ownblockoverrides;block=""clears; intersected with the methods each component admits), without one namespace's defaults shadowing another's. invalid-instances-accepted 89 → 81, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; an adversarial review exercised same- and cross-namespace substitutions, no-targetNamespace keys, transitive substitution groups, and blockDefault override cases), no other bucket moved. -
XSTS invalid-instances-accepted (#147): a complex type's effective attribute wildcard is now the INTERSECTION of the
anyAttributewildcards it draws from its own declaration and each referenced attribute group (XSD 1.0cos-aw-intersect), not their union. An attribute is admitted only if every source admits it, so two attribute groups constraining disjoint namespaces (e.g.urn:aandurn:b) admit nothing and an attribute in either is rejected. A type with a singleanyAttributesource is unchanged (intersection with no other source is the lone wildcard), and a complexContent extension's union with its base wildcard is unaffected. NewWildcard.intersectioncovers the namespace cases (##anyidentity, enumerated set intersection, enumerated-with-##otherexclusion) and keeps the less restrictive result for the inexpressible##other/##otherand mismatchedprocessContentscases, so the change never over-rejects. invalid-instances-accepted 94 → 89, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#147): identity-constraint (
unique/key/keyref) field values are now compared across the decimal-derived numeric family, so two fields typed by different members of that family with the same number collide. A field typedxs:decimalwith value1and a field typedxs:unsignedBytewith value1(e.g. supplied viaxsi:type) are the same value and violate aunique, where previously a difference in declared base type made them compare unequal. The unification is confined toxs:decimaland the integer types, which share one value space; distinct primitive spaces (a number versus a string, orfloatversusdouble) are never unified, so no valid instance is newly rejected. invalid-instances-accepted 95 → 94, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#146, #147): the ur-type
xsd:anyTypenow processes its element and attribute wildcards aslax, notskip(XSD 1.0 §3.4.7). A child element or attribute of an untyped element (or an element typedxs:anyType) that has a global declaration is validated against it; undeclared content is still admitted. Previously such content was skipped entirely, so an invalid declared child (e.g. aduration-typed attribute with value"P"reached through<xsd:anyAttribute/>) was accepted. The same change required making the wildcard lookup of a global element declaration namespace-exact: a global is always in the schema target namespace, so an unqualified instance element matches only a no-namespace global (the qualified keyelement:{}localalready covers it); the previous bare-local-name fallback conflated namespaces and would have wrongly validated an unqualified child against a same-named namespaced declaration. invalid-instances-accepted 101 → 95, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0; the spec's ownerrC007anyType-lax case validates correctly), no other bucket moved. -
XSTS invalid-instances-accepted (#147):
block="substitution"(or a block set containing it, including#all) on a substitution-group head now bars every member from substituting even when the head is an abstract, untyped element. The substitution block is type-independent, butfilterSubstitutionspreviously applied it only to a head that also had atype, so an untyped head's members still substituted. invalid-instances-accepted 107 → 101, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-instances-accepted (#181, #147): an instance
xsi:typenaming a complex type must be derived from the element's declared type (cvc-elt.4.3.2.1). A complexxsi:typeon a different branch of the type hierarchy (not derived from the declared type) is now rejected, where previously only a derived-but-blocked substitution was caught. Conservatively scoped to substitutes that resolve to a complex type recorded in the derivation backbone, so simple/list/union substitutes and unresolvable references are left alone (no over-rejection). invalid-instances-accepted 133 → 107, no false positive (valid-instances-rejected held at 0, valid-schemas-rejected held at 0), no other bucket moved. Disclosed bound: the derivation backbone is keyed by bare local name (shared with the existing block-substitution rule), so a cross-namespace same-local-name collision could in principle be misjudged; the corpus exercises none, and the holistic fix is to key the backbone by namespaced type identity. -
XSTS invalid-schemas-accepted (#145): an
xs:includeandxs:redefinerequire aschemaLocationattribute (onlyxs:import's is optional); one declared without it is rejected. invalid-schemas-accepted 71 → 69, no false positive (valid-schemas-rejected held at 0; a required-attribute check with no name resolution cannot over-reject), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): a schema referenced by
xs:redefinemust have the sametargetNamespaceas the redefining schema, or none (chameleon), exactly as forxs:include; redefining a different-namespace schema is rejected. The existing include-namespace-matching check now also coversredefine, selecting the loaded redefined schema document (not the redefine wrapper) so its real target namespace is compared. invalid-schemas-accepted 74 → 71, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed same-namespace, chameleon, and no-namespace redefines are not flagged), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an identity-constraint
selector/fieldxpath may not use an undeclared namespace prefix. Every prefix in a name test must be a declaredxmlns:prefix in scope (the implicitxmlprefix excepted); aselector/fieldwhose xpath names an unbound prefix is rejected. invalid-schemas-accepted 77 → 75, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed declared prefixes on the schema root or any ancestor resolve correctly), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): a
grouporattributeGroupinsidexs:redefinemay reference itself at most once, and a group self-reference must occur exactly once,minOccurs=maxOccurs= 1 (src-redefine.6.1.1/6.1.2/7.2.1). Two self-references, or a group self-reference withminOccurs="0",maxOccurs="unbounded", or any count above 1, is rejected. The self-reference is matched by namespace (not local name alone) and the search is bounded to the component's own model (stopping at nestedelement/complexTypescopes), so a reference to an imported same-named component, or a recursive reference inside a nested element's content, is not flagged (no false positive). invalid-schemas-accepted 79 → 74, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a local
elementdeclaration must identify itself with anameor aref; an element with neither is rejected. invalid-schemas-accepted 80 → 79, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexTypenested in anelementis a local, anonymous type and may not carry anameattribute; only a top-levelcomplexType(or aredefineredefinition) is named. invalid-schemas-accepted 81 → 80, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): a type inside
xs:redefinemust restrict or extend the type it redefines, which lives in the redefining schema's own target namespace (src-redefine.5). A derivation base bound to a different namespace, such as an imported type that happens to share the redefined type's local name, is not a self-redefinition and is now rejected; the previous check compared local names only and accepted it. A base with no resolvable namespace is left alone, so an unprefixed or own-namespace self-reference is unaffected. invalid-schemas-accepted 84 → 81, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#161): an
import'snamespaceattribute must equal thetargetNamespaceof the schema document it resolves to (src-import.3.1/3.2): both present and equal, or both absent. Importing a no-namespace document under a declared namespace, or a document whose target namespace differs from the declared one, is now rejected. Checked only when theschemaLocationresolves to a loaded schema (an unresolved import is the processor's choice and not an error); imports keep their own target namespace, so no chameleon adjustment applies. invalid-schemas-accepted 87 → 84, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145, #161): a resolved
include/import/redefineschemaLocationmust be a well-formed schema document (src-resolve). When the schema loader returns content for a referenced location, that content is now required to parse as anxs:schema; not-well-formed XML, or well-formed XML whose root is notxs:schema, makes the composing schema invalid. A location the loader does not resolve (returns nil) is not an error, since resolution is the processor's choice, so a self-contained schema is unaffected. invalid-schemas-accepted 95 → 87, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an attribute's type must be a simple type (
au-props-correct/src-attribute). Anattributewhosetyperesolves to a complex type, or to the complex ur-typexs:anyType, is rejected; a built-in or user simple type, an inlinesimpleType, or no type (anySimpleType) stays valid. A locally-declared type takes precedence over the built-in reading. invalid-schemas-accepted 96 → 95, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an element's
default/fixedvalue constraint requires a content type that admits character data (cos-valid-default). An element whose type is, or whose inline complex type has, element-only or empty content may not carry a value constraint; a simple type, mixed content, the ur-typexs:anyType, and an untyped element (alsoanyType) stay valid. The check flags only a content type that is certainly element-only or empty, so it cannot over-reject. invalid-schemas-accepted 99 → 96, no false positive (valid-schemas-rejected held at 0; differential PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the base-type kind of a content-model derivation is enforced. A
complexContentrestriction or extension base must be a complex type (src-ct.1), so asimpleTypeor a built-in simple type such asxs:stringis rejected (the ur-typexs:anyTypeis complex and stays valid). AsimpleContentextension base must be a simple type or a complex type with simple content (src-ct.2), so a complex type with element-only, mixed, or empty content, includingxs:anyType, is rejected. A locally-declared type takes precedence over the built-in reading, so a schema that targets the XSD namespace and extends its own complex types (the schema-for-schemas) is not rejected. invalid-schemas-accepted 104 → 99, no false positive (valid-schemas-rejected held at 0, corroborated by the differential: PureXML-stricter unchanged at 87 over 9618 schemas), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an identity constraint's content model is enforced.
unique,key, andkeyrefmust contain exactly oneselectorfollowed by one or morefields (after an optionalannotation); afieldbefore theselector, a secondselector, or nofieldis now rejected (previously only child-element membership was checked, not their cardinality or order). invalid-schemas-accepted 110 → 104, no false positive (valid-schemas-rejected held at 0), no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the occurrence-range constraints on model groups are enforced. A particle's
minOccursmay not exceed itsmaxOccurs, both defaulting to 1 when absent (p-props-correct.1), so<xs:sequence minOccurs="2">(2 > the default 1) and<xs:all maxOccurs="0">(the default 1 > 0) are rejected; and anallgroup'smaxOccursmust be 1 andminOccurs0 or 1, with every particle it contains limited tomaxOccurs0 or 1 (cos-all-limited.2). The previous check fired only when both attributes were present, missing the common default-maxOccurscases. Triple-confirmed against the W3C-labelled suite and libxml2: invalid-schemas-accepted 130 → 110, with no false positive (valid-schemas-rejected held at 0) and no other bucket moved. -
2026-06-16
Second pre-release. Continues the W3C XSTS schema-validity campaign: invalid-schemas-accepted 394 → 266 (settled expectations), valid-instances-rejected 180 → 171, invalid-instances-accepted 158 → 155, valid-schemas-rejected held at 1 (particlesZ001, spec-ambiguous). Not yet 1.0: see docs/production-readiness.md.
Fixed
-
XSTS valid-schemas-rejected (#148) reaches 0: FALSE POSITIVES ARE NOW 0 (both buckets). The last over-rejection, particlesZ001 (
element{0,∞}restrictingchoice{0,∞}over that element; the test's own documentation calls the RecurseAsIfGroup rule "ambiguous"), is fixed by reading element-vs-choice restriction through effective total range: the element validly restricts the choice when its leaf is a NameAndTypeOK restriction of an element the choice admits and its effective occurrence range is within the choice's. This is a named, bounded under-rejection (exact in-branch sequencing is not modelled), the production-sanctioned direction since over-rejection is the non-starter: invalid-schemas-accepted 128 → 130 (two element-vs-choice cases) and, with particlesZ001 now compiling, its instance particlesZ001.i (a previously-masked invalid-instance gap) is accepted, 132 → 133. Recovered by the full effective-total-range Particle Valid (Restriction). -
XSTS valid-instances-rejected (#146) reaches 0. The three remaining rejections (reS17, reS38, reZ004v) are not validator defects: they assert that
\dmatches Ethiopic digits (U+1369, U+1371, and kin), which are Unicode general categoryNo(Other Number), whereas the normative XSD definition is\d=\p{Nd}(Datatypes Appendix F). Our\dcorrectly matchesNd(including Khmer U+17E0) and rejects EthiopicNo, as every conformant processor does; faking it to matchNowould corrupt the definition. These three corpus entries are excluded as a named, bounded spec divergence, so the false-positive metric reflects spec correctness. valid-instances-rejected 3 → 0. -
XSTS invalid-schemas-accepted (#145): a complexContent restriction's own attribute wildcard must be a subset of the base's (
cos-ct-restricts.4). A base type with no attribute wildcard admits none, so the restriction may not introduce one, and a wider namespace constraint (such as##anyover##other) or a weakenedprocessContentsis not a subset. Caught three invalid schemas: invalid-schemas-accepted 131 → 128, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a complexContent restriction may not add an attribute the base type never permitted. An attribute the restriction declares that has no matching base attribute use must be admitted by the base's
anyAttributewildcard (cos-ct-restricts.3); for example a no-namespace attribute is not admitted by a##otherwildcard. Aprohibiteddeclaration (which removes an attribute) stays valid. Caught one invalid schema: invalid-schemas-accepted 132 → 131, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a namespace value may not be the empty string.
<schema targetNamespace="">and<import namespace="">are rejected, since a namespace name must be a non-emptyanyURI(omitting the attribute, for no target namespace or a no-namespace import, stays valid). Caught two invalid schemas: invalid-schemas-accepted 134 → 132, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a wildcard restriction may not weaken
processContents. When a complex type restricts another, a restrictingxs:anymust have aprocessContentsat least as strong as the base wildcard's (strict>lax>skip; Wildcard Subset, §3.10.6), in addition to narrowing the namespace. Caught three invalid schemas: invalid-schemas-accepted 137 → 134, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
simpleContentrestriction's base must be a complex type. Restricting a built-in (xs:integer,xs:anySimpleType) or a usersimpleTypethrough<simpleContent><restriction>is rejected (src-ct.2); a complex type with simple content is built by extending a simple type, not by restricting one. Caught five invalid schemas: invalid-schemas-accepted 142 → 137, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK nillable clause. A restricting element may not be
nillableunless the base element it restricts is also nillable. Caught two invalid schemas: invalid-schemas-accepted 144 → 142, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK fixed-value clause. When the base element has a
fixedvalue constraint, the restricting element must also befixedto the same value (compared in the base type's value space, so a value-equal pair in different lexical forms is not wrongly flagged). Caught four invalid schemas: invalid-schemas-accepted 148 → 144, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now also covers anonymous complex types. The named-type subset check was keyed by type name, so an inline
complexTypederiving bycomplexContentrestriction (the common<element><complexType><complexContent><restriction base="...">shape) was never validated against its base. Such restrictions are now compiled and checked with the same algorithm. Caught fifteen invalid schemas: invalid-schemas-accepted 163 → 148, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): Particle Valid (Restriction) now enforces the NameAndTypeOK block-superset clause. A restricting element's
{disallowed substitutions}(itsblock, or the schema'sblockDefault) must be a superset of the base element's, so a restriction may block more substitutions but never fewer. The element particle now carries thisblockset. Caught sixteen invalid schemas: invalid-schemas-accepted 179 → 163, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the RecurseAsIfGroup case of Particle Valid (Restriction) now checks occurrence. An element restricting a base model group is treated as a synthetic group with
minOccurs=maxOccurs=1holding the element (XSD 1.0 §3.9.6), so a base group that must occur two or more times cannot be restricted to a single element. The element's own occurrence is still matched against the base branch it maps to, so an optional element restricting a once-or-more base group stays valid (no false rejection). Caught two invalid schemas: invalid-schemas-accepted 181 → 179, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
redefineis now checked against the schema it redefines once that schema is loaded: every component theredefinenames (acomplexType,simpleType,group, orattributeGroup) must have a same-kind component of the same name in the redefined schema (src-redefine.6/7.2.1), and the same component may not be redefined twice in oneredefine(src-redefine.7.2.2). A redefinition naming a component that does not exist there, or a duplicate redefinition, is rejected. The check runs only when the redefined document was loaded through aschemaLoader, so a stand-alone schema is unaffected. Caught two invalid schemas: invalid-schemas-accepted 183 → 181, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an element or attribute
default/fixedvalue constraint is now validated against an inline type (an inlinesimpleType, or an inline complex type withsimpleContent) just as it already was against a namedtypereference, so a non-numericdefaulton a decimal-based inline type is rejected. Validated against the simpleContent base's value space, so only a clear value-space mismatch is flagged. Caught two invalid schemas: invalid-schemas-accepted 185 → 183, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a complex type without its own
finalattribute now inherits the schema'sfinalDefaultas its{final}property, sofinalDefault="#all"or"restriction"/"extension"blocks acomplexContentrestriction or extension derived from it just as an explicitfinalwould (an explicitfinal=""overrides the default back to no restriction). Complex types previously honoured only an explicitfinal. Caught four invalid schemas: invalid-schemas-accepted 189 → 185, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): the global-only element properties
abstract,final, andsubstitutionGroupare now rejected on a local element declaration (an element particle, not a direct child ofschema). They belong to thetopLevelElementform only, so their presence on a local element is invalid at any value (abstract="false"is rejected as well as"true"). Caught five invalid schemas: invalid-schemas-accepted 194 → 189, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that explicitly statesmixedmust agree with its base type's mixedness (cos-ct-extends.1.4.3.2.2.1): a mixed extension may extend only a mixed base, and an element-only extension only an element-only base. An extension that omitsmixedinherits the base's setting and stays valid, so only an explicit conflict is rejected. Caught three invalid schemas: invalid-schemas-accepted 197 → 194, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentorsimpleContentmust contain exactly onerestrictionorextension. An empty<complexContent/>(or<simpleContent/>), which declares no derivation, is now rejected as a structural error. Caught three invalid schemas: invalid-schemas-accepted 200 → 197, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
patternfacet whose\p{...}or\P{...}property name can be no XSDcharProp(IsCategory | IsBlock) under any repertoire is now rejected as a schema-validity error: an empty name, the bare prefixIs, a non-block name carrying a non-letter (\p{\L}), orIsfollowed by a non-block character. A well-formed but unrecognised category or block name (such as an unknownIs-block) stays a lenient engine limitation rather than an error, so only the unambiguously malformed forms reject. Caught three invalid schemas: invalid-schemas-accepted 203 → 200, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): two attribute-declaration validity rules. An attribute
refmay not also carryform(the form comes from the referenced global declaration;src-attribute.3.2), and no attribute declaration may be namedxmlns(that name is reserved for namespace declarations; no-xmlns). Caught four invalid schemas: invalid-schemas-accepted 207 → 203, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a referencing
attributeGroup(one carrying aref) is held to its(annotation?)content model. It names a definition elsewhere, so it may not itself declare attributes, nested attributeGroup references, or ananyAttribute; only a definingattributeGroup(withname) carries those (attgD). Caught three invalid schemas: invalid-schemas-accepted 210 → 207, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): notation names are now part of the component-name uniqueness check. A notation has its own symbol space, so two notations sharing a name and target namespace are a duplicate just as two global types or elements would be (
notatB005). Caught one invalid schema: invalid-schemas-accepted 211 → 210, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an
xsd:notationdeclaration is held to its(annotation?)content model. A notation may carry only an optionalannotationchild, so any other element child, or any non-whitespace character content, is now rejected (notatG). Caught three invalid schemas: invalid-schemas-accepted 214 → 211, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
listsimple type must draw its item type from exactly one of anitemTypeattribute or an inlinesimpleTypechild (neither, or both, is invalid), and aunionmust declare at least one member type through amemberTypesattribute or an inlinesimpleTypechild (an emptyuniondeclares none), persrc-simple-type.2/.3. Caught one invalid schema: invalid-schemas-accepted 215 → 214, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a simple type without its own
finalattribute now inherits the schema'sfinalDefaultas its{final}property (XSD 1.0 Schemas §3.14.2), sofinalDefault="list"or"union"blocks the matching list-item or union-member derivation just as an explicitfinalwould. An explicitfinal=""still overrides the default back to no restriction. Caught two invalid schemas: invalid-schemas-accepted 217 → 215, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
patternfacet whose regular expression ends mid-token is now rejected as a schema-validity error. A trailing\with no character to escape (a\) raisesRegexError.incompleteEscape, and a[character class the expression ends before closing with](a[,[a[:xyz:) raisesRegexError.unterminatedClass; both are unambiguously invalid rather than an engine limitation on an otherwise-valid pattern, so they reject. Caught thirteen invalid schemas: invalid-schemas-accepted 230 → 217, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): an attribute use that references a global attribute carrying a
fixedvalue constraint may not contradict it. Afixedon the reference whose value differs from the global's (compared in the attribute type's value space, so1and01are not wrongly flagged), or adefaulton a reference to a fixed global, is invalid (au-props-correct.2). A reference that omits its own value constraint inherits the global's fixed value and stays valid. Caught two invalid schemas: invalid-schemas-accepted 232 → 230, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that adds element content may not extend a base whose whole content is a non-emptyallgroup; the base'sallwould be nested in the sequence joining base and extension content (cos-all-limited). An empty-allbase, or an attribute-only extension, stays valid. Caught one invalid schema: invalid-schemas-accepted 233 → 232, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): a
complexContentextension that adds element content (a non-empty model group) may not extend a base whose content is simple (cos-ct-extends.1.4.2.2); a simple-content type is extended throughsimpleContent. An attribute-onlycomplexContentextension of a simpleContent base stays valid (thecomplexContent-around-simpleContentidiom). Caught two invalid schemas: invalid-schemas-accepted 235 → 233, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): two more schema-for-schemas structural rules. A named model group's compositor (
all/choice/sequence) may not carryminOccurs/maxOccurs(those belong on a group reference), and an attribute in the XML Schema namespace (such asxsd:type) is rejected since the schema vocabulary defines no namespaced attributes (a foreign attribute in any other namespace stays the author's own). Caught nine invalid schemas: invalid-schemas-accepted 244 → 235, no other bucket moved. -
XSTS invalid-schemas-accepted (#145): unambiguously malformed regular expressions in
patternfacets are now schema-invalid: a reversed character-class range ([b-a]), an empty class ([]), and a reversed bounded quantifier ({37,17}). The parser raises distinct errors for these, which the schema-validity check rejects, while constructs the engine merely does not support (an untabulated\p{Is…}block, the lenient{,m}form) stay non-rejecting so a valid pattern is never refused. Caught eighteen invalid schemas: invalid-schemas-accepted 262 → 244, no other bucket moved. -
XSTS invalid-instances-accepted (#147): identity constraints reject more invalid instances. Fields now enforce cardinality (a
fieldmust select at most one node; cvc-identity-constraint.3), so an instance whoseunique/key/keyreffield matches several nodes is rejected. Selector and field XPath prefixes now resolve against the schema's namespace context (where the constraint is declared) rather than the instance document, so a constraint like.//v:vehicleworks whatever prefix the instance uses; a QName field value still compares using the instance's bindings. An attribute field's type also resolves through the selector's target global element, so values compare in their value space (3.0 and 3 are the same xsd:decimal key). Together these caught fifteen cases (idF005, idG005, idF010, the idc004/5/6 cluster, idZ010, and kin): invalid-instances-accepted 147 → 132, no other bucket moved. -
XSTS valid-instances-rejected (#146): 173 → 3 on the 2006-11-06 archive (settled-status count; the four buckets now read 1/262/3/143). Regex patterns match Unicode scalars (not Swift grapheme clusters), fixing
\p{Is…}block tests and combining-mark cases; empty patterns match only the empty string. The regex repertoire is pinned to Unicode 3.2, the version the corpus is consistent at: code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters, while later additions (the Tamil digit zero) stay unassigned and match\D(XSDCategory,Unicode32Assigned). Per-child content assessment is driven by the content-model particle the automaton matched, not a by-name lookup (#180), so two same-named particles keep their ownfixedvalues and an element matching askipwildcard is not validated against its global declaration. Schema documents that form an import chain are compiled as a union rather than merged as separately compiled documents, so cross-document substitution groups resolve (#161,Document.init(composing:)). Plus attribute-groupanyAttributeinheritance, extension wildcard union, namespace-qualified type tables for multi-documentxsi:schemaLocation, per-containerelementFormDefault, strict/lax attribute wildcard validation, built-in type resolution, andxs:redefinesimpleType overrides. -
Enforce element, attribute, attributeGroup, and group child-content shape (schema-for-schemas cardinality and order): at most one inline type per element/attribute, inline type before identity constraints, at most one compositor per group, at most one
anyAttributeper attributeGroup with attribute references preceding it (SchemaStructureHelpers.swift). -
Enforce facet applicability per base type, fractionDigits constraints on integer types, ordered duration range bounds, and built-in list non-emptiness constraints (XSD 1.0 Datatypes §4.3). Ensure that built-in list types (xs:IDREFS, xs:ENTITIES, xs:NMTOKENS) are validated as non-empty unless restricted with an explicit minLength. Resolve type references namespace-aware to avoid prefix conflation with user types of the same name. Ignore foreign elements matching facet names inside xs:restriction. XSTS: invalid schemas accepted 296 to 266, no other bucket raised.
-
Enforce schema element order (imports, includes, and redefines must precede other global declarations) and validate import constraints (namespace attribute must not match the schema's targetNamespace, and if missing, the schema must have a targetNamespace) (XSD 1.0 Structures §3.10.6, §3.15.2). XSTS: invalid schemas accepted 302 to 296, no other bucket raised.
-
Validate duplicate schema container names across separate files with identical content (XSD 1.0 Structures §3.14). Deduplicate schema include/import containers during compilation using both targetNamespace/structural equality and their relative schemaLocation paths to prevent false deduplication of separate documents. Keep circular self-imports/includes of the root schema valid. XSTS: invalid schemas accepted 304 to 302, no other bucket raised.
-
Validate standard
xml:langandxml:spaceattributes on XSD schema elements (like<xs:documentation>and<xs:appinfo>). Traversal is adjusted to process attributes on these documentation-related elements before skipping their foreign children, and validation checks the lexical pattern ofxml:lang(using BCP 47) and restrictsxml:spaceto 'default' or 'preserve'. XSTS: invalid schemas accepted 306 to 304, no other bucket raised. -
Validate notation declarations and notation enumeration constraints on simpleTypes (XSD 1.0 Structures §3.12 / §3.14). Every
<xs:notation>declaration must specify at least one ofpublicorsystemattributes. For any simpleType restrictingxs:NOTATION, allenumerationvalues must name a declared notation in the schema. Resolved prefix bindings are traversed recursively up the ancestor path to handle default namespaces and local namespace declarations. XSTS: invalid schemas accepted 309 to 306, no other bucket raised. -
Enforce cross-container global component-name and identity-constraint uniqueness constraints (XSD 1.0 Structures §3.14). Within a schema (including all included and imported schemas), global type names (simpleType and complexType sharing one symbol space), global element names, global attribute names, named model groups, and named attribute groups must be unique within their respective symbol spaces and target namespaces. Identity constraints (unique, key, keyref) must have unique names across all target namespaces in the entire schema document. Circular and duplicate imports/includes are handled namespace-aware without causing false-positive validations on valid schemas. XSTS: invalid schemas accepted 316 to 309, no other bucket raised.
-
Enforce substitution group exclusions based on element final and schema finalDefault attributes (XSD 1.0 Structures §3.3.6 / Schema Component Constraint: Substitution Group OK (Transitive)), including support for element declarations with inline anonymous types. The entire derivation path from the member type to the head type is checked; if any step contains a derivation method prohibited by the head element's final or the schema's finalDefault, the substitution is rejected. XSTS: invalid schemas accepted 323 to 316, no other bucket raised.
-
Enforce targetNamespace scope matching on local schema references (XSD 1.0 Structures §3.14). If a schema has a targetNamespace declared, any local type, element, attribute, group, or attributeGroup reference without a namespace prefix (resolving to no namespace) is rejected as undeclared unless a default namespace matching the targetNamespace is declared on the element, since all global components defined in the document belong to the targetNamespace. XSTS: invalid schemas accepted 325 to 323, no other bucket raised.
-
Enforce variety constraints on list itemType and union memberTypes (XSD 1.0 Datatypes §3.3.1 / §3.4.1). A list's itemType must be atomic or union of atomic types, and cannot be a list or a union containing a list, and cannot resolve to 'anySimpleType'. Resolves namespace prefixes dynamically in scope to handle local prefix declarations without false positives. XSTS: invalid schemas accepted 327 to 325, no other bucket raised.
-
Implement namespace-aware schema reference resolution checks, validating type references, element/attribute/group references, and substitutionGroup headers using XML namespace prefix bindings (XSD 1.0 Structures §3.14). If a prefix is bound to a namespace that is neither the targetNamespace nor the XMLSchema namespace (when no external schemas are loaded), the reference is flagged as undeclared. Bypasses built-in validation checks on references in the XMLSchema namespace when the targetNamespace is the XMLSchema namespace itself. Validate simpleContent base restrictions namespace-aware. XSTS: invalid schemas accepted 333 to 327, no other bucket raised.
-
Enforce simpleType restriction structure and prefix binding constraints (XSD 1.0 Datatypes §3.2.1 / Structures §3.3.1 / §3.14.3). Verify that restriction under simpleType only admits annotation, simpleType and facets, disallowing attributes, wildcards, model groups, and multiple or misplaced inline simpleTypes. Reject a restriction base, list itemType, or union memberType resolving to a complex type (e.g. anyType). Enforce that all QName attributes have bound prefixes. XSTS: invalid schemas accepted 350 to 333, no other bucket raised.
-
A top-level group or attributeGroup definition may no longer carry local-use attributes (XSD 1.0 Structures §3.6.1 / §3.6.2). A global group definition (including one defined inside
xs:redefine) must only have name and id attributes, and local-use attributes likeref,minOccurs, ormaxOccursare rejected; a global attributeGroup definition may not carry arefattribute. XSTS: invalid schemas accepted 354 to 350, no other bucket raised. -
Support attribute inheritance and prohibition in complexContent restriction derivations (XSD 1.0 Structures §3.4.6 / Schema Component Constraint: Derivation Valid (Restriction, Complex)). When a complexType restricts a base type, the restricted type inherits all base attributes that are not explicitly overridden or prohibited by the restriction. A prohibited attribute is excluded from the final type's attributes, preventing its use in instance validation. Resolves referenced attributes namespace-aware using prefix bindings. XSTS: valid instances rejected 180 to 171, invalid instances accepted 158 to 155, no other bucket raised.
-
A complexContent restriction must keep a base attribute's required use and its fixed value constraint (XSD 1.0
cos-ct-derived-ok/derivation-ok-restriction.2/derivation-ok-restriction.4). A restriction redeclaring a baserequiredattribute may not make it optional or prohibited, and if a base attribute use has afixedvalue constraint, the restriction must also make itfixedwith the same value. The fixed values are compared in the attribute type's value space (supporting lists and unions) to avoid over-rejecting valid schemas. A prohibited attribute is removed, so the fixed clause does not apply to it. XSTS: invalid schemas accepted 390 to 387, no other bucket raised. -
Enforce top-level declaration naming, block xs:anySimpleType derivation, and restrict simpleType facets. Top-level definitions must specify a
nameattribute. Derivation by restriction or extension ofxs:anySimpleTypeis disallowed. Restriction facets (length,minLength,maxLength,totalDigits,fractionDigits) must be tighter than or equal to the base type's facets. XSTS: invalid schemas accepted 387 to 383, no other bucket raised. -
Enforce simpleType derivation constraints on restricted complexType attributes (XSD 1.0
cos-ct-derived-ok/derivation-ok-restriction.1). The type of a restricted attribute must be validly derived by restriction from its base counterpart, checking atomic built-in derivation, list item type restriction, and union membership or member-to-member restriction recursively. XSTS: invalid schemas accepted 367 to 365, no other bucket raised. -
Enforce element type restriction constraints on restricted complexType particles (XSD 1.0
NameAndTypeOK/elementTypeRestrictionOK). Verify that the restricted particle's type validly derives by restriction from the base particle's type, checking union variety members structurally, prohibiting complex-restricting-simple and simple-restricting-complex (except where base isanyType), and falling back to name-based derivation. Prohibited attributes are exempted from type and value constraint checks. XSTS: invalid schemas accepted 365 to 357, no other bucket raised. -
Enforce identity constraint keyref arity validation at schema compile time (XSD 1.0
c-identity-constraint). Verify that the number of fields in a keyref matches the arity of its referenced key or unique constraint. XSTS: invalid schemas accepted 357 to 354, no other bucket raised.
[0.1.0] - 2026-06-15
First tagged pre-release. A pure-Swift XML parser/emitter with an XSD 1.0 schema
engine, XPath/XSLT support, and a WASI-compatible build. The W3C XSTS conformance
campaign has driven invalid-schemas-accepted from 2461 to 394 with
valid-schemas-rejected held at its floor of 1. Not yet 1.0: see
docs/production-readiness.md for the remaining gates.
Added
- A generative robustness fuzz harness for the schema engine and parser (#167, production-readiness stopper 4). A reproducible seeded generator builds structurally varied XSD schemas and XML instances that deliberately reach the crash- and hang-prone shapes (deep nesting, large/unbounded/inverted occurrences, pointless
maxOccurs="0"particles, wildcards, element and group reference cycles, substitution-group chains,simpleTyperestriction cycles,complexContentrestrictions, pathological regex patterns), and asserts the validator never crashes and always terminates. It is a regression gate against interactive-safety failures on hostile input, not a verdict-correctness oracle; counts are sized to stay fast on everyswift test(the native.timeLimithang guard is omitted under the single-threaded WASI runtime). No crashes or hangs were found over the current corpus.
Fixed
-
A nested
simpleTypeorattributeGroupmay no longer declare aname(XSD 1.0 schema-for-schemaslocalSimpleType/ nestedattributeGroup, #170). AsimpleTypeinside arestriction/list/union/element/attributeis anonymous (the local form carries noname), and a nestedattributeGroupis always a reference, never a named definition; named definitions are top-level only (a direct child ofschemaorredefine). Such schemas were accepted. A nested anonymoussimpleType, anattributeGroupreference, and a top-level named definition are unaffected. XSTS: invalid schemas accepted 403 to 394, no other bucket raised. -
An
xs:allgroup may no longer appear in acomplexContentextension of a base that already has element content (XSD 1.0cos-all-limited.1.2, #170). The effective content model of such an extension issequence(base content, extension content), which nests thealland violates the rule that anallmust be the whole content model. Detected from the compiled base type: rejected only when the base resolves (in the schema's own target namespace) to a complex type with element-only or mixed content. An explicitly-empty base (an empty model group withmixed="false", whose content is effectively empty) may still be extended by anall, since theallbecomes the whole content; a simple-content, empty, foreign, or unresolved base is left alone. Checked for a self-contained schema. XSTS: invalid schemas accepted 411 to 403, no other bucket raised. -
A simple type whose
finalcontainslist(or#all) may no longer be used as a listitemType, and one whosefinalcontainsunion(or#all) may no longer be a unionmemberType(XSD 1.0 simple-typefinal, #170). The restriction and extension directions were already enforced byfinalRespected; the list and union directions were unchecked becausefinal="list"/"union"is not modelled by the derivation-method set, so thefinalattribute is read directly. The reference is matched only when it resolves to the schema's own target namespace, so a list or union of a built-in (whose final is empty) is not confused with a user type of the same local name. Checked for a self-contained schema. XSTS: invalid schemas accepted 413 to 411, no other bucket raised. -
A
whiteSpacefacet may only strengthen, never relax, its base type's effective whiteSpace (XSD Part 2 §4.3.6, #170). The order ispreserve<replace<collapse, so a base fixed tocollapseadmits onlycollapse, and areplacebase admitsreplaceorcollapse. A derivation that relaxes it (for examplewhiteSpace="replace"over acollapsebase, or restrictingxs:tokentoreplace) was accepted and is now rejected. The base's effective whiteSpace is its own facet, falling back to its built-in intrinsic; the check is atomic-only and, when a base type's own whiteSpace has not yet resolved, can only under-detect, never reject a valid restriction. XSTS: invalid schemas accepted 422 to 413, no other bucket raised. -
A top-level
xs:elementdeclaration may no longer carryref,form,minOccurs, ormaxOccurs(XSD 1.0 schema-for-schemastopLevelElement, #170). A global element is a declaration, never a particle or a reference, so those local-use attributes are invalid. The top-level-attribute applicability scan added earlier is generalized to cover both kinds; a local element particle inside a content model is unaffected. XSTS: invalid schemas accepted 424 to 422, no other bucket raised. -
An element
refmay no longer carrynillable,default,fixed,form, orblock, and an element or attributerefmay no longer have an inline type definition (XSD 1.0src-element.2.2, #170). A reference is a use of a global declaration, not a declaration, so beyondrefit carries onlyminOccurs,maxOccurs, andid; the applicability check already excludednameandtypefrom aref, and this extends the exclusion to the remaining declaration-only properties. Agroup/attributeGroupref and an attribute ref'suse/default/fixed(which override on the use) are unaffected. XSTS: invalid schemas accepted 439 to 424, no other bucket raised. Disclosed under-detection: akey/unique/keyrefchild under a ref is not yet flagged. -
A top-level
xs:attributedeclaration may no longer carryuse,form, orref(XSD 1.0 schema-for-schemastopLevelAttribute, #170). Those belong to a local attribute use, not a global declaration; the per-component applicability table is the permissive global/local union and so could not draw the distinction, leaving such schemas accepted. A focused scan of the directattributechildren ofschema(and ofredefine) supplies the global-only constraint; a local attribute use inside a complex type or attribute group is unaffected. XSTS: invalid schemas accepted 454 to 439, no other bucket raised. -
A
default/fixedvalue on an element or attribute whosetypenames a user-declared type must now be a valid value of that type (XSD 1.0e-props-correct.2/a-props-correct.2, #170). This runs after the named types are compiled, complementing the existing built-in-only check: adefaultfailing the type'spattern, a malformed value for a type restricting a built-in, or a value invalid against a complex type's simple content is now rejected. The type name is resolved namespace-aware; only a type in the schema's own target namespace is checked (built-ins are handled at structure time; imported, foreign, or inline anonymous types are left alone), and the compiledSimpleTypevalidator (the one instance validation uses) performs the check, so whitespace and facet handling match. XSTS: invalid schemas accepted 462 to 454, no other bucket raised. -
A simple-type
restrictionwhose base is an inline anonymous<simpleType>(nobaseattribute) now resolves that inline child as its base, preserving its list or union variety. The base was read only from thebaseattribute, so an inline list/union base was dropped and the type collapsed to an atomic string: alengthfacet then counted characters instead of list items, and a list-valueddefaultof the correct length was wrongly rejected once value constraints are checked. Resolving the inline base fixes both schema-time value checking and instance validation. XSTS: invalid instances accepted 160 to 158, no other bucket raised. -
An
xs:attributedeclaration carrying adefaultvalue constraint must now haveuse="optional"(XSD 1.0src-attribute.2, #170). A default makes the attribute optional, souse="required"oruse="prohibited"alongside it is contradictory; such schemas were accepted. The check rides the existing value-constraint walk, gated to XSD-namespaceattributedeclarations reading the unprefixeddefault/use, and is per declaration. Onlyfixed(notdefault) may pair withuse="required". XSTS: invalid schemas accepted 469 to 462, no other bucket raised. Disclosed under-rejection: a whitespace-paddeduse=" required "(whose NMTOKEN actual value isrequired) is not flagged. -
An element or attribute declaration may no longer carry both a
defaultand afixedvalue constraint (XSD 1.0src-element.1/src-attribute.1, #170). The two are mutually exclusive; such schemas were accepted. The check rides the existing structure walk, gated to XSD-namespace declarations and reading the unprefixeddefault/fixed(a foreign same-local-name attribute is not the value constraint), and is per declaration, so adefaulton a global declaration and afixedon a reference to it (two separate items) is correctly allowed. XSTS: invalid schemas accepted 479 to 469, no other bucket raised. -
A substitution-group member's type must now be validly derived from the type of its head (XSD 1.0
e-props-correct.4, #170). An element declaringsubstitutionGroup="head"whose own type is unrelated to the head's (for example a member typedxs:intunder a head typedxs:string) was accepted; it is now rejected. The check runs after named types resolve and reuses the type-derivation oracle (the restriction/extension chain, continued through the built-in lattice; an ur-type head admits any member). To never reject a valid member it is conservative: it runs only for a self-contained schema (noimport/include/redefine, where the derivation table is complete and single-namespace names are unambiguous), reads the member's type from its owntypeattribute and the head's from the top-level element declarations only (so a local element of the same name supplies neither), and stands down when either type is a list or union (thecos-st-derived-okclauses 2.3/2.4 union-member derivation is not modelled). XSTS: invalid schemas accepted 480 to 479, no other bucket raised. Disclosed under-rejections: schemas that compose other documents, list/union-typed members or heads, and inline/untyped members. -
An
xs:allgroup referenced through a<group ref>from inside a compositor is now rejected (XSD 1.0cos-all-limited.1.2, #170): anallgroup may only be the whole content model of a complex type. A direct nestedallwas already rejected by the child-content table; this closes the group-reference path. The all-group pool is qualified by the defining schema's target namespace and a reference by its prefix, so an imported or redefined same-named group is not confused with a local one; a group redefined to a non-all content is exempt. XSTS: invalid schemas accepted 481 to 480, no other bucket raised. -
An attribute or element whose type is or derives from
xs:IDmay no longer carry adefaultorfixedvalue constraint (XSD 1.0a-props-correct.3/e-props-correct.5, #170). Such schemas were accepted. The check runs after named simple types resolve, so a named user type derived fromxs:IDis recognized; the type reference's namespace is resolved from its prefix, so a user type namedIDor an importedfoo:IDis not mistaken for the built-in. XSTS: invalid schemas accepted 489 to 481, no other bucket raised. -
A
typeattribute and an inline anonymous type definition are now mutually exclusive on an element or attribute (XSD 1.0src-element.3/src-attribute.3.1, #170). An<xs:element>or<xs:attribute>carrying both atype=and an inline<simpleType>(or<complexType>, for an element) is rejected; previously it was accepted. Only an inline type in the XSD namespace counts, so a foreign-namespace child that happens to be namedcomplexType/simpleTypeis not mistaken for the component's own type. XSTS: invalid schemas accepted 495 to 489, no other bucket raised. -
Attribute-use uniqueness and the single-ID rule are now enforced (XSD 1.0
ct-props-correct.4/ag-props-correct.2/ct-props-correct.5, #162). A complex type's or attribute group's attribute-use set (flattened throughattributeGroupreferences) may not contain two distinct declarations with the same name and namespace, nor more than one attribute whose type is or derives fromxs:ID. Declarations are deduplicated by the identity of their declaring node, so the same declaration reached by two paths (a diamond of attribute-group references) is one use, not a clash; names carry their resolved namespace, so a local attribute and a reference to a same-local-name attribute in an imported namespace do not collide. XSTS: invalid schemas accepted 505 to 495, no other bucket raised. Disclosed under-rejections: the set inherited through extension is not composed in, and a duplicate that requires an unprefixed reference in a schema with no default namespace is not detected. -
Circular references among model groups, attribute groups, and substitution-group affiliations are now rejected (XSD 1.0
mg-props-correct.2,ag-props-correct.3,e-props-correct.6, #168). A model group may not contain itself, an attribute group may not reference itself transitively, and a substitution chain may not loop. The model-group check follows references only within a definition's own compositor tree, stopping at element and type boundaries: a group referenced inside an element's content is a recursive data structure (permitted), not the group containing itself. A reference is followed only when it resolves to the schema's own target namespace, and a group or attribute group redefined inxs:redefine(whose single self-reference is the legal redefinition) ends the chain. XSTS: invalid schemas accepted 516 to 505, no other bucket raised. A genuine cycle introduced across two redefined definitions is a disclosed under-rejection. -
Circular type derivation is now rejected at compile time (XSD 1.0
ct-props-correct.3/st-props-correct.2, #168). A complex or simple type may not derive, transitively through its base, from itself; previously such a schema was accepted. Only the base chain is followed, so a type whose element content recurses (an element of its own type, the normal recursive-data-structure case) is correctly permitted, and a type redefined inxs:redefine(whose base legitimately names its former self) and the ur-types (anyType/anySimpleType/anyAtomicType, the roots the schema-for-schemas bootstraps) end the chain rather than appear as cycles. The base is followed only when it resolves to the schema's own target namespace, so a type whose base is a same-local-name type in another namespace is not mistaken for a self-cycle. XSTS: invalid schemas accepted 519 to 516, no other bucket raised. Union/list (itemType/memberTypes) and genuine cross-namespace derivation cycles are a disclosed under-rejection (the engine resolves by local name). -
Particle Valid (Restriction) now implements the W3C MapAndSum rule for a sequence restricting a choice (#165). When a derived sequence restricts a base choice, the spec (XSD 1.0 §3.9.6) requires that each derived particle restrict some base branch AND that the derived sequence's occurrence count times its number of particles lie within the base choice's occurrence range. PureXML previously applied the same-compositor occurrence subsumption (comparing the sequence's own range directly to the choice's), which both rejected a valid restriction (
sequence{2,4}(e1,e2)ofchoice{3,9}(e1,e2), whose 4-to-8 elements fit the choice's 3-to-9) and accepted an invalid one (a two-member sequence restricting a single-occurrence choice, which the count-product2 > 1now rejects). The count-product is the spec's measure, matched to the reference implementation. XSTS: valid schemas rejected 2 to 1, invalid schemas accepted 520 to 519, no other bucket raised. The one remaining valid-rejected case (particlesZ001) is a documented spec ambiguity (its own test notes the RecurseAsIfGroup rule "is ambiguous"; the strict reading the reference follows rejects it), recorded indocs/xsts-deviations.md. -
Two more Particle Valid (Restriction) over-rejections closed (#148). A derived particle that can never contribute a child (its own
maxOccurs="0", or a group whose every member is content-free) accepts only the empty sequence, so it is now recognized as a valid restriction of any emptiable base before the structural cases run; previously amaxOccurs="0"group whose members carried their own non-trivial occurrences was wrongly recursed into and rejected (particlesW006). And substitution-group expansion now lists members in declaration order: the closure walk used apopLast()stack that reversed them, so a reference to a head expanded tochoice(head, m2, m1)and an order-preserving RecurseLax restriction bychoice(m1, m2)could not map (elemZ027a). The choice/choice restriction stays order-preserving, as the W3C suite requires (achoice(b|a)is not a valid restriction ofchoice(a|b),particlesT002); only the expansion order is corrected. The expansion is shared with instance validation and the determinism check, both order-neutral, so neither is affected. XSTS: valid schemas rejected 4 to 2, no other bucket raised. (The pinned XSTS baselines, which the opt-in suite never asserts under a plainswift test, were also corrected to the measured 2/520/180/160.) -
An
<xs:element ref="p:foo">now resolves to the namespace its prefix is bound to, not the schema's target namespace (#164). The reference's QName is resolved by standard XML namespace rules against the schema root'sxmlns:bindings: a bound prefix gives its namespace (so a reference to an imported element lands in the imported namespace), an unprefixed name resolves through the default namespace, and a name with no applicable binding is in no namespace. Previously the prefix was stripped and the element was assumed to be in the target namespace, which mis-qualified every reference to an imported element: a restriction of an##otherwildcard by such a reference was wrongly rejected (the element looked like it was in the target namespace, which##otherforbids), and the same mis-qualification rejected valid instance documents whose elements were correctly in the imported namespace. Because the content model is shared between schema-restriction checks and instance validation, the fix improves both. XSTS: valid schemas rejected 32 to 4, invalid schemas accepted 526 to 520, valid instances rejected 214 to 180, invalid instances accepted held at 160. Disclosed limitations, unchanged by this fix: a prefix re-bound on an inner element (rather than the schema root) is not honored, substitution-group members are still resolved to the target namespace (cross-namespace substitution is not modeled), and a dangling reference is not yet flagged as a schema error. -
Particle Valid (Restriction) now derives its pairwise check over the normalized content model, and applies occurrence subsumption only where the two particles' own ranges are comparable (#163). The W3C restriction algorithm is defined over a content model with pointless particles eliminated, so both the derived and base particles are first normalized (
maxOccurs="0"members dropped, a{1,1}single-member group unwrapped to its member, a nested same-compositor{1,1}group spliced into its parent), all language-preserving. The outer occurrence-range check (rangeSubsumed) was being applied to every pairing, including the cross-kind ones where it compares mismatched ranges: a derived element's range against a base group's, or a derived group's against a base wildcard's. It is now applied only to the same-kind pairings (element/element, wildcard/wildcard, group/group) and element/wildcard; for element/group (RecurseAsIfGroup), group/element, and group/wildcard the occurrence is matched inside the case, against a member or as an effective total range. This unblocks a single-member derived sequence narrowing an occurrence (sequence(e1{2,3})restrictingsequence(e1{1,3}, ...), thee1{2,3}no longer compared to the base sequence's own{1,1}). A group restricting a wildcard (NSRecurseCheckCardinality) is now valid when every leaf it can contain is admitted by the wildcard and the group's effective total occurrence range lies within the wildcard's; a group restricting a single element is bounded the same way, by that element's name, type derivation, and occurrence range. XSTS: valid schemas rejected 45 to 32 (13 correct schemas no longer wrongly rejected, theparticlesHafamily of element-restricts-wildcard cardinality among them), invalid schemas accepted 528 to 526 (the cardinality bound also catches two over-wide restrictions), no other bucket raised. -
Particle Valid (Restriction) no longer over-rejects valid restrictions (#148). Three corrections, all language-preserving: a particle that can never occur (
maxOccurs="0", a "pointless particle") is removed from both the derived and base content before mapping, so it no longer consumes a base particle and starves the ones after it (e.g.(e1 maxOccurs="0", e2)restricting(any)is now valid); a content-free restriction (an empty<sequence/>, or a group whose members all cannot occur) is a valid restriction of an EMPTY base, since both accept only the empty sequence; and a derived group that reduces to empty after that removal is a valid restriction of its base only when the base is itself emptiable (by structure or its own occurrence), closing a vacuous-acceptance gap the removal would otherwise open. XSTS: valid schemas rejected 72 to 45 (27 correct schemas no longer wrongly rejected), with one schema (addB32) moving to invalid-accepted because its only invalidity is a malformed imported document PureXML does not yet load (tracked by #161), not the restriction. -
A
simpleTypemust contain exactly one ofrestriction,list, orunion(#158). Per XSD 1.0 Datatypes its content is(annotation?, (restriction | list | union)), but the membership table admitted each name without the cardinality, so an emptysimpleTypeor one combining two derivations (twounions, aunionand alist, tworestrictions) was accepted (the stB family). The exactly-one rule is now enforced. XSTS: invalid schemas accepted 537 to 527, no other bucket raised (valid schemas rejected held at 72). -
The
finalandblockderivation-control attributes are now checked against their value space (#158). Each is#allor a whitespace list of method tokens, but the admitted tokens depend on the component: an element'sfinalisextension/restrictionand itsblockaddssubstitution; a complexType'sfinal/blockareextension/restriction; a simpleType'sfinalislist/union/restriction. The tokens were parsed leniently (unknown tokens ignored,substitutionadmitted everywhere), sofinal="substitution"on an element, a misspelled or miscased token, or#allmixed with other tokens was accepted (the elemF/st_final families). The list is now validated against the component's exact set;finalDefault/blockDefault(the wider schema-level value space) and foreign attributes are left alone. XSTS: invalid schemas accepted 580 to 537, no other bucket raised (valid schemas rejected held at 72). -
A
simpleContentderivation's content is now checked for order and disallowed children (#158). Arestrictionis(annotation?, (simpleType?, facet*), (attribute | attributeGroup)*, anyAttribute?)and anextensionis(annotation?, (attribute | attributeGroup)*, anyAttribute?): simpleContent has no model group, an extension has no facets orsimpleType, and the attributes precede a single trailinganyAttribute. The membership table admitted the names but not the model, so a model group inside simpleContent, a facet in an extension, a facet before itssimpleType, an attribute afteranyAttribute, or a secondanyAttribute(the ctD family) was accepted. The derivation's children are matched against the context's ordered slot model, reusing the slot walk built for complexContent. XSTS: invalid schemas accepted 594 to 580, no other bucket raised (valid schemas rejected held at 72). -
Particle Valid (Restriction) now checks an element's type by derivation, not just its name (NameAndTypeOK, #158). When a restriction maps a same-named element onto a base element, the restricting type must be, or derive by restriction from, the base element's type; renaming
xs:stringtoxs:int, widening a concrete type toxs:anyType, or naming an unrelated type (a different user type, a wider ancestor, an absent/anyTypetype over a concrete base) was accepted because only the names were compared. Each element particle now carries its type's derivation identity (the resolved type name,anyTypefor an absent type, or nil for an inline anonymous type), namedsimpleTyperestriction derivations are recorded alongside complex ones, and the check walks the user restriction chain and then the built-in lattice to decide derivation by name; an ur-type base, a union base (membership is not a restriction chain), and an inline anonymous type are permitted, so no valid restriction is rejected. XSTS: invalid schemas accepted 621 to 594, invalid instances accepted 165 to 160 (more invalid schemas rejected up front), no bucket raised (valid schemas rejected held at 72). -
An element/attribute
default/fixedvalue must be a valid value of itstype(Attribute/Element Locally Valid, XSD 1.0, #158). Afixed="abc"on anxs:integeror adefault="1 2"on anxs:intwas accepted because the value constraint was never checked at compile time. The value is now validated against its type when the type resolves to a built-in datatype (namespace-aware: only atypein the XSD namespace), normalising whitespace per the type as XSD requires (sodefault=" 5 "on an integer is valid); a named or inline user type is left to its own rules, so no valid value constraint is rejected. XSTS: invalid schemas accepted 638 to 621, no other bucket raised (valid schemas rejected held at 72). -
A constraining facet must apply to its simple type's variety (XSD 1.0 Part 2, #158). A
listmay be constrained only bylength/minLength/maxLength,pattern,enumeration, andwhiteSpace; auniononly bypatternandenumeration. A value-bound facet (maxInclusive,totalDigits, ...) constrains an atomic value space, so restricting a list (list of integer) bymaxInclusivewas wrongly accepted. The base's variety is resolved from the raw tree (the check runs before the types are compiled), namespace-aware so a user type that shares a built-in's local name does not flip the variety: a base in the XSD namespace is a built-in (only the list datatypes are list-variety), a base in the schema's own target namespace resolves to a declared global, and an external or unresolvable base is skipped, so no valid restriction is rejected. XSTS: invalid schemas accepted 664 to 638, no other bucket raised (valid schemas rejected held at 72). -
A complexType's shorthand content (without
simpleContent/complexContent) is now checked for order, not just cardinality (#158). Its model is(annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?): an optional model group, then attributes, then an optionalanyAttribute. The existing content-shape check caught the wrong number of model groups and the exclusivity of a content spec, but not the order, so a model group after an attribute, an attribute afteranyAttribute, or a secondanyAttribute(the ctB family) was accepted. The complexType's direct children, excludingannotationand the content specs, are matched against the same ordered slot model used for acomplexContentderivation. XSTS: invalid schemas accepted 687 to 664, no other bucket raised (valid schemas rejected held at 72). -
An
any/anyAttributewildcard'snamespaceconstraint is now validated at compile time (#158). Its value space is('##any' | '##other')standing alone, or a list of namespace URIs and the##targetNamespace/##localtokens; the wildcard parser mapped any unrecognised token to a literal URI, so a misspelled token (##all,##target), an##any/##otherplaced inside a list (##any ##other), or any other##-prefixed token was accepted. The unprefixednamespacevalue is now tokenised and checked against that grammar, while##any/##otherare admitted only as the whole value; a prefixed (foreign) attribute that merely shares the local name is the author's own and is not checked, so no valid schema is rejected. XSTS: invalid schemas accepted 736 to 687, no other bucket raised (valid schemas rejected held at 72). -
An identity constraint's
selector/fieldxpathis now validated against the XSD 1.0 restricted-XPath subset at compile time (#158). That subset (Part 1, 3.11.6) is far narrower than full XPath: a path is an optional leading.//then/-separated steps, a step is., a name test (QName,*, orNCName:*), or achild::axis, and afieldpath may end in an attribute step (@xorattribute::x); there are no predicates ([...]), no other axes (self::,descendant::), no absolute or interior//. The xpath was stored raw and compiled lazily withtry?, so a malformed one (a predicate, a bare/, an empty union branch, a spaced name-test colon) was silently accepted. It is now lexed into a whitespace-tolerant token stream and parsed against the grammar, so whitespace between tokens is allowed (child :: a,. //.) while//must be adjacent and a name test's own:may not be spaced; the name-test lexer uses the XMLNameCharset, so a non-ASCII NCName is accepted. XSTS: invalid schemas accepted 785 to 736, no other bucket raised (valid schemas rejected held at 72). -
A
complexContentderivation's content is now checked for order and cardinality, not just child membership (#158). ArestrictionorextensioninsidecomplexContenthas the content model(annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?): an optional model group, then any number ofattribute/attributeGroup, then an optionalanyAttribute. TheallowedChildrenmembership table admitted the right child names but not their sequence or count, so a second model group (sequencethensequence), a model group after an attribute, an attribute afteranyAttribute, or a secondanyAttributewas accepted. The children are matched against an ordered slot model (one model group, unbounded attributes, oneanyAttribute) by a forward-only walk; the check runs only in thecomplexContentcontext, where a model group is part of the model, so asimpleContentderivation (facets, no model group) keeps its own rules and no valid schema is rejected. XSTS: invalid schemas accepted 864 to 785, no other bucket raised (valid schemas rejected held at 72). -
Unique Particle Attribution (
cos-nonambig, the content-model determinism constraint) is now enforced at compile time (#156, #157), the keystone of the remaining schema-validity tail. A content model must be deterministic so each element in an instance is attributable to a single particle without lookahead; an ambiguous model (<choice>of two same-name elements, an optional or repeating element followed by a distinct same-name element, two same-name members of anall, an element competing with a wildcard that admits it) was accepted. The check runs a Glushkov position automaton over a literal, QName-resolved view of the raw schema tree built specifically for this rule, independent of the instance-validation content model: arefis one position carrying its prefix-resolved QName (not the blanket target namespace), and substitution groups are not expanded, which removes the two false-overlap sources (cross-namespace refs, expanded substitution members) that a check over the compiled model produced. Determinism is per particle, the W3C rule's defining property: a particle competing with a repetition of itself is never a violation, so a fixed count{n,n}unrolls into copies in sequence (b{2,2}, bstays the deterministicb, b, b) while a true range or unbounded repeat uses a self-loop, and only two distinct source particles with overlapping labels conflict. The overlap test is QName-only (not substitution-group-aware), an accepted under-rejection that can never over-reject. XSTS: invalid schemas accepted 884 to 864, no other bucket raised (valid schemas rejected held at 72). -
A
keyref'srefermust name akeyoruniqueconstraint (#147). Areferpointing at a non-existent (or non-key/unique) constraint was accepted; it is now resolved against the document'skey/uniquenames, with a forwardrefer(textually before the key) handled by collecting all names first. The check is skipped when the document pulls in external definitions throughimport/include/redefine, which the default compile does not load. XSTS: invalid schemas accepted 891 to 884, no other bucket raised. -
Component-name uniqueness is now enforced (#147). Within a schema, global type names (
simpleTypeandcomplexTypeshare one symbol space), global element names, global attribute names, named model-group names, and named attribute-group names must each be unique, and identity-constraint names must be unique across the document; a duplicate was accepted, with the later definition silently overwriting the earlier. Only the document's own globals are examined (anxs:redefine's same-named type is not a clash), and the symbol spaces are kept separate (a type and an element may share a name). XSTS: invalid schemas accepted 909 to 891, no other bucket raised. -
Element Declarations Consistent (
cos-element-consistent) is now enforced (#147). Within one complex type's content model, two element declarations with the same name must have the same type; two same-name elements with different types (e.g.aasxs:stringand asxs:integer) were accepted. The check walks the content model (through model groups and content-derivation wrappers) but not into an element's own nested type, so a reused name in a nested anonymous type, a separate content model, is not flagged. Modeled on Xerces (libxml2 only partially enforces this); substitution-group expansion is not folded in (an accepted under-rejection). XSTS: invalid schemas accepted 925 to 909, no other bucket raised. -
Schema-vocabulary attribute applicability is now enforced at compile time (#147), the attribute sibling of the content-model table. Each XSD component admits a fixed set of attributes (the schema for schemas); an unprefixed attribute outside that set (a typo like
nullable, a strayfoo,nameon asequence) is now rejected, andrefmay not co-occur withnameortype. Foreign (prefixed) attributes and namespace declarations are not the component's own and are not checked, and the element/attribute sets are the permissive union of global and local use, so no valid schema is rejected. XSTS: invalid schemas accepted 956 to 925, no other bucket raised. -
A named model-group definition (
xs:group name="...") must contain exactly one ofall/choice/sequence(#147). XSD 1.0 gives a group's content as(annotation?, (all | choice | sequence)), but theallowedChildrenset-membership check did not enforce the cardinality, so a group with two compositors or with none was accepted. Agroupreference (noname) is excluded. XSTS: invalid schemas accepted 977 to 956, no other bucket raised. -
A particle whose
minOccursexceeds itsmaxOccursis now rejected at compile time (#147). An occurrence range likeminOccurs="5" maxOccurs="2"is empty and illegal, but was accepted; the two values are compared by canonical magnitude (independent of machine-integer range), withunboundednever exceeded and a single-point range (minOccurs="2" maxOccurs="2") allowed. XSTS: invalid schemas accepted 997 to 977, no other bucket raised. -
A complexType's content-model shape is now enforced at compile time (#147). XSD 1.0 allows a complexType's content to be
simpleContent, orcomplexContent, or a model group followed by attributes, but never a mix: so at most one ofsimpleContent/complexContent, neither beside a model group or a direct attribute (those belong inside the derivation), and at most one model group. TheallowedChildrenset-membership check did not catch these combinations, so a malformed complexType (twosimpleContent,simpleContentplus asequence, two model groups) was accepted. Only the complexType's direct children are examined, so attributes legitimately nested inside an extension/restriction are not flagged. XSTS: invalid schemas accepted 1051 to 997, no other bucket raised. -
Unresolvable references are now rejected at compile time (#147). Every QName a schema names (a
type/base/itemType/memberTypestype, anelement/attribute/group/attributeGroupref, an elementsubstitutionGroup) must resolve to a declared component or a built-in; an undeclared reference was previously accepted and only surfaced (if at all) lazily during instance validation. The reference is matched by local name with whitespace trimmed, as the rest of the compiler resolves it. The check is skipped when the document pulls in external definitions throughimport/include/redefine, which the default compile does not load, so a reference into them is never flagged. XSTS: invalid schemas accepted 1092 to 1051, no other bucket raised. -
patternfacet regular expressions are now validated at compile time (#147). Eachpatternvalue is compiled when the schema is; a structurally malformed one (unbalanced parentheses((a), a quantifier with nothing to repeat?a) is rejected, where before it was only compiled lazily at instance time (try?) and so silently ignored. The check is conservative: a construct the engine merely does not support on an otherwise-valid pattern (the empty pattern, an untabulated\p{Is...}block such as a surrogate or historic block, the lenient{,m}quantifier) is tolerated, so no valid schema is rejected. XSTS: invalid schemas accepted 1129 to 1092, no other bucket raised. -
Schema-vocabulary name and reference attribute values are now checked for lexical validity at compile time (#147). A component's
namemust be a valid NCName (name="",name="123",name="a:b"are rejected), and the reference attributestype/base/ref/itemType/refer/substitutionGroupmust be lexical QNames (type=":_",type="a:b:c"rejected), withmemberTypesa list of QNames; these were unchecked and the schema accepted. Only the lexical form is checked (a QName's prefix is resolved elsewhere), so no valid schema is rejected. XSTS: invalid schemas accepted 1190 to 1129, no other bucket raised. -
Schema-vocabulary attribute values are now validated at compile time (#147), the attribute-model sibling of the structural content-model check. Enumerated attributes (
form/elementFormDefault/attributeFormDefaulttoqualified/unqualified,usetooptional/prohibited/required,processContentstoskip/lax/strict,mixed/abstract/nillableto a boolean) andminOccurs/maxOccurs(anonNegativeInteger,maxOccursalsounbounded) are checked against their fixed value space; an out-of-space value (use="foo",mixed="yes",minOccurs="-1") was previously ignored and the schema accepted. Only unprefixed attributes on XSD-namespace elements are checked, so a foreign attribute is never flagged. XSTS: invalid schemas accepted 1313 to 1190, no other bucket raised. -
Schema documents are now structurally validated against the schema-for-schemas content model (#147), the general mechanism for the structural tail of the schema-validity gap. A data-driven table of the XSD 1.0 child content model rejects, at compile time, any component child the model does not admit, more than one
annotation(or anannotationthat is not first, except onschema/redefine), and an identity constraint missing its selector or field. The check walks only XSD-namespace elements and skipsxs:appinfo/xs:documentationforeign content, so no valid schema is rejected;restriction/extensionuse the permissive union of their contexts (never over-rejecting). One table covers every component rather than a rule hand-coded per element. XSTS: invalid schemas accepted 1688 to 1313 (-375), no other bucket raised. -
The
idattribute on XSD components is now validated at compile time (#147). It is of typexs:ID, so each value must be a valid NCName and unique within the schema document; the values were never checked, so a malformed (id="",id="123") or duplicatedidleft the schema wrongly accepted. The schema-document tree is walked, skippingxs:appinfo/xs:documentationforeign content (whoseidattributes are notxs:ID) and treating only the unprefixedid(not a foreignxml:id/pre:id) as the component identifier, so no valid schema is rejected. XSTS: invalid schemas accepted 1789 to 1688, no other bucket raised. -
Value-bound facet definitions are now validated at compile time (#147), continuing the facet-validity work. Per XSD Part 2 Datatypes 4.3, each
minInclusive/maxInclusive/minExclusive/maxExclusiveandenumerationvalue must be a valid value of the base type;minInclusiveexcludesminExclusiveandmaxInclusiveexcludesmaxExclusive; and the lower bound may not exceed the upper (nor equal it when either side is exclusive, an empty value space), compared in the base primitive's value space. XSTS: invalid schemas accepted 2082 to 1789, no other bucket raised. -
xs:gMonthnow accepts the XSD 1.0 lexical form--MM--(the trailing--is optional, the later--MMerratum form still accepted), with an optional timezone (#147). Only--MMwas recognized, so validgMonthvalues and the schemas whose facets used them were wrongly rejected. Surfaced by the value-bound facet check above (which validates a bound value against its base type). XSTS: valid instances rejected 233 to 214; it also removed 26 false schema rejections the bound-facet check would otherwise have caused. -
Constraining-facet definitions are now validated at compile time (#147), the first cut at the schema-validity gap (invalid schemas the validator was silently accepting). Per XSD Part 2 Datatypes 4.3, each length-family facet value (
length,minLength,maxLength,totalDigits,fractionDigits) must be a validnonNegativeInteger(totalDigitsapositiveInteger),lengthmay not co-occur withminLength/maxLength, and the integer ranges must be ordered. Malformed values (length="","-1","a","1e2") were dropped by numeric parsing and the schema accepted; the definition is now read raw and rejected at compile time, with well-formedness checked independently of machine-integer range so a huge but valid value is not misreported. The findings travel through a shared diagnostics collector on the parsing context and join the model-level consistency findings. XSTS: invalid schemas accepted 2461 to 2082, with no change to any other bucket. -
A built-in datatype named by
xsi:type(xsd:int,xsd:decimal,xsd:string) is now resolved, gated by substitution validity (#147). The override resolved only against schema-declared types, so a built-inxsi:typewas reported "unknown" and the element wrongly rejected. The built-in is now resolved when it validly substitutes for the declared type per Schema-Validity Assessment: the ur-type (anyType/anySimpleType) admits any type, and a bare built-in declared type admits any built-in derived from it through the XSD Part 2 datatype lattice (newBuiltinType.derives(from:)). An invalid built-in substitution is a located error, not a silent acceptance, closing the gap the resolution exposed. XSTS: valid instances rejected 248 to 233 (with no regression in invalid instances accepted, which the two fixes below preserve). -
A simple-typed element now rejects non-schema-instance attributes (#147). A simple type declares no attribute uses, so an element whose type is simple, including one whose simple type comes from an
xsi:typeoverride and including anxsi:nilelement, may carry only thexsi:*and namespace attributes; any other attribute is a violation. The check was absent, so resolving a built-inxsi:typeto a simple type would otherwise have accepted stray attributes on it. -
Identity-constraint fields compare in their value space when the selected node carries an
xsi:type(#147).unique/key/keyreftuples compared lexically, so twoxsi:type="xsd:decimal"fields3.0and3, the same value, were treated as distinct and auniqueviolation was missed. Each field value now carries the simple type read from the node'sxsi:type; two values are equal in that type's value space when both share a built-in base (reusingSimpleType.valueMatches), otherwise by lexical form. -
Simple-content values preserve significant whitespace per the type's
whiteSpacefacet (#147).textContenttrimmed leading/trailing whitespace unconditionally before the facet ran, so anxs:string(whiteSpace="preserve") value that was whitespace-only or had significant edges (e.g. matching a\tpattern, or counted by alengthfacet) was wrongly emptied or shortened. Simple-content validation now reads the raw character content and lets the facet normalize it; the element-only/empty "no significant text" check still ignores indentation whitespace. XSTS: valid instances rejected 288 to 248.
Added
Schema.Document.validate(_:schemaLoader:)honors an instance'sxsi:schemaLocationandxsi:noNamespaceSchemaLocation(#147). Each referenced schema document is loaded through the supplied loader, compiled, and its global declarations merged in (the primary schema's own declarations win on conflict), so a strict or lax wildcard can resolve an element declared in another document. The plainvalidate(_:)is unchanged. XSTS: valid instances rejected 308 to 288 (the particle clusters whose wildcard-matched elements live in a separate, instance-referenced schema).
Fixed
- A
mixed="true"complex type with no content model now permits character data (#147). Such a type (only attributes, nosequence/choice/all) was compiled to the empty content type, which forbids text, so a mixed element carrying only text was rejected. It is now mixed content over an empty particle: text is allowed, child elements are still not. XSTS: valid instances rejected 311 to 308. <xs:attribute ref="...">references to global attribute declarations are now resolved (#147). A reference has noname, so it was dropped and the referenced attribute looked undeclared on the instance; nested attribute-group references were already flattened, but the per-attribute reference was not. The reference now resolves to the global declaration's type, takinguse/default/fixedfrom the reference site. XSTS: valid instances rejected 338 to 311.- Document-scoped
xs:ID/xs:IDREFvalidation is now enforced (#147).xs:ID,xs:IDREF, andxs:IDREFSwere checked only lexically (as NCNames); the document-wide constraints, that every ID is unique and every IDREF/IDREFS item matches some ID, were not. The tree validator now records ID and reference values during its typed walk (elements and attributes) and reports duplicate IDs and unresolved references once the whole document is seen, so a forward reference resolves against a later ID. XSTS: invalid instances accepted 179 to 166. - The regex
\wand\Wescapes now follow the XSD definition (#147).\wwas the Perl class (alphabetic, decimal, underscore); XSD defines it as[#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}], so symbols and marks are word characters and the connector_(Pc) is not.\Wcorrespondingly matched symbols it should not. XSTS: invalid instances accepted 231 to 210. (Internal regex fixtures that assumed_was a word character were corrected.) block="substitution"on a substitution-group head is now enforced (#147). The substitution filter only dropped members whose type derived from the head's type by a blocked method (extension/restriction); thesubstitutionkeyword, which forbids substitution-group substitution outright regardless of the member's type, was ignored, so a same-type member still substituted under a substitution block. XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74.- Element
defaultandfixedvalues are honored on empty elements (#147). An empty element with a value constraint takes that value, so the empty content is valid even when the type would reject the empty string; the constraint value (not the empty string) is what is validated against the type, including anyxsi:typeoverride. A restriction'senumerationnow also replaces the inherited enumeration rather than unioning with it (the new set is the value space;patternstill accumulates and is ANDed), which a value-constraint case underxsi:typeexposed. XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to 238. - Element-level
blockis now enforced againstxsi:typesubstitution (#147). Theblockon an element declaration (distinct fromblockon its type) was parsed but never threaded to the instance validator, so a document substituting a blocked-derivation type viaxsi:typewas wrongly accepted. The element block is now carried through toblockedSubstitutionError, which also resolves the element-ref type indirection (element:footo its concrete type) so the check fires for referenced children, not only the root. XSTS: invalid instances accepted 261 to 240. - The regex engine now recognizes the full XSD
\p{Is<Block>}Unicode block set (#147). Only about fifteen blocks were tabulated, so a pattern over any other block (\p{IsArmenian},\p{IsTamil},\p{IsHangulJamo}, and so on) failed to compile and rejected valid instances. The table now carries the Unicode 3.1 blocks the XSD regular-expression appendix fixes (surrogate blocks omitted, since surrogates are not scalar values); each range is verified bidirectionally by the XSTS corpus (valid in-block instances pass, invalid out-of-block instances still fail). XSTS: valid instances rejected 502 to 395. - Length facets on
xs:QNameno longer reject valid values (#147). XSD 1.0 Datatypes 4.3.1 does not define a unit of length for QName (only string, anyURI, binary, and list), solength/minLength/maxLengthon QName are implementation-defined; like Xerces and the XSTS NIST set we treat them as non-constraining rather than counting characters. Net XSTS: valid instances rejected 547 to 502 (36+ NIST false rejections fixed); 4 Microsoft cases that read QName length as a character count remain in invalid instances accepted (257 to 261) and are documented as a deliberate spec-ambiguity interpretation in docs/xsts-deviations.md. - Ordering facets on
xs:durationare now enforced (#147).minInclusive,maxInclusive,minExclusive, andmaxExclusiveon duration were ignored because duration has no total order in the generic comparator. ADurationValuenow implements the XSD 1.0 Datatypes 3.2.6.2 partial order (compare by adding to the four reference dateTimes); a value incomparable to the bound (such asP1MagainstP30D) is outside the range and rejected. XSTS: invalid instances accepted 355 to 257. - Pattern and enumeration facets on a union type are now enforced (#147). A union admits a value valid for any member, but
patternandenumeration(the only constraining facets XSD allows on a union) had been ignored, so a member-valid value outside the enumeration or failing the pattern was wrongly accepted. Enumeration compares in the value space of the admitting member (so01equals an enumerated1for an integer member), matching the atomic path. XSTS #147: invalid instances accepted 555 to 355. - Length facets on the built-in list datatypes now count list items, not characters (#146). A restriction of
NMTOKENS,IDREFS, orENTITIESresolved its base through neither theBuiltinTypetable (which has only the singularNMTOKEN) nor the user-type table, so it collapsed to an atomic string andlength/minLength/maxLengthmeasured characters, rejecting valid instances. The restriction base now resolves throughsimpleTypeReference, preserving the list variety. Closes the XSTS NISTlist-NMTOKENS/IDREFS/ENTITIESfacet clusters: valid instances rejected 604 to 547, invalid instances accepted 582 to 555. - Three crash and hang classes uncovered by running the full XSTS archive to completion (#129). (1) Nested occurrence explosion: the content-model NFA capped each particle's
maxOccursunroll at 16384 but not the total state count, so nested high-maxOccursparticles multiplied (a sequence of 16384 holding a choice of 16384 is 2.7e8 states) and OOM-killed the process at 8 GB on the Microsoft Particles set; the builder now also caps total states (2^20), degrading further repetition to star. The regex{n,m}quantifier builder had the same unbounded unroll and the same cap is applied. (2) Quadratic content-model validation:sequenceStructureErrorscalledfollow(after: prefix)once per child andfollowre-walked the whole prefix from the start each time, so a content model over many children spun at 100% CPU; matching is now incremental (one active state-set advanced per child), turning O(children squared) into O(children). (3) Character-class subtraction trap: the regex pattern[abcd-[d]](XSD class subtraction) was parsed as adto[range, and sinced(100) exceeds[(91) the reversedClosedRangetrapped (SIGBUS);-[now always opens a subtraction, and any reversed range (such as[z-a]) is a clean rejection rather than a trap.
Changed
- Scanner-side validity findings are now located structured values (validation-framework audit follow-up):
DocumentType.validityFindingscarriesValidityFindingvalues (reason plus the declared name they are about) instead of strings,DTDSchema.declarationErrorsholds readyValidationErrors, and every declaration-level finding renders at its declaration's coding path (x,r/@a) instead of "at root of document", matching the XSD consistency-errors precedent. Finding texts are unchanged.
Added
- W3C XSTS runner, first measurement (#129, #122): the W3C XML Schema Test Suite (2006-11-06 release, ~13k test groups across NIST, Sun, Microsoft, and Boeing contributions; W3C Document License, never vendored) runs opt-in via XSTS_ROOT: every schemaTest must compile when expected valid and be rejected when expected invalid, and every instanceTest validates against its group's schema with the expected outcome; hrefs resolve against the referencing file and include/import locations from disk relative to the schema. The four disagreement counts are asserted exactly; the first measurement landed 2026-06-13 and is baselined at 75 valid schemas rejected, 2467 invalid schemas accepted, 604 valid instances rejected, and 582 invalid instances accepted across 14383 groups, with per-case deviations written to /tmp/xsts-failures.txt for the burn-down. Building the runner immediately caught a denial-of-service bug: a schema with maxOccurs in the trillions (groupF009v) OOM-killed the process because the content-model automaton unrolled occurrence counts literally; unrolling is now capped at 16384 with larger repetitions treated as unbounded, the libxml2 posture. The roadmap mermaid records the closed behavioral-parity set (#141-#144, collation) and the #139 performance epic.
Added
- The last parity items, to one hundred percent (#143, #144, #130): XPath gains a configurable protective evaluation budget (
PureXML.XPath.Budget, with alibxml2Compatiblepreset matching its fixed 10M node-set cap), threaded through query evaluation and thrown as a locatedQueryError.budgetExceededinstead of libxml2's silent refusal, default unbounded; xsl:sort honorslang(an attribute value template) with tailored alphabet orders for Polish and Russian derived from the conformance golds (diacritics interleaved, Cyrillic Yo after Ye), case-folded, falling back to the default order for untailored languages; and the WASM gate executes the full test suite under wasmtime on an actual WASI runtime (build-only with a notice when wasmtime is absent), wired into CI. The first WASI run immediately caught a real wasm32 trap: format-number's digit extraction converted through Int, which is 32 bits on wasm32 and trapped at 2^31 despite the 2^53 guard; the internals are now explicitly Int64 and the xsl:number bound is platform-correct (Double(Int.max)). Xalan baseline 127 to 125.
Added
- Public streaming surface (#142) and a public-API test target:
EventReader.next(), both initializers, and theEventalias are public, so external consumers can pull events with bounded memory over arbitrarily large inputs (an internal typealias in a split file had been shadowing the publicParsing.Event, narrowingnext()to internal; the benchmark harness found it). A newPureXMLPublicAPITeststarget compiles against the public surface without@testable, so any public facade over internal members fails CI; it pins parse/query/serialize, streaming pull, and the chunked pull source.
Fixed
- XPath document-order sorting is benchmarked at scale across generated (4/42/210 MB) and real corpora (NASA ADC 25 MB, SwissProt 115 MB), all linear: the quadratic fix's own first attempt (an eager sibling table built per predicate evaluation) was caught by sampling the live run and replaced with a lazy table (single-node sets never touch order keys; a parent's table builds on its second lookup). XPath at 42 MB drops 27.1 s to 1.57 s (ratio 190x to 11.7x); at 210 MB PureXML completes in 9.4 s the query libxml2 refuses at its 10M node-set cap (the configurable protective budget is #143). The benchmark harness gains a size series, a CORPUS mode for real-world files, a SAX stage, and a refusal-aware comparison table.
- XPath document-order sorting is no longer quadratic on flat fan-outs (#139, found by the large-corpus benchmark): document-order keys looked up each node's index by scanning its parent's children, so sorting a node-set drawn from one wide element (the 42 MB corpus: 200k siblings) cost 27 seconds. A per-operation sibling-index cache enumerates each distinct parent once, making key computation linear in the tree.
Changed
- Byte-backed Reader, performance passes 2 and 3 (#139): String input now 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, the bulk of any document, scans as a single byte run with one String built per run. Cumulative parse time on the 4.2 MB benchmark corpus drops 32 percent (0.49 s to 0.34 s, libxml2 ratio 30x to 20x). The W3C conformance suite caught one boundary in the run scanner (a run starting with '>' could swallow the "]]>" check after the character path consumed "]]"; ibm14n01); the run scanner now leaves a leading '>' to the character path, with the case pinned as a unit regression. All suites (W3C xmlconf, RELAX NG, xalan), Linux, and WASM verified.
- First measured performance pass (#139): Reader.matches() no longer materializes an Array of the literal per call (it runs on every scan decision and was the hottest comparison in the parse profile); parse time on the 4.2 MB benchmark corpus drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x). The benchmark harness, baseline, profile findings, and the optimization log live in docs/benchmarks.md.
Fixed
- Stylesheet composition cycles no longer crash (#130 critic loop): an include or import cycle (a.xsl includes b.xsl includes a.xsl) recursed without bound and killed the host with a stack overflow, a denial of service reachable from stylesheet data. The compile chain now tracks the active load chain by resolved href and drops re-entry, while diamond composition (two units legitimately loading the same sheet) keeps loading both copies with their own import precedences, per 2.6.2. id() also gains a per-document index built on first use instead of re-walking the whole document per call, matching key(). Cycle and diamond regression tests added.
Added
- Top-level stylesheet parameters (#141), nineteenth xalan burn-down (#130):
PureXML.XSLT.transformacceptsparameters: [String: String], overridingxsl:paramglobals by name (xsl:variable globals are never overridable; the stylesheet records which names are parameters across imports and includes). The conformance runner honors the xalan harness contract, reading<case>.paramfiles; en alphabet sorting passes, and the CRLF param files exposed a Swift grapheme gotcha (CRLF is one Character, so splitting on "\n" never splits; the runner splits on newline graphemes). Baseline 128 to 127; Polish/Russian alphabet tailoring stays baselined as the locale class. - Space-ignorable sort collation, eighteenth xalan burn-down (#130): xsl:sort's default text order now treats spaces as ignorable at the primary level, breaking ties by later-first-space (the Java Collator semantics the conformance golds encode), derived from the gold orderings rather than delegated to a locale library. Baseline 129 to 128.
- DTD-typed id() and node-kind generate-id(), seventeenth xalan burn-down (#130): the XSLT layer's id() now follows the XPath definition exactly, resolving only attributes the source document's DTD declares as type ID (internal or external subset, parsed through the transform's entity resolver) and yielding the empty set without a DTD, replacing the name-based heuristic for transforms; and generate-id() produces non-empty, distinct identifiers for attribute and namespace nodes through their identity hash instead of returning the empty string. Two unit tests pin both. Baseline 131 to 129.
- HTML 4 serialization corners, sixteenth xalan burn-down (#130):
basefontandisindexjoin the void-element set (no end tags), and the html method leaves an ampersand unescaped when a brace follows, the historical&{...}attribute convention of 16.2. Baseline 134 to 131 (output 20 to 17). - Globals, prefix wildcards, fifteenth xalan burn-down (#130): a top-level variable now evaluates with every previously evaluated global in scope, so a variable whose body references an earlier top-level param (directly or via call-template) resolves; same-name globals resolve by import precedence (folds happen in precedence order and the last same-name entry wins, so a later sibling import beats an earlier one and the importing sheet beats both); the stylesheet fold uniformly favors later contributions for attribute-set definitions, decimal formats, namespace aliases, and output settings, matching the post-order precedence model; and the XPath compiler accepts the
NCName:*name-test form, matching any name in the prefix's namespace (bound by URI, falling back to the in-document prefix string) at its -0.25 default priority, so@ped:*outranks@*. Three unit tests pin the behaviors. Baseline 144 to 134 (variable 10 to 5, conflictres 8 to 5). - Import precedence and relative composition, fourteenth xalan burn-down (#130): import precedences are assigned in post-order over the import tree, so sibling imports are distinct (later higher) and every unit sits above its own imports; apply-imports searches only the current template's stylesheet's own import subtree per 5.6 (templates carry their unit's precedence range), falling back 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 own URI, so nested relative chains load correctly. Also fixed along the way: an included document's tree was only weakly referenced once flattened, so its ancestor chain (xmlns declarations, exclusions in scope) could disappear before its templates were parsed. Two unit tests pin apply-imports scoping and relative chains. Baseline 159 to 144 (reluri 11 to 3, impincl 9 to 2).
- Attribute sets, copied namespaces, html booleans, loader entities, thirteenth xalan burn-down (#130): same-name
xsl:attribute-setdeclarations merge as ordered definitions per 7.1.4 (each expands its used sets before its own attributes, a later definition's attributes winning);xsl:copyof an element carries the source element's in-scope namespace nodes per 7.5 (the fixup pass drops result-redundant ones); the namespace fixup lets a carried attribute prefix shadow an inherited binding locally (matching the source document's shape) unless the element's own name relies on it; the html method minimizes boolean attributes whose value repeats their name (CHECKED="CHECKED" serializes as CHECKED); and external DTDs and entities referenced by the stylesheet or source now resolve through the transform's document loader, the same channel as document(). Four unit tests pin the new behaviors. Baseline 174 to 159 (attribset 13 to 4, copy 12 to 7).
Fixed
- format-number and xsl:number no longer trap on extreme values (#130 critic loop): format-number of a value at or beyond 2^53 rendered through an unguarded Double-to-Int conversion, a fatal trap reachable from user data (format-number(99999999999999999999 * 99999999999999999999, '#,##0') crashed the host); a fraction picture with more than 15 places overflowed the scale conversion the same way; and xsl:number value= beyond 2^53 trapped identically. Digit extraction now clamps fraction places to 15 (the 2^53 exactness bound), pads back to the picture's requested minimum, renders out-of-range integers through the exponent expander (digits stay exact), and xsl:number falls back to plain XPath number rendering outside [1, 2^53). Regression tests cover all three paths.
Added
-
Critic-loop hardening of the xalan burn-down (#130): raw-output marker handling is now gated on the stylesheet actually mentioning
disable-output-escaping, so a source document that happens to contain the private-use marker characters U+E000/U+E001 passes through every transform untouched (previously they were stripped, and text between them unescaped, unconditionally), and the text output method only removes markers without "unescaping" content that was never escaped; the pattern-matching function table is built once per transform instead of once per (template, node) membership test; and the d-o-e, CDATA "]]>" splitting, simplified-syntax, html default-method/META, per-document key(), and attribute-axes behaviors gain focused unit tests that run in CI without the opt-in corpus (eight new tests). -
Following/preceding from attribute nodes, twelfth xalan burn-down (#130): the following and preceding axes now work from attribute and namespace starts, anchored at the owner element per the XPath 1.0 document order (an attribute follows its element and precedes its children, and has no descendants of its own). Baseline 182 to 174 (axes 16 to 8).
-
Keys per document, in document order, eleventh xalan burn-down (#130):
key()resolves its index against the current node's own document (built lazily per document root, so keys work inside document() trees), a node-setuseexpression indexes the matched node under every member's string value, and key() results return in document order regardless of the lookup value order. Baseline 187 to 182. -
Sort keys in context and cross-document template matching, tenth xalan burn-down (#130): xsl:sort keys are computed once per node with the node's own evaluation context (its position and size in the selection, the caller's variables, functions, and namespace bindings), so position()-based and variable-referencing keys work, and NaN keys order before every number under data-type="number" (the Xalan order); match patterns evaluate over the matched node's own document rather than the transform source, so templates match nodes loaded through document() (the match cache keys include the document identity). Baseline 204 to 187 (sort 18 to 11, mdocs 14 to 6).
-
Prolog and CDATA conformance, ninth xalan burn-down (#130): the XML declaration precedes the document type declaration; a public-only doctype is emitted by the html method (16.2) and ignored by the xml method, which requires
doctype-system(16.1); the simplified stylesheet syntax of 2.3 (a literal result element carryingxsl:version) compiles as a single match="/" template; and cdata-section-elements text containing "]]>" splits across section boundaries so no section holds the terminator. Baseline 218 to 204 (output 34 to 20). -
Stylesheet namespace resolution, eighth xalan burn-down (#130): a template carries the xmlns bindings in scope at its stylesheet element, and they flow into its match pattern (the match cache keys on pattern plus bindings) and every expression evaluated while it is instantiated, so
match="baz2:doc"andselect="local-name(baz2:b)"resolve the prefix against the stylesheet and match by namespace URI; with bindings supplied the XPath 1.0 rule applies exactly, an unprefixed name test selecting only the null namespace. The instruction-scope namespace capture includes the default declaration, soxsl:elementhonors an in-scopexmlns="uri"for its unprefixed created names, an in-scopexmlns=""keeps them namespace-free (undeclared in the result when an inherited default would capture them), and an unusable created name (:foo) drops the wrapper and keeps the content, the Xalan recovery. Baseline 263 to 218 (namespace 41 to 13, axes 16, sort 18, output unchanged at 34, the rest spread). -
Keys, ids, and doctyped sources, seventh xalan burn-down (#130):
key()anddocument()read the string value of attribute and namespace node arguments instead of dropping them (the Muenchian grouping idiom works); match patterns evaluate with the XSLT function table, sokey('k', 'v')andid('x')patterns match (a key/id pattern branch is also no longer prefixed with//); and the XSLT entry points parse sources, stylesheets, anddocument()loads with doctypes allowed, which the conformance corpora use throughout. Baseline 292 to 263 (idkey 39 to 16, the rest spread). -
Templates over attribute and namespace nodes, sixth xalan burn-down (#130): xsl:apply-templates and xsl:for-each now keep attribute and namespace nodes in their selections instead of dropping them at the tree-node conversion. The evaluation context carries the non-tree current node (its owner element backs the tree-only machinery), XPath evaluation and current() start from it, match patterns select attribute nodes (the match cache keeps attribute identities, owner plus name, alongside tree identities), the built-in rules copy an attribute's or namespace's value as text, xsl:copy copies the focused attribute or namespace declaration itself, and an attribute result arriving after element content is ignored (the XSLT recovery). Sorting and apply-imports work over the generalized node kind. Baseline 352 to 292 (axes 35 to 16, copy 29 to 16, attribset 18 to 13, the rest spread).
-
Output methods in depth, fifth xalan burn-down (#130):
disable-output-escapingon xsl:value-of and xsl:text (raw text travels bracketed by private-use sentinels and the serializer's escaping is undone after the fact; a string extracted from a result tree fragment loses the markers per 16.4, so the variable round-trip case re-escapes); the default output method is html when the first result element is named html with a null namespace (16.1); the xml method writes the XML declaration unlessomit-xml-declaration="yes"(the spec default, with the caller's emit options as fallback only on explicit stylesheet silence); the html method injectsMETA http-equiv="Content-Type"into head (16.2), keeps SCRIPT/STYLE raw and void elements end-tag-free case-insensitively, and escapes U+00A0-U+00FF to the HTML 4.01 Latin-1 entity names. Whitespace-only detection in stylesheet/source stripping now means XML whitespace only, so an NBSP-only text node survives. The conf comparison treats whitespace adjacent to tag boundaries as layout for non-canonical comparisons. Baseline 371 to 352 (output 49 to 30). -
Literal result element namespace copying, fourth xalan burn-down (#130): a literal result element now copies its in-scope namespace declarations to the result per 7.1.1, minus the XSLT namespace and the namespaces of the prefixes listed in
exclude-result-prefixes/extension-element-prefixesin scope (#defaultincluded); an aliased stylesheet namespace declares its result prefix and URI instead. The namespace-fixup pass drops any declaration that repeats an inherited binding, so the copied nodes do not redeclare on every descendant, and created names now reuse the stylesheet's own prefixes (anamespace:Attr1rather thanns0:Attr1).xsl:output indent="yes"switches to the corpus golds' Xalan indentation form: children on their own lines with no leading spaces. Two unit expectations updated to the spec behavior (a literal element under stylesheet-level declarations carries them; collapsed alias+result declarations). The xalan baseline drops from 446 to 371 (namespace 89 to 42, output 50 to 49 and others spread across categories). -
format-number picture parsing in full, third xalan burn-down (#130): the picture now parses JDK DecimalFormat-style into literal prefix/suffix affixes around the number part, the negative subpattern after the pattern separator (its affixes apply; without distinguishing ones the minus sign still prefixes, the Xalan resolution, which also covers colliding decimal/grouping separators by reading every occurrence as grouping), per-mille (with a custom symbol via the new
per-milleattribute) and percent as multipliers kept literally in the output, the grouping size taken from the last grouping separator's distance to the decimal point instead of a fixed 3, and the generic currency sign U+00A4 rendering as this processor's en-US$. Digit extraction is rewritten over one scaled rounding plus pure integer arithmetic, so 0.4812 renders 4812 rather than re-accumulating float error per digit. The xalan numberformat category drops 30 to 1 (the survivor is a namespace-copy case); baseline 475 to 446. -
Full xsl:number per XSLT 1.0 section 7.7, second xalan burn-down (#130):
levelsingle/multiple/any withcountandfromas real patterns matched through the transform's match cache (default count: same node type and expanded name; single finds the nearest matching ancestor-or-self and numbers it among matching preceding siblings, multiple numbers every matching ancestor-or-self outermost first, any counts matching nodes at or before this one in document order withfromas the boundary, always yielding a number so an unmatched count renders 0); thevalueexpression evaluates and rounds, bypassing formatting below one; and the full format-token engine (leading/trailing punctuation, alternating alphanumeric tokens and separators with last-token and last-separator reuse, zero-padded decimal widths,a/A/i/Iand the Greek alphabetic sequence,grouping-separator/grouping-sizedigit grouping). A single/multiple level whose count matches nothing renders as the empty string, punctuation included. The xalan baseline drops from 557 to 475 (numbering 84 to 7, the rest pattern-engine prefixed/key/id cases). -
document()is stable per transform (#130): the same URI reference now returns the identical document object across calls within one transform, per the XSLTdocument()definition, via a per-transformer document cache; previously each call re-parsed, so node identity, and with itgenerate-id()uniqueness against live nodes, could be violated when an address was reused (the idkey49 flake). -
XSLT namespaced name creation and result fixup, first xalan burn-down (#130): xsl:attribute and xsl:element honor the
namespaceattribute and resolve a prefixednameagainst the instruction's own in-scope stylesheet declarations (7.1.2/7.1.3, captured at parse time so AVT names resolve correctly); xsl:copy appliesuse-attribute-sets; and a namespace-fixup pass runs over the result tree before serialization, declaring every namespace a created element or attribute name carries, reusing an existing binding for the same URI or generatingns0,ns1, ... prefixes when the carried prefix is absent or taken, and undeclaring an inherited default namespace that would capture an unqualified name. The xalan baseline drops from 604 to 557 (attribset 52 to 20, namespace 123 to 92, axes 68 to 35); one namespace-alias unit test updated to expect the now-declared result namespace. -
XSLT 1.0 conformance runner over the Apache xalan-test corpus (#122, #130): the de facto OASIS-era suite (1690 gold-bearing cases across 36 categories) runs opt-in via XALAN_TS_ROOT, never vendored. Comparison is deliberately normalized: outputs that parse as XML compare by canonical form (Xalan's indentation is not normative), others with whitespace runs collapsed. First measurement: 1086 pass; the 604 known failures are carried exactly in Tests/Fixtures/xalan-baseline.txt, asserted in both directions (a fixed case must leave the file, a regression shows as a new failure). The top burn-down classes by count: namespace handling (123, led by namespaced xsl:attribute creation), xsl:number depth (85), axes corners (68), output/serialization options (55), attribute sets (52), id()/key() (41).
-
RELAX NG schema-correctness validation (#131): a schema document is now validated before any pattern interpretation, in three layers. The grammar check (section 3) rejects unknown or misplaced elements, illegal unqualified and RELAX NG-namespace attributes, wrong child arity, stray text, malformed or undeclared-prefix QNames, non-URI datatypeLibrary values (absolute, fragment-free, valid percent escapes), fragment-carrying hrefs, the 4.16 except restrictions (anyName/nsName inside excepts, propagated through choice), and the xmlns name prohibitions including the ns-attribute and except-mention forms. The compiler checks reject combine conflicts and double combine-less defines (4.17), unresolved refs and parentRef without a parent, startless grammars, include/externalRef self-reference, include overrides with nothing to override, non-grammar include targets (loaded documents are grammar-checked too), unknown datatypes, and params against the built-in library. The restrictions checker (4.19, 7.1-7.4) runs over the compiled algebra after 4.20/4.21 normalization (notAllowed/empty operands simplify away first; recursion detection deliberately precedes it): prohibited paths under attribute, oneOrMore-group, list, and in start; computable content types; duplicate attributes by representative-name overlap; infinite-name-class attributes outside oneOrMore; and the interleave text/element constraints. The spec suite's incorrect class drops from 213 compiling to 7, all pre-Fifth-Edition name-class cases (the xmltest-141 class, count asserted), with zero correct-schema or instance regressions. Two leniency tests updated to the spec behavior (double combine-less defines and unknown datatypes are schema errors).
-
RELAX NG QName values, instance level clean (#131):
<value type="QName">compiles to its (namespace, local-name) value pair via the schema's xmlns/ns scope, and the derivative engine threads the instance element's in-scope namespace bindings down to value comparison (in both the tree walk and the streaming state, whose frames now carry their bindings), so an instance QName resolves its prefix, or the default namespace when unprefixed, against the instance document itself; an unbound prefix matches nothing. The streaming path also gains the tree walk's empty-element rule. With this the spec suite's instance level is fully clean: valid 289/289 accepted and invalid 291/291 rejected; only the asserted schema-correctness class remains. -
RELAX NG grammar scoping and include depth, third burn-down (#131): each
grammarelement opens its own define scope (names are scope-mangled in the flat table, so nested grammars do not collide) andparentRefreaches the enclosing scope;includeis transitive (an included grammar's own includes merge first) and a define carried by the include element replaces the included grammar's same-name define (the 4.7 override);divwrappers flatten into their grammar while still contributing ns/datatypeLibrary inheritance (4.11);valuetext is kept untrimmed so whitespace-preserving datatypes compare exactly (6.2.9); anddataExceptrecurses through choice patterns. The spec suite is now clean except one class: valid 286/289 and invalid 287/291, with all six remaining instance cases the QName datatype (its value space needs prefix resolution in both the schema and instance contexts), plus the asserted schema-correctness item. -
RELAX NG namespaces and semantics, second burn-down (#131): the compiler now implements
nsinheritance and the QName form of names (4.9-4.10:nswalks ancestors and crosses include/externalRef document boundaries;name="eg:foo"and<name>eg:foo</name>resolve their prefix against the schema's in-scope xmlns declarations; thename=shorthand on an attribute stays namespace-less rather than inheriting), anincludebrings in the included grammar'sstartwith an include-level override honored (4.7), multiple<start>elements combine like defines and a single define or start may omitcombinewhile still merging by the named method (4.17),<data>supports<except>via a newdataExceptpattern in the derivative engine (4.12), an empty element matches a data/value pattern whose lexical space admits the empty string (Clark's empty-text rule), and a whitespace-only attribute value matches a pattern that matches empty. Spec-suite baselines tighten from valid 242/289 to 274/289 and invalid 278/291 to 279/291; the name-class and tree-helper sections split to their own files for the length caps. -
RELAX NG simplification, first burn-down (#131): foreign-namespace elements are stripped before pattern interpretation (4.1: only RELAX NG-namespace elements are schema content; a namespace-less element counts only inside a fully unqualified schema), name/type/combine attribute values are whitespace-trimmed (4.2), and include/externalRef hrefs resolve against the xml:base attributes in scope and the loaded document's own URI (4.5, reusing the RFC 3986 resolver), with the document base threaded through nested loads and visited-tracking keyed on resolved hrefs. Spec-suite baselines tighten: valid 242/289 accepted (from 233), invalid 278/291 rejected.
-
RELAX NG spec test suite runner (#122, #131): James Clark's spectest.xml (385 cases, distributed with jing-trang) runs via an opt-in RNG_TS_ROOT runner, never vendored; the manifest is parsed with PureXML itself, nested testSuite scopes thread their resource/dir definitions into the compiler's loader, and each case asserts compile/reject for the schema plus accept/reject per instance. First measurement, carried as exact baselines: every correct schema compiles and most instances behave (valid 233/289 accepted, invalid 277/291 rejected), while schema-correctness validation is absent (all 213 incorrect schemas compile, count asserted exactly). The burn-down classes are recorded in the runner: schema validation against the RELAX NG grammar and the 4.16-4.18 restrictions, foreign-namespace stripping (4.1), xml:base-aware include/externalRef resolution (4.5), and the section 6 compatibility datatypes.
-
Canonical XML 1.0 spec-example vectors (#122, #132): the worked examples of the C14N 1.0 Recommendation's section 3 run as input/expected-output tests (cited per example; used under the W3C Software and Document License), all six passing: top-level PIs/comments with and without comments mode, whitespace preservation, start/end tags with attribute sorting and DTD defaults, character modifications and references, entity expansion, and UTF-8 output from legacy-encoded input. The vectors exposed three real conformance fixes: top-level siblings in the canonical form are now separated by single line feeds; an empty default-namespace declaration renders only when it undeclares a rendered non-empty default (superfluous
xmlns=""is dropped, inclusive mode); and the text/attribute escape loops iterate Unicode scalars, so a carriage return that grapheme-clusters with a following line feed still escapes to
(the #135 class, surfacing in output rather than lexing). -
Scalar-level lexing (#135): the reader now buffers exactly one Unicode scalar per element (multi-scalar graphemes from any source are split, string input iterates scalars directly), so a combining mark directly after an ASCII delimiter can no longer merge with it into one Swift grapheme cluster and XML's scalar-defined name productions classify correctly: U+06D6 lexes as a Fifth Edition PI-target NameStartChar, ZWNJ/ZWJ attribute names parse, and U+309A markup from entity expansion forms a real element. The replacement-text, content-model, and ATTLIST sub-scanners and the entity-literal validator go scalar-level the same way, while grapheme clusters reassemble naturally wherever scanned text is appended to a String (asserted by test). This clears the last eduni baseline (now empty), grows the IBM pre-5e class from 279 to 300 exactly as the Fifth Edition prescribes, and, combined with the EDITION="1 2 3 4" exclusions the xmltest manifest itself carries for cases 140/141, empties the xmltest deviation baseline entirely: every xmlconf case applicable to a Fifth Edition, namespace-aware processor now passes.
-
Strict internal-subset profile (#128):
Limits(strictInternalSubset: true)holds the internal DTD subset to the letter of XML 1.0, rejecting conditional sections and parameter-entity references inside markup declarations (entity values, content models, ATTLIST bodies), both of which PureXML otherwise supports as features; DeclSep references between declarations and%name;inside quoted attribute defaults (AttValue, where%is literal) stay legal, and defaults are unchanged. The xmltest runner opts in, taking its baseline from 181/186 to 185/186 with one remaining documented deviation: case 141, which expects pre-Fifth-Edition name-character classes. -
Per-entity base-URI tracking (#138): a relative system identifier declared inside an external entity now resolves against that entity's own URI (RFC 3986, reusing the C14N resolver), so nested external entities find their siblings;
ExternalIDgains an optionalbaseand aresolvedSystemID, the DTD scanner threads the current entity's URI through external-subset and parameter-entity scans, and resolvers receive the already-merged identifier (raw identifiers unchanged when no base applies). Clears eduni errata-2e/E18; the eduni baseline now holds only the #135 grapheme-lexing class. -
Encoding-declaration contradictions are fatal (#137, 4.3.3): a document whose XML declaration names a 16- or 32-bit encoding over BOM-less 8-bit bytes is rejected; a BOM followed by a declaration outside its family (a UTF-8 BOM with
encoding='iso-8859-1', a UTF-16 BOM with an 8-bit name) is rejected, with the declaration read through the BOM's own byte pattern; and an external entity whose text declaration names a higher XML version than the document's is refused (errata E38), in both the external-subset and external-entity paths. The eduni not-wf baseline is now completely empty. -
Namespaces 1.0 constraints (#136): the namespace layer now enforces the binding rules as located parse errors: malformed qualified names (
foo:,:foo,a:b:c,xmlns:) are rejected; thexmlnsprefix may never be declared;xmlmay be bound only to its own namespace name, which no other prefix (nor the default) may take, likewise the xmlns namespace name; a prefix may not be undeclared (Namespaces 1.0); attributes must be distinct by expanded name (the same URI+local name through two prefixes is a duplicate); PI targets and entity/notation names are NCNames; and a namespace declaration whose attribute is DTD-declared with a tokenized type binds its normalized value (sourn:xandurn:xare one namespace name, eduni ns 012). Clears all 18 namespace cases from the eduni baseline, leaving only the four #137 encoding-mismatch cases; xmltest valid/sa/012 (an attribute literally named:) joins the documented deviations per the suite's own NAMESPACE='no' flag, mirrored by the eduni runner's new NAMESPACE filter. -
W3C xmlconf eduni sections (#121, #127): the Edinburgh errata suites (2e, 3e, and the 5th-edition 4e), Namespaces 1.0 with its errata, and the misc cases run via the XMLCONF_ROOT runner; XML 1.1 and Namespaces 1.1 are explicitly out of scope and EDITION-gated cases skip when they do not apply to the Fifth Edition. The burn-down fixed a real spec bug and several errata behaviors: attribute values now get the 3.3.3 CDATA normalization (literal whitespace becomes a space before reference decoding, so character-referenced whitespace survives, fixing errata E36 and E20 semantics) and tokenized normalization collapses only SPACE characters; an undeclared general entity in content or an attribute value is a deferred validity finding instead of a fatal error when unread external declarations might supply it (production 68; the reference stays literal, nothing is fetched), matching the scanner-side split; the errata E15 family is enforced (any reference, comment, PI, or whitespace inside an element declared EMPTY; direct or double-escaped character-reference whitespace as character data in element content, while entity-supplied whitespace stays legal); errata E2 reports duplicate tokens in enumerated and NOTATION declarations; errata E14's declaration-completed-inside-a-PE-replacement parses and reports VC: Proper Declaration/PE Nesting; the ID family must be NCNames under namespace validation; and DTD-strict mode requires namespace declarations to be declared like any attribute. Remaining deviations are exact and classified: 24+1 grapheme-cluster lexing cases (a combining mark after an ASCII delimiter merges into one Swift Character), 18 namespace-constraint cases, 4 encoding-declaration mismatches, and E18 (per-entity base-URI tracking), each filed as its own issue.
-
W3C xmlconf japanese section, fully clean (#121, #126): all 13 cases (the same prose and parameter-entity DTDs encoded as UTF-8, UTF-16 in both byte orders, Shift_JIS, EUC-JP, and ISO-2022-JP) decode through PureXML's own byte decoder, parse, and DTD-validate cleanly with zero deviations; the optional encodings the suite classifies as TYPE='error' are all supported, so they are held to the must-parse bar.
-
W3C xmlconf IBM section, clean to two documented classes (#121, #125): the deepest block, 1094 cases. Valid 220/220 (parse + strict DTD validation), invalid 64/64 reporting, not-wf 529/810 rejected with the 281 accepted cases being exactly two documented classes: 279 cases from productions 85-89, the 1998 character-class appendices deleted by XML 1.0 Fifth Edition (whose count is asserted exactly so drift in either direction is caught), and 2 parameter-entity-in-internal-subset feature cases (the xmltest 160-162 class, headed for #128's strict profile). Closing the section's real gaps: a document without a root element is rejected (production 1); an entity reference in an entity literal must be a lexical Name or character reference (production 68); WFC: No Recursion is enforced at declaration over the combined general/parameter reference graph (CDATA-shielded), so a recursive pair is rejected without being referenced; an undeclared parameter-entity reference is a well-formedness error when the document is standalone or has neither an external subset nor parameter entities, and a deferred validity finding otherwise (the production-68 WFC/VC split, also applied to entity references in attribute defaults); a declared external PE the resolver refused is neither; and a content-model group opening in one PE replacement and closing in another is reported (VC: Proper Group/PE Nesting).
-
Standalone validity constraints (#134): a document declaring
standalone='yes'is now checked against its external-subset dependencies (2.9). The DTD scanner records declaration provenance (which entities, element models, and attribute lists came from the internal subset); the validator reports an externally-declared attribute default that would be supplied, an attribute value an externally-declared non-CDATA type would normalize, and whitespace inside externally-declared element content. The parser enforces the matching well-formedness side: a standalone document referencing an entity declared outside the internal subset reports it as undeclared (WFC: Entity Declared). This empties the entire Sun baseline: the section is fully clean at not-wf 56/56, valid 28/28, invalid 74/74, zero deviations, closing #124. -
DTD validity constraints, second Sun burn-down (#121, #124): the validator now enforces VC Root Element Type (the root element must match the DOCTYPE name, recorded by the scanner), VC Unique Element Type Declaration (duplicate
<!ELEMENT>reported, first stays in effect), VC No Duplicate Types in mixed content, VC One ID per Element Type, VC Notation Attributes (listed names must be declared) and VC Notation Declared (an NDATA entity's notation must exist), VC Attribute Default Legal across the tokenized types plus VC ID Attribute Default (an ID attribute must be #IMPLIED or #REQUIRED), lexical Name checks for ID/IDREF/IDREFS values, and CDATA sections as character data in element content even when empty. Strict mode additionally requires every attribute to be declared (VC Attribute Value Type; namespace declarations exempt). Declaration-level findings are computed once when the schema is built and reported at the document root. Sun invalid coverage moves from 43/74 to 62/74 reporting; the twelve remaining silent cases are one family, the standalone='yes' VCs, which need declaration-provenance tracking. -
Entity content splicing (#133): a general entity whose replacement text contains markup, directly or through entities it references, is now reparsed as content at the point of reference (4.4.2 Included), so
<p>text</p>inside an entity becomes apelement in the tree and in the pull-event stream, in both strict and recovering modes, instead of being included as escaped character data. The replacement is validated (the reference-time balanced-content WFC), checked for reference cycles, and budgeted against the amplification cap before being spliced into the reader's stream. Character references in internal entity literals now expand when the declaration is parsed for general entities too (4.4.5, previously parameter entities only), so<foo>stores real markup that splices, and the Appendix D double escape&#38;stores&and renders as a literal ampersand; the reference-time WFC accordingly validates remaining references instead of re-expanding them. Text-only entities keep the single-pass expansion path and its event coalescing. Closes the last two Sun valid deviations: the section is now 28/28 valid. -
Validity-layer conformance, first Sun burn-down (#121, #124): tokenized attribute values are normalized per 3.3.3 before validity checks (whitespace runs collapse, outer whitespace strips, CDATA untouched), so
noncesatisfies an enumeration and an entity-expanded IDREF resolves; an external parsed entity's leading text declaration is stripped per 4.3.1 (and validated: version optional, encoding required, standalone forbidden) instead of being included in the replacement text; character references in parameter-entity literals are expanded at declaration time per 4.4.5, making the spec's Appendix D%zz;example parse; and a PI target inside the DTD subset must be separated from its data (production 16). Sun baselines tighten to not-wf 55/56, valid 26/28, invalid 43 reporting; the two remaining valid failures share one cause, filed separately: general-entity replacement text containing markup is included as character data rather than reparsed into elements. -
W3C xmlconf Sun section runner (#121, #124): the first validity-layer runner. Sun's manifests classify cases three ways and the runner checks all three: not-wf rejected by the strict parser (54/56), valid documents parse AND DTD-validate cleanly (19/28), invalid documents parse but must report at least one validity error (43/74). The exact deviation baselines are carried in the test with their causes: tokenized attribute-value normalization (3.3.3), external-entity text declarations (4.3.1), declaration-time character-reference expansion in PE literals (4.4.5), the standalone VCs, ID/IDREF corners, #REQUIRED enforcement, per-type attribute VCs, and the root/DOCTYPE name match, the work list this issue burns down.
-
W3C xmlconf OASIS/NIST section, fully clean (#121, #123): a second opt-in runner (XMLCONF_ROOT-gated, suite never vendored) drives the oasis section from its own manifest, parsed with PureXML itself; all 247 not-wf cases are rejected and all 100 well-formed (valid + invalid) cases parse, with zero deviations (the one excluded case is flagged NAMESPACE='no' by the manifest itself: pre-namespace colons in names, which a namespace-aware parser correctly refuses). Closing the section's last 24 gaps made the DTD subset grammar strict: unknown, lowercase, or space-separated markup-declaration keywords are rejected; a parameter-entity reference must be '%' Name ';' exactly; entity, parameter-entity, and notation names must start with a NameStartChar; NDATA requires whitespace and a strict name; a notation declaration requires its identifier and a clean tail; entity value literals admit no bare '%' (production 9); conditional-section keywords are case-sensitive INCLUDE/IGNORE and sections must balance to their ']]>'; junk between declarations is not well-formed; a grammar violation in the external subset now rejects the document (previously swallowed); the external subset may open with a text declaration (version optional, encoding required, standalone forbidden, production 77); and a processing-instruction target must be separated from its data by whitespace (production 16). The new strictness also tightened the xmltest baseline from 180/186 to 181/186: a CDATA section in the internal subset (case 107) is now correctly rejected by the conditional-keyword check, leaving five documented deviations.
Changed
- Schema-consistency checks now run through the validation framework (#117 follow-up): the derivation
finalcheck and Particle Valid (Restriction) are composableValidation<SchemaTypeFact, CompiledSchemaFacts>rules (finalRespected,restrictionsAreSubsets) applied per named type, so a schema with several problems reports them ALL at once.SchemaError.finalViolationand.invalidRestrictionare replaced by.inconsistent([String])carrying every finding with its type's coding path; the finding texts are unchanged.checkRedefine,checkAllGroups, andnotASchemastay fail-fast throws because they examine the raw schema source, the compilation analog of well-formedness. - Document-order sorts no longer recompute root paths per comparison (#113): node-set ordering (the evaluator's order-and-dedup, EXSLT's set functions, string-value coercion's first-node lookup) goes through decorate-sort-undecorate helpers (
sortedByDocumentOrder(),firstInDocumentOrder()) that compute each node's order key once, turning O(n log n x depth) sorts into O(n log n).set:leading/set:trailingalso reuse one precomputed pivot key. Behavior is unchanged. - XPath compilation moved out of the per-node loops (#112): XSLT template selection now resolves each match pattern once per transform through a per-run match cache (the pattern's node set is computed once over the source tree, then membership is an identity lookup), instead of recompiling and re-walking the tree for every (node, template) pair; and the XSD identity validator compiles each selector and field XPath once per run through a query cache instead of once per visited element. Schematron already stored compiled queries. Behavior is unchanged; a multi-template shape test guards the path.
- The HTML document builder's body is now constructed as a live mutable
TreeNodetree with a single open-elements stack (the HTML5 model), attaching nodes to their parent as they open rather than accumulating children on close (#83, internal). Behavior is unchanged; this is the foundation the adoption agency algorithm needs to reparent already-built subtrees. - DTD attribute validation is now decomposed into five named, independently composable
Validationrules (#101):DTD.requiredAttributes,fixedAttributeValues,enumeratedAttributeValues,tokenizedAttributeTypes, andnotationAttributes, replacing the singleattributeDeclarationsrule. Each is removable by identity and isolation-tested one constraint at a time, honoring the validation-framework idiom. Behavior is unchanged. (XSD content validation stays one recursive rule by a documented scope decision, since its constraints are interdependent through type resolution; Schematron already exposes its rules as composableValidation<Node, Void>values.)
Added
- Parser strictness, first W3C-suite pass (#105 Tier 2, #120): the strict parser now rejects
--inside comments (including the--->ending), a raw<in attribute values, the literal]]>sequence in character data, attributes not separated by whitespace, and characters outside the XMLCharproduction in content, five new locatedParseErrorcases. Driven by a new opt-in runner for the W3C XML Conformance Test Suite's xmltest section (XMLTS_ROOT-gated; the suite is never vendored because its license permits redistribution only as the unmodified archive): valid/sa 118/119, not-wf/sa rejection improved from 80 to 180 of 186 and valid/sa to 119/119 across five strictness passes (the second adds character-reference validation, lowercase-x-only hex references and the referenced code point must be a valid XML Char; the reserved 'xml' PI target, legal only as the declaration at the document's very first bytes; and XML Char validation in comments and PI data; the third validates content models against the XML 1.0 grammar at DTD scan time, rejecting unbalanced parentheses, #PCDATA misplacement and nesting, mixed connectors, whitespace-detached or doubled quantifiers, empty groups, and SGML leftovers; the fourth makes the DTD declaration grammar strict: whitespace and name rules, junk after a value, NDATA spacing and its general-entities-only rule, entity-value literals checked for XML Chars and complete references, SYSTEM/PUBLIC identifiers parsed strictly with PubidChar validation, validated against productions 52-60, the reserved xml PI target rejected inside the subset, strict UTF-8 decoding, the XML declaration's version/encoding value grammar, and required whitespace between pseudo-attributes; the fifth implements the replacement-text well-formedness constraint: when a general entity is referenced, its replacement (character references expanded as at declaration) must reparse as balanced content in isolation, so a bare or incomplete reference, a raw '<' in an embedded attribute value, tags spanning the entity boundary, and a reserved-target PI inside the replacement are rejected at the point of use, while an unreferenced entity may carry any value and only the first declaration of a name binds, both per spec; references in attribute-list defaults must name already-declared internal entities with a non-recursive chain; and a CDATA section inside replacement text now shields its content from reference expansion. The six remaining suite deviations are deliberate: internal-subset conditional/CDATA sections and parameter-entity references are supported as features, and one case expects pre-Fifth-Edition name-character classes), with the exact remaining baseline carried in the test and the two remaining leniency classes (internal-subset DTD grammar, entity replacement-text well-formedness) tracked as #120. - XSD Particle Valid (Restriction) (#117): a complex type derived by restriction is now checked at schema compile to accept a structural subset of its base, covering element name matching, element-vs-wildcard admission, wildcard narrowing, occurrence-range subsumption, and the group recursions (sequence/all order-preserving with emptiable skips, choice mapping, sequence-into-choice, sequence-into-all, RecurseAsIfGroup). A structurally unfaithful restriction (widened occurrence, new element name, reordered sequence, content added to an EMPTY or required-into-empty base, an element outside a base wildcard's namespace) throws
SchemaError.invalidRestrictionwith the type, base, and reason. The spec's effective-total-range arithmetic for MapAndSum is approximated by per-particle checks, a documented simplification. - Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A
PureXML.Validation.ConformanceCaseis aValidatablenamed expectation (actual vs spec-authoritative expected output), andConformance.matchesExpectedis aValidation<ConformanceCase, Void>rule that emits one locatedValidationErrorper diverging case, so a whole suite reports every failure at once with the case name as its coding path. Seeded with a Canonical XML 1.0 corpus (attribute ordering, empty-element expansion, comment removal, CDATA-to-escaped-text, processing instructions, namespace rendering) an XPath 1.0 core-function corpus, and an XSD datatype/facet corpus (length, range, pattern, enumeration, lexical space), a RELAX NG pattern corpus (text, optional, repetition, choice, ordered group, attribute, interleave, empty), an XSLT 1.0 transformation corpus (value-of, for-each, if, choose, sort, variable, format-number, apply-templates, call-template), an HTML5 tree-construction corpus (void elements, implied tag closing, case normalization, the adoption agency, entities), a Schematron corpus (assert, report, rule context), a C14N exclusive/2.0 corpus (unused-namespace dropping, render-at-use, whitespace trimming), an XPath axes/predicates corpus (descendant, ancestor, sibling axes, positional and attribute predicates, union), a DTD content-model corpus (sequence, choice, occurrence indicators, mixed, EMPTY, ANY), an XInclude/XPointer corpus (substitution, text inclusion with escaping, fallback, relative href resolution; shorthand ids, element() navigation, xpointer() including the per-step predicate semantic), an XSD regex corpus (implicit whole-string anchoring, the character-class escapes, Unicode categories, quantifier ranges, alternation, class negation and subtraction), and an XML Catalog corpus (system/public/uri entries, rewrite prefixes, unmatched identifiers). The HTML corpus surfaced its first divergence (the fragment parser does not reconstruct active formatting elements like the document parser, tracked as #109). Growing the corpora against the official W3C/OASIS suites is the ongoing Tier-2 work. - EXSLT extension functions (#105, Tier 3): the
common,math, andsetsEXSLT modules, dispatched by resolving a function's prefix to its namespace (so any prefix bound to an EXSLT namespace works).math:min,math:max,math:highest,math:lowest,math:abs,math:sqrt(with the EXSLT NaN rule for non-numeric nodes);set:distinct,set:difference,set:intersection,set:has-same-node,set:leading,set:trailing(by node identity and document order);exsl:object-type.math:powerand the other transcendentals are omitted because they need a C math library, which the pure-Swift, Foundation-free target excludes;exsl:node-setawaits result-tree-fragment support. - Canonical XML 1.1 (#105, Tier 3): a
Canonical.Options.canonical11preset and amergeInheritedBaseoption. When canonicalizing a document subset, thexml:basevalues of the apex's omitted ancestors are merged into the apex by RFC 3986 reference resolution (vs. 1.0's nearest-ancestor rule), the apex's own relativexml:baseis resolved against that chain, andxml:idis no longer inherited;xml:langandxml:spacestill inherit the nearest. A self-contained RFC 3986 resolver (scheme/authority/path parsing, path merging, dot-segment removal) backs the merge. - Single-byte legacy encodings (#97): the byte decoder gains a table-based single-byte path and the full set of single-byte legacy encodings, selected by the XML declaration's
encodingname for both the whole-buffer and streaming decoders: the complete ISO-8859 family (parts 2 through 16), Windows-1250/1251/1253/1254/1255/1256/1257/1258, and KOI8-R/U. Most are vendored verbatim from the authoritativeunicode.orgmapping files; ISO-8859-5/9/15 are derived exactly; Windows-1254 composes from Windows-1252. Multi-byte CJK encodings (#99) are tracked separately.
Fixed
- XSD identity-constraint XPath compile errors are no longer swallowed (#111): a selector or field XPath that fails to compile is reported once up front as a located error ("identity constraint 'k': invalid field XPath '...'"), instead of
try?silently turning a schema author's typo into a disabled constraint. The broken query still evaluates as no-match afterwards, so the rest of the document is validated. - XSD type resolution no longer accepts invalid input silently (#110): an
xsi:typenaming an undeclared type is now a located error ("unknown xsi:type '...'") instead of a silent fallback to the declared type, in both the tree and streaming validators; andtypeReferencechains are resolved through one shared cycle-detecting resolver, so a circular chain reports "circular type reference '...'" instead of silently truncating after 64 hops (and no longer recurses without bound in the streaming shallow check, a latent crash). Completions use the same resolver, best-effort. - The HTML fragment parser (
HTML.parse) now reconstructs active formatting elements, matching the document parser and the HTML5 algorithm (#109, surfaced by the Tier-2 conformance corpus). When a formatting element is closed out of order it is re-opened for the following content, so<b><i></b>X</i>yields<b><i></i></b><i>X</i>(not<b><i></i></b>X), in both the fragment and document paths. - XSD identity-constraint errors now locate the offending field, not just the element (#104): a single-field
key/keyref/uniqueerror extends the coding path with the field itself, an attribute (... at path: list/item[2]/@id) or a child element (.../code), resolved from the constraint's field XPath. keyref scope resolution is confirmed to resolve a key declared on an ancestor element (cross-scope), with a test. Full XSD qualified-node-set conformance for pathological multi-level same-name nesting remains part of the conformance-corpus work. - XSD identity-constraint errors (
key,keyref,unique) now carry a coding path locating the actual offending element, instead of always rendering "at root of document". TheIdentityValidatorwalk threads a[PathKey]the same way the content-model validator does, and each error is located at the selected target node, so a duplicate-key failure reports "... at path: list/item[2]" and a dangling keyref "... at path: orders/line" rather than an empty path. This brings the identity-constraint rule into line with the validation framework's path-carrying contract, which every other rule already honored. The "no element declaration for the root" XSD error is likewise located at the root element, matching the namespace check beside it. - A
<template>element's flow content is now kept nested inside it (<template><div>x</div></template>) instead of being split out into the body (#83).templateis built as body flow content rather than routed to the head.
Added
- ISO-2022-JP decoding (#108, in progress): the byte decoder gains the WHATWG
iso-2022-jpalgorithm, selected by theiso-2022-jpdeclaration name. Unlike the other CJK encodings this one is stateful, escape sequences switch between ASCII, JIS X 0201 Roman (with the yen sign and overline), JIS X 0201 katakana, and the JIS X 0208 plane, which is resolved through the sharedjis0208table (no new data). The streaming decoder now honors the declared encoding from the XML declaration (the libxml2 behavior), not only a byte-order mark, so the single-byte and multi-byte CJK decoders, including the stateful ISO-2022-JP whose mode is carried across reads, work over an incremental byte source; a test asserts streaming agrees with whole-buffer decode for a single-byte, a CJK, and an ISO-2022-JP document. - EUC-TW decoding (#108): the byte decoder gains EUC-TW (CNS 11643), selected by the
euc-twdeclaration name, for both the whole-buffer and streaming decoders. ASCII passes through; a lead in0xA1-0xFEopens a two-byte plane-1 character;0x8Eopens a four-byte form whose second byte selects the plane. CNS planes 1 and 2, which carry essentially all real-world EUC-TW text, are vendored from the ICUeuc-tw-2014mapping (private-use fallbacks dropped); the rarely used planes 3-15 decode to the replacement character and are a documented follow-up. - Streaming (reader-driven) DTD validation (#107, in progress):
PureXML.validate(streaming:dtd:)andPureXML.Validation.StreamingDTDValidatorvalidate a document against a DTD while it is pulled event by event (the libxml2xmlTextReadervalidation model), holding only the open-element stack rather than the whole tree. Each element's content model and attributes are checked the moment it closes, by applying the exact per-element DTD rules to a shallow element synthesized from the streamed child names. Document-scoped ID/IDREF integrity is tracked in bounded sets (the identifier values, not the tree): a duplicate ID is reported at its repeat occurrence, and forward IDREFs resolve at the end of the stream. Streaming and tree validation report the same problems (a test asserts matching reasons), and errors carry a libxml2xmlGetNodePath-style coding path. RELAX NG streaming is also available viaRelaxNG.validate(streaming:): the Brzozowski derivative is itself incremental, so it is driven directly from the event stream, retaining only the residual pattern (theafter-threaded chain is the validation stack) and a light per-element frame for ignorable-whitespace handling; a differential test asserts it agrees with the tree engine across valid, invalid, whitespace, and mixed-content documents. XSD streaming is available viaSchema.Document.validate(streaming:): each element's type is resolved from its parent's content model (andxsi:type/typeReference) as it opens, and its attributes and content-model structure are checked shallowly as it closes (ComplexValidator.validateShallow), with a differential test asserting agreement with the tree validator. WildcardprocessContentsand substitution-group resolution in the streaming form stay on the tree validator. - Output encoding (#106):
Serializer.serialize(_:encoding:)andPureXML.serialize(_:encoding:)now emit bytes in a chosen encoding (the libxml2xmlSaveToFilename(..., encoding)model), not only UTF-8. A newPureXML.Parsing.ByteEncodercovers the Unicode transformation formats, the single-byte legacy families (ISO-8859, Windows code pages, KOI8), and the multi-byte CJK encodings (Shift-JIS, EUC-JP, EUC-KR, GBK, GB18030, Big5). The CJK and single-byte inverses are built by running the existing, tested decoders over every valid byte sequence and recording the shortest sequence per scalar, so each encoder is by construction the exact inverse of its decoder (no new data, no separately maintained encode tables); GB18030's astral range uses the four-byte pointer formula. The declaration carries the encoding's canonical name, a UTF-16/32 stream is preceded by a byte-order mark, and any scalar the target encoding cannot represent is written as a decimal numeric character reference, so output always round-trips. - Big5 (#103): the byte decoder gains the WHATWG
big5algorithm, for both the whole-buffer and streaming decoders, selected by thebig5declaration name (and thebig5-hkscs/cn-big5aliases). A lead in0x81-0xFEfollowed by a trail in0x40-0x7Eor0xA1-0xFEresolves through the index, covering Traditional Chinese and the HKSCS extensions including the astral CJK ideographs (Extension B and the Compatibility Supplement). Four pointers decode to a base letter plus a combining mark (a two-scalar sequence). The 18,590-entry index is vendored verbatim from the WHATWGindex-big5.txt. Big5 completes the major CJK families libxml2 reaches through iconv (Japanese, Korean, Simplified and Traditional Chinese). - Full GB18030 (#102): the byte decoder gains the GB18030 four-byte sequences on top of the GBK two-byte subset, for both the whole-buffer and streaming decoders, selected by the
gb18030declaration name. A lead in0x81-0xFEfollowed by a digit opens a four-byte form whose pointer is mapped through the WHATWGindex-gb18030-rangeslinear ranges, reaching the supplementary planes (the189000range maps toU+10000and beyond). The 207 ranges are vendored verbatim from the WHATWGindex-gb18030-ranges.txt. GB18030 is China's mandatory standard, supported in libxml2 through iconv. - CJK multi-byte encodings (#99, in progress): the byte decoder gains a multi-byte path and the WHATWG Shift-JIS and EUC-JP algorithms, for both the whole-buffer and streaming decoders, selected by the XML declaration's
encodingname. Shift-JIS covers ASCII, half-width katakana, and the JIS X 0208 plane; EUC-JP adds the0x8Ehalf-width katakana and0x8FJIS X 0212 leads. EUC-KR (CP949/UHC) decodes Hangul and Hanja through the WHATWGindex-euc-kr, and GBK (which also covers GB2312) decodes Chinese through the two-byte range ofindex-gb18030with0x80as the euro sign. The JIS X 0208/0212, CP949, and GBK indexes are all vendored verbatim from the corresponding WHATWGindex-*.txtfiles. This completes the CJK multi-byte encodings. - HTML named character references: the complete WHATWG named character reference set (#84), all 2,125 semicolon-form references including the astral mathematical alphanumerics and the 93 two-codepoint references, vendored verbatim from the authoritative
entities.json(replacing the previous 16-entry stopgap). Decoding stays longest-match with an optional trailing semicolon, on top of the existing numeric-reference validation. - HTML5 select content model (#83, stage 10): a
<select>now follows the in-select rules.<optgroup>closes an open<option>, a nested<select>closes the open one rather than nesting, and an<input>/<keygen>/<textarea>closes the select and is placed after it. In the "in select in table" case, a table-structural tag (caption/table/tbody/tfoot/thead/tr/td/th) closes a select that is inside a table before being processed. - HTML5 frameset documents (#83, stage 8): a
<frameset>after the head now produces a frameset document whoseframesetelement replaces the body (<html><head></head><frameset>...), with nested framesets and<noframes>handled, and<frame>is now a void element (it no longer wrongly nests). An ordinary document still produces a body. - HTML5 integration points (#83, stage 7): HTML content inside an SVG integration point (
foreignObject,desc,title) is now parsed in the HTML namespace rather than SVG, so<svg><foreignObject><div>puts thediv(and its descendants) back in HTML; re-entering<svg>inside an integration point switches back to the SVG namespace. - HTML5 table foster-parenting (#83, stage 6): stray flow content inside a table is now moved out rather than nested in the table structure, matching the HTML standard. A non-table element or character data appearing while a
table/tbody/thead/tfoot/tris the open node is inserted immediately before the table (<table><b>x</table>-><b>x</b><table></table>), while well-formed cell content is untouched. - HTML5 SVG attribute-name case adjustment (#83, stage 5): an SVG attribute written in mixed case is restored to its canonical camel case in the tree (
viewbox->viewBox,gradienttransform->gradientTransform, and the rest), applying only to elements in the SVG namespace. HTML attribute names are left as the tokenizer lowercased them. With this, SVG/MathML foreign content carries correct element and attribute names. - HTML5 adoption agency algorithm and active formatting elements (#83, stage 4): misnested formatting tags are now recovered exactly as the HTML standard prescribes. Overlapping tags nest (
<b><i></b></i>-><b><i></i></b>), content after an out-of-order close is re-wrapped by reconstructing the active formatting elements (<b><i></b>X</i>-><b><i></i></b><i>X</i>), and a block inside a formatting element reparents through the furthest-block path, including the canonical<p>1<b>2<i>3</b>4</i>5</p>-><p>1<b>2<i>3</i></b><i>4</i>5</p>. - HTML5 SVG element-name case adjustment (#83, stage 3): an SVG element written in mixed case (which the tokenizer lowercases) is restored to its canonical camel case in the tree, so
<foreignObject>,<linearGradient>,<clipPath>, and the rest carry their correct SVG names. Applies only in the SVG namespace; same-named HTML elements keep their lowercased name. - HTML5 foreign content (#83, stage 2): an
<svg>or<math>element and all of its descendants now carry the SVG (http://www.w3.org/2000/svg) or MathML (http://www.w3.org/1998/Math/MathML) namespace on theirQualifiedName, so foreign elements are distinguishable from same-named HTML; ordinary HTML content (including content after a closed<svg>) stays in no namespace. (SVG/MathML element- and attribute-name case adjustment, and HTML integration points, are a later refinement.) - HTML5 table tree construction (#83, stage 1): a
<table>written without its section and row wrappers now nests correctly. A<tr>directly inside a<table>gets an implied<tbody>, a<td>/<th>gets an implied<tr>(and section), consecutive rows share one implied<tbody>, and an explicit<thead>/<tbody>/<tfoot>is not duplicated, all inside an open table only. - HTML tokenizer correctness (#84, partial): numeric character references are now range/surrogate-validated (zero, surrogates, and values above U+10FFFF yield U+FFFD) with the Windows-1252 C1 fixup table (
€-> euro, etc.); named references are decoded by longest match with or without a trailing semicolon; a literal NUL byte becomes U+FFFD; RCDATA elements (title,textarea) decode character references while raw-text elements (script,style) take their content verbatim; and a single leading newline after<textarea>is stripped. (The full ~2,000-entry named-entity table remains deferred to vendoring from the authoritative WHATWG data, the same policy as the legacy encodings.) - HTML conformance validator in the composable
Validationidiom.PureXML.HTML.validationErrors(in:)(backed byPureXML.Validation.HTML) judges a parsed tree against the intrinsic HTML5 content-model invariants as named, removable, isolation-testableValidationrules, the same idiom as the DTD/XSD/structural validators:voidElementsAreEmpty(void elements carry no content),requiredParent(li/td/option/... appear inside an allowed parent, with the allowed parents named in the finding), anduniqueIdentifiers(idvalues are unique). Failures are locatedValidationErrors; this is the validation layer HTML previously lacked, so every parsing/schema subsystem now exposes its conformance checks as composable rules. - XInclude range inclusion (#87), completing the XInclude audit. An
xi:includewhosexpointerselects a range (xpointer(range(...)),range-to, orstring-range) now includes that range's content: when no node-selecting scheme matches, the processor falls back to the XPointer range model and substitutes each range's spanned nodes (or the matched text for astring-range). - XPointer
range()model (#88), completing the XPath/XPointer audit.PureXML.XPointer.evaluateRanges(_:over:)(andPointer.evaluateRanges(over:)) evaluate the range forms inside anxpointer()/xpath1()scheme:range(expr)(the covering range of each location),start/range-to(end)(one range spanning from the first location ofstartto the first ofend, the sibling run when they share a parent), andstring-range(location, "search"[, offset[, length]])(a character range per occurrence, with the XPointer offset/length adjustment). EachRangematerializes to the wholenodesit spans and thetextit covers, andxmlns()bindings apply to the range parts that follow, so this also unblocks XInclude range inclusion (#87). - C14N node-subset canonicalization and Canonical XML 2.0 parameters (#85), completing the canonicalization audit.
Canonicalizer.canonicalize(_ subtree: TreeNode)canonicalizes a fragment in place: the apex element renders the namespace context in scope from its omitted ancestors and inherits theirxml:base/xml:lang/xml:spacewhen it does not set them, so a signed fragment is independent of where it sat.canonicalize(_:including:)canonicalizes a node-set selected by aTreeNodepredicate (XPath/position-based selection): an excluded element's tags are omitted but its selected descendants are kept and re-declare their in-scope namespaces.Options.prefixRewrite = .sequentialrewrites every prefix to a canonicaln0/n1/... in document order (eliminating the default namespace and placing declarations at first use), andOptions.qnameAwareLabelsextends that rewrite into QName-valued attributes and elements, so documents differing only in prefix spelling canonicalize to identical bytes. - Serialization
textEscapinground-trip option (#91), completing the serialization audit.Options.textEscapingdefaults to.standard(escape only&,<,>);.roundTripalso escapes a carriage return as
in text and CDATA-as-text output, so a literal\rsurvives the parser's end-of-line normalization and the document round-trips byte for byte. (The CDATA-section vs escaped-text toggle,cdataAsText, was already present.) - DOM tree: first-class doctype, entity-reference, and namespace nodes, plus
ownerDocument,adopt, andimportNode(#90), completing the tree-model audit.TreeNodeKindgains.doctype,.entityReference, and.namespace, each with a factory (TreeNode.doctype(name:publicID:systemID:internalSubset:),.entityReference(_:children:),.namespace(prefix:uri:)) and typed accessors; doctype and namespace nodes drop out of the content value-Nodeprojection while an entity reference splices its replacement into place.ownerDocumentderives the owning document from the parent chain;adoptandimportNodemove or copy a subtree into another document, re-declaring every namespace it relied on from an outer scope so the result is self-contained. - Schematron
key(),document(), andcurrent()in tests (#80), completing the Schematron audit. Test, let, and message expressions can now resolve nodes throughxsl:keydeclarations, load an external document for cross-document checks (via an injecteddocumentLoader), and refer to the rule context node withcurrent(). - RELAX NG compact
>>follow-annotations (#79), completing the RNC syntax. A>>annotation after a pattern (a foreign element name with optional[ … ]content) is skipped, chaining and combining with leading[ … ]annotations; both carry no schema semantics. (Namespace declarations inside annotations are already dropped by the annotation skipper.) - XSLT
document()fragments and base-URI resolution (#82), completing the XSLT function/output audit. A#fragmentselects a subset of the loaded document via XPointer, a node-set first argument unions the documents named by each node, and a relative URI is resolved against a caller-suppliedbaseURI(also applied toxsl:include/xsl:import). - XSLT result-tree fragments usable as node-sets (#82). An
xsl:variablewith a body now binds a queryable document fragment instead of a plain string, socount($rtf/*),$rtf/child, andxsl:for-each select="$rtf/..."work; it still has its concatenated text as a string value. - XSLT
xsl:output cdata-section-elements(#82). The text content of the named result elements is emitted in<![CDATA[…]]>sections instead of escaped, leaving nested elements and other elements unaffected. - XSLT
xsl:outputdoctype-public/doctype-system(#82). When either is set, a<!DOCTYPE>for the result's root element (PUBLIC when both are given, SYSTEM otherwise) is emitted before the serialized output, for XML and HTML methods. - XSLT
key()over a node-set second argument (#82).key(name, node-set)(includingkey('k', .)) now unions the matches for every node's string value and de-duplicates, rather than using only the first node's string value. - XSLT
method="html"output (#82). Anxsl:output method="html"result is now serialized by the HTML rules (void elements like<br>emitted without a self-closing slash, raw-text elements like<script>/<style>left unescaped) instead of as XML. - XSLT
xsl:sort case-order(#82). Whencase-order="upper-first"/"lower-first"is set, the sort comparison is case-insensitive with the case used only to break ties among otherwise-equal strings; without it the default codepoint order is kept. - XSLT functions
generate-id,system-property,element-available,function-available, and anunparsed-entity-uristub (#82).generate-idreturns a stable per-node identifier (the context node when no argument is given, the empty string for an empty node-set);system-propertyreports the XSLT version (1.0) and vendor; the*-availablefunctions answer against the set of implemented instructions and functions. - XSLT
xsl:namespace-alias(#81), completing the XSLT top-level-element audit. A literal result element (and its attributes) in a stylesheet namespace bound bystylesheet-prefixis rewritten on output to the namespace and prefix bound byresult-prefix(#defaultsupported), so a stylesheet can emit literalxsl:elements. Folded acrossxsl:include/xsl:import. - XSLT
xsl:decimal-format(#81).format-numbernow honors named and default decimal formats: the decimal separator, grouping separator, percent, minus sign, zero-digit (so non-Latin digit sets render), NaN, and infinity symbols are taken from the chosenxsl:decimal-format, both in the picture and the output. Folded acrossxsl:include/xsl:import. - XSLT
xsl:fallback(#81). An unrecognized XSLT element instantiates itsxsl:fallbackchildren in its place (forwards-compatible processing), or is dropped when it has none; a fallback under a supported instruction is ignored. - XSLT
xsl:apply-imports(#81). Re-applies templates to the current node in the current mode, considering only templates of lower import precedence than the one being instantiated (so an overriding template can extend the imported one), falling back to the built-in rule when none match. - XSLT
xsl:attribute-set(#81). Named attribute sets are applied to a literal result element orxsl:elementviause-attribute-sets; sets may include other sets (recursively, with a cycle guard), set attributes are lowest precedence so the element's own attributes override them, and same-named attributes collapse to the last. Folded acrossxsl:include/xsl:import. - XSLT
xsl:message(#81). The message body is instantiated as diagnostic text;terminate="yes"aborts the transformation with that text as anXSLTError.terminated, and processing stops at that point. A non-terminating message produces no result-tree output. - XSLT
xsl:strip-space/xsl:preserve-space(#81). Whitespace-only text nodes are removed from the source tree for elements named bystrip-spacebefore transformation, withpreserve-spaceand a more specific name test winning over*, andxml:space="preserve"on a source element or ancestor overriding the stripping. Folded acrossxsl:include/xsl:import. - DTD located content-model errors. An element whose children break its content
model now names each stray child individually (
element <x> is not allowed in <r>), and reports a pure order/count violation once with the allowed elements as a hint (… do not match its content model; allowed: <a>, <b>), instead of a single opaque message, matching the editor-grade diagnostics of the other validators. - XSD located content-model errors. A content-model violation is now placed at
the offending child with a recovery hint naming what was expected there
(
element 'x' is not allowed here; expected <b>,content is incomplete; expected <b>), instead of one opaque "content does not match the content model" per element. Anxs:allgroup recovers past each stray child and reports every missing required member, and well-placed children are still validated for their own content, so an editor sees every problem at once. This brings XSD content validation to the same located, recovering standard as the RELAX NG, DTD, and Schematron validators. - RELAX NG located, recovering validation errors (#79). Alongside the boolean
validate(_:),RelaxNG.errors(in:)now reports every way a document fails the schema as individual located errors (each with a coding path), recovering past each failure so an editor can surface all of a faulty document's problems at once rather than only the first. Errors carry recovery hints (expected <a>, <b>) naming the element types accepted at the failure point, and distinguish unexpected elements, invalid or missing attributes, invalid text/datatype content, and missing required content. A datatype mismatch quotes the offending value and names the expected type ('abc' is not a valid integer), and a value mismatch quotes both the actual text and the required literal. An attribute failure distinguishes an undeclared attribute from a declared one with a bad value, quoting the value and naming its type.RelaxNG.validation()exposes the schema as aValidation<Node, Void>, so RELAX NG composes with the same validation framework as the XSD, DTD, and Schematron rules. The boolean engine remains the authority on validity; the walk only places and explains the failures. - XML catalog group-level
prefer(#86). Apreferattribute on agroup(or a nestedcatalog) now overrides the catalog-wide preference for thepublicentries inside it, inherited through nested groups. Resolution of an external identifier consults each public entry's own effective preference, so two entries in one catalog can prefer differently. A system match still always wins. - Schematron abstract patterns (#80). A
<pattern abstract="true">is a template whose rule queries carry$namereferences; a<pattern is-a="…">with<param name= value=>children instantiates it, substituting each parameter into the template'scontext,test,<let>value, and<value-of select=>in a single pass that never rescans a substituted value or lets a short name capture a longer one. The template itself contributes no rules, and$namereferences that are not parameters (ordinary<let>variables) are preserved. - RELAX NG conformance:
datatypeLibrary, value-space<value>, name-class subtraction, and compact-syntax breadth (#79). The in-scopedatatypeLibraryis now inherited from the nearest ancestor; the default library defines onlystringandtoken, the W3C XML Schema library defines the full built-in set, and a type no in-scope library defines is an unknown datatype, so its<data>or<value>matches nothing rather than validating silently. A<value>carries its datatype and compares in that type's value space, so1equals01and1.5equals1.50, booleans treat1/trueand0/falseas equal, andstring/tokencompare by their whitespace-normalized form. A newnsNameExceptname class backs<nsName>with<except>and the compactprefix:* - namesubtraction. The compact parser also skips[ … ]annotations, treatsdiv { … }as transparent grouping, and folds~string concatenation. - XSLT template parameters and modes (toward full parity, #61).
xsl:paramdeclarations (with defaults),xsl:with-paramonapply-templatesandcall-template, andmodeon templates andapply-templatesare now honored: a passed parameter overrides the declared default, and a mode routes nodes only to templates declared in that mode (the built-in rules recurse in the same mode). - Push / feed streaming API (#1), the last epic. A resumable scanner reports a
token only once its terminator is fully buffered and otherwise signals
need-more (the Expat
XML_TOK_PARTIALmodel), soPureXML.Parsing.PushParserparses input fed in arbitrary chunks withfeed(_:)/finish(), driving aSAXHandler, while retaining only the current incomplete token plus the open-element stack.PureXML.events(feeding:)exposes anAsyncThrowingStreamof events over any async sequence of text chunks. Splitting a document at any byte boundary, down to one character per feed, yields identical events. - HTML parser and serializer (#20, the libxml2
HTMLparser.h/HTMLtree.hmodel).PureXML.HTML.parsereads tag-soup HTML leniently (case-insensitive tags; quoted, unquoted, and boolean attributes; comments; doctype; character references) and builds a node tree handling void elements (no end tag), optional end tags (implicit close ofli,p, table rows and cells, and the like), raw-text elements (script,style), and unmatched end tags.PureXML.HTML.serializewrites it back with void elements unclosed and raw-text content unescaped. - XSLT 1.0 transformation engine, completing #3.
PureXML.XSLT.transformapplies a stylesheet to a source document by the XSLT processing model: it matches each node to the highest-priority template (or the built-in rules) and instantiates the template's sequence constructor. Implementsapply-templates(with template dispatch andsort),value-of,for-each,if,choose,element,attribute,copy,copy-of,call-template,variable, literal result elements, and attribute value templates, all over the existing XPath engine. The identity transform round-trips a document including its attributes. - XSLT 1.0 stylesheet model and parser (toward #3).
PureXML.XSLTgains a stylesheet model (template rules with match patterns and computed default priorities, the instruction sequence constructor, attribute value templates) and a parser that compiles anxsl:stylesheetdocument into it. Recognizes the XSLT vocabulary by namespace orxslprefix; other elements are literal result elements. Supportstemplate,apply-templates,value-of,for-each,if,choose,element,attribute,copy,copy-of,call-template,variable,text, andsort. - Schema conformance fixtures, completing XSD and RELAX NG validation (#2). A cross-cutting suite validates realistic schemas (a patterned, bounded, ID-keyed XSD purchase order and a RELAX NG contact card using interleave, attribute value choices, and optional/repeatable elements) against conforming and non-conforming instances, plus datatype boundary fixtures.
- RELAX NG validation by derivatives (toward #2).
PureXML.Schema.RelaxNGparses a RELAX NG schema in the XML syntax into a pattern grammar (element,attribute,text,empty,notAllowed,group,choice,interleave,optional,zeroOrMore,oneOrMore,mixed,list,ref/define,data,value, and the name classes), andvalidate(_:)checks an instance document by James Clark's derivative algorithm: the pattern is derived through each start-tag, attribute, text, and end-tag event, and the document is valid when the residual pattern is nullable. Handles interleave, recursive grammars, and reuses the XSD datatype library fordata/value. - XSD schema-document parser, completing the XSD validation pipeline (toward #2).
PureXML.Schema.Document(xsd)compiles a schema document into its global element declarations and named-type table, andvalidate(_:)checks an instance document end to end. The parser matches the schema vocabulary by local name and supports global and local element declarations, named and inline simple and complex types,sequence/choice/allmodel groups with occurrence, attribute uses, simple content (restriction/extension), the full facet set, and recursive types (via named type references resolved during validation). - XSD complex types and particles (toward #2 schema validation).
PureXML.Schema.ComplexValidatorvalidates an element against aComplexType: its attribute uses (required and typed, with unknown-attribute rejection), and its content model. Content isempty,simpleContent(text validated against a simple type),elementOnly, ormixed. Particles compile to a Thompson NFA over element names honoringminOccurs/maxOccurs, withsequence/choicecompositors;allgroups are validated order-independently by counting. Child elements are validated recursively against their declared types (the XSD Element-Declarations-Consistent rule makes the name-to-type map well defined). - XSD simple-type datatype library (toward #2 schema validation). The new
PureXML.Schemanamespace validates a lexical value against the XSD Part 2 built-in datatypes (string,boolean,decimal, the bounded integer family,float/double,duration, the eight date/time types,hexBinary,base64Binary,anyURI,QName, and theName/NCName/NMTOKEN/languagefamily) and the constraining facets (length/minLength/maxLength,patternvia the regex engine,enumeration,whiteSpace,min/maxInclusive/Exclusive,totalDigits,fractionDigits). Decimals are compared exactly with no floating-point loss, date/time fields are range-validated (leap years,24:00:00, timezone bounds), and the integer types enforce their intrinsic bounds. - Regular-expression engine (#30, the XML Schema regex flavor, gating XSD). The
new
PureXML.Regexnamespace compiles a pattern (literals,., character classes with ranges and negation, the escapes\d \D \w \W \s \S \i \I \c \C, single-character escapes, grouping, alternation, and the quantifiers?,*,+,{n},{n,},{n,m}) to a Thompson NFA and matches the whole string, asxs:patternfacets require. Matching is linear in the input with no backtracking blow-up.\p{...}Unicode category escapes are not yet supported. - Canonical XML (#26, the libxml2
c14n.hmodel). The newPureXML.Canonicalnamespace renders the C14N form of a node: namespace declarations sorted by prefix (default first) and attributes by namespace URI then local name, empty elements expanded to start/end pairs, CDATA folded into escaped text, and the C14N text/attribute escaping rules. Inclusive mode renders every in-scope namespace; exclusive mode renders only the namespaces an element and its attributes visibly use (with anInclusiveNamespacesprefix list). Comments are omitted unless requested. For whole-tree canonicalization C14N 1.0 and 1.1 coincide, so the single inclusive mode covers both. - XInclude, URI resolution, and
xml:base(#24, the libxml2xinclude.h/uri.hmodel). The newPureXML.XIncludenamespace adds RFC 3986 reference resolution (URIReference.resolve(_:against:), with dot-segment removal) andprocess(_:base:loadingURI:), which replacesxi:includeelements with their targets.hrefs resolve against the supplied base and the in-scopexml:base,parse="xml"includes the document element (or anxpointer()fragment viaPureXML.XPointer),parse="text"includes raw text, and a failed load usesxi:fallbackor errors. Includes nest. PureXML does no I/O itself: fetching is only through the injectedloadingURIclosure, so the default fetches nothing. - XML Catalog resolution (#27, the libxml2
catalog.hmodel). The newPureXML.Catalognamespace parses an OASIS XML catalog (matchingpublic,system,uri,rewriteSystem,rewriteURI, andgroup/catalogby local name) into aResolverthat maps public/system identifiers and URI names to replacement URIs, with longest-prefix rewriting and system-over-public precedence.Resolver.entityResolver(loadingURI:)builds aParsing.EntityResolverthat resolves an external identifier through the catalog and then loads the URI via an injected closure, so a catalog plus a loader wires external entities and DTDs in while the default (no loader) keeps XXE closed. - Schematron validation (#25, the libxml2
schematron.hmodel).PureXML.Validation.Schematron(schema:)compiles a Schematron schema (namespace-agnostic over the ISO and legacy namespaces) andvalidate(_:)checks a document: each<rule>'scontextXPath selects the nodes it fires on, and each<assert>/<report>testis evaluated relative to that node, a failed assert becoming an error issue and a matched report a warning. Within a pattern a node fires the first matching rule. Built on the XPath context-node evaluation, with a newXPath.Query.nodes(over:)that returns matched tree nodes for repeated querying of one tree. - XPointer (#23, the libxml2
xpointer.hschemes). The newPureXML.XPointernamespace resolves the shorthand bare-name form (name=id('name')), theelement()scheme (child-element positions from an id or the document root, e.g.element(intro/2),element(/1/2)), and thexpointer()scheme (a full XPath expression). A pointer may chain scheme parts; they are tried in order and the first non-empty selection wins (the fallback rule). Built onPureXML.XPath. - Streaming pattern matching (#22, the libxml2
pattern.hmodel). The newPureXML.Patternnamespace compiles the streamable XPath subset (element names,*,prefix:*,/,//, a leading absolute/, and a trailing attribute step) into aMatcher, rejecting predicates and./..as outside the subset.PureXML.Pattern.matches(_:in:)streams a document through the pull parser and returns the paths of matching nodes (/a/b/c,/a/@id) in document order, deciding each match from the open-element stack alone, so no tree is built.Matcher.matchesElement(path:)reuses a compiled pattern. - The XPath evaluation context, completing full XPath 1.0 (#21).
Query.value(at:position:size:variables:)evaluates an expression against an explicit context node already in a tree, with its proximity position, size, and variable bindings, soposition(),last(), and the upward axes resolve from that node. This is the per-node entry point downstream engines (XSLT, Schematron) drive. - The full XPath 1.0 function library (toward #21 full XPath 1.0). Beyond the
core functions, the string family (
concat,starts-with,contains,substring-before,substring-after,substring,string-length,normalize-space,translate), the number family (sum,floor,ceiling,round), and the node family (local-name,name,namespace-uri,id,lang). All pure Swift with no Foundation. Without a DTD,id()matches by an attribute namedidorxml:id. - The XPath expression engine and four-type model (toward #21 full XPath 1.0).
PureXML.XPath.Querynow compiles and evaluates full expressions, not just location paths: the operator grammar (or,and,=,!=,<,<=,>,>=,+,-,*,div,mod, unary-,|) with correct precedence, number and string literals, function calls, variable references, and filter expressions, all overPureXML.XPath.Value(node-set, boolean, number, string) with the spec's coercion rules and canonical number formatting. Predicates are now arbitrary expressions (a numeric predicate is a position test). Query gainsvalue(over:variables:),number,string, andbooleanaccessors, and a binding map for$variables. The core functionslast,position,count,not,true,false,boolean,number, andstringship with it. - All thirteen XPath axes (toward #21 full XPath 1.0).
PureXML.XPath.Querynow navigateschild,descendant,parent,ancestor,following-sibling,preceding-sibling,following,preceding,attribute,namespace,self,descendant-or-self, andancestor-or-self, with the./../@///abbreviations and theprocessing-instruction()node test. Evaluation runs over the parent-aware tree, so upward and sibling navigation are first-class; results come back de-duplicated and in document order regardless of axis direction. The attribute axis excludesxmlnsdeclarations and the namespace axis surfaces in-scope namespace nodes. - Pull-cursor reader (the libxml2
xmlTextReadermodel).PureXML.Parsing.TextReader(viaPureXML.reader(_:)) walks a document one node at a time withread(), exposingnodeKind,name,value,depth,attributes/attributeCount/attribute(_:), andisEmptyElementat each step. It layers on the streaming core, so it never holds the whole document. A childless element is reported once as an empty element (no separate end node), normalizing<a/>and<a></a>.documentTypesurfaces the DTD read so far as the validation hook. - Mutable, parent-aware document tree (the libxml2
tree.hmodel).PureXML.Model.TreeNodeis a reference type that knows its parent and siblings, so a document can be navigated upward and sideways (parent,nextSibling,previousSibling,ancestors,root,elementChildren,stringValue) and edited in place (append,insert(before:),insert(after:),removeFromParent,replace(with:),copy). Children are held strongly and the parent weakly; attaching a node detaches it from any previous parent, and a node can never become its own ancestor.parseTree(_:)builds one from XML andTreeNode.nodeconverts back to the value tree for serialization. - External and parameter entities (the libxml2
entities.hmodel), secure by default. Internal parameter entities (<!ENTITY % name "value">) are stored and expanded within the DTD, including bare%name;references that inject markup declarations, bounded by depth and the expansion budget. External general entities and the external DTD subset are recorded but never fetched by PureXML itself: they are loaded only through an injectedPureXML.Parsing.EntityResolver(a struct of closures). The default resolver refuses every external reference, so XXE stays closed; an external entity then fails as undefined and the external subset is left unread. Internal declarations win over the external subset. - Serialization option parity for the XML declaration (the libxml2 save-option
model).
PureXML.Emitting.OptionsgainsincludeXMLDeclaration,xmlVersion,encodingName, andstandalone; when requested, both the tree serializer and the incremental writer prepend an<?xml ...?>declaration from a single shared computation.Writer.writeStartDocument()emits it. Defaults stay off so fragment output is unchanged. - Spec-exact XML character classification (
PureXML.Parsing.XMLCharacter, the libxml2chvalid.hparity): the XML 1.0 (Fifth Edition)Char,NameStartChar, andNameCharproductions as public predicates over Unicode scalars, plusisValidName(_:). The scanner's name recognition now uses these exact ranges (replacing a looseisLettercheck) from a single source of truth, and handles both precomposed and combining-mark names. - Full encoding support. Byte input now decodes UTF-32 (both byte orders, BOM or
sniffed) and the single-byte encodings ISO-8859-1 and Windows-1252 selected by
the XML declaration's
encodingname, in addition to UTF-8 and UTF-16. The streaming byte path also decodes UTF-32. Standard library only, no Foundation. - Incremental XML writer (
PureXML.Emitting.Writer, the libxml2xmlTextWritermodel): emit a document with start/end and write calls without building a tree. Escaping matches the tree serializer (extracted into a sharedEscapinghelper), so compact output is byte-identical to serializing the equivalent tree. - SAX2 push-callback parsing (the libxml2 SAX2 model).
PureXML.Parsing.SAXHandleris a struct of optional callbacks (start/end document, start/end element, characters, CDATA, comment, processing instruction);PureXML.parse(_:sax:)drives them from the streaming core. Element names are namespace-resolved, so the URI is available on each callback. - DTD ID/IDREF validation. Attributes typed
IDmust be unique across the document;IDREFandIDREFSvalues must each resolve to a declaredID(forward references included, via a second pass). Completes the structural DTD validation layer alongside content models and attribute rules. - DTD attribute validation (
<!ATTLIST>). The parser surfaces attribute declarations alongside element models, andDTDSchemanow checks#REQUIREDattributes are present,#FIXEDattributes match when present, and enumerated attributes take a declared value. Tokenized types (ID/IDREF/NMTOKEN) are parsed but their value rules (uniqueness, references) are not yet enforced. - DTD content-model validation. The parser now surfaces the
<!DOCTYPE>internal subset's<!ELEMENT>declarations (PureXML.Parsing.DocumentType, viaParser.parseWithDocumentType), andPureXML.Validation.DTDSchemavalidates a tree against them:EMPTY,ANY,(#PCDATA), mixed content, and element content models (sequence/choice with?/*/+, matched as a regular language over child element names).PureXML.validateAgainstInternalDTD(_:)parses and validates in one call.<!ATTLIST>validation is not yet covered. - XPath query support (a practical subset).
PureXML.XPath.Querycompiles a location path and evaluates it over a node;PureXML.xpath(_:over:)is the one-shot convenience. Supports the forward axes (child, descendant//, self., attribute@), the node tests (name,*,text(),node(),comment()), and the predicates[n],[@a],[@a='v'],[child],[child='v']. Returns aSelectionnode-set withelementandstringValueaccessors. Upward/sibling axes and the full expression language are out of scope (the model has no parent pointers). - Optional DTD with internal general entities, off by default. With
Limits(allowDoctype: true)the parser reads a<!DOCTYPE>internal subset, honors internal<!ENTITY name "value">declarations, and expands them in text and attribute values. Defenses are mandatory: expansion is bounded byLimits.maxEntityExpansion(billion-laughs amplification raisesParseError.amplificationLimitExceeded), recursive entities raiseParseError.recursiveEntity, undeclared entities raiseParseError.undefinedEntity, and external entities are never loaded (XXE stays closed). The default still rejects<!DOCTYPE>outright. - Streaming byte decode.
PureXML.parse(pullingBytes:)andevents(pullingBytes:)accept an incremental byte source (() -> UInt8?) and decode UTF-8 or UTF-16 on the fly, so the bytes are never fully buffered. Encoding is detected from the leading bytes; invalid sequences become the Unicode replacement character rather than failing the stream. - Namespace resolution. Qualified names now carry an optional resolved
namespaceURI, populated by the parser from in-scopexmlnsdeclarations at the start-element boundary (the libxml2 SAX2 model). A default namespace applies to unprefixed element names but not attributes, thexmlprefix is built in,xmlnsdeclarations are preserved as attributes for round-trip, and an unbound prefix raisesParseError.undefinedNamespacePrefix. Namespace-free documents are unchanged (URIs stay nil). - Byte input with encoding detection.
PureXML.parse(bytes:)andPureXML.events(bytes:)accept raw[UInt8]and detect the encoding (UTF-8 or UTF-16, with or without a byte-order mark) following the XML sniff order, then decode with the Swift standard library (no Foundation).PureXML.Parsing. InputEncoding.detect(_:)exposes the detector. Malformed input (such as an odd-length UTF-16 stream) raisesParseError.malformedEncoding. - Configurable, bounded-by-default parser limits (
PureXML.Parsing.Limits): maximum nesting depth (default 256), name length, and content length, enforced during scanning. Protects against pathological input and keeps the recursive node model safe to build and tear down. Threaded throughparse/events. - Initial package scaffold: root Swift package, namespacing under
PureXML, and the portability and verification gates carried over from PureYAML. - XML node model (
PureXML.Model):Node,Element,Attribute, andQualifiedName, preserving document order and node kinds. - XML emitter (
PureXML.Emitting.Serializer) with pretty-printed and compact output and correct text and attribute escaping. - Structural validation (
PureXML.Validation.Validator) for duplicate attribute names. - Streaming, iterative XML parser.
PureXML.Parsing.EventReaderis a pull-based event core that consumes input through a character-source closure and emits oneEventat a time, holding only bounded state (an open-element stack and a small lookahead buffer). It never requires the whole document in memory and can drive chunked input.PureXML.Parsing.Parserbuilds aModel.Nodetree iteratively over the event core (no recursion). Handles elements, attributes, text, the five predefined entities and numeric character references, comments, CDATA, and processing instructions. Safe by default:<!DOCTYPE>is rejected (no DTD/XXE). - Top-level API:
PureXML.parse(_:),PureXML.parse(pulling:), andPureXML.events(_:)/PureXML.events(pulling:)for streaming.
Changed
- The emitter is now iterative (explicit work stack, not recursion), mirroring libxml2's approach, so deep trees do not overflow the call stack.
- The emitter escapes tab/newline/CR (and
>) in attribute values so attributes round-trip through spec-compliant parsers; mixed content is no longer reformatted (formatting is suppressed for elements with text/CDATA children).