Add five well-formedness checks the W3C xmltest suite surfaced: '--' inside comments (also rejecting the ---> ending), raw '<' in attribute values, the literal ]]> sequence in character data, attributes not separated by whitespace, and characters outside the XML Char production in content, each a located ParseError. The attribute scanners move to EventReaderAttributes to stay under the type-body cap. Add the opt-in W3C xmltest runner (XMLTS_ROOT-gated; the suite is never vendored, James Clark's license permits redistribution only as the unmodified archive; Foundation is used in this one test file for file access only). Baseline: valid/sa 118/119, not-wf/sa 93/186 rejected (up from 80), the remaining 93 carried as an exact known-deviations baseline so progress and regressions are both visible. The two remaining leniency classes (internal- subset DTD grammar, entity replacement-text well-formedness) are #120. Part of #105 (Tier 2)
60 lines
2.7 KiB
Swift
60 lines
2.7 KiB
Swift
/// The start-tag attribute scanners, in their own file to keep the reader's
|
|
/// type body under the length cap. Behavior is part of EventReader proper.
|
|
extension PureXML.Parsing.EventReader {
|
|
mutating func scanAttributes() throws -> [PureXML.Model.Attribute] {
|
|
var attributes: [PureXML.Model.Attribute] = []
|
|
var seen: Set<String> = []
|
|
while true {
|
|
reader.skipSpace()
|
|
guard let next = reader.peek(), next != ">", next != "/" else {
|
|
return attributes
|
|
}
|
|
let mark = reader.mark
|
|
let name = try scanName()
|
|
reader.skipSpace()
|
|
guard reader.consume("=") else {
|
|
throw PureXML.Parsing.ParseError.expectedEquals(reader.mark)
|
|
}
|
|
reader.skipSpace()
|
|
let value = try scanAttributeValue()
|
|
guard seen.insert(name.description).inserted else {
|
|
if recovering {
|
|
// Keep the element: record the duplicate and drop the later
|
|
// occurrence rather than failing the whole start tag.
|
|
diagnostics.append(PureXML.Parsing.Diagnostic(PureXML.Parsing.ParseError.duplicateAttribute(name: name.description, mark)))
|
|
continue
|
|
}
|
|
throw PureXML.Parsing.ParseError.duplicateAttribute(name: name.description, mark)
|
|
}
|
|
attributes.append(PureXML.Model.Attribute(name: name, value: value))
|
|
// A following attribute needs whitespace before it.
|
|
if let next = reader.peek(), next != ">", next != "/", next != "?", !next.isWhitespace {
|
|
throw PureXML.Parsing.ParseError.missingSpaceBeforeAttribute(reader.mark)
|
|
}
|
|
}
|
|
}
|
|
|
|
mutating func scanAttributeValue() throws -> String {
|
|
let mark = reader.mark
|
|
guard let quote = reader.peek(), quote == "\"" || quote == "'" else {
|
|
throw PureXML.Parsing.ParseError.unquotedAttributeValue(reader.mark)
|
|
}
|
|
reader.advance()
|
|
var raw = ""
|
|
var length = 0
|
|
while let character = reader.peek(), character != quote {
|
|
length += 1
|
|
try checkContent(length, mark)
|
|
if character == "<" {
|
|
throw PureXML.Parsing.ParseError.rawLessThanInAttribute(reader.mark)
|
|
}
|
|
raw.append(character)
|
|
reader.advance()
|
|
}
|
|
guard reader.consume(String(quote)) else {
|
|
throw PureXML.Parsing.ParseError.unexpectedEndOfInput(reader.mark)
|
|
}
|
|
return try PureXML.Parsing.EntityDecoder.decode(raw, entities: documentType.entities, budget: &entityBudget, at: mark)
|
|
}
|
|
}
|