A top-level variable evaluates with every previously evaluated global in scope, so referencing an earlier top-level param works, directly or via call-template; same-name globals resolve by import precedence with the last fold winning, so a later sibling import beats an earlier one; the stylesheet fold uniformly favors later contributions, matching the post-order precedence model; and the XPath compiler accepts NCName:* name tests, matching any name in the prefix's namespace at the -0.25 default priority, so @ped:* outranks @*. Xalan baseline 144 -> 134 (variable 10 -> 5, conflictres 8 -> 5).
146 lines
6.2 KiB
Swift
146 lines
6.2 KiB
Swift
extension PureXML.XPath.Evaluator {
|
|
typealias Node = PureXML.XPath.Node
|
|
typealias Step = PureXML.XPath.Step
|
|
typealias NodeTest = PureXML.XPath.NodeTest
|
|
typealias Axis = PureXML.XPath.Axis
|
|
typealias Expression = PureXML.XPath.Expression
|
|
typealias EvaluationContext = PureXML.XPath.EvaluationContext
|
|
typealias Value = PureXML.XPath.Value
|
|
typealias PrincipalKind = PureXML.XPath.PrincipalKind
|
|
|
|
/// Walks the steps from a starting node-set, threading each step's result into
|
|
/// the next. Predicates run with proximity position within each context node's
|
|
/// axis result.
|
|
static func evaluateSteps(_ steps: [Step], from start: [Node], _ context: EvaluationContext) -> [Node] {
|
|
var current = start
|
|
for step in steps {
|
|
var result: [Node] = []
|
|
var seen: Set<Node> = []
|
|
for contextNode in current {
|
|
let matched = PureXML.XPath.AxisNavigation.nodes(on: step.axis, from: contextNode)
|
|
.filter { matches($0, step.test, on: step.axis, namespaces: context.namespaces) }
|
|
let filtered = (try? applyPredicates(step.predicates, to: matched, context)) ?? matched
|
|
for node in filtered where seen.insert(node).inserted {
|
|
result.append(node)
|
|
}
|
|
}
|
|
current = result
|
|
}
|
|
return current
|
|
}
|
|
|
|
/// Filters nodes through each predicate in turn. A numeric predicate is a
|
|
/// position test; any other is taken as a boolean.
|
|
static func applyPredicates(
|
|
_ predicates: [Expression],
|
|
to nodes: [Node],
|
|
_ context: EvaluationContext,
|
|
) throws -> [Node] {
|
|
var current = nodes
|
|
for predicate in predicates {
|
|
var kept: [Node] = []
|
|
let size = current.count
|
|
for (offset, node) in current.enumerated() {
|
|
let focused = context.focused(on: node, position: offset + 1, size: size)
|
|
if try keeps(predicate, focused, position: offset + 1) {
|
|
kept.append(node)
|
|
}
|
|
}
|
|
current = kept
|
|
}
|
|
return current
|
|
}
|
|
|
|
private static func keeps(_ predicate: Expression, _ context: EvaluationContext, position: Int) throws -> Bool {
|
|
let value = try eval(predicate, context)
|
|
if case let .number(number) = value {
|
|
return Int(number) == position
|
|
}
|
|
return value.boolean
|
|
}
|
|
|
|
// MARK: Node tests
|
|
|
|
static func matches(_ node: Node, _ test: NodeTest, on axis: Axis, namespaces: [String: String] = [:]) -> Bool {
|
|
switch test {
|
|
case let .name(name):
|
|
nameMatches(node, name, axis.principalKind, namespaces)
|
|
case .wildcard:
|
|
wildcardMatches(node, axis.principalKind)
|
|
case .text:
|
|
isTreeKind(node, in: [.text, .cdata])
|
|
case .node:
|
|
true
|
|
case .comment:
|
|
isTreeKind(node, in: [.comment])
|
|
case let .processingInstruction(target):
|
|
processingInstructionMatches(node, target)
|
|
}
|
|
}
|
|
|
|
private static func nameMatches(_ node: Node, _ name: String, _ kind: PrincipalKind, _ namespaces: [String: String]) -> Bool {
|
|
switch kind {
|
|
case .element:
|
|
guard case let .tree(tree) = node, tree.kind == .element, let qualified = tree.name else {
|
|
return false
|
|
}
|
|
return qualifiedMatches(qualified, name, namespaces)
|
|
case .attribute:
|
|
guard case let .attribute(_, attribute) = node else { return false }
|
|
return qualifiedMatches(attribute.name, name, namespaces)
|
|
case .namespace:
|
|
guard case let .namespace(_, prefix, _) = node else { return false }
|
|
return prefix == name
|
|
}
|
|
}
|
|
|
|
/// Matches a node's qualified name against a test name. When an eval-time
|
|
/// prefix binding covers the test's prefix, the match is by namespace URI and
|
|
/// local name; otherwise it falls back to the in-document prefix string (the
|
|
/// behavior when no bindings are supplied).
|
|
private static func qualifiedMatches(_ qualified: PureXML.Model.QualifiedName, _ name: String, _ namespaces: [String: String]) -> Bool {
|
|
if !namespaces.isEmpty {
|
|
if let colon = name.firstIndex(of: ":") {
|
|
let prefix = String(name[..<colon])
|
|
if let uri = namespaces[prefix] {
|
|
let local = String(name[name.index(after: colon)...])
|
|
// The NCName:* form matches every name in the namespace.
|
|
if local == "*" { return (qualified.namespaceURI ?? "") == uri }
|
|
return qualified.localName == local && (qualified.namespaceURI ?? "") == uri
|
|
}
|
|
} else {
|
|
// With bindings supplied, the XPath 1.0 rule applies exactly:
|
|
// an unprefixed name test selects the null namespace.
|
|
return qualified.localName == name && (qualified.namespaceURI ?? "").isEmpty
|
|
}
|
|
}
|
|
if name.hasSuffix(":*"), let colon = name.firstIndex(of: ":") {
|
|
// Without bindings the prefix-wildcard falls back to the
|
|
// in-document prefix string, like plain prefixed tests.
|
|
return qualified.prefix == String(name[..<colon])
|
|
}
|
|
return qualified.description == name || qualified.localName == name
|
|
}
|
|
|
|
private static func wildcardMatches(_ node: Node, _ kind: PrincipalKind) -> Bool {
|
|
switch kind {
|
|
case .element: return isTreeKind(node, in: [.element])
|
|
case .attribute: if case .attribute = node { return true }
|
|
return false
|
|
case .namespace: if case .namespace = node { return true }
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static func processingInstructionMatches(_ node: Node, _ target: String?) -> Bool {
|
|
guard case let .tree(tree) = node, tree.kind == .processingInstruction else { return false }
|
|
guard let target else { return true }
|
|
return tree.name?.description == target
|
|
}
|
|
|
|
private static func isTreeKind(_ node: Node, in kinds: [PureXML.Model.TreeNodeKind]) -> Bool {
|
|
guard case let .tree(tree) = node else { return false }
|
|
return kinds.contains(tree.kind)
|
|
}
|
|
}
|