diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6924793..30b33ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- `xsl:namespace-alias` now keeps the literal (stylesheet) prefix on aliased result elements, attributes, and namespace declarations, remapping only the namespace URI (XSLT 1.0 7.1.1: the result namespace node keeps the prefix used in the stylesheet). The serializer was adopting the `result-prefix` as the output prefix, so `axsl:foo` aliased to the XSLT namespace came out as `xsl:foo`; the `result-prefix` now only selects the replacement namespace. Closes Apache Xalan conformance cases `namespace19`, `namespace23`, `namespace24`, `namespace35`, and `namespace113`.
+
- `xsl:key` with a union `match` pattern now indexes every branch. The match was turned into a path by prepending `//`, but for a union like `match="Level1 | Level2"` the `//` applied only to the first branch (`//Level1 | Level2`), so the second branch became relative to the root and matched nothing, leaving nodes of that type out of the key. The `//` is now distributed over each top-level union branch (`//Level1 | //Level2`), with `|` inside a predicate or function call left intact. Closes Apache Xalan conformance cases `idkey45`, `idkey46`, `idkey47`, and `idkey48`.
- `xml:space="preserve"` on a literal result element (or an ancestor) in the stylesheet now keeps that element's whitespace-only text instead of stripping it (XSLT 1.0 3.4). The stylesheet whitespace stripping dropped every whitespace-only run unconditionally, ignoring an in-scope `xml:space="preserve"`; the nearest ancestor-or-self `xml:space` is now consulted, so `preserve` retains the run while `default` (or none) still strips it. Closes Apache Xalan conformance case `whitespace20`.
diff --git a/Sources/XSLT/XSLTElements.swift b/Sources/XSLT/XSLTElements.swift
index 4e6ff64..76370a8 100644
--- a/Sources/XSLT/XSLTElements.swift
+++ b/Sources/XSLT/XSLTElements.swift
@@ -13,10 +13,14 @@ extension PureXML.XSLT.Transformer {
}
/// Rewrites a literal name in an `xsl:namespace-alias`ed stylesheet namespace
- /// to its result namespace and prefix; other names pass through unchanged.
+ /// to its result namespace, keeping the literal prefix; other names pass
+ /// through unchanged.
func aliased(_ name: PureXML.Model.QualifiedName) -> PureXML.Model.QualifiedName {
guard let alias = stylesheet.namespaceAliases[name.namespaceURI ?? ""] else { return name }
- return PureXML.Model.QualifiedName(prefix: alias.prefix, localName: name.localName, namespaceURI: alias.uri)
+ // The literal prefix is kept and only its namespace URI is remapped (XSLT
+ // 1.0 7.1.1: the result namespace node keeps the stylesheet prefix); the
+ // result-prefix only selects the replacement namespace, it is not adopted.
+ return PureXML.Model.QualifiedName(prefix: name.prefix, localName: name.localName, namespaceURI: alias.uri)
}
/// The attributes with duplicates by name removed, keeping the last (so a
@@ -261,7 +265,8 @@ extension PureXML.XSLT.Transformer {
var declarations: [PureXML.XSLT.LiteralAttribute] = []
for (prefix, uri) in namespaces.sorted(by: { $0.key < $1.key }) {
let alias = stylesheet.namespaceAliases[uri]
- let resolvedPrefix = alias?.prefix ?? (prefix.isEmpty ? nil : prefix)
+ // Keep the literal prefix; an alias only remaps its namespace URI.
+ let resolvedPrefix = prefix.isEmpty ? nil : prefix
let resolvedURI = alias?.uri ?? uri
let attributeName = resolvedPrefix.map { "xmlns:" + $0 } ?? "xmlns"
declarations.append(PureXML.XSLT.LiteralAttribute(
diff --git a/Sources/XSLT/XSLTModel.swift b/Sources/XSLT/XSLTModel.swift
index a9ae9db..99d26b7 100644
--- a/Sources/XSLT/XSLTModel.swift
+++ b/Sources/XSLT/XSLTModel.swift
@@ -150,12 +150,12 @@ public extension PureXML.XSLT {
/// An `xsl:namespace-alias` target: the result namespace (and its prefix) that
/// a literal result element's stylesheet namespace is rewritten to on output.
struct NamespaceAlias: Sendable {
+ /// The result namespace URI an aliased stylesheet namespace maps to. The
+ /// literal prefix is kept on output (7.1.1), so no prefix is stored.
public var uri: String?
- public var prefix: String?
- public init(uri: String?, prefix: String?) {
+ public init(uri: String?) {
self.uri = uri
- self.prefix = prefix
}
}
diff --git a/Sources/XSLT/XSLTParserDeclarations.swift b/Sources/XSLT/XSLTParserDeclarations.swift
index a3239af..bac4632 100644
--- a/Sources/XSLT/XSLTParserDeclarations.swift
+++ b/Sources/XSLT/XSLTParserDeclarations.swift
@@ -1,16 +1,14 @@
/// Top-level declaration helpers for ``XSLTParser``, kept in a separate file so
/// the parser enum stays within the type-body and file length budgets.
extension PureXML.XSLT.XSLTParser {
- /// Records an `xsl:namespace-alias`: the namespace bound to `stylesheet-prefix`
- /// is rewritten to the one bound to `result-prefix` (with that prefix) on output.
+ /// Records an `xsl:namespace-alias`: on output, the namespace URI bound to
+ /// `stylesheet-prefix` is replaced by the one bound to `result-prefix`. The
+ /// literal prefix is kept (7.1.1), so only the result namespace URI is stored.
static func addNamespaceAlias(_ child: XSLTTree, into parts: inout Parts) {
let stylePrefix = XSLTNode.attribute(child, "stylesheet-prefix") ?? "#default"
let resultPrefix = XSLTNode.attribute(child, "result-prefix") ?? "#default"
let key = resolvePrefix(stylePrefix, at: child) ?? ""
- parts.namespaceAliases[key] = PureXML.XSLT.NamespaceAlias(
- uri: resolvePrefix(resultPrefix, at: child),
- prefix: resultPrefix == "#default" ? nil : resultPrefix,
- )
+ parts.namespaceAliases[key] = PureXML.XSLT.NamespaceAlias(uri: resolvePrefix(resultPrefix, at: child))
}
/// Resolves a namespace prefix (or `#default`) to its URI from the `xmlns`
diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt
index eea285c..b0d3398 100644
--- a/Tests/Fixtures/xalan-baseline.txt
+++ b/Tests/Fixtures/xalan-baseline.txt
@@ -11,11 +11,6 @@ impincl/impincl08
impincl/impincl27
mdocs/mdocs07
mdocs/mdocs19
-namespace/namespace113
-namespace/namespace19
-namespace/namespace23
-namespace/namespace24
-namespace/namespace35
output/output22
output/output26
output/output63
diff --git a/Tests/XSLTNamespaceAliasTests.swift b/Tests/XSLTNamespaceAliasTests.swift
index 0d51928..4cd32a6 100644
--- a/Tests/XSLTNamespaceAliasTests.swift
+++ b/Tests/XSLTNamespaceAliasTests.swift
@@ -9,7 +9,7 @@ struct XSLTNamespaceAliasTests {
try PureXML.XSLT.transform(stylesheet: stylesheet, source: source)
}
- @Test("A literal element in the aliased namespace is rewritten to the result namespace")
+ @Test("A literal element in the aliased namespace keeps its prefix, with the result namespace")
func test_rewrite() throws {
let style = """
@@ -18,9 +18,11 @@ struct XSLTNamespaceAliasTests {
"""
- // The element's prefix is rewritten from the stylesheet alias to the
- // result, and the serializer declares the result namespace.
- #expect(try transform(style, "") == "")
+ // 7.1.1: the literal prefix `a` is kept and only its namespace URI is
+ // remapped to the result namespace; the result-prefix `b` selects the
+ // namespace, it is not adopted as the output prefix (Apache Xalan
+ // namespace19, namespace23, namespace24, namespace35, namespace113).
+ #expect(try transform(style, "") == "")
}
@Test("A non-aliased literal element still copies in-scope declarations")
@@ -32,7 +34,9 @@ struct XSLTNamespaceAliasTests {
"""
- #expect(try transform(style, "") == "")
+ // Both in-scope namespaces are copied; the aliased `a` keeps its prefix
+ // with its URI remapped to the result namespace, alongside `b`.
+ #expect(try transform(style, "") == "")
}
@Test("namespace-alias can produce literal xsl: elements (stylesheet generation)")
@@ -45,8 +49,9 @@ struct XSLTNamespaceAliasTests {
"""
let out = try transform(style, "")
- // The aliased axsl: element is emitted as a literal xsl: element, not run.
- #expect(out.contains("xsl:value-of"))
- #expect(!out.contains("axsl"))
+ // The aliased axsl: element keeps its prefix but is bound to the XSLT
+ // namespace, so it is a literal xsl:value-of (axsl maps to Transform),
+ // not run as an instruction.
+ #expect(out == "")
}
}