* perf(model): rebuild DTD-default application iteratively, not recursively applyingDefaults rebuilt the tree with a per-node recursive walk, so applying DTD attribute defaults to a deeply-nested document overflowed the stack (around 2000 deep on a Swift Task stack). It now rebuilds bottom-up through a new shared iterative spine, Node.rebuildingBottomUp, which transforms each node once its children are rebuilt via an explicit work stack. The same primitive will carry the other tree-to-tree transforms (canonical prefix rewrite, XSLT namespace fixup) in follow-ups. Defaults are applied identically: 1788 tests pass. Part of #350. * perf(canonical): serialize C14N iteratively, not recursively Canonicalizer.emit recursed on element children, so canonicalizing a deeply-nested document overflowed the stack (crashed around 5000 deep). The element serializer now drives a deferred-close work stack (the same shape as the main serializer): each step carries the in-scope and already-rendered namespace context, children push reversed to emit in document order, and a close step emits the end tag. The shallow document case and all namespace-rendering logic are unchanged. The canonical bytes are byte-identical: the C14N 1.0, 1.1, and exclusive conformance corpora pass, full suite 1789. A 50000-deep document now canonicalizes without crashing. The node-subset helpers moved to their own file to keep within the file-length limit. Part of #350.
71 lines
3.4 KiB
Swift
71 lines
3.4 KiB
Swift
extension PureXML.Canonical.Canonicalizer {
|
|
/// The apex element with its inherited `xml:*` attributes added, and (for
|
|
/// inclusive mode) the namespaces in scope from omitted ancestors merged in
|
|
/// as declarations so the apex renders its full namespace context.
|
|
static func augmentedApex(
|
|
_ apex: PureXML.Model.Element,
|
|
inheritedXML: [PureXML.Model.Attribute],
|
|
mergingNamespaces ancestorNamespaces: [String: String]?,
|
|
stripApexBase: Bool,
|
|
) -> PureXML.Model.Element {
|
|
// Under 1.1, the apex's own xml:base is folded into the merged base in
|
|
// inheritedXML, so drop the original to avoid emitting it twice.
|
|
let own = stripApexBase
|
|
? apex.attributes.filter { !($0.name.prefix == "xml" && $0.name.localName == "base") }
|
|
: apex.attributes
|
|
var attributes = own + inheritedXML
|
|
if let ancestorNamespaces {
|
|
let declared = Set(apex.attributes.map(\.name.description))
|
|
for (prefix, uri) in ancestorNamespaces {
|
|
let name = prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)"
|
|
if !declared.contains(name) { attributes.append(PureXML.Model.Attribute(name, uri)) }
|
|
}
|
|
}
|
|
return PureXML.Model.Element(name: apex.name, attributes: attributes, children: apex.children)
|
|
}
|
|
|
|
/// The namespace declarations in scope from a node's omitted ancestors,
|
|
/// nearest ancestor winning (so an inner redeclaration is preserved).
|
|
static func inScopeNamespaces(above node: PureXML.Model.TreeNode) -> [String: String] {
|
|
var result: [String: String] = [:]
|
|
var current = node.parent
|
|
while let ancestor = current {
|
|
for attribute in ancestor.attributes {
|
|
guard let prefix = declaredPrefix(of: attribute.name), result[prefix] == nil else { continue }
|
|
result[prefix] = attribute.value
|
|
}
|
|
current = ancestor.parent
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// The `xml:*` attributes in scope from a node's omitted ancestors that the
|
|
/// apex does not already set, nearest ancestor winning.
|
|
static func inheritedXMLAttributes(above node: PureXML.Model.TreeNode, apex: PureXML.Model.Element, mergeBase: Bool) -> [PureXML.Model.Attribute] {
|
|
let present = Set(apex.attributes.map(\.name.description))
|
|
if mergeBase { return inherited11XMLAttributes(above: node, apex: apex, present: present) }
|
|
// Canonical XML 1.0: the nearest of every in-scope xml:* attribute the
|
|
// apex does not already set.
|
|
var seen: Set<String> = []
|
|
var result: [PureXML.Model.Attribute] = []
|
|
var current = node.parent
|
|
while let ancestor = current {
|
|
for attribute in ancestor.attributes where attribute.name.prefix == "xml" {
|
|
let key = attribute.name.description
|
|
guard !present.contains(key), seen.insert(key).inserted else { continue }
|
|
result.append(attribute)
|
|
}
|
|
current = ancestor.parent
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// The prefix a namespace declaration binds (empty for the default
|
|
/// namespace), or nil when the attribute is not a namespace declaration.
|
|
static func declaredPrefix(of name: PureXML.Model.QualifiedName) -> String? {
|
|
if name.prefix == nil, name.localName == "xmlns" { return "" }
|
|
if name.prefix == "xmlns" { return name.localName }
|
|
return nil
|
|
}
|
|
}
|