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:
parent
fa3ab76c6f
commit
1e14d18f4d
@ -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`.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=\".\"/>")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user