PureXML/Sources/Parsing/Parser.swift
Mihaela Mihaljevic e4b2e0127d feat(parser): accept raw bytes with UTF-8/UTF-16 encoding detection
Add PureXML.parse(bytes:) and events(bytes:) for raw [UInt8] input. The encoding
is detected following the XML sniff order (BOM first, then the <? byte pattern,
defaulting to UTF-8) and decoded with the Swift standard library only, so the
package stays Foundation-free and portable. UTF-8 and UTF-16 (both byte orders,
with or without a BOM) are supported, the encoding set the XML spec requires.
Malformed input (odd-length UTF-16) raises ParseError.malformedEncoding.

PureXML.Parsing.InputEncoding.detect(_:) exposes the detector. The lint rule
optional_data_string_conversion is disabled because its suggested fix is a
Foundation API this package cannot use. Reimplemented from the documented XML
byte-detection algorithm; no upstream code copied.
2026-06-08 00:52:56 +02:00

90 lines
3.7 KiB
Swift

/// A partially-built element while its children stream in. File-scope and
/// private: an internal detail of the tree builder, not part of the namespace.
private struct ElementFrame {
let name: PureXML.Model.QualifiedName
let attributes: [PureXML.Model.Attribute]
var children: [PureXML.Model.Node] = []
}
public extension PureXML.Parsing {
/// Builds a ``PureXML/Model/Node`` tree by draining the streaming
/// ``EventReader`` with an explicit element stack. It is iterative, not
/// recursive, and the streaming core means it never requires the whole input
/// in memory at once: characters are pulled on demand. Callers that do not
/// need a full tree should consume ``EventReader`` directly.
struct Parser: Sendable {
public init() {}
/// Parses a single XML document from a string into a document node.
public func parse(_ xml: String, limits: Limits = .default) throws -> PureXML.Model.Node {
try build(EventReader(xml, limits: limits))
}
/// Parses a single XML document from raw bytes, detecting the encoding
/// (UTF-8 or UTF-16, with or without a byte-order mark) before parsing.
public func parse(bytes: [UInt8], limits: Limits = .default) throws -> PureXML.Model.Node {
try parse(ByteDecoder.decode(bytes), limits: limits)
}
/// Parses a single XML document from an incremental character source. The
/// closure returns the next character or nil at end of input, so the
/// document can arrive in chunks and is never held whole.
public func parse(
pulling pull: @escaping () -> Character?,
limits: Limits = .default,
) throws -> PureXML.Model.Node {
try build(EventReader(pulling: pull, limits: limits))
}
private func build(_ source: EventReader) throws -> PureXML.Model.Node {
var reader = source
var roots: [PureXML.Model.Node] = []
var stack: [ElementFrame] = []
var produced = false
while let event = try reader.next() {
produced = true
switch event {
case let .startElement(name, attributes):
stack.append(ElementFrame(name: name, attributes: attributes))
case .endElement:
guard let frame = stack.popLast() else {
throw ParseError.unexpectedEndOfInput(.start)
}
let element = PureXML.Model.Element(
name: frame.name,
attributes: frame.attributes,
children: frame.children,
)
attach(.element(element), to: &stack, roots: &roots)
case let .characters(text):
attach(.text(text), to: &stack, roots: &roots)
case let .cdata(text):
attach(.cdata(text), to: &stack, roots: &roots)
case let .comment(text):
attach(.comment(text), to: &stack, roots: &roots)
case let .processingInstruction(target, data):
attach(.processingInstruction(target: target, data: data), to: &stack, roots: &roots)
}
}
guard produced else {
throw ParseError.emptyDocument
}
return .document(roots)
}
private func attach(
_ node: PureXML.Model.Node,
to stack: inout [ElementFrame],
roots: inout [PureXML.Model.Node],
) {
if stack.isEmpty {
roots.append(node)
} else {
stack[stack.count - 1].children.append(node)
}
}
}
}