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
132 lines
5.2 KiB
Swift
132 lines
5.2 KiB
Swift
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)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|