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)
117 lines
4.8 KiB
Swift
117 lines
4.8 KiB
Swift
extension PureXML.Parsing.EventReader {
|
|
/// Records the first XML declaration's pseudo-attributes, throwing when they
|
|
/// are malformed. A later `<?xml?>` (already invalid) is left untouched.
|
|
mutating func recordDeclaration(_ data: String, at mark: PureXML.Parsing.Mark) throws {
|
|
guard xmlDeclaration == nil else { return }
|
|
guard let declaration = PureXML.Parsing.XMLDeclaration.parse(data) else {
|
|
throw PureXML.Parsing.ParseError.malformedDeclaration(mark)
|
|
}
|
|
xmlDeclaration = declaration
|
|
if declaration.version == "1.1" { reader.xml11 = true }
|
|
}
|
|
|
|
/// The current source offset, used to tell whether a failing ``next()`` made
|
|
/// progress before throwing.
|
|
var offset: Int {
|
|
reader.mark.offset
|
|
}
|
|
|
|
/// The current source position, for attaching spans to ranged tree nodes.
|
|
var mark: PureXML.Parsing.Mark {
|
|
reader.mark
|
|
}
|
|
|
|
/// Resynchronizes after a failed ``next()`` that started at `offset`, so
|
|
/// reading can resume past a recorded diagnostic. Returns false at end of
|
|
/// input. If the failed scan already advanced (it consumed the malformed
|
|
/// construct), reading retries from the new position; if it threw without
|
|
/// progress, the offending run is skipped up to the next markup start. Either
|
|
/// way at least one character is consumed when input remains, so a recovering
|
|
/// loop is guaranteed to terminate.
|
|
mutating func recover(from offset: Int) -> Bool {
|
|
pending.removeAll()
|
|
guard reader.peek() != nil else { return false }
|
|
if reader.mark.offset > offset { return true }
|
|
reader.advance()
|
|
while let character = reader.peek(), character != "<" {
|
|
reader.advance()
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// Pops the open-element stack down to (and including) the most recent element
|
|
/// named `found`, synthesizing an end-tag event for each closed element,
|
|
/// innermost first. Returns the first such event with the rest queued, so a
|
|
/// mismatched end tag implicitly closes the elements nested inside the match.
|
|
/// Returns nil when no open element matches, dropping the stray end tag.
|
|
mutating func scanComment() throws -> PureXML.Parsing.Event {
|
|
let mark = reader.mark
|
|
reader.consume("<!--")
|
|
var content = ""
|
|
var length = 0
|
|
while !reader.matches("-->") {
|
|
guard let character = reader.advance() else {
|
|
throw PureXML.Parsing.ParseError.unterminatedComment(mark)
|
|
}
|
|
length += 1
|
|
try checkContent(length, mark)
|
|
// XML 1.0: '--' may not appear inside a comment (this also rejects
|
|
// the '--->' ending, since its first two hyphens hit this check).
|
|
if character == "-", reader.peek() == "-" {
|
|
throw PureXML.Parsing.ParseError.doubleHyphenInComment(mark)
|
|
}
|
|
content.append(character)
|
|
}
|
|
reader.consume("-->")
|
|
return .comment(content)
|
|
}
|
|
|
|
mutating func scanCDATA() throws -> PureXML.Parsing.Event {
|
|
let mark = reader.mark
|
|
reader.consume("<![CDATA[")
|
|
var content = ""
|
|
var length = 0
|
|
while !reader.matches("]]>") {
|
|
guard let character = reader.advance() else {
|
|
throw PureXML.Parsing.ParseError.unterminatedCDATA(mark)
|
|
}
|
|
length += 1
|
|
try checkContent(length, mark)
|
|
content.append(character)
|
|
}
|
|
reader.consume("]]>")
|
|
return .cdata(content)
|
|
}
|
|
|
|
/// Handles a thrown error in recovering mode: records the diagnostic and
|
|
/// returns an event to emit (an end tag synthesized by popping to a match) or
|
|
/// nil to continue after resynchronizing. Marks the end of input on truncation
|
|
/// or an exhausted source.
|
|
mutating func recoveredEvent(from error: PureXML.Parsing.ParseError, startingAt start: Int) -> PureXML.Parsing.Event? {
|
|
diagnostics.append(PureXML.Parsing.Diagnostic(error))
|
|
switch error {
|
|
case .unexpectedEndOfInput:
|
|
reachedEnd = true
|
|
return nil
|
|
case let .mismatchedEndTag(_, found, _), let .unexpectedEndTag(found, _):
|
|
return closeTo(found)
|
|
default:
|
|
if !recover(from: start) { reachedEnd = true }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
mutating func closeTo(_ found: String) -> PureXML.Parsing.Event? {
|
|
guard let index = open.lastIndex(where: { $0.description == found }) else { return nil }
|
|
var events: [PureXML.Parsing.Event] = []
|
|
while open.count > index {
|
|
let closed = open.removeLast()
|
|
namespaces.leaveElement()
|
|
events.append(.endElement(name: closed))
|
|
}
|
|
guard let first = events.first else { return nil }
|
|
pending.append(contentsOf: events.dropFirst())
|
|
return first
|
|
}
|
|
}
|