PureXML/Tests/XSLTNumberingTests.swift
Mihaela Mihaljevic b84fdd00ef fix(xslt): no fatal traps on extreme format-number and xsl:number values
format-number of a value at or beyond 2^53 ran an unguarded
Double-to-Int conversion, a fatal trap reachable from user data; a
fraction picture beyond 15 places overflowed the scale conversion the
same way; and xsl:number value= beyond 2^53 trapped identically.
Digit extraction clamps fraction places to 15 (the 2^53 exactness
bound), pads back to the picture's minimum, renders out-of-range
integers through the exponent expander, and xsl:number falls back to
plain number rendering outside [1, 2^53). Sort-key rows always carry
one key per sort. Regression tests cover all three trap paths.
2026-06-10 15:35:36 +02:00

85 lines
3.7 KiB
Swift

@testable import PureXML
import Testing
/// 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=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>")
}
}