Commit Graph

171 Commits

Author SHA1 Message Date
83507939de feat(validation): conformance-corpus harness via the validation framework
Begin Tier-2 conformance checking, expressed through the OpenAPIKit validation
idiom rather than an ad-hoc loop. ConformanceCase is a Validatable named
expectation; Conformance.matchesExpected is a Validation<ConformanceCase, Void>
that emits one located ValidationError per diverging case, so a 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, PIs, namespace rendering) and a test proving a
divergence is reported as a located failure. Growing the corpora against the
official suites is the ongoing Tier-2 work.

Part of #105 (Tier 2)
2026-06-09 19:08:17 +02:00
963c7c74eb fix(xsd): locate identity-constraint errors at the offending field
Extend an identity error's coding path past the target element to the single
field the constraint names: an attribute (list/item[2]/@id) or a child element
(.../code), resolved from the field XPath. Multi-field or complex-path
constraints stay on the target. Also add a cross-scope keyref test confirming
a keyref resolves against a key declared on an ancestor element. Updates the
identity-path tests to the more precise locations.

Closes #104
2026-06-09 18:58:27 +02:00
69678d31a1 feat(xpath): EXSLT common, math, and sets extension functions
Add the EXSLT common/math/sets modules, dispatched by resolving a function's
prefix to its namespace (FunctionTable.call falls back to namespace lookup),
so any prefix bound to an EXSLT namespace works. math:min/max/highest/lowest/
abs/sqrt (with the NaN rule), set:distinct/difference/intersection/
has-same-node/leading/trailing (by node identity and document order), and
exsl:object-type. math:power and the transcendentals are omitted (they need a
C math library the Foundation-free target excludes); exsl:node-set awaits
result-tree-fragment support.

Part of #105 (Tier 3)
2026-06-09 18:31:50 +02:00
93f31d7758 feat(c14n): Canonical XML 1.1 (xml:base merging, no xml:id inheritance)
Add a canonical11 preset / mergeInheritedBase option. On a document subset,
the omitted-ancestor xml:base chain is merged into the apex by RFC 3986
reference resolution (vs 1.0 nearest-wins), the apex's own relative xml:base
is resolved against it, and xml:id is no longer inherited; xml:lang/xml:space
still inherit nearest. A self-contained RFC 3986 resolver (parse, path merge,
dot-segment removal) backs the merge, with tests covering the 1.1-vs-1.0
difference, dot segments, xml:id, and lang/space.

Part of #105 (Tier 3)
2026-06-09 17:59:35 +02:00
56e889f790 feat(parsing): EUC-TW (CNS 11643) decoding, closing #108
Add EUC-TW for the whole-buffer and streaming decoders: ASCII, a two-byte
plane-1 pair, and a 0x8E four-byte plane selector plus a pair. CNS planes 1
and 2, which carry essentially all real-world EUC-TW text, are vendored from
the ICU euc-tw-2014 mapping (private-use fallbacks dropped); planes 3-15 are a
documented follow-up. Tested for plane 1 (two-byte and four-byte forms), plane
2, and streaming. The streaming method moved to its own file to stay under the
type-body cap.

Closes #108
2026-06-09 17:44:22 +02:00
191d2245de feat(parsing): streaming honors the declared encoding; streaming ISO-2022-JP
The streaming decoder previously selected an encoding only from a byte-order
mark or the UTF sniff, so its single-byte and CJK decode paths were latent. It
now reads the encoding from the XML declaration (the libxml2 behavior) when
there is no BOM, activating those paths over an incremental byte source. Add
the streaming form of ISO-2022-JP, carrying the stateful mode across reads
(extracted to its own file to stay under the type-body cap). A test asserts
streaming agrees with whole-buffer decode for a single-byte (ISO-8859-1), a
CJK (Shift_JIS), and an ISO-2022-JP document.

Part of #108, #105
2026-06-09 17:33:52 +02:00
a7f5407552 feat(validation): streaming XSD validation, completing #107
Add Schema.Document.validate(streaming:) and ComplexValidator.validateShallow:
resolve each element's type from its parent's content model (and xsi:type /
typeReference) as it opens, then check its attributes and content-model
structure shallowly as it closes, against a shallow element synthesized from
the streamed child names. A child's deeper validity is checked when it closes,
so only the open-element stack is held. A differential test asserts streaming
agrees with the tree validator across valid and invalid documents (missing
attributes, bad simple values, missing/extra/misordered children). Wildcard
processContents and substitution-group resolution in the streaming form stay
on the tree validator. The streaming methods moved to ComplexValidatorStreaming
to keep ComplexValidator under the file/type-body caps.

Closes #107
2026-06-09 16:52:17 +02:00
1e815d6309 feat(parsing): ISO-2022-JP decoding
Add the WHATWG iso-2022-jp decoder, the first stateful encoding: escape
sequences switch between ASCII, JIS X 0201 Roman (yen, overline), JIS X 0201
katakana, and the JIS X 0208 plane, resolved through the shared jis0208 table
(no new data). Tested across all four modes, with the JIS X 0208 kanji
cross-checked against the EUC-JP decoder (same table, same pointer). EUC-TW and
the streaming form of ISO-2022-JP remain.

Part of #108, #105
2026-06-09 10:55:34 +02:00
051ef02fdb feat(validation): streaming RELAX NG validation
Add RelaxNG.validate(streaming:), driving the Brzozowski derivative directly
from the parse-event stream (the libxml2 xmlTextReader model). The derivative
is already incremental: the after-threaded pattern chain is the validation
stack, so only the residual pattern and a light per-element frame (for the
childrenDeriv ignorable-whitespace rule) are retained, never the tree. A
differential test asserts streaming agrees with the trusted tree engine across
valid, invalid, whitespace, and mixed-content documents. XSD streaming remains.

Part of #107, #105
2026-06-09 10:35:42 +02:00
f9f9985aac feat(validation): streaming DTD ID/IDREF integrity
Complete the DTD streaming validator with document-scoped ID/IDREF checks,
held 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 via finish(). The streaming-vs-tree agreement test now covers
a duplicate ID as well. DTD streaming validation (content, attributes,
ID/IDREF) is now complete; RELAX NG and XSD streaming remain.

Part of #107, #105
2026-06-09 10:21:20 +02:00
b5fa216a7c feat(validation): streaming DTD validation (reader-driven)
Add PureXML.Validation.StreamingDTDValidator and PureXML.validate(streaming:
dtd:), validating a document against a DTD while it is pulled event by event
(the libxml2 xmlTextReader model), holding only the open-element stack rather
than the whole tree. Each element's content model and attributes are checked
as it closes by applying the exact per-element DTD rules to a shallow element
synthesized from the streamed child names, so streaming and tree validation
agree (a test asserts matching reasons). Errors carry an xmlGetNodePath-style
coding path. RELAX NG/XSD streaming and ID/IDREF streaming remain on #107.

Part of #107, #105
2026-06-09 10:06:55 +02:00
b98c2ff41f feat(emitting): CJK output encodings, closing output-encoding parity
Extend ByteEncoder to the multi-byte CJK encodings (Shift-JIS, EUC-JP,
EUC-KR, GBK, GB18030, Big5). Each inverse is built by running the existing,
tested whole-buffer decoder over every valid byte sequence and recording
the shortest sequence per scalar, so the encoder is by construction the
exact inverse of the decoder, with no new data or separate encode tables.
GB18030's astral range uses the four-byte pointer formula, so every scalar
encodes. Round-trip tests cover Japanese, Korean, Chinese, and a GB18030
astral character.

Closes #106
2026-06-09 09:41:28 +02:00
e62995a7a8 feat(emitting): output encoding for UTF and single-byte families
Add PureXML.Parsing.ByteEncoder, the inverse of ByteDecoder, and a
Serializer.serialize(_:encoding:) / PureXML.serialize(_:encoding:) that
emits bytes in a chosen encoding (the libxml2 save-with-encoding model).
Covers the Unicode transformation formats and the single-byte legacy
families, built by inverting the existing vendored to-Unicode tables, so
no new data is introduced. The declaration carries the canonical encoding
name, a UTF-16/32 stream is preceded by a byte-order mark, and any
unrepresentable scalar becomes a decimal numeric character reference, so
output round-trips. CJK output encodings remain (tracked on #106).

Part of #106, #105
2026-06-09 09:32:39 +02:00
36a40eec08 fix(xsd): locate identity-constraint errors at the offending node
Refine the identity-constraint path: rather than locating at the element
that declares the key, walk from the constraint-bearing element down to the
selected target and report the actual offender, so a duplicate key renders
"at path: list/item[2]" and a dangling keyref "at path: orders/line".
Also locate the "no element declaration for the root" XSD error at the root
element, matching the namespace check beside it that already did so. Lock
both with coding-path assertions in the identity tests.
2026-06-09 08:57:33 +02:00
9730903803 fix(xsd): locate identity-constraint errors at their element
The IdentityValidator emitted every key/keyref/unique error with an empty
coding path (at: []), so each rendered "at root of document" regardless of
which element violated the constraint. Thread a [PathKey] through the walk,
the way the content-model validator already does, so the failures carry the
constraining element's path. This restores the validation framework's
path-carrying contract for the one rule that was bypassing it.
2026-06-09 08:50:39 +02:00
74acad4628 feat(parsing): Big5 (Traditional Chinese / HKSCS)
Add the WHATWG big5 decoder over the vendored index-big5 (18,590 entries),
for both the whole-buffer and streaming decoders. A lead in 0x81-0xFE plus
a trail in 0x40-0x7E or 0xA1-0xFE resolves through the index, covering
Traditional Chinese and the HKSCS extensions including the astral CJK
ideographs. Four pointers decode to a base letter plus a combining mark.
Selected by the big5 / big5-hkscs declaration names.

Closes #103
2026-06-09 08:30:57 +02:00
2b2c1fb692 feat(parsing): full GB18030 four-byte sequences
Extend the GBK two-byte subset to full GB18030: a lead byte followed by
a digit opens a four-byte form whose pointer is mapped through the WHATWG
index-gb18030-ranges linear ranges, reaching the supplementary planes.
The 207 ranges are vendored verbatim. Wired into the whole-buffer and
streaming decoders and the encoding-name table (gb18030/gb-18030).

Closes #102
2026-06-09 08:24:00 +02:00
b688952cef feat(parsing): GBK/GB2312 decoding completes the CJK encodings (#99) 2026-06-09 08:08:52 +02:00
7f2991a90b Merge #99 (stage 3): EUC-KR 2026-06-09 08:03:02 +02:00
d8b2db4943 feat(parsing): EUC-KR (CP949) decoding with vendored index (#99) 2026-06-09 08:02:14 +02:00
5b5ee7b3fe feat(parsing): EUC-JP decoding with vendored JIS X 0212 index (#99) 2026-06-09 07:57:52 +02:00
4ff9306a0e feat(parsing): Shift-JIS decoding with vendored JIS X 0208 index (#99) 2026-06-09 07:54:05 +02:00
a07537007e feat(html): vendor the complete WHATWG named entity set (#84) 2026-06-09 07:45:55 +02:00
7eb33c0d08 feat(html): full HTML 4.01 named entity set (#84) 2026-06-09 07:39:32 +02:00
3f45dacf62 feat(parsing): Windows-1255/1256/1258, KOI8-U complete the single-byte set (#97) 2026-06-09 07:29:25 +02:00
5646e81b94 feat(parsing): Windows-1250/1251/1253/1257, KOI8-R, ISO-8859-11 (#97) 2026-06-09 07:25:18 +02:00
3f293ec1e7 feat(parsing): ISO-8859-6/8/10/14/16 vendored from Unicode mappings (#97) 2026-06-09 07:19:50 +02:00
43dc7b00c4 feat(parsing): ISO-8859-3/4/7/13 vendored from Unicode mappings (#97) 2026-06-09 07:15:35 +02:00
5f35621ae6 feat(parsing): ISO-8859-2 (Latin-2) vendored from Unicode mapping (#97) 2026-06-09 07:08:19 +02:00
a005def333 feat(parsing): Windows-1254 (Turkish) encoding (#97) 2026-06-09 07:02:13 +02:00
94890172f2 feat(parsing): single-byte encoding framework + ISO-8859-5/9/15 (#97) 2026-06-09 06:57:21 +02:00
a5b032e9dc feat(html): in-select-in-table closes select on table tags (#83) 2026-06-09 06:37:35 +02:00
3ebbaff07c feat(html): select content model (optgroup/nested-select/input close) (#83) 2026-06-09 06:32:24 +02:00
7add3b3fa5 refactor(html): build head on the live TreeNode model, remove DocFrame 2026-06-09 06:29:00 +02:00
b245640ee2 fix(html): keep template content nested instead of splitting to body (#83) 2026-06-09 06:21:10 +02:00
f637b8e154 feat(html): frameset documents and void frame element (#83) 2026-06-09 06:17:55 +02:00
2945de0855 feat(html): SVG HTML integration points switch back to HTML namespace (#83) 2026-06-09 06:06:54 +02:00
47e0e3e644 feat(html): table foster-parenting for stray content (#83) 2026-06-09 06:04:01 +02:00
038774cfc2 feat(html): SVG attribute-name case adjustment in foreign content (#83) 2026-06-09 05:52:26 +02:00
9092ab3dbd feat(html): adoption agency algorithm and active formatting elements (#83) 2026-06-09 05:46:12 +02:00
fccb407797 refactor(html): build body as a live mutable TreeNode open-elements stack (#83) 2026-06-09 05:35:26 +02:00
40c5973d5a feat(html): SVG element-name case adjustment in foreign content (#83) 2026-06-09 05:29:14 +02:00
d64bcaf2fa feat(html): SVG/MathML foreign-content namespace switching (#83) 2026-06-09 05:22:37 +02:00
b7f7e15fbc feat(html): table tree construction with implied tbody/tr (#83) 2026-06-09 05:18:13 +02:00
e86c87af3c feat(html): numeric-ref validation, NUL, RCDATA decoding, semicolon-less refs (#84) 2026-06-09 05:11:18 +02:00
3a55eeeaa6 feat(html): conformance validator as composable Validation rules 2026-06-09 04:39:03 +02:00
506c502602 refactor(validation): decompose DTD attribute checks into per-constraint rules (#101) 2026-06-09 04:28:03 +02:00
ecb9b8c881 feat(xinclude): range inclusion via XPointer range model (#87) 2026-06-09 04:19:38 +02:00
8871fee977 feat(xpointer): range(), range-to(), and string-range() schemes (#88) 2026-06-09 04:16:23 +02:00
b3bbacb4e8 feat(canonical): C14N 2.0 sequential prefix rewrite, QName-aware values, node-set selection (#85) 2026-06-09 04:06:02 +02:00