fix(xslt): xsl:namespace-alias keeps the literal prefix, remapping only the URI (closes namespace19/23/24/35/113) (#273)

* fix(xslt): keep the literal prefix for xsl:namespace-alias, remap only the URI

XSLT 1.0 7.1.1: an aliased result element keeps the prefix used in the
stylesheet; only its namespace URI is replaced by the result namespace. The
serializer adopted 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; the element name, attributes, and copied namespace
declarations all keep their literal prefix.

Closes Apache Xalan conformance cases namespace19, namespace23, namespace24, namespace35, namespace113.

* refactor(xslt): drop the now-unused NamespaceAlias prefix field

The namespace-alias output keeps the literal prefix, so the result-prefix is no
longer stored; only the result namespace URI is needed. Removes the dead field
and corrects the addNamespaceAlias doc comment.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 15:43:02 +02:00 committed by GitHub
parent fa3ab76c6f
commit 1e14d18f4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 30 additions and 25 deletions

View File

@ -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`.

View File

@ -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(

View File

@ -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
}
}

View File

@ -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`

View File

@ -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

View File

@ -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 = """
<xsl:stylesheet version="1.0" \(xsl) xmlns:a="urn:from" xmlns:b="urn:to">
@ -18,9 +18,11 @@ struct XSLTNamespaceAliasTests {
<xsl:template match="/"><a:thing/></xsl:template>
</xsl:stylesheet>
"""
// The element's prefix is rewritten from the stylesheet alias to the
// result, and the serializer declares the result namespace.
#expect(try transform(style, "<x/>") == "<b:thing xmlns:b=\"urn:to\"/>")
// 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, "<x/>") == "<a:thing xmlns:a=\"urn:to\" xmlns:b=\"urn:to\"/>")
}
@Test("A non-aliased literal element still copies in-scope declarations")
@ -32,7 +34,9 @@ struct XSLTNamespaceAliasTests {
<xsl:template match="/"><plain/></xsl:template>
</xsl:stylesheet>
"""
#expect(try transform(style, "<x/>") == "<plain xmlns:b=\"urn:to\"/>")
// 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, "<x/>") == "<plain xmlns:a=\"urn:to\" xmlns:b=\"urn:to\"/>")
}
@Test("namespace-alias can produce literal xsl: elements (stylesheet generation)")
@ -45,8 +49,9 @@ struct XSLTNamespaceAliasTests {
</xsl:stylesheet>
"""
let out = try transform(style, "<x/>")
// 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 == "<axsl:value-of xmlns:axsl=\"http://www.w3.org/1999/XSL/Transform\" select=\".\"/>")
}
}