PureXML/Sources/Parsing/PushAsync.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

40 lines
1.7 KiB
Swift

public extension PureXML {
/// Parses an asynchronous sequence of text chunks (a socket, a file read loop)
/// into a stream of ``Parsing/Event`` values, driving the push parser as each
/// chunk arrives. The document is never held whole.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
static func events<Chunks: AsyncSequence & Sendable>(
feeding chunks: Chunks,
) -> AsyncThrowingStream<Parsing.Event, Error> where Chunks.Element == String {
AsyncThrowingStream { continuation in
let task = Task {
do {
var parser = Parsing.PushParser(sax: Self.streamingHandler(continuation))
for try await chunk in chunks {
try parser.feed(chunk)
}
try parser.finish()
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
private static func streamingHandler(
_ continuation: AsyncThrowingStream<Parsing.Event, Error>.Continuation,
) -> Parsing.SAXHandler {
Parsing.SAXHandler(
startElement: { continuation.yield(.startElement(name: $0, attributes: $1)) },
endElement: { continuation.yield(.endElement(name: $0)) },
characters: { continuation.yield(.characters($0)) },
cdata: { continuation.yield(.cdata($0)) },
comment: { continuation.yield(.comment($0)) },
processingInstruction: { continuation.yield(.processingInstruction(target: $0, data: $1)) },
)
}
}