// 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 . 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. } }