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.
179 lines
6.3 KiB
Swift
179 lines
6.3 KiB
Swift
extension PureXML.Parsing {
|
|
/// Validates an `<!ATTLIST>` declaration body (everything after the element
|
|
/// name) against the XML 1.0 grammar (productions 52-60): each definition is
|
|
/// `S Name S AttType S DefaultDecl`, where the type is `CDATA`, a tokenized
|
|
/// type, `NOTATION` with a name group, or an enumeration, and the default is
|
|
/// `#REQUIRED`, `#IMPLIED`, or an optionally-`#FIXED` quoted value. The
|
|
/// lenient `AttributeListParser` still drives behavior; this strict check
|
|
/// runs at DTD scan time.
|
|
enum DTDAttListGrammar {
|
|
private static let tokenizedTypes: Set<String> = [
|
|
"CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS",
|
|
]
|
|
|
|
static func isValid(_ body: String) -> Bool {
|
|
defaultValues(body) != nil
|
|
}
|
|
|
|
/// The quoted default values of a valid body, or nil when the body does
|
|
/// not match the grammar. The values feed the entity-reference checks
|
|
/// the spec puts on attribute defaults.
|
|
static func defaultValues(_ body: String) -> [String]? {
|
|
var defaults: [String] = []
|
|
var scanner = AttListScanner(body)
|
|
while !scanner.isAtEnd {
|
|
// Each definition starts with required whitespace (the body begins
|
|
// right after the element name).
|
|
guard scanner.requiredSpace() else { return nil }
|
|
if scanner.isAtEnd {
|
|
return defaults
|
|
} // trailing whitespace
|
|
guard scanner.name() else { return nil }
|
|
guard scanner.requiredSpace() else { return nil }
|
|
guard attType(&scanner) else { return nil }
|
|
guard scanner.requiredSpace() else { return nil }
|
|
guard let value = defaultDecl(&scanner) else { return nil }
|
|
if let value {
|
|
defaults.append(value)
|
|
}
|
|
}
|
|
return defaults
|
|
}
|
|
|
|
private static func attType(_ scanner: inout AttListScanner) -> Bool {
|
|
if scanner.peek() == "(" {
|
|
return scanner.nameGroup(nmtokens: true)
|
|
}
|
|
guard let keyword = scanner.keyword() else { return false }
|
|
if tokenizedTypes.contains(keyword) {
|
|
return true
|
|
}
|
|
if keyword == "NOTATION" {
|
|
guard scanner.requiredSpace() else { return false }
|
|
return scanner.nameGroup(nmtokens: false)
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// nil = malformed; .some(nil) = #REQUIRED/#IMPLIED; .some(value) = a
|
|
/// (possibly #FIXED) quoted default.
|
|
private static func defaultDecl(_ scanner: inout AttListScanner) -> String?? {
|
|
if scanner.consumeLiteral("#REQUIRED") || scanner.consumeLiteral("#IMPLIED") {
|
|
return .some(nil)
|
|
}
|
|
if scanner.consumeLiteral("#FIXED") {
|
|
guard scanner.requiredSpace() else { return nil }
|
|
}
|
|
guard let value = scanner.quotedValue() else { return nil }
|
|
return .some(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A tiny scanner over one attribute-list body.
|
|
private struct AttListScanner {
|
|
private let characters: [Character]
|
|
private var index = 0
|
|
|
|
init(_ text: String) {
|
|
// Scalar-level: a combining mark must not merge with a delimiter.
|
|
characters = text.unicodeScalars.map(Character.init)
|
|
}
|
|
|
|
var isAtEnd: Bool {
|
|
index >= characters.count
|
|
}
|
|
|
|
func peek() -> Character? {
|
|
index < characters.count ? characters[index] : nil
|
|
}
|
|
|
|
/// Consumes at least one whitespace character.
|
|
mutating func requiredSpace() -> Bool {
|
|
let start = index
|
|
while let character = peek(), character.isWhitespace {
|
|
index += 1
|
|
}
|
|
return index > start
|
|
}
|
|
|
|
mutating func name() -> Bool {
|
|
guard let first = peek(), first.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameStart) else {
|
|
return false
|
|
}
|
|
index += 1
|
|
while let next = peek(), next.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameChar) {
|
|
index += 1
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// An NMTOKEN: one or more name characters (the start may be any name char).
|
|
private mutating func nmtoken() -> Bool {
|
|
let start = index
|
|
while let next = peek(), next.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isNameChar) {
|
|
index += 1
|
|
}
|
|
return index > start
|
|
}
|
|
|
|
/// An uppercase keyword run (`CDATA`, `NOTATION`, ...).
|
|
mutating func keyword() -> String? {
|
|
let start = index
|
|
while let next = peek(), next.isLetter {
|
|
index += 1
|
|
}
|
|
guard index > start else { return nil }
|
|
return String(characters[start ..< index])
|
|
}
|
|
|
|
/// `'(' S? token (S? '|' S? token)* S? ')'` with names or nmtokens.
|
|
mutating func nameGroup(nmtokens: Bool) -> Bool {
|
|
guard consume("(") else { return false }
|
|
skipSpace()
|
|
guard nmtokens ? nmtoken() : name() else { return false }
|
|
skipSpace()
|
|
while !consume(")") {
|
|
guard consume("|") else { return false }
|
|
skipSpace()
|
|
guard nmtokens ? nmtoken() : name() else { return false }
|
|
skipSpace()
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func quotedValue() -> String? {
|
|
guard let quote = peek(), quote == "\"" || quote == "'" else { return nil }
|
|
index += 1
|
|
var value = ""
|
|
while let next = peek(), next != quote {
|
|
value.append(next)
|
|
index += 1
|
|
}
|
|
return consume(quote) ? value : nil
|
|
}
|
|
|
|
mutating func consumeLiteral(_ literal: String) -> Bool {
|
|
let count = literal.count
|
|
guard index + count <= characters.count,
|
|
String(characters[index ..< index + count]) == literal
|
|
else { return false }
|
|
index += count
|
|
return true
|
|
}
|
|
|
|
private mutating func consume(_ character: Character) -> Bool {
|
|
if peek() == character {
|
|
index += 1
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
private mutating func skipSpace() {
|
|
while let character = peek(), character.isWhitespace {
|
|
index += 1
|
|
}
|
|
}
|
|
}
|