fix(xslt): distribute // over union branches in an xsl:key match pattern (#272)
The xsl:key match was turned into a path by prepending //, but for a union match like "Level1 | Level2" the // applied only to the first branch (//Level1 | Level2), so the second branch matched relative to the root and found nothing, dropping those nodes from the key. Distribute // over each top-level union branch (// inside a predicate or function call is left alone). Closes Apache Xalan conformance cases idkey45, idkey46, idkey47, idkey48.
This commit is contained in:
parent
12af2fbc24
commit
fa3ab76c6f
@ -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`.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:output omit-xml-declaration="yes"/>
|
||||
<xsl:key name="k" match="a | b" use="@id"/>
|
||||
<xsl:template match="/"><out><xsl:for-each select="key('k','x')"><xsl:value-of select="name()"/>,</xsl:for-each></out></xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
let source = "<doc><a id=\"x\"/><wrap><b id=\"x\"/></wrap></doc>"
|
||||
#expect(try transform(style, source) == "<out>a,b,</out>")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user