PureXML/Sources/XPath/AxisNavigation.swift
Mihaela Mihaljevic 6b7853f2d5 feat(xpath): all thirteen axes via parent-aware navigation
Rebuild the XPath engine on the parent-aware tree. Add an XPath node
abstraction (tree, attribute, namespace) with document order, and navigate
all thirteen axes: child, descendant, parent, ancestor, following-sibling,
preceding-sibling, following, preceding, attribute, namespace, self,
descendant-or-self, ancestor-or-self. Support the ./../@/// abbreviations
and processing-instruction() node tests. Reverse axes number nearest-first
for positional predicates; results return de-duplicated in document order.
The attribute axis excludes xmlns declarations; the namespace axis surfaces
in-scope namespace nodes.

Closes #40
2026-06-08 05:15:14 +02:00

164 lines
6.9 KiB
Swift

extension PureXML.XPath {
/// Produces the nodes reachable from a context node along an axis, before any
/// node test or predicate. Forward axes come back in document order and
/// reverse axes nearest-first, so positional predicates number correctly.
enum AxisNavigation {
private static let xmlNamespaceURI = "http://www.w3.org/XML/1998/namespace"
static func nodes(on axis: Axis, from context: Node) -> [Node] {
verticalNodes(on: axis, from: context) ?? lateralNodes(on: axis, from: context)
}
/// The self, child, descendant, parent, and ancestor families; nil for an
/// axis handled by ``lateralNodes(on:from:)``.
private static func verticalNodes(on axis: Axis, from context: Node) -> [Node]? {
switch axis {
case .child: children(of: context)
case .descendant: descendants(of: context)
case .descendantOrSelf: [context] + descendants(of: context)
case .parent: context.parent.map { [$0] } ?? []
case .ancestor: ancestors(of: context)
case .ancestorOrSelf: [context] + ancestors(of: context)
case .selfAxis: [context]
default: nil
}
}
/// The sibling, following/preceding, attribute, and namespace axes.
private static func lateralNodes(on axis: Axis, from context: Node) -> [Node] {
switch axis {
case .followingSibling: followingSiblings(of: context)
case .precedingSibling: precedingSiblings(of: context)
case .following: following(of: context)
case .preceding: preceding(of: context)
case .attribute: attributes(of: context)
case .namespace: namespaces(of: context)
default: []
}
}
static func children(of node: Node) -> [Node] {
guard case let .tree(tree) = node else { return [] }
return tree.children.map(Node.tree)
}
static func descendants(of node: Node) -> [Node] {
var result: [Node] = []
for child in children(of: node) {
result.append(child)
result.append(contentsOf: descendants(of: child))
}
return result
}
static func ancestors(of node: Node) -> [Node] {
var result: [Node] = []
var current = node.parent
while let ancestor = current {
result.append(ancestor)
current = ancestor.parent
}
return result
}
private static func followingSiblings(of node: Node) -> [Node] {
guard let (parent, index) = siblingPosition(of: node) else { return [] }
return parent.children[(index + 1)...].map(Node.tree)
}
private static func precedingSiblings(of node: Node) -> [Node] {
guard let (parent, index) = siblingPosition(of: node) else { return [] }
return parent.children[..<index].reversed().map(Node.tree)
}
private static func following(of node: Node) -> [Node] {
let all = documentNodes(of: node)
guard let index = all.firstIndex(of: node) else { return [] }
let excluded = Set(descendants(of: node))
return all[(index + 1)...].filter { !excluded.contains($0) }
}
private static func preceding(of node: Node) -> [Node] {
let all = documentNodes(of: node)
guard let index = all.firstIndex(of: node) else { return [] }
let excluded = Set(ancestors(of: node))
return all[..<index].filter { !excluded.contains($0) }.reversed()
}
private static func attributes(of node: Node) -> [Node] {
guard case let .tree(tree) = node, tree.kind == .element else { return [] }
return tree.attributes
.filter { !isNamespaceDeclaration($0) }
.map { Node.attribute(owner: tree, $0) }
}
private static func namespaces(of node: Node) -> [Node] {
guard case let .tree(tree) = node, tree.kind == .element else { return [] }
var bindings: [String: String] = ["xml": xmlNamespaceURI]
var current: PureXML.Model.TreeNode? = tree
while let element = current {
if element.kind == .element {
collectBindings(from: element, into: &bindings)
}
current = element.parent
}
return bindings.compactMap { prefix, uri in
uri.isEmpty ? nil : Node.namespace(owner: tree, prefix: prefix, uri: uri)
}
}
private static func collectBindings(
from element: PureXML.Model.TreeNode,
into bindings: inout [String: String],
) {
for attribute in element.attributes {
let name = attribute.name
if name.prefix == nil, name.localName == "xmlns" {
if bindings[""] == nil { bindings[""] = attribute.value }
} else if name.prefix == "xmlns", bindings[name.localName] == nil {
bindings[name.localName] = attribute.value
}
}
}
private static func isNamespaceDeclaration(_ attribute: PureXML.Model.Attribute) -> Bool {
let name = attribute.name
return name.prefix == "xmlns" || (name.prefix == nil && name.localName == "xmlns")
}
private static func siblingPosition(of node: Node) -> (PureXML.Model.TreeNode, Int)? {
guard case let .tree(tree) = node, let parent = tree.parent else { return nil }
guard let index = parent.children.firstIndex(where: { $0 === tree }) else { return nil }
return (parent, index)
}
/// Every tree node of the containing document, in document order. Used to
/// derive the following and preceding axes by position.
private static func documentNodes(of node: Node) -> [Node] {
guard let root = rootTree(of: node) else { return [] }
var result: [Node] = []
appendSubtree(root, into: &result)
return result
}
private static func appendSubtree(_ tree: PureXML.Model.TreeNode, into result: inout [Node]) {
result.append(.tree(tree))
for child in tree.children {
appendSubtree(child, into: &result)
}
}
private static func rootTree(of node: Node) -> PureXML.Model.TreeNode? {
let start: PureXML.Model.TreeNode? = switch node {
case let .tree(tree): tree
case let .attribute(owner, _), let .namespace(owner, _, _): owner
}
guard var current = start else { return nil }
while let parent = current.parent {
current = parent
}
return current
}
}
}