diff --git a/Sources/HTML/HTMLDocumentBuilder.swift b/Sources/HTML/HTMLDocumentBuilder.swift
index 8a8c2fb..a68dd26 100644
--- a/Sources/HTML/HTMLDocumentBuilder.swift
+++ b/Sources/HTML/HTMLDocumentBuilder.swift
@@ -1,13 +1,3 @@
-/// A partially-built element on an open-element stack. File-scope and private.
-private struct DocFrame {
- let name: String
- let attributes: [PureXML.Model.Attribute]
- var children: [PureXML.Model.Node] = []
- /// The foreign-content namespace URI (SVG or MathML) when this element is in
- /// foreign content, or nil for ordinary HTML.
- var namespace: String?
-}
-
/// The foreign-content namespaces HTML5 switches into. File-scope and private.
private enum ForeignNamespace {
static let svg = "http://www.w3.org/2000/svg"
@@ -46,8 +36,8 @@ final class HTMLDocument {
private var htmlAttributes: [Attribute] = []
private var headAttributes: [Attribute] = []
private var bodyAttributes: [Attribute] = []
- private var headStack: [DocFrame] = []
- private var headChildren: [Node] = []
+ private let headRoot = PureXML.Model.TreeNode.element("head")
+ private lazy var openHead: [PureXML.Model.TreeNode] = [headRoot]
/// The body is built as a live mutable tree (the HTML5 model): nodes are
/// attached to their parent as soon as they open, and `openBody` is the stack
/// of currently-open elements into that tree, with the body element at the
@@ -64,12 +54,9 @@ final class HTMLDocument {
for token in tokens {
process(token)
}
- while !headStack.isEmpty {
- pop(&headStack, &headChildren)
- }
- // Open body elements are already attached to the live tree, so no closing
- // pass is needed: the body's children are read straight off bodyRoot.
- let head = PureXML.Model.Element(name: .init("head"), attributes: headAttributes, children: headChildren)
+ // Open elements are already attached to their live tree, so head and body
+ // children are read straight off their roots with no closing pass.
+ let head = PureXML.Model.Element(name: .init("head"), attributes: headAttributes, children: headRoot.children.map(\.node))
let secondChild: Node = if let frameset = framesetStack.first {
frameset.node
} else {
@@ -128,29 +115,43 @@ final class HTMLDocument {
private func processInHead(_ token: Token) {
switch token {
case let .startTag(name, attributes, selfClosing) where Self.headElements.contains(name):
- open(name, attributes, selfClosing: selfClosing, &headStack, &headChildren)
- case let .endTag(name) where name == "head":
- while !headStack.isEmpty {
- pop(&headStack, &headChildren)
- }
+ headOpen(name, attributes, selfClosing: selfClosing)
+ case .endTag("head"):
+ openHead = [headRoot]
mode = .afterHead
- case let .endTag(name) where headStack.contains(where: { $0.name == name }):
- close(name, &headStack, &headChildren)
+ case let .endTag(name) where openHead.contains(where: { tagName($0) == name }):
+ headClose(name)
case let .comment(value):
- attach(.comment(value), &headStack, &headChildren)
- case let .text(value) where !headStack.isEmpty:
- attach(.text(value), &headStack, &headChildren)
- case let .text(value) where value.trimmingXMLWhitespace().isEmpty:
- attach(.text(value), &headStack, &headChildren)
+ headInsert(.comment(value))
+ case let .text(value) where openHead.count > 1 || value.trimmingXMLWhitespace().isEmpty:
+ headInsert(.text(value))
default:
- while !headStack.isEmpty {
- pop(&headStack, &headChildren)
- }
+ openHead = [headRoot]
mode = .afterHead
process(token)
}
}
+ private func headOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) {
+ if let closes = PureXML.HTML.Elements.impliedClose[name] {
+ while let top = openHead.last, top !== headRoot, closes.contains(tagName(top)) {
+ openHead.removeLast()
+ }
+ }
+ let element = PureXML.Model.TreeNode.element(name, attributes: modelAttributes(attributes))
+ openHead.last?.append(element)
+ if !(PureXML.HTML.Elements.void.contains(name) || selfClosing) { openHead.append(element) }
+ }
+
+ private func headClose(_ name: String) {
+ guard let index = openHead.lastIndex(where: { tagName($0) == name }), index >= 1 else { return }
+ openHead.removeLast(openHead.count - index)
+ }
+
+ private func headInsert(_ node: PureXML.Model.TreeNode) {
+ openHead.last?.append(node)
+ }
+
private func processAfterHead(_ token: Token) {
if case let .startTag(name, attributes, _) = token, name == "frameset" {
framesetStack = [PureXML.Model.TreeNode.element("frameset", attributes: modelAttributes(attributes))]
@@ -292,40 +293,7 @@ final class HTMLDocument {
private static let svgIntegrationPoints: Set = ["foreignobject", "desc", "title"]
}
-/// The head's open-element stack uses the pop-on-close `DocFrame` model; these
-/// primitives drive it, in an extension to keep the class body within budget.
extension HTMLDocument {
- private func open(
- _ name: String,
- _ attributes: [(String, String)],
- selfClosing: Bool,
- _ stack: inout [DocFrame],
- _ roots: inout [Node],
- ) {
- if let closes = PureXML.HTML.Elements.impliedClose[name] {
- while let top = stack.last, closes.contains(top.name) {
- pop(&stack, &roots)
- }
- }
- ensureTableContext(for: name, &stack, &roots)
- let modeled = modelAttributes(attributes)
- let namespace = foreignNamespace(for: name, in: stack)
- if PureXML.HTML.Elements.void.contains(name) || selfClosing {
- attach(.element(PureXML.Model.Element(name: qualifiedName(name, namespace), attributes: modeled)), &stack, &roots)
- } else {
- stack.append(DocFrame(name: name, attributes: modeled, namespace: namespace))
- }
- }
-
- /// The namespace an element enters: SVG for `