diff --git a/CHANGELOG.md b/CHANGELOG.md
index df2d74e..3298964 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: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`.
diff --git a/Sources/XSLT/XSLTNumbering.swift b/Sources/XSLT/XSLTNumbering.swift
index 1d914a8..5e012f9 100644
--- a/Sources/XSLT/XSLTNumbering.swift
+++ b/Sources/XSLT/XSLTNumbering.swift
@@ -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 []
diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt
index 713f1e9..cfcc397 100644
--- a/Tests/Fixtures/xalan-baseline.txt
+++ b/Tests/Fixtures/xalan-baseline.txt
@@ -20,7 +20,6 @@ namespace/namespace19
namespace/namespace23
namespace/namespace24
namespace/namespace35
-numbering/numbering63
output/output22
output/output26
output/output63
diff --git a/Tests/XSLTNumberingTests.swift b/Tests/XSLTNumberingTests.swift
index bbe06d2..a22535d 100644
--- a/Tests/XSLTNumberingTests.swift
+++ b/Tests/XSLTNumberingTests.swift
@@ -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 = """
+
+
+
+ ,
+
+ """
+ #expect(try PureXML.XSLT.transform(stylesheet: style, source: "") == "1,2,")
+ }
+
@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