PureXML/Sources/HTML/HTMLDocumentTableAndForeign.swift

76 lines
3.2 KiB
Swift

/// Table-context and foreign-content (SVG/MathML) helpers, split out so
/// HTMLDocument's own body stays within the type-body length budget.
extension HTMLDocument {
/// Tags that close an open `<select>` (the "in select" rule): a nested select,
/// or an input/keygen/textarea.
static let closesSelect: Set<String> = ["select", "input", "keygen", "textarea"]
/// Table-structural tags that close a select which is inside a table (the "in
/// select in table" rule).
private static let closesSelectInTable: Set<String> = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
/// The SVG HTML integration points: their content is parsed as HTML.
private static let svgIntegrationPoints: Set<String> = ["foreignobject", "desc", "title"]
/// The open-stack index of a `<select>` that an opening table tag should close
/// (a select inside a table), or nil when there is none.
func selectInTableToClose(for name: String) -> Int? {
guard Self.closesSelectInTable.contains(name),
let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1,
openBody[..<selectIndex].contains(where: { tagName($0) == "table" }) else { return nil }
return selectIndex
}
func bodyEnsureTableContext(for name: String) {
let structural: Set = ["table", "tbody", "thead", "tfoot", "tr"]
guard let context = openBody.reversed().first(where: { structural.contains(tagName($0)) }).map(tagName) else { return }
switch name {
case "tr" where context == "table":
bodyPushImplied("tbody")
case "td", "th":
if context == "table" {
bodyPushImplied("tbody")
}
if context == "table" || ["tbody", "thead", "tfoot"].contains(context) {
bodyPushImplied("tr")
}
default:
break
}
}
private func bodyPushImplied(_ name: String) {
let element = PureXML.Model.TreeNode.element(name)
openBody.last?.append(element)
openBody.append(element)
}
/// Restores SVG attribute names to their canonical camel case.
func adjustedAttributes(_ attributes: [Attribute], namespace: String?) -> [Attribute] {
guard namespace == ForeignNamespace.svg else { return attributes }
return attributes.map { attribute in
guard let adjusted = PureXML.HTML.ForeignNames.svgAttributes[attribute.name.localName.lowercased()] else { return attribute }
return Attribute(adjusted, attribute.value)
}
}
/// The foreign-content namespace for a body element: SVG/MathML on entry, the
/// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
func bodyForeignNamespace(for name: String) -> String? {
if name == "svg" {
return ForeignNamespace.svg
}
if name == "math" {
return ForeignNamespace.mathml
}
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
}
}