PureXML/Sources/XSLT/XSLTParserStructural.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

102 lines
4.7 KiB
Swift

extension PureXML.XSLT.XSLTParser {
static func structuralInstruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
switch XSLTNode.localName(node) {
case "for-each":
.forEach(select: XSLTNode.attribute(node, "select") ?? "", sorts: sorts(node), body: body(node))
case "if":
.ifInstruction(test: XSLTNode.attribute(node, "test") ?? "", body: body(node))
case "choose":
choose(node)
case "element":
.element(
name: valueTemplate(XSLTNode.attribute(node, "name") ?? ""),
namespace: XSLTNode.attribute(node, "namespace").map(valueTemplate),
namespaces: inScopeNamespaces(node),
useAttributeSets: useAttributeSets(node),
body: body(node),
)
case "attribute":
.attribute(
name: valueTemplate(XSLTNode.attribute(node, "name") ?? ""),
namespace: XSLTNode.attribute(node, "namespace").map(valueTemplate),
namespaces: inScopeNamespaces(node),
body: body(node),
)
case "number":
.number(PureXML.XSLT.NumberSpec(
level: XSLTNode.attribute(node, "level") ?? "single",
count: XSLTNode.attribute(node, "count"),
from: XSLTNode.attribute(node, "from"),
value: XSLTNode.attribute(node, "value"),
format: XSLTNode.attribute(node, "format") ?? "1",
groupingSeparator: XSLTNode.attribute(node, "grouping-separator"),
groupingSize: XSLTNode.attribute(node, "grouping-size").flatMap { Int($0) },
))
case "comment":
.comment(body: body(node))
case "processing-instruction":
.processingInstruction(name: valueTemplate(XSLTNode.attribute(node, "name") ?? ""), body: body(node))
default:
nil
}
}
/// The namespace bindings in scope at a stylesheet node, for resolving
/// the prefix of a created name (7.1.2/7.1.3: the QName in a name
/// attribute is expanded using the instruction's own declarations).
static func inScopeNamespaces(_ node: XSLTTree) -> [String: String] {
var bindings: [String: String] = [:]
var current: XSLTTree? = node
while let candidate = current {
for attribute in candidate.attributes where attribute.name.prefix == "xmlns" {
if bindings[attribute.name.localName] == nil {
bindings[attribute.name.localName] = attribute.value
}
}
current = candidate.parent
}
return bindings
}
/// The namespace declarations a literal result element copies to the
/// result (7.1.1): its in-scope bindings minus the XSLT namespace and
/// the namespaces of the prefixes listed in `exclude-result-prefixes`
/// and `extension-element-prefixes` in scope.
static func copiedNamespaces(_ node: XSLTTree) -> [String: String] {
var bindings = inScopeNamespaces(node)
var current: XSLTTree? = node
while let candidate = current {
let defaults = candidate.attributes.filter { $0.name.prefix == nil && $0.name.localName == "xmlns" }
if bindings[""] == nil, let declared = defaults.first { bindings[""] = declared.value }
current = candidate.parent
}
var excludedURIs: Set<String> = [XSLTNode.namespace]
for prefix in excludedPrefixTokens(node) {
let key = prefix == "#default" ? "" : prefix
if let uri = bindings[key] { excludedURIs.insert(uri) }
}
return bindings.filter { !excludedURIs.contains($0.value) && !($0.key.isEmpty && $0.value.isEmpty) }
}
/// Every `exclude-result-prefixes`/`extension-element-prefixes` token
/// in scope at `node` (the unprefixed forms on xsl:stylesheet, the
/// xsl-prefixed forms on literal elements).
private static func excludedPrefixTokens(_ node: XSLTTree) -> [String] {
var tokens: [String] = []
var current: XSLTTree? = node
while let candidate = current {
for attribute in candidate.attributes {
let local = attribute.name.localName
guard local == "exclude-result-prefixes" || local == "extension-element-prefixes" else { continue }
let isXSLElement = XSLTNode.isXSL(candidate)
let qualified = attribute.name.prefix == "xsl"
if isXSLElement == !qualified {
tokens += attribute.value.split(whereSeparator: \.isWhitespace).map(String.init)
}
}
current = candidate.parent
}
return tokens
}
}