The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass.
129 lines
4.2 KiB
Swift
129 lines
4.2 KiB
Swift
/// Name-class and token-stream helpers for ``RNCParser``. Kept in a separate
|
|
/// file so the parser proper stays within the type/file length budget.
|
|
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 {
|
|
let base = parseNameClassPrimary(attribute: attribute)
|
|
// `*` and `ns:*` may be narrowed by `- nameClass` subtraction.
|
|
guard peek() == .symbol("-") else { return base }
|
|
advance()
|
|
return subtract(base, parseSimpleNameClass(attribute: attribute))
|
|
}
|
|
|
|
private func parseNameClassPrimary(attribute: Bool) -> NameClass {
|
|
switch peek() {
|
|
case .symbol("*"):
|
|
advance()
|
|
return .anyName
|
|
case .symbol("("):
|
|
advance()
|
|
let nameClass = parseNameClass(attribute: attribute)
|
|
expectSymbol(")")
|
|
return nameClass
|
|
// `prefix:*` lexes as the word `prefix:` (a `:` is a name character)
|
|
// followed by a `*` symbol: a namespace wildcard.
|
|
case let .word(word) where word.hasSuffix(":") && peek(1) == .symbol("*"):
|
|
advance()
|
|
advance()
|
|
return .nsName(namespaces[String(word.dropLast())] ?? "")
|
|
case let .word(word):
|
|
advance()
|
|
return qualifiedName(word, attribute: attribute)
|
|
default:
|
|
advance()
|
|
return .anyName
|
|
}
|
|
}
|
|
|
|
/// Applies name-class subtraction, which RELAX NG defines only for the open
|
|
/// classes `anyName` (`*`) and `nsName` (`prefix:*`).
|
|
private func subtract(_ base: NameClass, _ except: NameClass) -> NameClass {
|
|
switch base {
|
|
case .anyName: .anyNameExcept(except)
|
|
case let .nsName(namespace): .nsNameExcept(namespace: namespace, except: except)
|
|
default: base
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
func peek(_ offset: Int = 0) -> PureXML.Schema.RNCToken? {
|
|
let index = position + offset
|
|
return index < tokens.count ? tokens[index] : nil
|
|
}
|
|
|
|
@discardableResult
|
|
func advance() -> PureXML.Schema.RNCToken? {
|
|
defer { position += 1 }
|
|
return peek()
|
|
}
|
|
|
|
func wordValue() -> String {
|
|
if case let .word(word) = peek() {
|
|
advance()
|
|
return word
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func expectAssignment() -> String {
|
|
if case let .symbol(symbol) = peek(), symbol == "=" || symbol == "|=" || symbol == "&=" {
|
|
advance()
|
|
return symbol
|
|
}
|
|
return "="
|
|
}
|
|
|
|
func expectSymbol(_ symbol: String) {
|
|
if peek() == .symbol(symbol) {
|
|
advance()
|
|
}
|
|
}
|
|
|
|
func expectString() -> String {
|
|
if case let .string(value) = peek() {
|
|
advance()
|
|
return concatenated(value)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
/// Folds RNC string concatenation: `"a" ~ "b"` is the single literal `"ab"`,
|
|
/// the way long strings are split across lines. The leading string has been
|
|
/// consumed; this appends each following `~ "…"`.
|
|
func concatenated(_ value: String) -> String {
|
|
var result = value
|
|
while peek() == .symbol("~"), case let .string(next)? = peek(1) {
|
|
advance()
|
|
advance()
|
|
result += next
|
|
}
|
|
return result
|
|
}
|
|
|
|
func strip(_ qualified: String) -> String {
|
|
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
|
}
|
|
}
|