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 } } }