//! Tokenizer-owned HTML input and parser insertion frames. //! //! `html5ever` consumes one [`BufferQueue`] at a time, but `document.write()` //! can insert a new source in front of input that the tokenizer has not //! consumed yet. That unconsumed tail must not be discarded or appended to the //! inserted source. It becomes the parent frame and is restored after the //! inserted frame is empty: //! //! ```text //! before insertion: [ current tail ] //! during insertion: [ inserted input ] [ saved current tail ] //! nested insertion: [ inner input ] [ outer input ] [ original tail ] //! ``` //! //! This module owns only input that has already been admitted to the tokenizer. //! It does not decide whether an arriving chunk is parser-inserted or belongs //! at the end of the document stream, and it does not decide whether an empty //! parser should finish or wait for more input. Those decisions require runtime //! provenance and parser lifetime, respectively, and remain with their owners. //! The frames are explicit rather than represented by a stack-scoped guard //! because parser execution can yield for an external script and resume on a //! later runtime turn. use html5ever::{tendril::StrTendril, tokenizer::BufferQueue}; use std::rc::Rc; /// The tokenizer's current input plus suspended parent insertion frames. /// /// `parents` is stored from outermost to innermost. Consequently, pending input /// is consumed from `current`, followed by `parents` in reverse order. #[derive(Default)] pub(super) struct InputStack { current: Rc, parents: Vec>, } impl InputStack { /// Append ordinary input to the tokenizer's current queue. pub(super) fn push_back(&mut self, input: StrTendril) { self.current.push_back(input); } /// Start a parser-inserted frame before the current unconsumed input. /// /// Even an empty current queue is saved. Its presence records that there is /// an active insertion point to which a later write may belong. pub(super) fn begin_inserted(&mut self, input: StrTendril) { if input.is_empty() { return; } let parent = std::mem::take(&mut self.current); self.parents.push(parent); self.current.push_back(input); } /// Append after the unconsumed tail of the active inserted frame. /// /// This is different from starting another inserted frame: a nested frame /// would run before the current tail, while this input must run after it. /// Empty input is accepted as a no-op; non-empty input returns `false` when /// no parser insertion frame is active. pub(super) fn append_to_current_inserted(&mut self, input: StrTendril) -> bool { if input.is_empty() { return true; } if self.parents.is_empty() { return false; } self.current.push_back(input); true } /// Return the queue that `html5ever` may consume in the current parser step. pub(super) fn current(&self) -> Rc { self.current.clone() } pub(super) fn is_current(&self, input: &Rc) -> bool { Rc::ptr_eq(&self.current, input) } /// Restore one parent after the current frame has been fully consumed. /// /// At most one frame is restored per tokenizer boundary. The caller feeds /// the restored frame on its next step, preserving the same incremental /// parser behavior for every insertion depth. pub(super) fn restore_parent_if_current_empty(&mut self) -> bool { if !self.current.is_empty() { return false; } let Some(parent) = self.parents.pop() else { return false; }; self.current = parent; true } /// Return whether any frame still contains characters to tokenize. /// /// This deliberately says nothing about whether an insertion point exists: /// an active frame and all of its parents may legitimately be empty. pub(super) fn has_input(&self) -> bool { self.queues_in_consumption_order() .any(|input| !input.is_empty()) } pub(super) fn len(&self) -> usize { self.queues_in_consumption_order() .fold(0usize, |total, input| { total.saturating_add(queue_len(input)) }) } /// Snapshot pending characters in the order the tokenizer will see them. pub(super) fn snapshot(&self) -> String { let mut pending = String::new(); for input in self.queues_in_consumption_order() { append_queue_snapshot(input, &mut pending); } pending } /// Collapse every insertion frame into one queue for parser finalization. /// /// The innermost current frame stays first, followed by each parent from /// nearest to farthest. No input is reparsed or otherwise transformed. pub(super) fn into_buffer(mut self) -> BufferQueue { while let Some(parent) = self.parents.pop() { append_queue(&parent, &self.current); } Rc::try_unwrap(self.current) .unwrap_or_else(|_| panic!("cannot finish an active tokenizer input")) } fn queues_in_consumption_order(&self) -> impl Iterator { std::iter::once(self.current.as_ref()).chain(self.parents.iter().rev().map(Rc::as_ref)) } } fn queue_len(input: &BufferQueue) -> usize { let input = input.clone(); let mut len = 0usize; while let Some(chunk) = input.pop_front() { len = len.saturating_add(chunk.len()); } len } fn append_queue_snapshot(input: &BufferQueue, snapshot: &mut String) { let input = input.clone(); while let Some(chunk) = input.pop_front() { snapshot.push_str(&chunk); } } fn append_queue(source: &BufferQueue, destination: &BufferQueue) { while let Some(chunk) = source.pop_front() { destination.push_back(chunk); } } #[cfg(test)] mod tests { use super::*; fn tendril(input: &str) -> StrTendril { StrTendril::from(input) } #[test] fn nested_frames_are_snapshotted_and_flattened_in_consumption_order() { let mut input = InputStack::default(); input.push_back(tendril("e")); // Original parser tail. input.begin_inserted(tendril("r")); // Tail behind a nested external script. assert!(input.append_to_current_inserted(tendril("k"))); // Same inserted frame. input.begin_inserted(tendril("wo")); // Writes made by that external script. assert_eq!(input.snapshot(), "worke"); assert_eq!(input.len(), 5); let flattened = input.into_buffer(); let mut snapshot = String::new(); append_queue_snapshot(&flattened, &mut snapshot); assert_eq!(snapshot, "worke"); } #[test] fn an_empty_boundary_restores_exactly_one_parent() { let mut input = InputStack::default(); input.push_back(tendril("outer")); input.begin_inserted(tendril("middle")); input.begin_inserted(tendril("inner")); while input.current.pop_front().is_some() {} assert!(input.restore_parent_if_current_empty()); assert_eq!(input.snapshot(), "middleouter"); assert!(!input.restore_parent_if_current_empty()); } #[test] fn appending_to_an_inserted_frame_requires_an_active_insertion_point() { let mut input = InputStack::default(); assert!(!input.append_to_current_inserted(tendril("outside"))); input.begin_inserted(tendril("inside")); assert!(input.append_to_current_inserted(tendril(" tail"))); assert_eq!(input.snapshot(), "inside tail"); } }