PureXML/Sources/XPath/Query.swift
Mihaela Mihaljevic be8a39394d fix(schema): advance XSTS valid-instances-rejected to 4 (#146)
Consolidates the in-progress #146 work; valid-instances-rejected reaches 4
on the 2006-11-06 XSTS archive (buckets now 1/262/4/147).

Regex: match Unicode scalars rather than grapheme clusters; the empty
pattern matches only the empty string; pin the category repertoire to
Unicode 3.2, the version the corpus is consistent at, so code points added
in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions
(the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a
3.2 assignment table derived from DerivedAge, replacing the old Unicode31
files.

Content model: assess each child against the particle the content automaton
matched, not a by-name lookup (#180). Two same-named particles keep their
own fixed values, and an element matching a skip wildcard is no longer
validated against its global declaration. Selection is made reproducible
when an ambiguous schema slips past the UPA check.

XSTS runner: compile and merge every schemaDocument in a schemaTest, not
just the first; this also surfaced one previously-masked invalid-instance
acceptance (recorded in the baselines).

Also folds in attribute-group anyAttribute inheritance, extension wildcard
union, namespace-qualified type tables for multi-document
xsi:schemaLocation, per-container elementFormDefault, strict and lax
attribute wildcard validation, built-in type resolution, and xs:redefine
simpleType overrides.
2026-06-16 22:22:05 +02:00

141 lines
6.3 KiB
Swift

public extension PureXML.XPath {
/// A compiled XPath 1.0 expression: all thirteen axes with their
/// abbreviations, the node tests, the operator grammar (`or`, `and`, `=`,
/// `!=`, `<`, `<=`, `>`, `>=`, `+`, `-`, `*`, `div`, `mod`, unary `-`, `|`),
/// number and string literals, variable references, function calls, and
/// filter expressions, all over the four-type model (node-set, boolean,
/// number, string). Reimplemented from the XPath 1.0 specification.
struct Query: Sendable {
let expression: Expression
/// Compiles an XPath expression, throwing ``QueryError`` on a syntax error.
public init(_ path: String) throws {
expression = try Compiler.compile(path)
}
/// Evaluates the query over a node, returning the selected node-set in
/// document order. A non-node-set expression yields an empty list; use
/// ``value(over:variables:)`` for typed results.
public func evaluate(over node: PureXML.Model.Node) -> [Selection] {
Evaluator.evaluate(expression, over: node)
}
/// Evaluates the query with eval-time prefix bindings, so a name test like
/// `x:foo` resolves `x` to its URI and matches by namespace regardless of
/// the prefix the document uses.
public func evaluate(over node: PureXML.Model.Node, namespaces: [String: String]) -> [Selection] {
Evaluator.evaluate(expression, over: node, namespaces: namespaces)
}
/// Evaluates the query and returns the selected elements only.
public func elements(over node: PureXML.Model.Node) -> [PureXML.Model.Element] {
evaluate(over: node).compactMap(\.element)
}
/// Evaluates the query and returns the string-value of each selection.
public func strings(over node: PureXML.Model.Node) -> [String] {
evaluate(over: node).map(\.stringValue)
}
/// Evaluates the query to a typed ``Value`` with optional variable bindings.
public func value(
over node: PureXML.Model.Node,
variables: [String: Value] = [:],
) throws -> Value {
try Evaluator.value(expression, over: node, variables: variables)
}
/// Evaluates the query over a pre-built tree and returns the matched tree
/// nodes in document order. Build the tree once (``PureXML/parseTree(_:)``)
/// and query it repeatedly; pair with ``value(at:position:size:variables:)``
/// to evaluate further expressions relative to each result.
public func nodes(over root: PureXML.Model.TreeNode, namespaces: [String: String] = [:]) -> [PureXML.Model.TreeNode] {
Evaluator.nodes(expression, over: root, namespaces: namespaces)
}
/// Evaluates the query over a pre-built tree and returns the matched tree
/// nodes in document order. Build the tree once (``PureXML/parseTree(_:)``)
/// and query it repeatedly; pair with ``value(at:position:size:variables:)``
/// to evaluate further expressions relative to each result.
public func nodes(over root: PureXML.Model.TreeNode) -> [PureXML.Model.TreeNode] {
nodes(over: root, namespaces: [:])
}
/// 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] = [:],
namespaces: [String: String] = [:],
budget: Budget? = nil,
) throws -> Value {
try Evaluator.value(expression, at: node, position: position, size: size, variables: variables, namespaces: namespaces, budget: budget)
}
/// Like ``value(at:position:size:variables:)`` but with an extra function
/// table merged in (for engine-specific functions such as XSLT's `key`).
func value(
at node: PureXML.Model.TreeNode,
position: Int,
size: Int,
variables: [String: Value],
functions: FunctionTable,
) throws -> Value {
try Evaluator.value(
expression,
at: node,
position: position,
size: size,
variables: variables,
functions: functions,
)
}
/// Like the tree-node form, but the context may be any XPath node:
/// XSLT applies templates to attribute and namespace nodes too.
func value(
atNode node: Node,
position: Int,
size: Int,
variables: [String: Value],
functions: FunctionTable,
namespaces: [String: String] = [:],
) throws -> Value {
try Evaluator.value(
expression,
atNode: node,
position: position,
size: size,
variables: variables,
functions: functions,
namespaces: namespaces,
)
}
/// Like ``nodes(over:)`` but with an extra function table merged in.
func nodes(over root: PureXML.Model.TreeNode, functions: FunctionTable) -> [PureXML.Model.TreeNode] {
Evaluator.nodes(expression, over: root, functions: functions)
}
/// 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
}
/// Evaluates the query and coerces the result to a string.
public func string(over node: PureXML.Model.Node, variables: [String: Value] = [:]) throws -> String {
try value(over: node, variables: variables).string
}
/// Evaluates the query and coerces the result to a boolean.
public func boolean(over node: PureXML.Model.Node, variables: [String: Value] = [:]) throws -> Bool {
try value(over: node, variables: variables).boolean
}
}
}