PureXML/Sources/Parsing/PushParser.swift
Mihaela Mihaljevic 4e5debd557 feat(parsing): resumable push / feed streaming API
Add a resumable XML scanner that reports a token only once its terminator is
fully buffered and otherwise signals need-more (the Expat XML_TOK_PARTIAL
model), and PushParser with feed(_:)/finish() driving a SAXHandler while
retaining only the current incomplete token plus the open-element stack.
events(feeding:) exposes an AsyncThrowingStream of events over any async
sequence of text chunks. Splitting a document at any byte boundary, down to
one character per feed, yields identical events. Collapse the completed
roadmap into the legend summary.

Closes #4
Closes #5
Closes #6
Closes #7
Closes #1
2026-06-08 07:21:22 +02:00

87 lines
3.3 KiB
Swift

public extension PureXML.Parsing {
/// A push-style XML parser: feed input as it arrives with ``feed(_:)`` and end
/// the document with ``finish()``. It drives a ``SAXHandler``, holding only a
/// small buffer (the current incomplete token) plus the open-element stack, so
/// it never requires the whole document and resumes cleanly across chunk
/// boundaries (the Expat `XML_TOK_PARTIAL` model).
struct PushParser {
private var buffer: [Character] = []
private var namespaces = NamespaceContext()
private var open: [PureXML.Model.QualifiedName] = []
private let handler: SAXHandler
private var started = false
/// Creates a push parser delivering events to `sax`.
public init(sax: SAXHandler) {
handler = sax
}
/// Feeds the next chunk of input, delivering events for every token that is
/// now complete.
public mutating func feed(_ text: String) throws {
buffer += Array(text)
try drain(final: false)
}
/// Ends the document, flushing any trailing token and the document-end
/// callback.
public mutating func finish() throws {
ensureStarted()
try drain(final: true)
handler.endDocument?()
}
private mutating func ensureStarted() {
guard !started else { return }
started = true
handler.startDocument?()
}
private mutating func drain(final: Bool) throws {
ensureStarted()
while case let .token(token, consumed) = PushScanner.scan(buffer, final: final) {
buffer.removeFirst(consumed)
try deliver(token)
}
}
private mutating func deliver(_ token: PushToken) throws {
switch token {
case let .open(name, attributes, selfClosing):
try open(name, attributes, selfClosing: selfClosing)
case let .close(name):
close(name)
case let .text(value):
if !value.isEmpty { handler.characters?(value) }
case let .comment(value):
handler.comment?(value)
case let .cdata(value):
handler.cdata?(value)
case let .processingInstruction(target, data):
if target.lowercased() != "xml" { handler.processingInstruction?(target, data) }
case .ignorable:
break
}
}
private mutating func open(_ name: String, _ attributes: [PushAttribute], selfClosing: Bool) throws {
let rawName = PureXML.Model.QualifiedName(name)
let rawAttributes = attributes.map { PureXML.Model.Attribute(name: .init($0.name), value: $0.value) }
let resolved = try namespaces.enterElement(name: rawName, attributes: rawAttributes, at: .start)
handler.startElement?(resolved.name, resolved.attributes)
if selfClosing {
namespaces.leaveElement()
handler.endElement?(resolved.name)
} else {
open.append(resolved.name)
}
}
private mutating func close(_: String) {
guard let top = open.popLast() else { return }
namespaces.leaveElement()
handler.endElement?(top)
}
}
}