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 == "") } }