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.
106 lines
3.5 KiB
Swift
106 lines
3.5 KiB
Swift
extension PureXML.Parsing {
|
|
/// A streaming character cursor. It pulls characters from a source closure
|
|
/// one at a time, keeping only a small lookahead buffer, so it never holds
|
|
/// the whole input in memory. Tracks line, column, and offset for
|
|
/// diagnostics. Internal to the parser; not part of the API.
|
|
struct Reader {
|
|
private let pull: () -> Character?
|
|
private var buffer: [Character] = []
|
|
private var exhausted = false
|
|
private(set) var offset = 0
|
|
private(set) var line = 1
|
|
private(set) var column = 1
|
|
|
|
init(pulling pull: @escaping () -> Character?) {
|
|
self.pull = pull
|
|
}
|
|
|
|
init(_ string: String) {
|
|
var iterator = string.makeIterator()
|
|
self.init(pulling: { iterator.next() })
|
|
}
|
|
|
|
/// Ensures the lookahead buffer holds at least `count` characters (or the
|
|
/// source is exhausted). The buffer stays bounded by the largest lookahead
|
|
/// any single scan needs, never by the document size.
|
|
private mutating func ensure(_ count: Int) {
|
|
while buffer.count < count, !exhausted {
|
|
if let character = pull() {
|
|
buffer.append(character)
|
|
} else {
|
|
exhausted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
mutating func peek(_ ahead: Int = 0) -> Character? {
|
|
ensure(ahead + 1)
|
|
return ahead < buffer.count ? buffer[ahead] : nil
|
|
}
|
|
|
|
var mark: Mark {
|
|
Mark(line: line, column: column, offset: offset)
|
|
}
|
|
|
|
@discardableResult
|
|
mutating func advance() -> Character? {
|
|
ensure(1)
|
|
guard !buffer.isEmpty else { return nil }
|
|
let character = buffer.removeFirst()
|
|
offset += 1
|
|
if character == "\n" {
|
|
line += 1
|
|
column = 1
|
|
} else {
|
|
column += 1
|
|
}
|
|
return character
|
|
}
|
|
|
|
/// True if the upcoming characters equal `literal`, without consuming.
|
|
mutating func matches(_ literal: String) -> Bool {
|
|
let target = Array(literal)
|
|
ensure(target.count)
|
|
guard buffer.count >= target.count else { return false }
|
|
for index in target.indices where buffer[index] != target[index] {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// If the upcoming characters equal `literal`, consume them and return true.
|
|
@discardableResult
|
|
mutating func consume(_ literal: String) -> Bool {
|
|
guard matches(literal) else { return false }
|
|
for _ in literal {
|
|
advance()
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func skipSpace() {
|
|
while let character = peek(), character.isXMLWhitespace {
|
|
advance()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Character {
|
|
/// XML S production: space, tab, carriage return, line feed.
|
|
var isXMLWhitespace: Bool {
|
|
self == " " || self == "\t" || self == "\r" || self == "\n"
|
|
}
|
|
|
|
/// A character allowed to start an XML name (a practical subset of the spec
|
|
/// NameStartChar production: letters, underscore, and colon).
|
|
var isXMLNameStart: Bool {
|
|
isLetter || self == "_" || self == ":"
|
|
}
|
|
|
|
/// A character allowed within an XML name after the first.
|
|
var isXMLNameContinuation: Bool {
|
|
isXMLNameStart || isNumber || self == "-" || self == "."
|
|
}
|
|
}
|