Add the PureXML.Pattern namespace: a compiler for the streamable XPath subset (names, *, prefix:*, /, //, leading absolute /, trailing @attr) into a Matcher, with predicates and . / .. rejected. The backward-matching algorithm tests an element's ancestor-or-self path. Pattern.matches(_:in:) streams a document through the pull parser and returns matching node paths in document order from the open-element stack alone, no tree built. Closes #44 Closes #45 Closes #22
55 lines
2.0 KiB
Swift
55 lines
2.0 KiB
Swift
public extension PureXML.Pattern {
|
|
/// Streams `xml` through the pull parser and returns the paths of every node
|
|
/// that matches `pattern`, in document order, without building a tree. Each
|
|
/// path is rendered root-first as `/a/b/c`, and an attribute match as
|
|
/// `/a/b/@id`. The match is decided from the open-element stack alone, so the
|
|
/// document is never held whole.
|
|
static func matches(
|
|
_ pattern: String,
|
|
in xml: String,
|
|
limits: PureXML.Parsing.Limits = .default,
|
|
) throws -> [String] {
|
|
let matcher = try Matcher(pattern)
|
|
return try matcher.matches(in: xml, limits: limits)
|
|
}
|
|
}
|
|
|
|
public extension PureXML.Pattern.Matcher {
|
|
/// Streams `xml` and returns the matching node paths in document order.
|
|
func matches(
|
|
in xml: String,
|
|
limits: PureXML.Parsing.Limits = .default,
|
|
) throws -> [String] {
|
|
var reader = PureXML.Parsing.EventReader(xml, limits: limits)
|
|
var stack: [PureXML.Model.QualifiedName] = []
|
|
var results: [String] = []
|
|
while let event = try reader.next() {
|
|
switch event {
|
|
case let .startElement(name, attributes):
|
|
stack.append(name)
|
|
collect(stack: stack, attributes: attributes, into: &results)
|
|
case .endElement:
|
|
if !stack.isEmpty { stack.removeLast() }
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
private func collect(
|
|
stack: [PureXML.Model.QualifiedName],
|
|
attributes: [PureXML.Model.Attribute],
|
|
into results: inout [String],
|
|
) {
|
|
let base = "/" + stack.map(\.description).joined(separator: "/")
|
|
if matchesAttributes {
|
|
for attribute in matchingAttributes(path: stack, attributes: attributes) {
|
|
results.append(base + "/@" + attribute.name.description)
|
|
}
|
|
} else if matchesElement(path: stack) {
|
|
results.append(base)
|
|
}
|
|
}
|
|
}
|