extension HTMLDocument { /// The formatting elements tracked on the active-formatting list and recovered /// by the adoption agency. private static let formatting: Set = [ "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u", ] /// The "special" category: an element of this kind, open below a formatting /// element, is the furthest block that triggers reparenting. private static let special: Set = [ "address", "applet", "area", "article", "aside", "blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "dd", "details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "html", "iframe", "img", "input", "li", "listing", "main", "marquee", "menu", "nav", "object", "ol", "p", "plaintext", "pre", "section", "select", "summary", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "title", "tr", "ul", "wbr", "xmp", ] /// The elements that bound "element in scope" lookups. private static let scopeBoundaries: Set = [ "applet", "caption", "html", "table", "td", "th", "marquee", "object", ] func isFormatting(_ name: String) -> Bool { Self.formatting.contains(name) } private func openIndex(_ node: PureXML.Model.TreeNode) -> Int? { openBody.firstIndex { $0 === node } } private func formattingIndex(_ node: PureXML.Model.TreeNode) -> Int? { activeFormatting.firstIndex { $0 === node } } /// Whether `target` is in scope: reachable up the open stack without crossing a /// scope boundary. private func hasInScope(_ target: PureXML.Model.TreeNode) -> Bool { for node in openBody.reversed() { if node === target { return true } if Self.scopeBoundaries.contains(tagName(node)) { return false } } return false } /// The HTML5 "reconstruct the active formatting elements" step: re-open any /// formatting elements that were closed out of order, so content after them is /// re-wrapped (the `X` -> `X` behavior). func reconstructActiveFormatting() { guard var index = activeFormatting.indices.last else { return } if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { return } while index > 0 { index -= 1 if activeFormatting[index] == nil || isOpen(activeFormatting[index]) { index += 1 break } } while index < activeFormatting.count { if let template = activeFormatting[index] { let clone = template.shallowCopy() openBody.last?.append(clone) openBody.append(clone) activeFormatting[index] = clone } index += 1 } } private func isOpen(_ node: PureXML.Model.TreeNode?) -> Bool { guard let node else { return false } return openBody.contains { $0 === node } } /// The HTML5 adoption agency algorithm: recovers a misnested formatting element /// end tag, reparenting the intervening block (the "furthest block") and /// cloning the formatting element around it. func adoptionAgency(_ subject: String) { if let current = openBody.last, current !== bodyRoot, tagName(current) == subject, formattingIndex(current) == nil { openBody.removeLast() return } var outer = 0 while outer < 8 { outer += 1 guard let formattingIndexInList = lastFormatting(tag: subject), let formattingElement = activeFormatting[formattingIndexInList] else { bodyClose(subject) return } guard let formattingOpenIndex = openIndex(formattingElement) else { activeFormatting.remove(at: formattingIndexInList) return } if !hasInScope(formattingElement) { return } guard let furthestBlockIndex = openBody[(formattingOpenIndex + 1)...].firstIndex(where: { Self.special.contains(tagName($0)) }) else { openBody.removeLast(openBody.count - formattingOpenIndex) activeFormatting.remove(at: formattingIndexInList) return } reparent(formattingElement: formattingElement, formattingIndexInList: formattingIndexInList, furthestBlockIndex: furthestBlockIndex) } } private func lastFormatting(tag: String) -> Int? { for index in activeFormatting.indices.reversed() { guard let entry = activeFormatting[index] else { return nil } // a marker bounds the search if tagName(entry) == tag { return index } } return nil } /// Steps 8-19 of the algorithm: walk the open elements between the formatting /// element and the furthest block, cloning formatting nodes, then clone the /// formatting element around the furthest block's content. private func reparent(formattingElement: PureXML.Model.TreeNode, formattingIndexInList: Int, furthestBlockIndex: Int) { let furthestBlock = openBody[furthestBlockIndex] guard let formattingOpenIndex = openIndex(formattingElement), formattingOpenIndex >= 1 else { return } let commonAncestor = openBody[formattingOpenIndex - 1] var bookmark = formattingIndexInList var lastNode = furthestBlock var nodeIndex = furthestBlockIndex var inner = 0 while inner < 3 { inner += 1 nodeIndex -= 1 let node = openBody[nodeIndex] if node === formattingElement { break } guard let activeIndex = formattingIndex(node) else { openBody.remove(at: nodeIndex) continue } let clone = node.shallowCopy() activeFormatting[activeIndex] = clone openBody[nodeIndex] = clone if lastNode === furthestBlock { bookmark = activeIndex + 1 } clone.append(lastNode) lastNode = clone } commonAncestor.append(lastNode) let formattingClone = formattingElement.shallowCopy() for child in furthestBlock.children { formattingClone.append(child) } furthestBlock.append(formattingClone) if let removeIndex = formattingIndex(formattingElement) { activeFormatting.remove(at: removeIndex) if removeIndex < bookmark { bookmark -= 1 } } activeFormatting.insert(formattingClone, at: min(bookmark, activeFormatting.count)) if let removeOpen = openIndex(formattingElement) { openBody.remove(at: removeOpen) } // The clone becomes a child of the furthest block, so it sits immediately // above it on the open-elements stack; inserting below would re-find the // same furthest block and loop. if let blockIndex = openIndex(furthestBlock) { openBody.insert(formattingClone, at: blockIndex + 1) } } }