The compact-syntax name class * - nameClass now parses as anyName except the subtracted class (the model already had anyNameExcept). ns:* - name would need an nsName-except model case and is left as a refinement. Part of #79
401 lines
13 KiB
Swift
401 lines
13 KiB
Swift
private typealias RNCToken = PureXML.Schema.RNCToken
|
|
private typealias RNCLexer = PureXML.Schema.RNCLexer
|
|
|
|
/// A recursive-descent parser from the token stream to the pattern algebra.
|
|
/// File-scope and private.
|
|
private final class RNCParser {
|
|
typealias Pattern = PureXML.Schema.Pattern
|
|
typealias NameClass = PureXML.Schema.NameClass
|
|
|
|
private let tokens: [RNCToken]
|
|
private var position = 0
|
|
private let loader: (String) -> String?
|
|
private(set) var defines: [String: Pattern] = [:]
|
|
private var startPattern: Pattern?
|
|
private var defaultNamespace = ""
|
|
private var namespaces: [String: String] = [:]
|
|
private var visited: Set<String>
|
|
|
|
init(_ tokens: [RNCToken], loader: @escaping (String) -> String?, visited: Set<String> = []) {
|
|
self.tokens = tokens
|
|
self.loader = loader
|
|
self.visited = visited
|
|
}
|
|
|
|
func parse() -> (start: Pattern, defines: [String: Pattern]) {
|
|
skipDeclarations()
|
|
if looksLikeGrammar() {
|
|
parseGrammarBody()
|
|
return (startPattern ?? .notAllowed, defines)
|
|
}
|
|
return (parsePattern(), defines)
|
|
}
|
|
|
|
// MARK: Declarations
|
|
|
|
private func skipDeclarations() {
|
|
while case let .word(keyword) = peek() {
|
|
switch keyword {
|
|
case "namespace": parseNamespace(isDefault: false)
|
|
case "default": parseDefault()
|
|
case "datatypes": parseDatatypes()
|
|
default: return
|
|
}
|
|
}
|
|
}
|
|
|
|
private func parseDefault() {
|
|
advance() // default
|
|
if case .word("namespace") = peek() { parseNamespace(isDefault: true) }
|
|
}
|
|
|
|
private func parseNamespace(isDefault: Bool) {
|
|
advance() // namespace
|
|
var prefix = ""
|
|
if case let .word(name) = peek(), name != "=" { prefix = name
|
|
advance()
|
|
}
|
|
expectSymbol("=")
|
|
let uri = expectString()
|
|
if isDefault { defaultNamespace = uri }
|
|
if !prefix.isEmpty { namespaces[prefix] = uri }
|
|
}
|
|
|
|
private func parseDatatypes() {
|
|
advance() // datatypes
|
|
if case .word = peek() { advance() }
|
|
expectSymbol("=")
|
|
_ = expectString()
|
|
}
|
|
|
|
// MARK: Grammar body
|
|
|
|
private func looksLikeGrammar() -> Bool {
|
|
guard case let .word(word) = peek() else { return false }
|
|
if word == "include" || word == "start" { return true }
|
|
// A define is a word followed by one of =, |=, &=.
|
|
if case let .symbol(symbol) = peek(1), symbol == "=" || symbol == "|=" || symbol == "&=" { return true }
|
|
return false
|
|
}
|
|
|
|
private func parseGrammarBody() {
|
|
while position < tokens.count, peek() != .symbol("}") {
|
|
if case .word("include") = peek() {
|
|
parseInclude()
|
|
} else {
|
|
parseDefine()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func parseDefine() {
|
|
guard case let .word(name) = peek() else { advance()
|
|
return
|
|
}
|
|
advance()
|
|
let combinator = expectAssignment()
|
|
let pattern = parsePattern()
|
|
if name == "start" {
|
|
startPattern = combine(startPattern, pattern, combinator)
|
|
} else {
|
|
defines[name] = combine(defines[name], pattern, combinator)
|
|
}
|
|
}
|
|
|
|
private func parseInclude() {
|
|
advance() // include
|
|
let href = expectString()
|
|
if let text = loader(href), !visited.contains(href) {
|
|
visited.insert(href)
|
|
let sub = RNCParser(RNCLexer.tokens(text), loader: loader, visited: visited)
|
|
sub.skipDeclarations()
|
|
sub.parseGrammarBody()
|
|
for (name, pattern) in sub.defines {
|
|
defines[name] = pattern
|
|
}
|
|
if startPattern == nil { startPattern = sub.startPattern }
|
|
visited = sub.visited
|
|
}
|
|
// An override block re-defines names after the include, overriding them.
|
|
if peek() == .symbol("{") {
|
|
advance()
|
|
parseGrammarBody()
|
|
expectSymbol("}")
|
|
}
|
|
}
|
|
|
|
private func combine(_ existing: Pattern?, _ new: Pattern, _ combinator: String) -> Pattern {
|
|
guard let existing else { return new }
|
|
switch combinator {
|
|
case "|=": return .choice(existing, new)
|
|
case "&=": return .interleave(existing, new)
|
|
default: return new
|
|
}
|
|
}
|
|
|
|
// MARK: Patterns
|
|
|
|
private func parsePattern() -> Pattern {
|
|
var patterns = [parseParticle()]
|
|
guard case let .symbol(operatorSymbol) = peek(), operatorSymbol == "," || operatorSymbol == "|" || operatorSymbol == "&" else {
|
|
return patterns[0]
|
|
}
|
|
while peek() == .symbol(operatorSymbol) {
|
|
advance()
|
|
patterns.append(parseParticle())
|
|
}
|
|
return patterns.dropFirst().reduce(patterns[0]) { combinePatterns($0, $1, operatorSymbol) }
|
|
}
|
|
|
|
private func combinePatterns(_ lhs: Pattern, _ rhs: Pattern, _ operatorSymbol: String) -> Pattern {
|
|
switch operatorSymbol {
|
|
case "|": .choice(lhs, rhs)
|
|
case "&": .interleave(lhs, rhs)
|
|
default: .group(lhs, rhs)
|
|
}
|
|
}
|
|
|
|
private func parseParticle() -> Pattern {
|
|
let primary = parsePrimary()
|
|
switch peek() {
|
|
case .symbol("?"): advance()
|
|
return .choice(primary, .empty)
|
|
case .symbol("*"): advance()
|
|
return .choice(.oneOrMore(primary), .empty)
|
|
case .symbol("+"): advance()
|
|
return .oneOrMore(primary)
|
|
default: return primary
|
|
}
|
|
}
|
|
|
|
private func parsePrimary() -> Pattern {
|
|
guard case let .word(word) = peek() else { return parseNonWordPrimary() }
|
|
if let leaf = leafKeyword(word) {
|
|
advance()
|
|
return leaf
|
|
}
|
|
return blockKeyword(word)
|
|
}
|
|
|
|
/// The keyword patterns that take no block and map to a constant.
|
|
private func leafKeyword(_ word: String) -> Pattern? {
|
|
switch word {
|
|
case "text": .text
|
|
case "empty": .empty
|
|
case "notAllowed": .notAllowed
|
|
case "string", "token": .data(PureXML.Schema.SimpleType(base: .string))
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
/// The keyword patterns that introduce a block, reference, or external
|
|
/// schema; a bare word falls through to a datatype or define reference.
|
|
private func blockKeyword(_ word: String) -> Pattern {
|
|
switch word {
|
|
case "element": advance()
|
|
return parseElement()
|
|
case "attribute": advance()
|
|
return parseAttribute()
|
|
case "list": advance()
|
|
return .list(parseBraced())
|
|
case "mixed": advance()
|
|
return .interleave(parseBraced(), .text)
|
|
case "external": advance()
|
|
return externalPattern(expectString())
|
|
case "grammar": advance()
|
|
return nestedGrammar()
|
|
case "parent": advance()
|
|
return .ref(wordValue())
|
|
default: return wordPrimary(word)
|
|
}
|
|
}
|
|
|
|
private func parseNonWordPrimary() -> Pattern {
|
|
switch peek() {
|
|
case let .string(value): advance()
|
|
return .value(value)
|
|
case .symbol("("): advance()
|
|
let pattern = parsePattern()
|
|
expectSymbol(")")
|
|
return pattern
|
|
default: advance()
|
|
return .notAllowed
|
|
}
|
|
}
|
|
|
|
/// A `:`-qualified word is a datatype (optionally with a parameter block or a
|
|
/// value literal); a bare word is a reference to a define.
|
|
private func wordPrimary(_ word: String) -> Pattern {
|
|
advance()
|
|
guard word.contains(":") else { return .ref(word) }
|
|
let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string
|
|
var facets = PureXML.Schema.Facets()
|
|
if peek() == .symbol("{") { parseParams(into: &facets) }
|
|
if case let .string(value) = peek() { advance()
|
|
return .value(value)
|
|
}
|
|
return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
|
|
}
|
|
|
|
/// Parses a datatype parameter block `{ name = "value" ... }` into facets (the
|
|
/// compact-syntax form of `<param>`).
|
|
private func parseParams(into facets: inout PureXML.Schema.Facets) {
|
|
advance() // {
|
|
while position < tokens.count, peek() != .symbol("}") {
|
|
guard case let .word(name) = peek() else { advance()
|
|
continue
|
|
}
|
|
advance()
|
|
if peek() == .symbol("=") { advance() }
|
|
guard case let .string(value) = peek() else { continue }
|
|
advance()
|
|
PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets)
|
|
}
|
|
if peek() == .symbol("}") { advance() }
|
|
}
|
|
|
|
private func parseElement() -> Pattern {
|
|
let nameClass = parseNameClass(attribute: false)
|
|
return .element(nameClass, parseBraced())
|
|
}
|
|
|
|
private func parseAttribute() -> Pattern {
|
|
let nameClass = parseNameClass(attribute: true)
|
|
return .attribute(nameClass, parseBraced())
|
|
}
|
|
|
|
private func parseBraced() -> Pattern {
|
|
expectSymbol("{")
|
|
let pattern = parsePattern()
|
|
expectSymbol("}")
|
|
return pattern
|
|
}
|
|
|
|
private func nestedGrammar() -> Pattern {
|
|
expectSymbol("{")
|
|
parseGrammarBody()
|
|
expectSymbol("}")
|
|
return startPattern ?? .notAllowed
|
|
}
|
|
|
|
private func externalPattern(_ href: String) -> Pattern {
|
|
guard let text = loader(href), !visited.contains(href) else { return .notAllowed }
|
|
visited.insert(href)
|
|
let sub = RNCParser(RNCLexer.tokens(text), loader: loader, visited: visited)
|
|
let (start, subDefines) = sub.parse()
|
|
for (name, pattern) in subDefines {
|
|
defines[name] = pattern
|
|
}
|
|
visited = sub.visited
|
|
return start
|
|
}
|
|
}
|
|
|
|
/// Name-class and token-stream helpers for ``RNCParser``.
|
|
private extension RNCParser {
|
|
// MARK: Name classes
|
|
|
|
func parseNameClass(attribute: Bool) -> NameClass {
|
|
var nameClass = parseSimpleNameClass(attribute: attribute)
|
|
while peek() == .symbol("|") {
|
|
advance()
|
|
nameClass = .choice(nameClass, parseSimpleNameClass(attribute: attribute))
|
|
}
|
|
return nameClass
|
|
}
|
|
|
|
private func parseSimpleNameClass(attribute: Bool) -> NameClass {
|
|
switch peek() {
|
|
case .symbol("*"):
|
|
advance()
|
|
// `* - nameClass` is any name except the subtracted class.
|
|
if peek() == .symbol("-") {
|
|
advance()
|
|
return .anyNameExcept(parseSimpleNameClass(attribute: attribute))
|
|
}
|
|
return .anyName
|
|
case .symbol("("):
|
|
advance()
|
|
let nameClass = parseNameClass(attribute: attribute)
|
|
expectSymbol(")")
|
|
return nameClass
|
|
case let .word(word):
|
|
advance()
|
|
return qualifiedName(word, attribute: attribute)
|
|
default:
|
|
advance()
|
|
return .anyName
|
|
}
|
|
}
|
|
|
|
private func qualifiedName(_ word: String, attribute: Bool) -> NameClass {
|
|
let parts = word.split(separator: ":", maxSplits: 1).map(String.init)
|
|
if parts.count == 2 {
|
|
if parts[1] == "*" { return .nsName(namespaces[parts[0]] ?? "") }
|
|
return .name(namespace: namespaces[parts[0]] ?? "", localName: parts[1])
|
|
}
|
|
return .name(namespace: attribute ? "" : defaultNamespace, localName: word)
|
|
}
|
|
|
|
// MARK: Token helpers
|
|
|
|
private func peek(_ offset: Int = 0) -> RNCToken? {
|
|
let index = position + offset
|
|
return index < tokens.count ? tokens[index] : nil
|
|
}
|
|
|
|
@discardableResult
|
|
private func advance() -> RNCToken? {
|
|
defer { position += 1 }
|
|
return peek()
|
|
}
|
|
|
|
private func wordValue() -> String {
|
|
if case let .word(word) = peek() { advance()
|
|
return word
|
|
}
|
|
return ""
|
|
}
|
|
|
|
private func expectAssignment() -> String {
|
|
if case let .symbol(symbol) = peek(), symbol == "=" || symbol == "|=" || symbol == "&=" {
|
|
advance()
|
|
return symbol
|
|
}
|
|
return "="
|
|
}
|
|
|
|
private func expectSymbol(_ symbol: String) {
|
|
if peek() == .symbol(symbol) { advance() }
|
|
}
|
|
|
|
private func expectString() -> String {
|
|
if case let .string(value) = peek() { advance()
|
|
return value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
private func strip(_ qualified: String) -> String {
|
|
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
|
}
|
|
}
|
|
|
|
extension PureXML.Schema {
|
|
/// Parses a RELAX NG schema in the compact syntax (RNC) into a start pattern
|
|
/// and named `define` patterns, the same algebra the XML syntax compiles to.
|
|
/// Supports element/attribute declarations with name classes and namespaces,
|
|
/// the `,` `|` `&` combinators, `?` `*` `+` cardinality, `text`, `empty`,
|
|
/// `notAllowed`, `list`, `mixed`, datatypes and value literals, grammars with
|
|
/// `start` and named definitions (`=`, `|=`, `&=`), nested grammars, and
|
|
/// `include` and `external` resolved through `loader`.
|
|
enum RelaxNGCompactParser {
|
|
static func parse(
|
|
_ rnc: String,
|
|
loader: @escaping (String) -> String? = { _ in nil },
|
|
) throws -> (start: Pattern, defines: [String: Pattern]) {
|
|
RNCParser(RNCLexer.tokens(rnc), loader: loader).parse()
|
|
}
|
|
}
|
|
}
|