test(conformance): add an HTML5 tree-construction corpus
Grow the Tier-2 conformance corpus with 8 HTML5 cases (void elements, implied li/p closing, tag-case normalization, the adoption agency with and without trailing text, entity decoding, stray-end-tag) checked through the validation-driven harness against the HTML5 parsing algorithm. The corpus surfaced its first real divergence: HTML.parse (fragment) does not reconstruct active formatting elements the way HTML.parseDocument (document, spec-correct) does, so <b><i></b>X</i> yields <b><i></i></b>X instead of <b><i></i></b><i>X</i>. The corpus uses the document path (spec-correct) and the divergence is filed as #109.
This commit is contained in:
parent
821cc2cf88
commit
80af0067e9
@ -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<ConformanceCase, Void>` 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<ConformanceCase, Void>` 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.
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 = "<html><head></head><body>"
|
||||
guard full.hasPrefix(wrapper), full.hasSuffix("</body></html>") else { return full }
|
||||
return String(full.dropFirst(wrapper.count).dropLast("</body></html>".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: "<p>a<br>b<img src=\"x\">c</p>", expected: "<p>a<br>b<img src=\"x\">c</p>"),
|
||||
Spec(name: "implied-li-close", input: "<ul><li>one<li>two</ul>", expected: "<ul><li>one</li><li>two</li></ul>"),
|
||||
Spec(name: "implied-p-close", input: "<p>first<p>second", expected: "<p>first</p><p>second</p>"),
|
||||
Spec(name: "tag-case-normalized", input: "<DIV><SPAN>x</SPAN></DIV>", expected: "<div><span>x</span></div>"),
|
||||
Spec(name: "adoption-agency", input: "<b><i></b></i>", expected: "<b><i></i></b>"),
|
||||
Spec(name: "adoption-agency-with-text", input: "<b><i></b>X</i>", expected: "<b><i></i></b><i>X</i>"),
|
||||
Spec(name: "entities-decoded", input: "<p>a & b < c A</p>", expected: "<p>a & b < c A</p>"),
|
||||
Spec(name: "stray-end-tag-ignored", input: "<b>bold</i></b>", expected: "<b>bold</b>"),
|
||||
]
|
||||
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: "<a></a>", expected: "<b></b>")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user