feat(xpath): explicit evaluation context entry point
Add Query.value(at:position:size:variables:) to evaluate an expression against a context node already in a tree, with its proximity position, size, and variable bindings. position(), last(), and the upward axes resolve from that node. This completes full XPath 1.0: the per-node entry point that downstream engines (XSLT, Schematron) drive. Closes #43
This commit is contained in:
parent
cef46fe3ae
commit
4fe8771fb9
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- The XPath evaluation context, completing full XPath 1.0 (#21).
|
||||
`Query.value(at:position:size:variables:)` evaluates an expression against an
|
||||
explicit context node already in a tree, with its proximity position, size,
|
||||
and variable bindings, so `position()`, `last()`, and the upward axes resolve
|
||||
from that node. This is the per-node entry point downstream engines (XSLT,
|
||||
Schematron) drive.
|
||||
- The full XPath 1.0 function library (toward #21 full XPath 1.0). Beyond the
|
||||
core functions, the string family (`concat`, `starts-with`, `contains`,
|
||||
`substring-before`, `substring-after`, `substring`, `string-length`,
|
||||
|
||||
@ -89,7 +89,7 @@ flowchart TB
|
||||
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
|
||||
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
|
||||
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
|
||||
XPathFull["#21 Full XPath 1.0"]:::epic
|
||||
XPathFull["#21 Full XPath 1.0"]:::done
|
||||
Pattern["#22 xmlPattern streaming match"]:::epic
|
||||
XPointer["#23 XPointer"]:::epic
|
||||
XPathFull --> Pattern
|
||||
|
||||
@ -29,6 +29,26 @@ extension PureXML.XPath {
|
||||
try eval(expression, rootContext(node, variables: variables))
|
||||
}
|
||||
|
||||
/// Evaluates an expression against an explicit context: a node already in a
|
||||
/// tree, its proximity position and size, and variable bindings. This is
|
||||
/// the entry point downstream engines (XSLT, Schematron) drive per node.
|
||||
static func value(
|
||||
_ expression: Expression,
|
||||
at node: PureXML.Model.TreeNode,
|
||||
position: Int,
|
||||
size: Int,
|
||||
variables: [String: Value],
|
||||
) throws -> Value {
|
||||
let context = EvaluationContext(
|
||||
node: .tree(node),
|
||||
position: position,
|
||||
size: size,
|
||||
variables: variables,
|
||||
functions: library,
|
||||
)
|
||||
return try eval(expression, context)
|
||||
}
|
||||
|
||||
private static func rootContext(_ node: PureXML.Model.Node, variables: [String: Value]) -> EvaluationContext {
|
||||
let root = PureXML.Model.TreeNode(node)
|
||||
return EvaluationContext(
|
||||
|
||||
@ -38,6 +38,20 @@ public extension PureXML.XPath {
|
||||
try Evaluator.value(expression, over: node, variables: variables)
|
||||
}
|
||||
|
||||
/// Evaluates the query against an explicit context: a node already in a
|
||||
/// tree (``PureXML/Model/TreeNode``), its one-based proximity `position`
|
||||
/// within a node-set of `size`, and variable bindings. Downstream engines
|
||||
/// (XSLT, Schematron) drive this per context node; `position()` and
|
||||
/// `last()` reflect the supplied values.
|
||||
public func value(
|
||||
at node: PureXML.Model.TreeNode,
|
||||
position: Int = 1,
|
||||
size: Int = 1,
|
||||
variables: [String: Value] = [:],
|
||||
) throws -> Value {
|
||||
try Evaluator.value(expression, at: node, position: position, size: size, variables: variables)
|
||||
}
|
||||
|
||||
/// Evaluates the query and coerces the result to a number.
|
||||
public func number(over node: PureXML.Model.Node, variables: [String: Value] = [:]) throws -> Double {
|
||||
try value(over: node, variables: variables).number
|
||||
|
||||
39
Tests/XPathContextTests.swift
Normal file
39
Tests/XPathContextTests.swift
Normal file
@ -0,0 +1,39 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("XPath evaluation context")
|
||||
struct XPathContextTests {
|
||||
@Test("A relative query evaluates against an explicit context node")
|
||||
func test_contextNode() throws {
|
||||
let tree = try PureXML.parseTree("<r><a><b>deep</b></a><a><b>other</b></a></r>")
|
||||
let firstA = tree.firstChild?.firstChild
|
||||
let value = try #require(firstA)
|
||||
let result = try PureXML.XPath.Query("b").value(at: value)
|
||||
#expect(result.string == "deep")
|
||||
}
|
||||
|
||||
@Test("position() and last() reflect the supplied context")
|
||||
func test_positionAndSize() throws {
|
||||
let tree = try PureXML.parseTree("<r/>")
|
||||
let node = try #require(tree.firstChild)
|
||||
#expect(try PureXML.XPath.Query("position()").value(at: node, position: 3, size: 7).number == 3)
|
||||
#expect(try PureXML.XPath.Query("last()").value(at: node, position: 3, size: 7).number == 7)
|
||||
}
|
||||
|
||||
@Test("Variable bindings are visible to the evaluated expression")
|
||||
func test_variableBindings() throws {
|
||||
let tree = try PureXML.parseTree("<r/>")
|
||||
let node = try #require(tree.firstChild)
|
||||
let value = try PureXML.XPath.Query("$x * 2").value(at: node, variables: ["x": .number(21)])
|
||||
#expect(value.number == 42)
|
||||
}
|
||||
|
||||
@Test("Upward axes resolve from the context node's place in the tree")
|
||||
func test_upwardFromContext() throws {
|
||||
let tree = try PureXML.parseTree("<r id=\"root\"><a><b/></a></r>")
|
||||
let deepB = tree.firstChild?.firstChild?.firstChild
|
||||
let node = try #require(deepB)
|
||||
let result = try PureXML.XPath.Query("ancestor::r/@id").value(at: node)
|
||||
#expect(result.string == "root")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user