Add PureXML.Validation.Schematron: compile a Schematron schema (matching schema/pattern/rule/assert/report by local name, so the ISO and legacy namespaces both work) and validate a document. A rule's context XPath selects the nodes it fires on; each assert/report test is evaluated relative to the node via the XPath context-node API. A failed assert is an error, a matched report a warning; within a pattern a node fires the first matching rule. Adds XPath.Query.nodes(over:) returning matched tree nodes so one tree can be queried repeatedly. Closes #52 Closes #53 Closes #25
63 lines
3.0 KiB
Swift
63 lines
3.0 KiB
Swift
extension PureXML.Validation {
|
|
/// Parses a Schematron schema document into patterns. Namespace-agnostic: it
|
|
/// matches the `schema`, `pattern`, `rule`, `assert`, and `report` elements by
|
|
/// local name, so both the ISO and the legacy Schematron namespaces work.
|
|
enum SchematronParser {
|
|
static func parse(_ xml: String) throws -> [SchematronPattern] {
|
|
let root = try PureXML.parseTree(xml)
|
|
return try descendants(root, localName: "pattern").map(pattern)
|
|
}
|
|
|
|
private static func pattern(_ node: PureXML.Model.TreeNode) throws -> SchematronPattern {
|
|
try SchematronPattern(rules: children(node, localName: "rule").compactMap(rule))
|
|
}
|
|
|
|
private static func rule(_ node: PureXML.Model.TreeNode) throws -> SchematronRule? {
|
|
guard let context = attribute(node, "context") else { return nil }
|
|
let assertions = try node.children
|
|
.filter { $0.kind == .element && isAssertion($0) }
|
|
.compactMap(assertion)
|
|
return try SchematronRule(context: PureXML.XPath.Query(contextExpression(context)), assertions: assertions)
|
|
}
|
|
|
|
private static func assertion(_ node: PureXML.Model.TreeNode) throws -> SchematronAssertion? {
|
|
guard let test = attribute(node, "test") else { return nil }
|
|
let isReport = node.name?.localName == "report"
|
|
return try SchematronAssertion(
|
|
isReport: isReport,
|
|
test: PureXML.XPath.Query(test),
|
|
message: node.stringValue.trimmingXMLWhitespace(),
|
|
)
|
|
}
|
|
|
|
private static func isAssertion(_ node: PureXML.Model.TreeNode) -> Bool {
|
|
node.name?.localName == "assert" || node.name?.localName == "report"
|
|
}
|
|
|
|
/// A relative context selects matching nodes anywhere, so it is searched
|
|
/// from the document via `//`; an absolute context is used as written.
|
|
private static func contextExpression(_ context: String) -> String {
|
|
context.hasPrefix("/") ? context : "//" + context
|
|
}
|
|
|
|
private static func attribute(_ node: PureXML.Model.TreeNode, _ name: String) -> String? {
|
|
node.attributes.first { $0.name.localName == name || $0.name.description == name }?.value
|
|
}
|
|
|
|
private static func children(_ node: PureXML.Model.TreeNode, localName: String) -> [PureXML.Model.TreeNode] {
|
|
node.children.filter { $0.kind == .element && $0.name?.localName == localName }
|
|
}
|
|
|
|
private static func descendants(_ node: PureXML.Model.TreeNode, localName: String) -> [PureXML.Model.TreeNode] {
|
|
var result: [PureXML.Model.TreeNode] = []
|
|
for child in node.children {
|
|
if child.kind == .element, child.name?.localName == localName {
|
|
result.append(child)
|
|
}
|
|
result.append(contentsOf: descendants(child, localName: localName))
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|