From 971a7e9f560c2e9110fc46320b6dfe8e70375a7c Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Fri, 26 Jun 2026 00:40:16 +0200 Subject: [PATCH] perf(xslt): make xsl:number level="any" quadratic, not cubic (#309) walkPreceding visits a node and everything before it in document order. It found each preceding sibling with precedingSibling(of:), which rescans the parent's whole child list, so visiting k preceding siblings was O(k^2) and numbering n nodes under a wide parent was O(n^3): 1000 items took ~4.7s and 4000 did not finish in minutes. Find the candidate's index in the child list once, then walk the children before it directly (parent.children[.. Tree? { - guard let parent = node.parent else { return nil } - var previous: Tree? - for sibling in parent.children { - if sibling === node { return previous } - previous = sibling - } - return nil - } - // MARK: Format tokens /// Renders `numbers` with the 7.7.1 format engine. An empty list (a diff --git a/Tests/XSLTNumberAnyTests.swift b/Tests/XSLTNumberAnyTests.swift new file mode 100644 index 0000000..bf27c00 --- /dev/null +++ b/Tests/XSLTNumberAnyTests.swift @@ -0,0 +1,42 @@ +import Testing +@testable import PureXML + +/// `xsl:number level="any"` numbers a node by the count of matching nodes that +/// precede it (in document order, ancestors included) plus one. The walk over +/// preceding nodes now visits each preceding sibling's subtree directly instead +/// of rescanning the child list per sibling. These pin that the numbering is +/// unchanged: sequential in document order across nesting, counting only the +/// nodes the `count` pattern matches. +@Suite("xsl:number level=any") +struct XSLTNumberAnyTests { + private func numbers(_ source: String, count: String = "item") throws -> [String] { + let style = """ + + + + + + """ + let out = try PureXML.XSLT.transform(stylesheet: style, source: source) + // Extract the digits between .... + return out.components(separatedBy: "").dropFirst().map { String($0.prefix(while: { $0 != "<" })) } + } + + @Test("items are numbered sequentially in document order across nesting") + func test_sequential() throws { + let source = "" + #expect(try numbers(source) == ["1", "2", "3", "4"]) + } + + @Test("only nodes the count pattern matches are counted") + func test_countPattern() throws { + // elements are interleaved but not counted; items still 1..3. + let source = "" + #expect(try numbers(source) == ["1", "2", "3"]) + } + + @Test("a single item is number one") + func test_single() throws { + #expect(try numbers("") == ["1"]) + } +}