Surface the parsed <!DOCTYPE> internal subset's <!ELEMENT> declarations from the parser and validate a tree against them. Reimplemented from the DTD content-model spec; no upstream code copied. - DoctypeScanner captures <!ELEMENT name model> declarations; the parser surfaces them as Parsing.DocumentType (via parseWithDocumentType). - Validation.ContentModel + ContentModelParser parse a model string (EMPTY, ANY, (#PCDATA), mixed, and element content with sequence/choice and ?/*/+). - ContentModelMatcher matches the child element-name sequence against a model as a regular language (NFA-position matching). - Validation.DTDSchema.validate reports issues for EMPTY/PCDATA/mixed violations, element content with stray text, and child sequences that do not match. - PureXML.validateAgainstInternalDTD(_:) parses and validates in one call. - ATTLIST validation is out of scope. 9 tests; 70 total.
76 lines
3.3 KiB
Swift
76 lines
3.3 KiB
Swift
extension PureXML.Validation {
|
|
/// Matches a sequence of child element names against a content-model particle.
|
|
/// A content model is a regular expression over child element names, so this
|
|
/// is an NFA-style matcher: it tracks the set of positions reachable in the
|
|
/// name sequence, and the whole sequence matches when the end is reachable.
|
|
enum ContentModelMatcher {
|
|
static func matchesChildren(_ particle: Particle, _ names: [String]) -> Bool {
|
|
advance(particle, names, from: [0]).contains(names.count)
|
|
}
|
|
|
|
/// End positions reachable by matching `particle` (with its occurrence)
|
|
/// once from each given start position.
|
|
private static func advance(_ particle: Particle, _ names: [String], from starts: Set<Int>) -> Set<Int> {
|
|
switch occurrence(of: particle) {
|
|
case .once:
|
|
return starts.reduce(into: Set<Int>()) { $0.formUnion(matchOnce(particle, names, from: $1)) }
|
|
case .optional:
|
|
var result = starts
|
|
for start in starts {
|
|
result.formUnion(matchOnce(particle, names, from: start))
|
|
}
|
|
return result
|
|
case .zeroOrMore:
|
|
return closure(particle, names, starts, includingStarts: true)
|
|
case .oneOrMore:
|
|
return closure(particle, names, starts, includingStarts: false)
|
|
}
|
|
}
|
|
|
|
/// End positions from matching the particle's structure exactly once,
|
|
/// ignoring its own occurrence (which ``advance(_:_:from:)`` applies).
|
|
private static func matchOnce(_ particle: Particle, _ names: [String], from start: Int) -> Set<Int> {
|
|
switch particle {
|
|
case let .name(name, _):
|
|
return start < names.count && names[start] == name ? [start + 1] : []
|
|
case let .sequence(items, _):
|
|
var positions: Set<Int> = [start]
|
|
for item in items {
|
|
positions = positions.reduce(into: Set<Int>()) { $0.formUnion(advance(item, names, from: [$1])) }
|
|
}
|
|
return positions
|
|
case let .choice(items, _):
|
|
return items.reduce(into: Set<Int>()) { $0.formUnion(advance($1, names, from: [start])) }
|
|
}
|
|
}
|
|
|
|
private static func closure(
|
|
_ particle: Particle,
|
|
_ names: [String],
|
|
_ starts: Set<Int>,
|
|
includingStarts: Bool,
|
|
) -> Set<Int> {
|
|
var result = includingStarts ? starts : Set<Int>()
|
|
var frontier = starts
|
|
while !frontier.isEmpty {
|
|
var next: Set<Int> = []
|
|
for position in frontier {
|
|
for end in matchOnce(particle, names, from: position) where !result.contains(end) {
|
|
next.insert(end)
|
|
}
|
|
}
|
|
result.formUnion(next)
|
|
frontier = next
|
|
}
|
|
return result
|
|
}
|
|
|
|
private static func occurrence(of particle: Particle) -> Occurrence {
|
|
switch particle {
|
|
case let .name(_, occurrence), let .sequence(_, occurrence), let .choice(_, occurrence):
|
|
occurrence
|
|
}
|
|
}
|
|
}
|
|
}
|