feat(html): SVG HTML integration points switch back to HTML namespace (#83)

This commit is contained in:
Mihaela Mihaljevic 2026-06-09 06:06:54 +02:00
parent 6dab33a89f
commit 2945de0855
4 changed files with 29 additions and 4 deletions

View File

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- 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 the `div` (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`/`tr` is 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>`.

View File

@ -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 **738 tests in 115 suites** (`swift test`).
currently runs **740 tests in 115 suites** (`swift test`).
### Shipped (libxml2-aligned surface)

View File

@ -250,13 +250,23 @@ final class HTMLDocument {
}
}
/// The foreign-content namespace for a body element: SVG/MathML on entry, or
/// the nearest open foreign ancestor's namespace inside one.
/// The foreign-content namespace for a body element: SVG/MathML on entry, the
/// nearest open foreign ancestor's namespace inside one, or HTML (nil) when
/// that ancestor is an HTML integration point (`foreignObject`/`desc`/`title`),
/// whose content is parsed as ordinary HTML.
private func bodyForeignNamespace(for name: String) -> String? {
if name == "svg" { return ForeignNamespace.svg }
if name == "math" { return ForeignNamespace.mathml }
return openBody.reversed().compactMap { $0.name?.namespaceURI }.first
for node in openBody.reversed() {
guard let namespace = node.name?.namespaceURI else { continue }
if Self.svgIntegrationPoints.contains(tagName(node)) { return nil }
return namespace
}
return nil
}
/// The SVG elements that are HTML integration points: their content is HTML.
private static let svgIntegrationPoints: Set<String> = ["foreignobject", "desc", "title"]
}
/// The head's open-element stack still uses the pop-on-close `DocFrame` model (it

View File

@ -79,4 +79,18 @@ struct HTMLForeignContentTests {
let element = find("p", in: "<p viewbox=\"x\"></p>")
#expect(element?.attributes.contains { $0.name.description == "viewbox" } == true)
}
@Test("HTML content inside an SVG integration point is back in the HTML namespace")
func test_integrationPoint() {
let html = "<svg><foreignObject><div><span>x</span></div></foreignObject></svg>"
#expect(find("foreignObject", in: html)?.name.namespaceURI == svg)
#expect(find("div", in: html)?.name.namespaceURI == nil)
#expect(find("span", in: html)?.name.namespaceURI == nil)
}
@Test("Re-entering svg inside an integration point switches back to SVG")
func test_reentrantForeign() {
let html = "<svg><foreignObject><svg><rect></rect></svg></foreignObject></svg>"
#expect(find("rect", in: html)?.name.namespaceURI == svg)
}
}