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.
117 lines
5.5 KiB
Swift
117 lines
5.5 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// The full `xsl:number` of XSLT 1.0 section 7.7 (#130): level
|
|
/// single/multiple/any with count and from patterns, the format-token engine
|
|
/// with punctuation and separators, digit grouping, and the value expression.
|
|
@Suite("XSLT numbering")
|
|
struct XSLTNumberingTests {
|
|
private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
|
|
|
|
private func transform(_ number: String, source: String) throws -> String {
|
|
let style = """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:output omit-xml-declaration="yes"/>
|
|
<xsl:template match="/"><out><xsl:apply-templates select="//mark"/></out></xsl:template>
|
|
<xsl:template match="mark">\(number)<xsl:text> </xsl:text></xsl:template>
|
|
</xsl:stylesheet>
|
|
"""
|
|
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
|
|
// sequence; f="A. " numbers the items A. B. (Apache Xalan namedtemplate12).
|
|
let emit = "<xsl:call-template name=\"n\">"
|
|
+ "<xsl:with-param name=\"f\">A. </xsl:with-param></xsl:call-template>"
|
|
let style = """
|
|
<xsl:stylesheet version="1.0" \(xsl)>
|
|
<xsl:output omit-xml-declaration="yes"/>
|
|
<xsl:template match="/"><out><xsl:apply-templates select="doc/i"/></out></xsl:template>
|
|
<xsl:template match="i">\(emit)</xsl:template>
|
|
<xsl:template name="n"><xsl:param name="f">1. </xsl:param><xsl:number format="{$f}"/></xsl:template>
|
|
</xsl:stylesheet>
|
|
"""
|
|
#expect(try PureXML.XSLT.transform(stylesheet: style, source: "<doc><i/><i/></doc>") == "<out>A. B. </out>")
|
|
}
|
|
|
|
@Test("level=multiple numbers every matching ancestor-or-self, outermost first")
|
|
func test_levelMultiple() throws {
|
|
let source = """
|
|
<doc><c><s><mark/></s><s><mark/><mark/></s></c><c><s><mark/></s></c></doc>
|
|
"""
|
|
let result = try transform(
|
|
"<xsl:number level=\"multiple\" count=\"c|s|mark\" format=\"1.1.\"/>",
|
|
source: source,
|
|
)
|
|
#expect(result == "<out>1.1.1. 1.2.1. 1.2.2. 2.1.1. </out>")
|
|
}
|
|
|
|
@Test("level=any counts matching nodes before this one, bounded by from")
|
|
func test_levelAny() throws {
|
|
let source = "<doc><c><mark/><mark/></c><c><mark/></c></doc>"
|
|
let across = try transform("<xsl:number level=\"any\" count=\"mark\"/>", source: source)
|
|
#expect(across == "<out>1 2 3 </out>")
|
|
let bounded = try transform("<xsl:number level=\"any\" count=\"mark\" from=\"c\"/>", source: source)
|
|
#expect(bounded == "<out>1 2 1 </out>")
|
|
}
|
|
|
|
@Test("single/multiple with no matching node renders as the empty string")
|
|
func test_emptyMatchIsEmptyOutput() throws {
|
|
let result = try transform(
|
|
"<xsl:number level=\"multiple\" count=\"missing\" format=\"1.1. \"/>",
|
|
source: "<doc><mark/></doc>",
|
|
)
|
|
#expect(result == "<out> </out>")
|
|
}
|
|
|
|
@Test("format tokens: padding, alphabetic, roman, punctuation, last-token reuse")
|
|
func test_formatTokens() {
|
|
#expect(XSLTNumbering.format([3], "(001) ") == "(003) ")
|
|
#expect(XSLTNumbering.format([2, 4], "A.i. ") == "B.iv. ")
|
|
#expect(XSLTNumbering.format([1, 2, 3], "1-1. ") == "1-2-3. ")
|
|
#expect(XSLTNumbering.format([1, 2, 3], "1") == "1.2.3")
|
|
#expect(XSLTNumbering.format([], "(1) ").isEmpty)
|
|
}
|
|
|
|
@Test("digit grouping applies the separator from the right")
|
|
func test_grouping() {
|
|
#expect(XSLTNumbering.format([1234], "1", (separator: ",", size: 3)) == "1,234")
|
|
#expect(XSLTNumbering.format([123], "1", (separator: ":", size: 2)) == "1:23")
|
|
#expect(XSLTNumbering.format([12], "1", (separator: ",", size: 3)) == "12")
|
|
}
|
|
|
|
@Test("value= evaluates the expression; below one it bypasses formatting")
|
|
func test_valueExpression() throws {
|
|
let formatted = try transform("<xsl:number value=\"7\" format=\"(I)\"/>", source: "<doc><mark/></doc>")
|
|
#expect(formatted == "<out>(VII) </out>")
|
|
let zero = try transform("<xsl:number value=\"0\" format=\"(I) \"/>", source: "<doc><mark/></doc>")
|
|
#expect(zero == "<out>0 </out>")
|
|
}
|
|
|
|
@Test("A value beyond 2^53 renders as a plain number without trapping")
|
|
func test_hugeValue() throws {
|
|
let result = try transform(
|
|
"<xsl:number value=\"99999999999999999999 * 99999999999999999999\"/>",
|
|
source: "<doc><mark/></doc>",
|
|
)
|
|
#expect(result == "<out>1" + String(repeating: "0", count: 40) + " </out>")
|
|
}
|
|
}
|