PureXML/Sources/XSLT/XSLTNamespaceFixup.swift
Mihaela Mihaljevic 255f7508fd feat(xslt): literal result element namespace copying
A literal result element copies its in-scope namespace declarations to
the result per 7.1.1, minus the XSLT namespace and the namespaces of
prefixes listed in exclude-result-prefixes and
extension-element-prefixes in scope (#default included); an aliased
stylesheet namespace declares its result prefix and URI instead. The
fixup pass drops declarations that repeat an inherited binding so the
copied nodes do not redeclare per descendant, and created names reuse
stylesheet prefixes. xsl:output indent=yes switches to the Xalan
indentation form (own lines, no leading spaces). Xalan baseline
446 -> 371 (namespace 89 -> 42).
2026-06-10 12:55:48 +02:00

148 lines
6.7 KiB
Swift

/// Namespace fixup over an XSLT result tree (the serialization step of
/// XSLT 1.0 section 7.1): every element and attribute name that carries a
/// namespace URI gets an in-scope declaration, with `ns0`, `ns1`, ... prefixes
/// generated when the carried prefix is absent or bound to a different URI.
enum XSLTNamespaceFixup {
static func apply(_ node: PureXML.Model.Node) -> PureXML.Model.Node {
var counter = 0
return fix(node, inScope: ["xml": "http://www.w3.org/XML/1998/namespace"], counter: &counter)
}
/// Declarations the element already carries enter scope first.
private static func enterDeclarations(_ attributes: [PureXML.Model.Attribute], into scope: inout [String: String]) {
for attribute in attributes {
if attribute.name.prefix == "xmlns" {
scope[attribute.name.localName] = attribute.value
} else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
scope[""] = attribute.value
}
}
}
/// Declares the element's own namespace, or undeclares an inherited
/// default that would capture an unqualified name.
private static func fixElementName(_ name: PureXML.Model.QualifiedName, scope: inout [String: String], attributes: inout [PureXML.Model.Attribute]) {
if let uri = name.namespaceURI, !uri.isEmpty {
let prefix = name.prefix ?? ""
if scope[prefix] != uri {
scope[prefix] = uri
attributes.append(.init(prefix.isEmpty ? "xmlns" : "xmlns:\(prefix)", uri))
}
} else if name.namespaceURI == nil, name.prefix == nil, let inherited = scope[""], !inherited.isEmpty {
scope[""] = ""
attributes.append(.init("xmlns", ""))
}
}
/// A namespaced attribute needs a prefix bound to its URI, generated
/// fresh when absent or taken by a different binding.
private static func fixAttributeName(at index: Int, attributes: inout [PureXML.Model.Attribute], scope: inout [String: String], counter: inout Int) {
let attributeName = attributes[index].name
guard let uri = attributeName.namespaceURI, !uri.isEmpty,
attributeName.prefix != "xmlns", attributeName.prefix != "xml"
else { return }
var prefix = attributeName.prefix
if let candidate = prefix, scope[candidate] == uri {
return // Already bound correctly.
}
if prefix == nil || scope[prefix ?? ""] != nil {
if let existing = scope.first(where: { $0.value == uri && !$0.key.isEmpty })?.key {
prefix = existing
} else {
while scope["ns\(counter)"] != nil {
counter += 1
}
prefix = "ns\(counter)"
counter += 1
}
}
guard let resolved = prefix else { return }
if scope[resolved] != uri {
scope[resolved] = uri
attributes.append(.init("xmlns:\(resolved)", uri))
}
attributes[index] = .init(
name: .init(prefix: resolved, localName: attributeName.localName, namespaceURI: uri),
value: attributes[index].value,
)
}
/// A prefix bound to `uri`: the carried one when free, an existing
/// binding for the same URI, or a fresh `ns<n>`.
private static func resolvedPrefix(for uri: String, carried: String?, scope: [String: String], counter: inout Int) -> String {
if let carried, scope[carried] == nil { return carried }
if let existing = scope.first(where: { $0.value == uri && !$0.key.isEmpty })?.key {
return existing
}
var fresh = counter
while scope["ns\(fresh)"] != nil {
fresh += 1
}
counter = fresh + 1
return "ns\(fresh)"
}
private static func fix(_ node: PureXML.Model.Node, inScope: [String: String], counter: inout Int) -> PureXML.Model.Node {
switch node {
case let .document(children):
.document(children.map { fix($0, inScope: inScope, counter: &counter) })
case let .element(element):
.element(fix(element, inScope: inScope, counter: &counter))
default:
node
}
}
private static func fix(_ element: PureXML.Model.Element, inScope: [String: String], counter: inout Int) -> PureXML.Model.Element {
var scope = inScope
// A declaration that repeats an inherited binding is dropped: copied
// 7.1.1 namespace nodes travel on every literal element and would
// otherwise redeclare on each descendant.
var attributes = element.attributes.filter { attribute in
if attribute.name.prefix == "xmlns" {
return inScope[attribute.name.localName] != attribute.value
}
if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
return (inScope[""] ?? "") != attribute.value
}
return true
}
enterDeclarations(attributes, into: &scope)
fixElementName(element.name, scope: &scope, attributes: &attributes)
// Attribute names: a namespaced attribute always needs a prefix.
for index in attributes.indices {
let attributeName = attributes[index].name
guard let uri = attributeName.namespaceURI, !uri.isEmpty,
attributeName.prefix != "xmlns", attributeName.prefix != "xml"
else { continue }
var prefix = attributeName.prefix
if let candidate = prefix, scope[candidate] == uri {
continue // Already bound correctly.
}
if prefix == nil || scope[prefix ?? ""] != nil {
// Generate a fresh prefix when absent or taken by another URI.
if let existing = scope.first(where: { $0.value == uri && !$0.key.isEmpty })?.key {
prefix = existing
} else {
while scope["ns\(counter)"] != nil {
counter += 1
}
prefix = "ns\(counter)"
counter += 1
}
}
guard let resolved = prefix else { continue }
if scope[resolved] != uri {
scope[resolved] = uri
attributes.append(.init("xmlns:\(resolved)", uri))
}
attributes[index] = .init(
name: .init(prefix: resolved, localName: attributeName.localName, namespaceURI: uri),
value: attributes[index].value,
)
}
let children = element.children.map { fix($0, inScope: scope, counter: &counter) }
return .init(name: element.name, attributes: attributes, children: children)
}
}