fix(xpath): parse a lone / as the root node before a terminator (#267)
The XPath grammar makes the relative part after / optional, but the compiler only treated / as the root location path when it was at the end of the expression (isAtEnd). When a / was followed by ), ], |, or an operator (as in count(/), generate-id(/), or / = .), it fell through and tried to parse a step from the terminator, yielding an empty node-set. Recognize / as the root whenever no location step follows. Closes Apache Xalan conformance cases position106 and mdocs15.
This commit is contained in:
parent
70fdcab840
commit
9070ccc3f1
@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A lone `/` (the root node) is now parsed correctly when it is followed by something other than the end of the expression, such as a `)`, `]`, `|`, or an operator. The XPath grammar makes the relative part after `/` optional, but the compiler only treated `/` as the root path at the very end of the expression, so `count(/)`, `generate-id(/)`, and `/ = .` tried to parse a step from the following token and yielded an empty node-set. The compiler now recognizes a `/` as the root whenever no location step follows it. Closes Apache Xalan conformance cases `position106` and `mdocs15`.
|
||||
|
||||
- A template match pattern written with an explicit `child::` or `attribute::` axis now gets the default priority of its node test (XSLT 1.0 5.5), not 0. `attribute::node()` is a `node()` node test, so its default priority is -0.5 (the same as `@*`), but it was treated as a name test and given priority 0, which made it wrongly outrank `@*`. The axis is now stripped before the node test is classified. Closes Apache Xalan conformance cases `conflictres29` and `conflictres30`.
|
||||
|
||||
- `xsl:strip-space` and `xsl:preserve-space` now match their `elements` name tests by namespace rather than by prefix (XSLT 1.0 3.4). A specifier `prefix:*` or `prefix:local` resolves the prefix to its namespace URI at parse time, and a source element is matched on its expanded name in order of specificity (an exact `{uri}local` over a namespace wildcard `{uri}*` over `*`), so a stylesheet that strips `ns1:*` strips a source element in that namespace whatever prefix the source bound it to, while leaving another namespace untouched. The tests previously compared the raw prefixed name strings, so a differing prefix did not match. Closes Apache Xalan conformance cases `whitespace06` and `whitespace07`.
|
||||
|
||||
@ -111,10 +111,13 @@ extension PureXML.XPath.Compiler {
|
||||
if consume("/") {
|
||||
steps.append(Self.descendantOrSelfStep())
|
||||
try steps.append(parseStep())
|
||||
} else if isAtEnd {
|
||||
return (true, [])
|
||||
} else {
|
||||
} else if startsStep() {
|
||||
try steps.append(parseStep())
|
||||
} else {
|
||||
// The relative part after `/` is optional, so a lone `/` is the
|
||||
// root node even when a `)`, `]`, `|`, or an operator follows it
|
||||
// rather than the end of the expression (e.g. `count(/)`).
|
||||
return (true, [])
|
||||
}
|
||||
} else {
|
||||
try steps.append(parseStep())
|
||||
@ -135,6 +138,15 @@ extension PureXML.XPath.Compiler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the next token begins a location step (an axis, `@`, `.`/`..`, a
|
||||
/// `*` wildcard, or a name/node-test), as opposed to ending the path (`)`,
|
||||
/// `]`, `|`, an operator, or the end). Used to recognize a lone `/` as root.
|
||||
private mutating func startsStep() -> Bool {
|
||||
skipSpace()
|
||||
guard let character = peek() else { return false }
|
||||
return character.isLetter || character == "_" || character == "@" || character == "." || character == "*"
|
||||
}
|
||||
|
||||
private mutating func parseStep() throws -> Step {
|
||||
skipSpace()
|
||||
if matches("..") {
|
||||
|
||||
@ -14,7 +14,6 @@ idkey/idkey48
|
||||
impincl/impincl08
|
||||
impincl/impincl27
|
||||
mdocs/mdocs07
|
||||
mdocs/mdocs15
|
||||
mdocs/mdocs19
|
||||
namedtemplate/namedtemplate12
|
||||
namespace/namespace113
|
||||
@ -29,7 +28,6 @@ output/output63
|
||||
output/output70
|
||||
output/output73
|
||||
output/output86
|
||||
position/position106
|
||||
processorinfo/processorinfo03
|
||||
sort/sort02
|
||||
sort/sort10
|
||||
|
||||
@ -25,6 +25,18 @@ struct XPathExpressionTests {
|
||||
try PureXML.XPath.Query(path).boolean(over: shop())
|
||||
}
|
||||
|
||||
@Test("a lone / is the root node, even before a closing paren or operator")
|
||||
func test_rootPathBeforeTerminator() throws {
|
||||
// The relative part after `/` is optional, so `/` followed by `)`, `|`, or
|
||||
// an operator is the root node, not a parse that swallows the terminator
|
||||
// (Apache Xalan position106, mdocs15).
|
||||
#expect(try number("count(/)") == 1)
|
||||
#expect(try number("count(/ | /)") == 1)
|
||||
#expect(try boolean("/ = /") == true)
|
||||
// Regression: `/` followed by a `*` wildcard still parses as a step.
|
||||
#expect(try number("count(/*)") == 1)
|
||||
}
|
||||
|
||||
// MARK: Arithmetic and precedence
|
||||
|
||||
@Test("Arithmetic honors precedence")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user