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.
90 lines
3.7 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|