Replace the notImplemented stub with a real parser built on a pull-based, streaming event core that never requires the whole document in memory. - Parsing.EventReader: pulls characters through a source closure with a bounded lookahead buffer and emits one Parsing.Event at a time, holding only an open-element stack. Drives chunked/streamed input. - Parsing.Parser: builds a Model.Node tree iteratively over the event core (explicit stack, no recursion). - Reader: streaming character cursor (closure-backed, bounded buffer) replacing the previous whole-string Array. - EntityDecoder: the five predefined entities and decimal/hex char references. - Handles elements, attributes, text, comments, CDATA, processing instructions, and an XML declaration in the prolog. Rejects <!DOCTYPE> by default (no DTD/XXE/entity-expansion exposure). - Top-level API: parse(_:), parse(pulling:), events(_:), events(pulling:). - 21 tests including event-stream and chunked-source coverage.
55 lines
2.3 KiB
Swift
55 lines
2.3 KiB
Swift
extension PureXML.Parsing {
|
|
/// Decodes XML references inside text and attribute values: the five
|
|
/// predefined entities and decimal/hex character references. General entity
|
|
/// declarations from a DTD are deliberately not supported (DTD processing is
|
|
/// off by default), so any other `&name;` is rejected rather than expanded.
|
|
enum EntityDecoder {
|
|
static func decode(_ raw: String, at mark: Mark) throws -> String {
|
|
guard raw.contains("&") else { return raw }
|
|
var result = ""
|
|
var iterator = raw.startIndex
|
|
while iterator < raw.endIndex {
|
|
let character = raw[iterator]
|
|
guard character == "&" else {
|
|
result.append(character)
|
|
iterator = raw.index(after: iterator)
|
|
continue
|
|
}
|
|
guard let semicolon = raw[iterator...].firstIndex(of: ";") else {
|
|
throw PureXML.Parsing.ParseError.invalidReference(String(raw[iterator...]), mark)
|
|
}
|
|
let body = String(raw[raw.index(after: iterator) ..< semicolon])
|
|
try result.append(resolve(body, at: mark))
|
|
iterator = raw.index(after: semicolon)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private static func resolve(_ body: String, at mark: Mark) throws -> Character {
|
|
switch body {
|
|
case "amp": return "&"
|
|
case "lt": return "<"
|
|
case "gt": return ">"
|
|
case "quot": return "\""
|
|
case "apos": return "'"
|
|
default:
|
|
break
|
|
}
|
|
|
|
guard body.hasPrefix("#") else {
|
|
throw PureXML.Parsing.ParseError.invalidReference("&\(body);", mark)
|
|
}
|
|
let digits = String(body.dropFirst())
|
|
let scalarValue: UInt32? = if digits.hasPrefix("x") || digits.hasPrefix("X") {
|
|
UInt32(digits.dropFirst(), radix: 16)
|
|
} else {
|
|
UInt32(digits, radix: 10)
|
|
}
|
|
guard let value = scalarValue, let scalar = Unicode.Scalar(value) else {
|
|
throw PureXML.Parsing.ParseError.invalidReference("&\(body);", mark)
|
|
}
|
|
return Character(scalar)
|
|
}
|
|
}
|
|
}
|