PureXML/Sources/Model/TreeNodeIdentity.swift

109 lines
4.7 KiB
Swift

public extension PureXML.Model.TreeNode {
/// The document node this node ultimately lives under, or nil when the node
/// is detached or rooted at something other than a document (the DOM
/// `ownerDocument`). Derived from the parent chain, so it stays correct after
/// any move or adoption without a separate identity field.
var ownerDocument: PureXML.Model.TreeNode? {
let top = root
return top.kind == .document ? top : nil
}
/// Detaches `node` from its current tree and makes it self-contained for this
/// document (the DOM `adoptNode`): every namespace the subtree relied on from
/// an outer scope is re-declared on the subtree, so moving it never silently
/// rebinds a prefix. Returns the adopted node, ready to insert with
/// ``append(_:)`` or one of the `insert` methods.
@discardableResult
func adopt(_ node: PureXML.Model.TreeNode) -> PureXML.Model.TreeNode {
Self.rebindNamespaces(on: node)
node.removeFromParent()
return node
}
/// A self-contained copy of `node` for use in this document (the DOM
/// `importNode`), leaving the original where it is. Deep by default; pass
/// `deep: false` for a shallow copy. Namespaces the subtree relied on from an
/// outer scope are re-declared on the copy so it carries its own bindings.
func importNode(_ node: PureXML.Model.TreeNode, deep: Bool = true) -> PureXML.Model.TreeNode {
let copy = deep ? node.copy() : node.shallowCopy()
Self.rebindNamespaces(on: copy)
return copy
}
// MARK: Typed accessors for the structural node kinds
/// The document-type (root element) name of a `.doctype` node.
var doctypeName: String? {
kind == .doctype ? name?.description : nil
}
/// The public identifier of a `.doctype` node, when it has an external subset.
var publicID: String? {
kind == .doctype ? attributeValue("public") : nil
}
/// The system identifier of a `.doctype` node, when it has an external subset.
var systemID: String? {
kind == .doctype ? attributeValue("system") : nil
}
/// The internal subset text of a `.doctype` node (empty when it has none).
var internalSubset: String? {
kind == .doctype ? value : nil
}
/// The entity name of a `.entityReference` node (without the `&` and `;`).
var entityReferenceName: String? {
kind == .entityReference ? name?.description : nil
}
/// The prefix bound by a `.namespace` node, or nil for the default namespace.
var namespacePrefix: String? {
guard kind == .namespace, let local = name?.localName, !local.isEmpty else { return nil }
return local
}
/// The URI bound by a `.namespace` node.
var namespaceBinding: String? {
kind == .namespace ? value : nil
}
/// Re-declares on `root` every namespace its subtree uses, so a moved or
/// copied subtree no longer depends on declarations that lived on outer
/// ancestors. Only adds a declaration the node does not already carry, so an
/// inner rebinding (which travels as its own `xmlns` attribute) is preserved.
private static func rebindNamespaces(on root: PureXML.Model.TreeNode) {
guard root.kind == .element else { return }
var declared = Set(root.attributes.map(\.name.description))
for (attributeName, uri) in usedNamespaces(in: root) where !declared.contains(attributeName) {
root.attributes.append(PureXML.Model.Attribute(attributeName, uri))
declared.insert(attributeName)
}
}
/// The `xmlns`/`xmlns:prefix` declarations the subtree's resolved names imply,
/// keyed by the declaration's attribute name. An element contributes its own
/// namespace; a prefixed attribute contributes its prefix's namespace.
private static func usedNamespaces(in node: PureXML.Model.TreeNode) -> [String: String] {
var result: [String: String] = [:]
var stack = [node]
while let current = stack.popLast() {
if current.kind == .element, let uri = current.name?.namespaceURI {
result[declarationName(forPrefix: current.name?.prefix)] = uri
}
for attribute in current.attributes {
if let prefix = attribute.name.prefix, prefix != "xmlns", let uri = attribute.name.namespaceURI {
result["xmlns:\(prefix)"] = uri
}
}
stack.append(contentsOf: current.children)
}
return result
}
private static func declarationName(forPrefix prefix: String?) -> String {
guard let prefix, !prefix.isEmpty else { return "xmlns" }
return "xmlns:\(prefix)"
}
}