The eduni sections run with exact classified baselines. Fixes: 3.3.3 CDATA attribute-value normalization (literal whitespace to space before reference decoding; char-ref whitespace survives; tokenized collapse on SPACE only); production-68 lenient undeclared entities in content and attribute values (validity finding, reference kept literal, nothing fetched); errata E15 (references/comments/PIs/whitespace in EMPTY, char-ref whitespace as data in element content, double-escape shape included); errata E2 duplicate enumeration tokens; errata E14 declarations completed inside PE replacements (VC: Proper Declaration/PE Nesting); NCName ID values; namespace declarations subject to the every-attribute-declared rule. Reader validity machinery split to EventReaderValidity for caps. Closes #127
66 lines
3.0 KiB
Swift
66 lines
3.0 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)
|
|
}
|
|
guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else {
|
|
throw PureXML.Parsing.ParseError.invalidCharacter(reader.mark)
|
|
}
|
|
// 3.3.3: a literal whitespace character in an attribute value
|
|
// normalizes to a space; this happens before reference decoding,
|
|
// so whitespace that arrives via a character reference survives.
|
|
raw.append(character.isXMLWhitespace ? " " : character)
|
|
reader.advance()
|
|
}
|
|
guard reader.consume(String(quote)) else {
|
|
throw PureXML.Parsing.ParseError.unexpectedEndOfInput(reader.mark)
|
|
}
|
|
return try decodeReferences(raw, at: mark)
|
|
}
|
|
}
|