diff --git a/CHANGELOG.md b/CHANGELOG.md index a30d656..6924793 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: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`. - `xsl:number level="single"` now counts the element that is the `from` boundary when that element also matches the `count` pattern (XSLT 1.0 7.7). The search for the count node stopped at the nearest `from`-matching ancestor before testing it against `count`, so a pattern like `count="a|b" from="a"` produced an empty number for an element inside an `a` (the count node is that `a`, which is also the boundary). The count pattern is now tested before the boundary stops the walk. Closes Apache Xalan conformance case `numbering63`. diff --git a/Sources/XSLT/XSLTFunctions.swift b/Sources/XSLT/XSLTFunctions.swift index db852b0..7e20a09 100644 --- a/Sources/XSLT/XSLTFunctions.swift +++ b/Sources/XSLT/XSLTFunctions.swift @@ -350,7 +350,7 @@ extension PureXML.XSLT { static func buildKeyIndex(stylesheet: Stylesheet, root: PureXML.Model.TreeNode) -> KeyIndex { var index: KeyIndex = [:] for key in stylesheet.keys { - let path = key.match.hasPrefix("/") ? key.match : "//" + key.match + let path = keyMatchPath(key.match) guard let matchQuery = try? PureXML.XPath.Query(path), let useQuery = try? PureXML.XPath.Query(key.use) else { continue } for node in matchQuery.nodes(over: root) { diff --git a/Sources/XSLT/XSLTLookups.swift b/Sources/XSLT/XSLTLookups.swift index b9a94ba..02304ae 100644 --- a/Sources/XSLT/XSLTLookups.swift +++ b/Sources/XSLT/XSLTLookups.swift @@ -31,6 +31,39 @@ extension PureXML.XSLT.Library { base.isEmpty ? reference : PureXML.XInclude.URIReference.resolve(reference, against: base) } + /// The `xsl:key` match pattern as a path selecting every matching node: `//` + /// is distributed over each top-level union branch (`a | b` becomes `//a | + /// //b`, not `//a | b`), since the match is a pattern matched anywhere, not a + /// single relative path. + static func keyMatchPath(_ match: String) -> String { + splitUnion(match).map { branch in + let trimmed = branch.drop { $0 == " " } + return trimmed.hasPrefix("/") ? String(trimmed) : "//" + trimmed + }.joined(separator: " | ") + } + + /// Splits a pattern on its top-level `|`, ignoring `|` inside a predicate + /// `[...]` or a function call `(...)`. + private static func splitUnion(_ pattern: String) -> [Substring] { + var parts: [Substring] = [] + var depth = 0 + var start = pattern.startIndex + var index = pattern.startIndex + while index < pattern.endIndex { + switch pattern[index] { + case "[", "(": depth += 1 + case "]", ")": depth -= 1 + case "|" where depth == 0: + parts.append(pattern[start ..< index]) + start = pattern.index(after: index) + default: break + } + index = pattern.index(after: index) + } + parts.append(pattern[start...]) + return parts + } + /// The XPath `id()` definition exactly: only attributes the DTD /// declares as type ID identify elements; a document without ID /// declarations yields the empty set. diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index 7fb8525..eea285c 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -7,10 +7,6 @@ extend/extend01 extend/extend02 extend/extend04 idkey/idkey31 -idkey/idkey45 -idkey/idkey46 -idkey/idkey47 -idkey/idkey48 impincl/impincl08 impincl/impincl27 mdocs/mdocs07 diff --git a/Tests/XSLTKeyNodeSetTests.swift b/Tests/XSLTKeyNodeSetTests.swift index 214a992..f41f30d 100644 --- a/Tests/XSLTKeyNodeSetTests.swift +++ b/Tests/XSLTKeyNodeSetTests.swift @@ -53,4 +53,20 @@ struct XSLTKeyNodeSetTests { """ #expect(try transform(style, source) == "2") } + + @Test("xsl:key with a union match pattern indexes every branch") + func test_unionMatchPattern() throws { + // match="a | b" indexes both a and b anywhere; the // must distribute over + // the union (previously only the first branch was matched anywhere, so a + // nested b was missed). Apache Xalan idkey45-48. + let style = """ + + + + , + + """ + let source = "" + #expect(try transform(style, source) == "a,b,") + } }