diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b153f..57d65b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `
` puts the `div` (and its descendants) back in HTML; re-entering `` 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 (`x
` -> `x
`), 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 (`` -> ``), content after an out-of-order close is re-wrapped by reconstructing the active formatting elements (`X` -> `X`), and a block inside a formatting element reparents through the furthest-block path, including the canonical `

12345

` -> `

12345

`. diff --git a/README.md b/README.md index 1da9c24..30bd579 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/Sources/HTML/HTMLDocumentBuilder.swift b/Sources/HTML/HTMLDocumentBuilder.swift index 9257ecf..5e8db4b 100644 --- a/Sources/HTML/HTMLDocumentBuilder.swift +++ b/Sources/HTML/HTMLDocumentBuilder.swift @@ -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 = ["foreignobject", "desc", "title"] } /// The head's open-element stack still uses the pop-on-close `DocFrame` model (it diff --git a/Tests/HTMLForeignContentTests.swift b/Tests/HTMLForeignContentTests.swift index f443ade..d25847c 100644 --- a/Tests/HTMLForeignContentTests.swift +++ b/Tests/HTMLForeignContentTests.swift @@ -79,4 +79,18 @@ struct HTMLForeignContentTests { let element = find("p", in: "

") #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 = "
x
" + #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 = "" + #expect(find("rect", in: html)?.name.namespaceURI == svg) + } }