Compile RELAX NG from both syntaxes and across documents: - A compact-syntax parser (lexer plus recursive descent) produces the same pattern algebra the XML syntax compiles to. It covers element and attribute declarations with name classes and namespace resolution, the , | & combinators and ? * + cardinality, text/empty/notAllowed, list and mixed, datatypes and value literals, grammars with start and named definitions (= |= &=), nested grammars, and include/external references. - The XML-syntax parser gains include (merging another grammar's definitions, with the include's own nested definitions overriding) and externalRef (resolving to an external schema's pattern), both through an injected loader. Both syntaxes reach the loader through a new schemaLoader parameter that returns nil by default, so compilation never touches the filesystem or network unless a loader is supplied. Closes #68
228 lines
8.7 KiB
Swift
228 lines
8.7 KiB
Swift
private typealias Tree = PureXML.Model.TreeNode
|
|
|
|
/// Tree helpers for the RELAX NG parser. File-scope and private.
|
|
private enum RNGNode {
|
|
static func localName(_ node: Tree) -> String? {
|
|
node.name?.localName
|
|
}
|
|
|
|
static func attribute(_ node: Tree, _ name: String) -> String? {
|
|
node.attributes.first { $0.name.localName == name }?.value
|
|
}
|
|
|
|
static func elementChildren(_ node: Tree) -> [Tree] {
|
|
node.children.filter { $0.kind == .element }
|
|
}
|
|
|
|
static func children(_ node: Tree, named name: String) -> [Tree] {
|
|
elementChildren(node).filter { localName($0) == name }
|
|
}
|
|
|
|
static func text(_ node: Tree) -> String {
|
|
node.stringValue.trimmingXMLWhitespace()
|
|
}
|
|
|
|
static func strip(_ qualified: String) -> String {
|
|
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
|
}
|
|
}
|
|
|
|
/// Compiles a RELAX NG XML-syntax document into the pattern algebra, resolving
|
|
/// `include` and `externalRef` through a loader. Holds the accumulating `define`
|
|
/// table so external grammars merge into it. File-scope and private.
|
|
private final class RNGCompiler {
|
|
typealias Pattern = PureXML.Schema.Pattern
|
|
typealias NameClass = PureXML.Schema.NameClass
|
|
typealias Compositor = PureXML.Schema.Compositor
|
|
|
|
private(set) var defines: [String: Pattern] = [:]
|
|
private let loader: (String) -> String?
|
|
private var visited: Set<String> = []
|
|
|
|
init(loader: @escaping (String) -> String?) {
|
|
self.loader = loader
|
|
}
|
|
|
|
func topLevel(_ node: Tree) -> Pattern {
|
|
RNGNode.localName(node) == "grammar" ? grammar(node) : pattern(node)
|
|
}
|
|
|
|
private func grammar(_ node: Tree) -> Pattern {
|
|
for include in RNGNode.children(node, named: "include") {
|
|
mergeInclude(include)
|
|
}
|
|
for define in RNGNode.children(node, named: "define") {
|
|
if let name = RNGNode.attribute(define, "name") {
|
|
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
|
}
|
|
}
|
|
let start = RNGNode.children(node, named: "start").first
|
|
return start.map { combined(RNGNode.elementChildren($0), .sequence) } ?? .notAllowed
|
|
}
|
|
|
|
/// Merges an `include`d grammar: its `define`s first, then the `include`'s own
|
|
/// nested `define`s, which override the included ones.
|
|
private func mergeInclude(_ node: Tree) {
|
|
guard let href = RNGNode.attribute(node, "href"), !visited.contains(href),
|
|
let text = loader(href), let root = try? PureXML.parseTree(text),
|
|
let grammar = RNGNode.elementChildren(root).first
|
|
else {
|
|
return
|
|
}
|
|
visited.insert(href)
|
|
for define in RNGNode.children(grammar, named: "define") {
|
|
if let name = RNGNode.attribute(define, "name") {
|
|
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
|
}
|
|
}
|
|
for define in RNGNode.children(node, named: "define") {
|
|
if let name = RNGNode.attribute(define, "name") {
|
|
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Patterns
|
|
|
|
private func pattern(_ node: Tree) -> Pattern {
|
|
leafPattern(node) ?? compositePattern(node)
|
|
}
|
|
|
|
private func leafPattern(_ node: Tree) -> Pattern? {
|
|
switch RNGNode.localName(node) {
|
|
case "empty": .empty
|
|
case "notAllowed": .notAllowed
|
|
case "text": .text
|
|
case "data": .data(PureXML.Schema.BuiltinType(rawValue: RNGNode.strip(RNGNode.attribute(node, "type") ?? "string")) ?? .string)
|
|
case "value": .value(RNGNode.text(node))
|
|
case "ref": .ref(RNGNode.attribute(node, "name") ?? "")
|
|
case "externalRef": externalRef(node)
|
|
case "element": element(node)
|
|
case "attribute": attribute(node)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
/// Resolves an `externalRef` to the referenced schema's pattern, merging any
|
|
/// grammar `define`s it carries into this compiler's table.
|
|
private func externalRef(_ node: Tree) -> Pattern {
|
|
guard let href = RNGNode.attribute(node, "href"), !visited.contains(href),
|
|
let text = loader(href), let root = try? PureXML.parseTree(text),
|
|
let top = RNGNode.elementChildren(root).first
|
|
else {
|
|
return .notAllowed
|
|
}
|
|
visited.insert(href)
|
|
return topLevel(top)
|
|
}
|
|
|
|
private func compositePattern(_ node: Tree) -> Pattern {
|
|
switch RNGNode.localName(node) {
|
|
case "group": combined(RNGNode.elementChildren(node), .sequence)
|
|
case "choice": combined(RNGNode.elementChildren(node), .choice)
|
|
case "interleave": combined(RNGNode.elementChildren(node), .all)
|
|
case "optional": .choice(group(node), .empty)
|
|
case "zeroOrMore": .choice(.oneOrMore(group(node)), .empty)
|
|
case "oneOrMore": .oneOrMore(group(node))
|
|
case "mixed": .interleave(group(node), .text)
|
|
case "list": .list(group(node))
|
|
default: .notAllowed
|
|
}
|
|
}
|
|
|
|
private func group(_ node: Tree) -> Pattern {
|
|
combined(RNGNode.elementChildren(node), .sequence)
|
|
}
|
|
|
|
private func element(_ node: Tree) -> Pattern {
|
|
let (nameClass, contentNodes) = nameClassAndContent(node)
|
|
return .element(nameClass, combined(contentNodes, .sequence))
|
|
}
|
|
|
|
private func attribute(_ node: Tree) -> Pattern {
|
|
let (nameClass, contentNodes) = nameClassAndContent(node)
|
|
let content = contentNodes.isEmpty ? Pattern.text : combined(contentNodes, .sequence)
|
|
return .attribute(nameClass, content)
|
|
}
|
|
|
|
private func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
|
|
let children = RNGNode.elementChildren(node)
|
|
if let name = RNGNode.attribute(node, "name") {
|
|
let namespace = RNGNode.attribute(node, "ns") ?? ""
|
|
return (.name(namespace: namespace, localName: name), children)
|
|
}
|
|
guard let first = children.first else { return (.anyName, []) }
|
|
return (nameClass(first), Array(children.dropFirst()))
|
|
}
|
|
|
|
// MARK: Combinators and name classes
|
|
|
|
private func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
|
|
let patterns = nodes.map(pattern)
|
|
let identity: Pattern = compositor == .choice ? .notAllowed : .empty
|
|
guard let first = patterns.first else { return identity }
|
|
return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
|
|
}
|
|
|
|
private func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
|
|
switch compositor {
|
|
case .sequence: .group(lhs, rhs)
|
|
case .choice: .choice(lhs, rhs)
|
|
case .all: .interleave(lhs, rhs)
|
|
}
|
|
}
|
|
|
|
private func nameClass(_ node: Tree) -> NameClass {
|
|
switch RNGNode.localName(node) {
|
|
case "name":
|
|
.name(namespace: RNGNode.attribute(node, "ns") ?? "", localName: RNGNode.text(node))
|
|
case "anyName":
|
|
anyName(node)
|
|
case "nsName":
|
|
.nsName(RNGNode.attribute(node, "ns") ?? "")
|
|
case "choice":
|
|
nameClassChoice(RNGNode.elementChildren(node))
|
|
default:
|
|
.anyName
|
|
}
|
|
}
|
|
|
|
private func anyName(_ node: Tree) -> NameClass {
|
|
guard let except = RNGNode.children(node, named: "except").first,
|
|
let inner = RNGNode.elementChildren(except).first
|
|
else {
|
|
return .anyName
|
|
}
|
|
return .anyNameExcept(nameClass(inner))
|
|
}
|
|
|
|
private func nameClassChoice(_ nodes: [Tree]) -> NameClass {
|
|
let classes = nodes.map(nameClass)
|
|
guard let first = classes.first else { return .anyName }
|
|
return classes.dropFirst().reduce(first) { .choice($0, $1) }
|
|
}
|
|
}
|
|
|
|
extension PureXML.Schema {
|
|
/// Parses a RELAX NG schema in the XML syntax into a grammar: a start pattern
|
|
/// and the named `define` patterns it refers to. The vocabulary is matched by
|
|
/// local name. Supports `element`, `attribute`, `text`, `empty`, `notAllowed`,
|
|
/// `group`, `choice`, `interleave`, `optional`, `zeroOrMore`, `oneOrMore`,
|
|
/// `mixed`, `list`, `ref`, `data`, `value`, the name classes, and `include`
|
|
/// and `externalRef` resolved through `loader`.
|
|
enum RelaxNGParser {
|
|
static func parse(
|
|
_ rng: String,
|
|
loader: @escaping (String) -> String? = { _ in nil },
|
|
) throws -> (start: Pattern, defines: [String: Pattern]) {
|
|
let root = try PureXML.parseTree(rng)
|
|
guard let top = RNGNode.elementChildren(root).first else {
|
|
throw PureXML.Schema.SchemaError.notASchema
|
|
}
|
|
let compiler = RNGCompiler(loader: loader)
|
|
let start = compiler.topLevel(top)
|
|
return (start, compiler.defines)
|
|
}
|
|
}
|
|
}
|