feat(html): table tree construction with implied tbody/tr (#83)
This commit is contained in:
parent
e978da9b41
commit
b7f7e15fbc
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- 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 `Validation` idiom. `PureXML.HTML.validationErrors(in:)` (backed by `PureXML.Validation.HTML`) judges a parsed tree against the intrinsic HTML5 content-model invariants as named, removable, isolation-testable `Validation` rules, 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), and `uniqueIdentifiers` (`id` values are unique). Failures are located `ValidationError`s; 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:include` whose `xpointer` selects a range (`xpointer(range(...))`, `range-to`, or `string-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 a `string-range`).
|
||||
|
||||
@ -89,7 +89,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 **717 tests in 113 suites** (`swift test`).
|
||||
currently runs **721 tests in 113 suites** (`swift test`).
|
||||
|
||||
### Shipped (libxml2-aligned surface)
|
||||
|
||||
|
||||
@ -172,6 +172,7 @@ private final class HTMLDocument {
|
||||
pop(&stack, &roots)
|
||||
}
|
||||
}
|
||||
ensureTableContext(for: name, &stack, &roots)
|
||||
let modeled = modelAttributes(attributes)
|
||||
if PureXML.HTML.Elements.void.contains(name) || selfClosing {
|
||||
attach(.element(PureXML.Model.Element(name: .init(name), attributes: modeled)), &stack, &roots)
|
||||
@ -180,6 +181,32 @@ private final class HTMLDocument {
|
||||
}
|
||||
}
|
||||
|
||||
/// HTML table tree construction: a `<tr>` inside a bare `<table>` gets an
|
||||
/// implied `<tbody>`, and a `<td>`/`<th>` gets an implied `<tr>` (and section),
|
||||
/// so a table written without its section and row wrappers still nests
|
||||
/// correctly. Only fires inside an open table.
|
||||
private func ensureTableContext(for name: String, _ stack: inout [DocFrame], _: inout [Node]) {
|
||||
guard let context = nearestTableContext(stack) else { return }
|
||||
switch name {
|
||||
case "tr" where context == "table":
|
||||
stack.append(DocFrame(name: "tbody", attributes: []))
|
||||
case "td", "th":
|
||||
if context == "table" { stack.append(DocFrame(name: "tbody", attributes: [])) }
|
||||
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) {
|
||||
stack.append(DocFrame(name: "tr", attributes: []))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// The nearest open table-structural element (`table`/`tbody`/`thead`/`tfoot`/
|
||||
/// `tr`), or nil when no table is open.
|
||||
private func nearestTableContext(_ stack: [DocFrame]) -> String? {
|
||||
let structural: Set = ["table", "tbody", "thead", "tfoot", "tr"]
|
||||
return stack.reversed().first { structural.contains($0.name) }?.name
|
||||
}
|
||||
|
||||
private func close(_ name: String, _ stack: inout [DocFrame], _ roots: inout [Node]) {
|
||||
guard let index = stack.lastIndex(where: { $0.name == name }) else { return }
|
||||
while stack.count > index {
|
||||
|
||||
@ -40,4 +40,31 @@ struct HTMLDocumentTests {
|
||||
func test_htmlAttributes() {
|
||||
#expect(document("<html lang=\"en\"><p>x</p></html>") == "<html lang=\"en\"><head></head><body><p>x</p></body></html>")
|
||||
}
|
||||
|
||||
// MARK: Table tree construction
|
||||
|
||||
/// Wraps a body fragment in the implied document structure for comparison.
|
||||
private func bodyDocument(_ inner: String) -> String {
|
||||
"<html><head></head><body>\(inner)</body></html>"
|
||||
}
|
||||
|
||||
@Test("A bare table row gets an implied tbody")
|
||||
func test_impliedTbody() {
|
||||
#expect(document("<table><tr><td>x</td></tr></table>") == bodyDocument("<table><tbody><tr><td>x</td></tr></tbody></table>"))
|
||||
}
|
||||
|
||||
@Test("A bare cell gets an implied tbody and row")
|
||||
func test_impliedRowAndSection() {
|
||||
#expect(document("<table><td>x</td></table>") == bodyDocument("<table><tbody><tr><td>x</td></tr></tbody></table>"))
|
||||
}
|
||||
|
||||
@Test("An explicit section is not duplicated")
|
||||
func test_explicitSection() {
|
||||
#expect(document("<table><thead><tr><th>h</th></tr></thead></table>") == bodyDocument("<table><thead><tr><th>h</th></tr></thead></table>"))
|
||||
}
|
||||
|
||||
@Test("Consecutive rows share one implied tbody")
|
||||
func test_consecutiveRows() {
|
||||
#expect(document("<table><tr><td>a</td><tr><td>b</td></table>") == bodyDocument("<table><tbody><tr><td>a</td></tr><tr><td>b</td></tr></tbody></table>"))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user