PureXML/Sources/Schema/RelaxNGStreaming.swift

92 lines
4.2 KiB
Swift

// Streaming RELAX NG validation, split out so RelaxNGDerivative.swift stays
// within the file length budget.
/// One open element while streaming a RELAX NG derivative: whether it has had an
/// element child yet, and the text buffered since the last structural event. The
/// `after`-threaded pattern is the validation stack; this frame only carries the
/// bookkeeping `childrenDeriv` needs to treat ignorable whitespace correctly.
private struct RelaxNGStreamFrame {
var hadElementChild = false
var pendingText = ""
/// The namespace bindings in scope at this element, for QName values.
var env: [String: String] = [:]
}
public extension PureXML.Schema {
/// The residual derivative plus per-element whitespace bookkeeping carried
/// across the events of a streamed RELAX NG validation.
struct RelaxNGStreamingState {
var current: PureXML.Schema.Pattern
fileprivate var frames: [RelaxNGStreamFrame]
}
}
extension PureXML.Schema.RelaxNGEngine {
/// The initial streaming state for a start pattern, with a document-level frame.
func streamingStart(_ start: PureXML.Schema.Pattern) -> PureXML.Schema.RelaxNGStreamingState {
PureXML.Schema.RelaxNGStreamingState(current: start, frames: [RelaxNGStreamFrame(env: Self.rootEnvironment)])
}
/// Derives the state by one parse event, the streaming form of `childDeriv`.
/// Text is buffered and applied at structural boundaries so ignorable
/// whitespace is treated exactly as the tree walk's `childrenDeriv` does.
func streamingConsume(_ event: PureXML.Parsing.Event, into state: inout PureXML.Schema.RelaxNGStreamingState) {
switch event {
case let .startElement(name, attributes):
flushText(&state, closing: false)
if !state.frames.isEmpty {
state.frames[state.frames.count - 1].hadElementChild = true
}
var env = state.frames.last?.env ?? Self.rootEnvironment
for attribute in attributes where Self.isNamespaceDeclaration(attribute) {
env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value
}
let opened = startTagOpenDeriv(state.current, name)
let attributed = attributes
.filter { !Self.isNamespaceDeclaration($0) }
.reduce(opened) { attributeDeriv($0, $1, env: env) }
state.current = startTagCloseDeriv(attributed)
state.frames.append(RelaxNGStreamFrame(env: env))
case let .characters(text), let .cdata(text):
if !state.frames.isEmpty {
state.frames[state.frames.count - 1].pendingText += text
}
case .endElement:
flushText(&state, closing: true)
state.current = endTagDeriv(state.current)
if !state.frames.isEmpty {
state.frames.removeLast()
}
case .comment, .processingInstruction:
break
}
}
/// Whether the streamed document satisfied the schema (the residual is nullable).
func streamingValid(_ state: PureXML.Schema.RelaxNGStreamingState) -> Bool {
nullable(state.current)
}
private func flushText(_ state: inout PureXML.Schema.RelaxNGStreamingState, closing: Bool) {
guard let index = state.frames.indices.last else { return }
let text = state.frames[index].pendingText
let env = state.frames[index].env
guard !text.isEmpty else {
// An empty element is optional empty text (the tree walk's rule),
// so a data/value pattern admitting "" matches <e/>.
if closing, !state.frames[index].hadElementChild {
state.current = choice(state.current, textDeriv(state.current, "", env: env))
}
return
}
state.frames[index].pendingText = ""
if !Self.isWhitespace(text) {
state.current = textDeriv(state.current, text, env: env)
} else if closing, !state.frames[index].hadElementChild {
// A lone whitespace text child is optional, the `childrenDeriv` rule.
state.current = choice(state.current, textDeriv(state.current, text, env: env))
}
// Whitespace among element siblings is ignorable and skipped.
}
}