PureXML/Sources/XPath/EXSLTFunctions.swift
Mihaela Mihaljevic 69678d31a1 feat(xpath): EXSLT common, math, and sets extension functions
Add the EXSLT common/math/sets modules, dispatched by resolving a function's
prefix to its namespace (FunctionTable.call falls back to namespace lookup),
so any prefix bound to an EXSLT namespace works. math:min/max/highest/lowest/
abs/sqrt (with the NaN rule), set:distinct/difference/intersection/
has-same-node/leading/trailing (by node identity and document order), and
exsl:object-type. math:power and the transcendentals are omitted (they need a
C math library the Foundation-free target excludes); exsl:node-set awaits
result-tree-fragment support.

Part of #105 (Tier 3)
2026-06-09 18:31:50 +02:00

123 lines
5.7 KiB
Swift

extension PureXML.XPath {
/// The EXSLT extension functions (http://exslt.org/...), dispatched by the
/// namespace a function's prefix resolves to: the `math`, `sets`, and the
/// `common` modules. Registered through ``FunctionTable`` so they are
/// available wherever a prefix is bound to an EXSLT namespace.
enum EXSLT {
static let common = "http://exslt.org/common"
static let math = "http://exslt.org/math"
static let sets = "http://exslt.org/sets"
/// The implementation for an EXSLT `local` name in namespace `uri`, or nil.
static func implementation(uri: String, local: String) -> FunctionImplementation? {
switch uri {
case math: mathFunction(local)
case sets: setsFunction(local)
case common: commonFunction(local)
default: nil
}
}
// MARK: common
private static func commonFunction(_ local: String) -> FunctionImplementation? {
guard local == "object-type" else { return nil }
return { arguments, _ in
switch arguments.first {
case .nodeSet: .string("node-set")
case .boolean: .string("boolean")
case .number: .string("number")
case .string, .none: .string("string")
}
}
}
// MARK: math
private static func mathFunction(_ local: String) -> FunctionImplementation? {
switch local {
case "min": { arguments, _ in .number(extremum(arguments.first, isMin: true)) }
case "max": { arguments, _ in .number(extremum(arguments.first, isMin: false)) }
case "highest": { arguments, _ in .nodeSet(extremeNodes(arguments.first, isMin: false)) }
case "lowest": { arguments, _ in .nodeSet(extremeNodes(arguments.first, isMin: true)) }
case "abs": { arguments, _ in .number(abs(arguments.first?.number ?? .nan)) }
case "sqrt": { arguments, _ in .number((arguments.first?.number ?? .nan).squareRoot()) }
default: nil
}
}
/// The minimum or maximum of a node-set's numeric values; NaN for an empty
/// set or any non-numeric node (the EXSLT rule).
private static func extremum(_ value: Value?, isMin: Bool) -> Double {
guard let nodes = value?.nodes, !nodes.isEmpty else { return .nan }
let numbers = nodes.map { PureXML.XPath.Value.parseNumber($0.stringValue) }
if numbers.contains(where: \.isNaN) { return .nan }
return isMin ? numbers.min() ?? .nan : numbers.max() ?? .nan
}
/// The nodes whose numeric value equals the set's maximum (or minimum),
/// in document order; empty when any value is NaN.
private static func extremeNodes(_ value: Value?, isMin: Bool) -> [Node] {
guard let nodes = value?.nodes, !nodes.isEmpty else { return [] }
let target = extremum(value, isMin: isMin)
guard !target.isNaN else { return [] }
return nodes
.filter { PureXML.XPath.Value.parseNumber($0.stringValue) == target }
.sorted(by: Node.precedes)
}
// MARK: sets
private static func setsFunction(_ local: String) -> FunctionImplementation? {
switch local {
case "distinct": { arguments, _ in .nodeSet(distinct(arguments.first)) }
case "difference": { arguments, _ in .nodeSet(difference(arguments)) }
case "intersection": { arguments, _ in .nodeSet(intersection(arguments)) }
case "has-same-node": { arguments, _ in .boolean(hasSameNode(arguments)) }
case "leading": { arguments, _ in .nodeSet(relative(arguments, leading: true)) }
case "trailing": { arguments, _ in .nodeSet(relative(arguments, leading: false)) }
default: nil
}
}
private static func distinct(_ value: Value?) -> [Node] {
guard let nodes = value?.nodes else { return [] }
var seen: Set<String> = []
var result: [Node] = []
for node in nodes.sorted(by: Node.precedes) where seen.insert(node.stringValue).inserted {
result.append(node)
}
return result
}
private static func difference(_ arguments: [Value]) -> [Node] {
let first = arguments.first?.nodes ?? []
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
return first.filter { !second.contains($0) }.sorted(by: Node.precedes)
}
private static func intersection(_ arguments: [Value]) -> [Node] {
let first = arguments.first?.nodes ?? []
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
return first.filter { second.contains($0) }.sorted(by: Node.precedes)
}
private static func hasSameNode(_ arguments: [Value]) -> Bool {
let second = Set(arguments.count > 1 ? arguments[1].nodes ?? [] : [])
return (arguments.first?.nodes ?? []).contains { second.contains($0) }
}
/// `set:leading` / `set:trailing`: the nodes of the first set that precede
/// (or follow) the first node, in document order, of the second set.
private static func relative(_ arguments: [Value], leading: Bool) -> [Node] {
let first = arguments.first?.nodes ?? []
guard let pivot = (arguments.count > 1 ? arguments[1].nodes ?? [] : []).min(by: Node.precedes) else {
return []
}
return first
.filter { leading ? Node.precedes($0, pivot) : Node.precedes(pivot, $0) }
.sorted(by: Node.precedes)
}
}
}