fix(xslt): count the from boundary node in xsl:number level=single when it matches count (#270)

XSLT 1.0 7.7: for level=single the count node may be the from-boundary node
itself when that node matches both the count and from patterns. The walk
stopped at the nearest from-matching ancestor before testing it against count,
so count="a|b" from="a" gave an empty number for a node inside an a. Test the
count pattern before the boundary stops the walk.

Closes Apache Xalan conformance case numbering63.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 10:47:49 +02:00 committed by GitHub
parent e7a8f473ef
commit 42a56abcbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 21 additions and 2 deletions

View File

@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- `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`.
- The `format` attribute of `xsl:number` is now evaluated as an attribute value template (XSLT 1.0 7.7), so `format="{$f}"` uses the value of `$f` rather than the literal string. It was stored and used verbatim, so a computed or passed-in format was never applied. Closes Apache Xalan conformance case `namedtemplate12`.
- A large integer-valued number now converts to a string with no decimal point (XPath 1.0 4.2). The number-to-string formatter only treated a value below 1e15 as an integer, so an integer such as `1234567890123456` (still exact in a double, which represents integers exactly up to 2^53) printed as `1234567890123456.0`. The integer bound is now 2^63, the range in which the conversion is safe. Closes Apache Xalan conformance case `string132`.

View File

@ -42,10 +42,13 @@ enum XSLTNumbering {
return [total]
default: // single
var current: Tree? = node
while let candidate = current, !matchesFrom(candidate) {
while let candidate = current {
// The count node may be the from node itself when it matches both
// patterns (7.7), so test count before stopping at the boundary.
if matchesCount(candidate) {
return [siblingPosition(of: candidate, matching: matchesCount)]
}
if matchesFrom(candidate) { break }
current = candidate.parent
}
return []

View File

@ -20,7 +20,6 @@ namespace/namespace19
namespace/namespace23
namespace/namespace24
namespace/namespace35
numbering/numbering63
output/output22
output/output26
output/output63

View File

@ -19,6 +19,21 @@ struct XSLTNumberingTests {
return try PureXML.XSLT.transform(stylesheet: style, source: source)
}
@Test("level=single counts the from node itself when it also matches count (7.7)")
func test_singleFromCountOverlap() throws {
// count and from both match `a`; a t's nearest count ancestor is its
// parent a, which is also the from boundary, so it is still counted
// (Apache Xalan numbering63).
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/"><out><xsl:apply-templates select="//t"/></out></xsl:template>
<xsl:template match="t"><xsl:number level="single" count="a|b" from="a"/>,</xsl:template>
</xsl:stylesheet>
"""
#expect(try PureXML.XSLT.transform(stylesheet: style, source: "<doc><a><t/></a><a><t/></a></doc>") == "<out>1,2,</out>")
}
@Test("the format attribute is an attribute value template (7.7)")
func test_formatIsAVT() throws {
// format="{$f}" evaluates the AVT, so a passed-in format string drives the