PureXML/Sources/XPath/EvaluatorSteps.swift
Mihaela Mihaljevic d83e5a88ce feat(xpath): expression engine and the four-type model
Compile and evaluate full XPath 1.0 expressions, not just location paths.
Add the four-type Value (node-set, boolean, number, string) with the spec's
coercion rules and canonical number formatting, a recursive-descent parser
for the operator grammar (or/and, equality, relational, additive,
multiplicative, unary minus, union) whose grammatical position disambiguates
* and the operator keywords, function calls, variable references, and filter
expressions. Predicates are now arbitrary expressions (a numeric predicate
is a position test). Comparison follows the node-set quantification rules.
Query gains value/number/string/boolean accessors and a $variable binding
map; the core functions last, position, count, not, true, false, boolean,
number, and string ship with it.

Closes #42
2026-06-08 05:33:13 +02:00

118 lines
4.5 KiB
Swift

extension PureXML.XPath.Evaluator {
typealias Node = PureXML.XPath.Node
typealias Step = PureXML.XPath.Step
typealias NodeTest = PureXML.XPath.NodeTest
typealias Axis = PureXML.XPath.Axis
typealias Expression = PureXML.XPath.Expression
typealias EvaluationContext = PureXML.XPath.EvaluationContext
typealias Value = PureXML.XPath.Value
typealias PrincipalKind = PureXML.XPath.PrincipalKind
/// Walks the steps from a starting node-set, threading each step's result into
/// the next. Predicates run with proximity position within each context node's
/// axis result.
static func evaluateSteps(_ steps: [Step], from start: [Node], _ context: EvaluationContext) -> [Node] {
var current = start
for step in steps {
var result: [Node] = []
var seen: Set<Node> = []
for contextNode in current {
let matched = PureXML.XPath.AxisNavigation.nodes(on: step.axis, from: contextNode)
.filter { matches($0, step.test, on: step.axis) }
let filtered = (try? applyPredicates(step.predicates, to: matched, context)) ?? matched
for node in filtered where seen.insert(node).inserted {
result.append(node)
}
}
current = result
}
return current
}
/// Filters nodes through each predicate in turn. A numeric predicate is a
/// position test; any other is taken as a boolean.
static func applyPredicates(
_ predicates: [Expression],
to nodes: [Node],
_ context: EvaluationContext,
) throws -> [Node] {
var current = nodes
for predicate in predicates {
var kept: [Node] = []
let size = current.count
for (offset, node) in current.enumerated() {
let focused = context.focused(on: node, position: offset + 1, size: size)
if try keeps(predicate, focused, position: offset + 1) {
kept.append(node)
}
}
current = kept
}
return current
}
private static func keeps(_ predicate: Expression, _ context: EvaluationContext, position: Int) throws -> Bool {
let value = try eval(predicate, context)
if case let .number(number) = value {
return Int(number) == position
}
return value.boolean
}
// MARK: Node tests
static func matches(_ node: Node, _ test: NodeTest, on axis: Axis) -> Bool {
switch test {
case let .name(name):
nameMatches(node, name, axis.principalKind)
case .wildcard:
wildcardMatches(node, axis.principalKind)
case .text:
isTreeKind(node, in: [.text, .cdata])
case .node:
true
case .comment:
isTreeKind(node, in: [.comment])
case let .processingInstruction(target):
processingInstructionMatches(node, target)
}
}
private static func nameMatches(_ node: Node, _ name: String, _ kind: PrincipalKind) -> Bool {
switch kind {
case .element:
guard case let .tree(tree) = node, tree.kind == .element, let qualified = tree.name else {
return false
}
return qualified.description == name || qualified.localName == name
case .attribute:
guard case let .attribute(_, attribute) = node else { return false }
return attribute.name.description == name || attribute.name.localName == name
case .namespace:
guard case let .namespace(_, prefix, _) = node else { return false }
return prefix == name
}
}
private static func wildcardMatches(_ node: Node, _ kind: PrincipalKind) -> Bool {
switch kind {
case .element: return isTreeKind(node, in: [.element])
case .attribute: if case .attribute = node { return true }
return false
case .namespace: if case .namespace = node { return true }
return false
}
}
private static func processingInstructionMatches(_ node: Node, _ target: String?) -> Bool {
guard case let .tree(tree) = node, tree.kind == .processingInstruction else { return false }
guard let target else { return true }
return tree.name?.description == target
}
private static func isTreeKind(_ node: Node, in kinds: [PureXML.Model.TreeNodeKind]) -> Bool {
guard case let .tree(tree) = node else { return false }
return kinds.contains(tree.kind)
}
}