PureXML/Sources/XPath/Selection.swift
Mihaela Mihaljevic 1ae6680d94 feat(xpath): add an XPath 1.0 location-path subset
Add PureXML.XPath: a compiled location-path query engine reimplemented from the
XPath 1.0 specification; no upstream code copied.

- Compiler parses a path into steps; Evaluator walks the tree returning a
  Selection node-set (node or attribute) in document order.
- Forward axes only (child, descendant //, self ., attribute @), since the value
  model has no parent pointers. Node tests: name, *, text(), node(), comment().
  Predicates: [n], [@a], [@a='v'], [child], [child='v'].
- Public surface: XPath.Query (init compiles, evaluate/elements/strings), the
  Selection enum (element / stringValue), QueryError, and the one-shot
  PureXML.xpath(_:over:). Upward/sibling axes and the full expression language
  are explicitly out of scope.
- 12 XPath tests; 61 total.
2026-06-08 01:32:23 +02:00

42 lines
1.5 KiB
Swift

public extension PureXML.XPath {
/// A node selected by an XPath query: either a tree node or an attribute
/// (attributes are not ``PureXML/Model/Node`` values, so they are surfaced
/// here alongside nodes).
enum Selection: Equatable, Sendable {
case node(PureXML.Model.Node)
case attribute(PureXML.Model.Attribute)
/// The selected element, when this selection is an element node.
public var element: PureXML.Model.Element? {
guard case let .node(node) = self, case let .element(element) = node else {
return nil
}
return element
}
/// The XPath string-value: an attribute's value, or the concatenation of
/// all descendant character data for a node.
public var stringValue: String {
switch self {
case let .attribute(attribute):
attribute.value
case let .node(node):
Self.stringValue(of: node)
}
}
private static func stringValue(of node: PureXML.Model.Node) -> String {
switch node {
case let .text(value), let .cdata(value), let .comment(value):
value
case let .processingInstruction(_, data):
data
case let .element(element):
element.children.reduce(into: "") { $0 += stringValue(of: $1) }
case let .document(children):
children.reduce(into: "") { $0 += stringValue(of: $1) }
}
}
}
}