PureXML/Sources/XPath/XPathNode.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

132 lines
5.2 KiB
Swift

public extension PureXML.XPath {
/// An XPath node. Most nodes wrap a parent-aware ``PureXML/Model/TreeNode``
/// (element, text, CDATA, comment, processing instruction, or the document
/// root). Attributes and namespaces are not tree nodes, so XPath surfaces them
/// as nodes owned by their element, as the data model requires.
///
/// Equality and hashing are by identity: two wrappers of the same tree node,
/// attribute, or namespace compare equal, which is what node-set de-duplication
/// needs. ``documentOrder`` gives the total order XPath results are sorted into.
enum Node: Hashable {
case tree(PureXML.Model.TreeNode)
case attribute(owner: PureXML.Model.TreeNode, PureXML.Model.Attribute)
case namespace(owner: PureXML.Model.TreeNode, prefix: String, uri: String)
public static func == (lhs: Node, rhs: Node) -> Bool {
switch (lhs, rhs) {
case let (.tree(left), .tree(right)):
left === right
case let (.attribute(leftOwner, left), .attribute(rightOwner, right)):
leftOwner === rightOwner && left.name == right.name
case let (.namespace(leftOwner, leftPrefix, _), .namespace(rightOwner, rightPrefix, _)):
leftOwner === rightOwner && leftPrefix == rightPrefix
default:
false
}
}
public func hash(into hasher: inout Hasher) {
switch self {
case let .tree(node):
hasher.combine(0)
hasher.combine(ObjectIdentifier(node))
case let .attribute(owner, attribute):
hasher.combine(1)
hasher.combine(ObjectIdentifier(owner))
hasher.combine(attribute.name)
case let .namespace(owner, prefix, _):
hasher.combine(2)
hasher.combine(ObjectIdentifier(owner))
hasher.combine(prefix)
}
}
/// The wrapped tree node, when this is one (nil for attribute/namespace).
var treeNode: PureXML.Model.TreeNode? {
guard case let .tree(node) = self else { return nil }
return node
}
/// The owning element of an attribute or namespace node, or the tree
/// node's own parent for a tree node.
var parent: Node? {
switch self {
case let .tree(node):
node.parent.map(Node.tree)
case let .attribute(owner, _), let .namespace(owner, _, _):
.tree(owner)
}
}
/// The XPath string-value: an attribute's value, a namespace URI, or the
/// concatenated character data of a tree node's subtree.
var stringValue: String {
switch self {
case let .tree(node):
node.stringValue
case let .attribute(_, attribute):
attribute.value
case let .namespace(_, _, uri):
uri
}
}
/// The node's expanded name, or nil for nodes without a name (text, CDATA,
/// comment, document). For a namespace node the local part is the prefix.
var qualifiedName: PureXML.Model.QualifiedName? {
switch self {
case let .tree(node):
switch node.kind {
case .element, .processingInstruction:
node.name
default:
nil
}
case let .attribute(_, attribute):
attribute.name
case let .namespace(_, prefix, _):
PureXML.Model.QualifiedName(prefix)
}
}
/// The path of child indices from the document root, with negative bands
/// placing namespace nodes before attribute nodes before child nodes, so a
/// lexicographic compare yields XPath document order.
var documentOrder: [Int] {
switch self {
case let .tree(node):
return Self.treePath(node)
case let .attribute(owner, attribute):
let index = owner.attributes.firstIndex { $0.name == attribute.name } ?? 0
return Self.treePath(owner) + [Self.attributeBand, index]
case let .namespace(owner, prefix, _):
return Self.treePath(owner) + [Self.namespaceBand, prefix.hashValue]
}
}
private static let namespaceBand = -2
private static let attributeBand = -1
private static func treePath(_ node: PureXML.Model.TreeNode) -> [Int] {
var path: [Int] = []
var current = node
while let parent = current.parent {
let index = parent.children.firstIndex { $0 === current } ?? 0
path.append(index)
current = parent
}
return path.reversed()
}
/// Whether `lhs` precedes `rhs` in document order.
static func precedes(_ lhs: Node, _ rhs: Node) -> Bool {
let left = lhs.documentOrder
let right = rhs.documentOrder
for (one, two) in zip(left, right) where one != two {
return one < two
}
return left.count < right.count
}
}
}