diff --git a/CHANGELOG.md b/CHANGELOG.md index 6585525..eb65d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A `PureXML.Validation.ConformanceCase` is a `Validatable` named expectation (actual vs spec-authoritative expected output), and `Conformance.matchesExpected` is a `Validation` rule that emits one located `ValidationError` per 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), and an XSLT 1.0 transformation corpus (value-of, for-each, if, choose, sort, variable, format-number, apply-templates, call-template). Growing the corpora against the official W3C/OASIS suites is the ongoing Tier-2 work. +- Conformance corpus harness (#105, Tier 2): the start of conformance-suite checking, expressed through the validation framework (the OpenAPIKit idiom). A `PureXML.Validation.ConformanceCase` is a `Validatable` named expectation (actual vs spec-authoritative expected output), and `Conformance.matchesExpected` is a `Validation` rule that emits one located `ValidationError` per 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), and an HTML5 tree-construction corpus (void elements, implied tag closing, case normalization, the adoption agency, entities). 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`, and `sets` EXSLT 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:power` and the other transcendentals are omitted because they need a C math library, which the pure-Swift, Foundation-free target excludes; `exsl:node-set` awaits result-tree-fragment support. - Canonical XML 1.1 (#105, Tier 3): a `Canonical.Options.canonical11` preset and a `mergeInheritedBase` option. When canonicalizing a document subset, the `xml:base` values 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 relative `xml:base` is resolved against that chain, and `xml:id` is no longer inherited; `xml:lang` and `xml:space` still 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 `encoding` name 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 authoritative `unicode.org` mapping files; ISO-8859-5/9/15 are derived exactly; Windows-1254 composes from Windows-1252. Multi-byte CJK encodings (#99) are tracked separately. diff --git a/README.md b/README.md index 7b6b54f..3d7c51a 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ flowchart TB PureXML is a working, dependency-free XML library today: parse, emit, validate, query, and stream documents on macOS, Linux, Windows, and WASM. The test suite -currently runs **813 tests in 125 suites** (`swift test`). +currently runs **814 tests in 125 suites** (`swift test`). ### Shipped (libxml2-aligned surface) diff --git a/Tests/ConformanceCorpusTests.swift b/Tests/ConformanceCorpusTests.swift index e943a17..3ec413b 100644 --- a/Tests/ConformanceCorpusTests.swift +++ b/Tests/ConformanceCorpusTests.swift @@ -200,6 +200,38 @@ struct ConformanceCorpusTests { } } + /// The serialized body content of a full HTML5 document parse, the spec's + /// complete tree-construction path (the fragment `HTML.parse` does not yet + /// reconstruct active formatting elements; see #109). + private func htmlBody(_ html: String) -> String { + let full = PureXML.HTML.serialize(PureXML.HTML.parseDocument(html)) + let wrapper = "" + guard full.hasPrefix(wrapper), full.hasSuffix("") else { return full } + return String(full.dropFirst(wrapper.count).dropLast("".count)) + } + + /// HTML5 tree-construction conformance: the parsed-then-serialized document + /// body, against the normalized form the HTML5 parsing algorithm prescribes. + private func htmlCorpus() -> [PureXML.Validation.ConformanceCase] { + let specs = [ + Spec(name: "void-elements", input: "

a
bc

", expected: "

a
bc

"), + Spec(name: "implied-li-close", input: "", expected: ""), + Spec(name: "implied-p-close", input: "

first

second", expected: "

first

second

"), + Spec(name: "tag-case-normalized", input: "
x
", expected: "
x
"), + Spec(name: "adoption-agency", input: "", expected: ""), + Spec(name: "adoption-agency-with-text", input: "X", expected: "X"), + Spec(name: "entities-decoded", input: "

a & b < c A

", expected: "

a & b < c A

"), + Spec(name: "stray-end-tag-ignored", input: "bold", expected: "bold"), + ] + return specs.map { spec in + PureXML.Validation.ConformanceCase( + name: spec.name, + actual: htmlBody(spec.input), + expected: spec.expected, + ) + } + } + @Test("The C14N conformance corpus passes with no located failures") func test_corpusConforms() throws { let failures = try PureXML.Validation.Conformance.failures(in: canonicalCorpus()) @@ -230,6 +262,12 @@ struct ConformanceCorpusTests { #expect(failures.isEmpty, "\(failures.map(\.reason))") } + @Test("The HTML5 tree-construction conformance corpus passes with no located failures") + func test_htmlCorpusConforms() { + let failures = PureXML.Validation.Conformance.failures(in: htmlCorpus()) + #expect(failures.isEmpty, "\(failures.map(\.reason))") + } + @Test("A divergent case is reported as a located conformance failure") func test_divergenceReported() { let bad = PureXML.Validation.ConformanceCase(name: "wrong", actual: "", expected: "")