PureXML.XPath.Budget is the configurable counterpart of libxml2's fixed 10M node-set cap (libxml2Compatible preset included), thrown as a located QueryError instead of a silent refusal, default unbounded. xsl:sort honors lang with tailored alphabet orders for Polish and Russian derived from the conformance golds, case-folded. The WASM gate executes all 985 tests under wasmtime on an actual WASI runtime, wired into CI; the first run caught a real wasm32 trap (Int is 32 bits there, so format-number's digit extraction trapped at 2^31 despite the 2^53 guard; internals are now Int64 and the xsl:number bound is platform-correct). Xalan baseline 127 -> 125. Closes #143, closes #144.
169 lines
7.0 KiB
Swift
169 lines
7.0 KiB
Swift
/// A node queued for sorting: its original offset and computed keys.
|
|
private struct SortEntry {
|
|
let offset: Int
|
|
let node: PureXML.XPath.Node
|
|
let keys: [String]
|
|
}
|
|
|
|
extension PureXML.XSLT.Transformer {
|
|
func sorted(_ nodes: [PureXML.XPath.Node], _ sorts: [PureXML.XSLT.Sort], _ context: XSLTContext) -> [PureXML.XPath.Node] {
|
|
guard !sorts.isEmpty else { return nodes }
|
|
// Keys are computed once per node with the node's own evaluation
|
|
// context (its position and size in the selection, the caller's
|
|
// variables and bindings), so position()-based keys work.
|
|
let keyed = nodes.enumerated().map { offset, xnode -> SortEntry in
|
|
guard let owner = Self.ownerNode(xnode) else { return SortEntry(offset: offset, node: xnode, keys: sorts.map { _ in "" }) }
|
|
var keyContext = XSLTContext(
|
|
node: owner,
|
|
current: xnode.treeNode == nil ? xnode : nil,
|
|
position: offset + 1,
|
|
size: nodes.count,
|
|
variables: context.variables,
|
|
)
|
|
keyContext.namespaces = context.namespaces
|
|
return SortEntry(offset: offset, node: xnode, keys: sorts.map { string($0.select, keyContext) })
|
|
}
|
|
// A lang attribute value template evaluates once per sort, in the
|
|
// caller's context; a known language selects its tailored alphabet.
|
|
let tailorings: [[Character: Int]?] = sorts.map { sort in
|
|
sort.lang.flatMap { PureXML.XSLT.Collation.table(for: avt($0, context)) }
|
|
}
|
|
return keyed.sorted { lhs, rhs in
|
|
for (index, sort) in sorts.enumerated() {
|
|
let order: Int
|
|
if !sort.numeric, sort.caseOrder == nil, let ranks = tailorings[index] {
|
|
let tailored = PureXML.XSLT.Collation.compare(lhs.keys[index], rhs.keys[index], ranks)
|
|
order = sort.descending ? -tailored : tailored
|
|
} else {
|
|
order = Self.compareKeys(lhs.keys[index], rhs.keys[index], sort)
|
|
}
|
|
if order != 0 { return order < 0 }
|
|
}
|
|
return lhs.offset < rhs.offset
|
|
}.map(\.node)
|
|
}
|
|
|
|
static func compareKeys(_ left: String, _ right: String, _ sort: PureXML.XSLT.Sort) -> Int {
|
|
var order: Int
|
|
if sort.numeric {
|
|
let leftNumber = PureXML.XPath.Value.parseNumber(left)
|
|
let rightNumber = PureXML.XPath.Value.parseNumber(right)
|
|
// NaN keys sort before every number (the Xalan order).
|
|
switch (leftNumber.isNaN, rightNumber.isNaN) {
|
|
case (true, true): order = 0
|
|
case (true, false): order = -1
|
|
case (false, true): order = 1
|
|
case (false, false): order = leftNumber == rightNumber ? 0 : (leftNumber < rightNumber ? -1 : 1)
|
|
}
|
|
} else if let caseOrder = sort.caseOrder {
|
|
order = caseInsensitiveCompare(left, right, caseOrder)
|
|
} else {
|
|
order = collate(left, right)
|
|
}
|
|
return sort.descending ? -order : order
|
|
}
|
|
|
|
/// Text sort order: spaces are ignorable at the primary level (Java
|
|
/// Collator semantics, which the conformance golds encode); when the
|
|
/// space-stripped strings tie, the string whose first space comes later
|
|
/// (or that has none) sorts first.
|
|
static func collate(_ left: String, _ right: String) -> Int {
|
|
let strippedLeft = left.filter { $0 != " " }
|
|
let strippedRight = right.filter { $0 != " " }
|
|
if strippedLeft != strippedRight {
|
|
return strippedLeft < strippedRight ? -1 : 1
|
|
}
|
|
let leftSpaces = spacePositions(left)
|
|
let rightSpaces = spacePositions(right)
|
|
if leftSpaces == rightSpaces { return 0 }
|
|
for (leftIndex, rightIndex) in zip(leftSpaces, rightSpaces) where leftIndex != rightIndex {
|
|
return leftIndex > rightIndex ? -1 : 1
|
|
}
|
|
return leftSpaces.count < rightSpaces.count ? -1 : 1
|
|
}
|
|
|
|
private static func spacePositions(_ text: String) -> [Int] {
|
|
text.enumerated().compactMap { $1 == " " ? $0 : nil }
|
|
}
|
|
|
|
func forEach(
|
|
_ select: String,
|
|
_ sorts: [PureXML.XSLT.Sort],
|
|
_ body: [PureXML.XSLT.Instruction],
|
|
_ context: XSLTContext,
|
|
) -> [ResultItem] {
|
|
let nodes = sorted(selectXPathNodes(select, context), sorts, context)
|
|
var items: [ResultItem] = []
|
|
for (offset, xnode) in nodes.enumerated() {
|
|
guard let owner = Self.ownerNode(xnode) else { continue }
|
|
let itemContext = XSLTContext(
|
|
node: owner,
|
|
current: xnode.treeNode == nil ? xnode : nil,
|
|
position: offset + 1,
|
|
size: nodes.count,
|
|
variables: context.variables,
|
|
)
|
|
items += instantiate(body, itemContext)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func chooseInstruction(
|
|
_ whens: [PureXML.XSLT.Branch],
|
|
_ otherwise: [PureXML.XSLT.Instruction],
|
|
_ context: XSLTContext,
|
|
) -> [ResultItem] {
|
|
for branch in whens where boolean(branch.test, context) {
|
|
return instantiate(branch.body, context)
|
|
}
|
|
return instantiate(otherwise, context)
|
|
}
|
|
|
|
func builtInRule(_ xnode: PureXML.XPath.Node, mode: String?, _ context: XSLTContext) -> [ResultItem] {
|
|
switch xnode {
|
|
case let .tree(node):
|
|
switch node.kind {
|
|
case .element, .document:
|
|
applyTemplates(to: node.children.map { .tree($0) }, mode: mode, parameters: [], context)
|
|
case .text, .cdata:
|
|
[.node(.text(node.value))]
|
|
default:
|
|
[]
|
|
}
|
|
case let .attribute(_, attribute):
|
|
[.node(.text(attribute.value))]
|
|
case let .namespace(_, _, uri):
|
|
[.node(.text(uri))]
|
|
}
|
|
}
|
|
|
|
func message(_ terminate: Bool, _ body: [PureXML.XSLT.Instruction], _ context: XSLTContext) -> [ResultItem] {
|
|
if terminate, termination.message == nil {
|
|
termination.message = Self.text(of: instantiate(body, context))
|
|
}
|
|
return []
|
|
}
|
|
|
|
/// Binds the template's parameters (a passed `with-param` wins over the
|
|
/// declared default) and instantiates its body.
|
|
func instantiateTemplate(
|
|
_ template: PureXML.XSLT.Template,
|
|
_ context: XSLTContext,
|
|
passing parameters: [PureXML.XSLT.Binding],
|
|
from caller: XSLTContext,
|
|
) -> [ResultItem] {
|
|
var context = context
|
|
context.importPrecedence = template.importPrecedence
|
|
context.importRangeLow = template.importRangeLow
|
|
context.namespaces = template.namespaces
|
|
for parameter in template.parameters {
|
|
if let passed = parameters.first(where: { $0.name == parameter.name }) {
|
|
context.variables[parameter.name] = variableValue(passed.select, passed.body, caller)
|
|
} else {
|
|
context.variables[parameter.name] = variableValue(parameter.select, parameter.body, context)
|
|
}
|
|
}
|
|
return instantiate(template.body, context)
|
|
}
|
|
}
|