use std::{ borrow::Cow, cell::{Cell, RefCell}, collections::VecDeque, rc::Rc, }; use html5ever::{ Attribute as HtmlAttribute, LocalName as HtmlLocalName, Namespace as HtmlNamespace, Prefix as HtmlPrefix, QualName as HtmlQualName, tendril::StrTendril as HtmlStrTendril, tree_builder::{ElementFlags as HtmlElementFlags, NodeOrText as HtmlNodeOrText}, }; use moli_dom::native::{DomHost, NativeDom, NativeNodeId}; use url::Url; use xml5ever::{ Attribute as XmlAttribute, ExpandedName, QualName as XmlQualName, TokenizerResult, buffer_queue::BufferQueue, driver::XmlParseOpts, interface::{ElementFlags as XmlElementFlags, QuirksMode as XmlQuirksMode}, tendril::StrTendril, tokenizer::{ProcessResult, Token, TokenSink, XmlTokenizer}, tree_builder::{NodeOrText as XmlNodeOrText, TreeSink, XmlTreeBuilder}, }; use crate::{ ParserBlockingStylesheetPause, ParserDomMutationConsumer, ParserDomReadConsumer, ParserElementCreationConsumer, ParserFinishDiscoverySignals, ParserMutationEffectConsumer, ParserPlanningReadView, ParserPumpOutcome, ParserPumpStep, ParserScriptHandoff, ParserYield, PreparedScript, html::ParseHandle, live_target::{ ParserMutationEffectDelivery, ParserRuntimeDomSinks, ParserStreamHtmlTreeSinkTarget, }, stream::prepare_parser_script_handoff_for_static_document, xml_tree_viewer::transform_parser_target_to_xml_tree_view, }; /// Incremental XML backend for an executable Document. /// /// Unlike xml5ever's high-level `XmlParser` driver, this owner surfaces /// `TokenizerResult::Script` instead of immediately feeding through it. The /// tokenizer, tree builder, unconsumed input, and live-DOM target therefore /// remain in one parser session across script and stylesheet suspension. pub struct XmlDocumentStream { parser: XmlParserSession, input: XmlParserInputStream, eof_declared: bool, } #[derive(Default)] struct XmlParserInputStream { end_segments: VecDeque, } struct XmlParserSession { tokenizer: XmlTokenizer, input_buffer: BufferQueue, } struct EmbedderPausingXmlTreeBuilder { inner: XmlTreeBuilder, } struct XmlStreamDocumentSink { target: RefCell, quirks_mode: Cell, } struct XmlParserStreamTarget { common: ParserStreamHtmlTreeSinkTarget, present_unstyled_top_level_document: bool, } #[derive(Debug, Clone)] struct XmlStreamParseHandle { inner: ParseHandle, element_name: Option>, suppress_append: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RawXmlParserStep { Script(NativeNodeId), BlockingStylesheet(NativeNodeId), InputDrained, } impl XmlStreamDocumentSink { fn mutate_target( &self, mutation: impl FnOnce(&mut ParserStreamHtmlTreeSinkTarget) -> ParserMutationEffectDelivery, ) { let delivery = mutation(&mut self.target.borrow_mut().common); delivery.consume(); let finish = self.target.borrow().common.mutation_finisher(); if let Some(finish) = finish { let _ = finish(); } } } impl XmlDocumentStream { /// Starts a raw XML stream. This is suitable for parser tests and inert /// consumers; executable top-level navigation uses /// `new_top_level_document` explicitly. pub fn new(final_url: Url) -> Self { Self::from_target(ParserStreamHtmlTreeSinkTarget::new_xml(final_url), false) } pub fn new_top_level_document(final_url: Url) -> Self { Self::from_target(ParserStreamHtmlTreeSinkTarget::new_xml(final_url), true) } pub fn new_live_document_root(final_url: Url, document_handle: NativeNodeId) -> Self { Self::from_target( ParserStreamHtmlTreeSinkTarget::new_live_xml_document_root(final_url, document_handle), false, ) } fn from_target( common: ParserStreamHtmlTreeSinkTarget, present_unstyled_top_level_document: bool, ) -> Self { let target = XmlParserStreamTarget { common, present_unstyled_top_level_document, }; Self { parser: XmlParserSession::new(target, XmlParseOpts::default()), input: XmlParserInputStream::default(), eof_declared: false, } } /// Appends decoded XML bytes to the parser-owned end segment chain. /// Appending does not run the tokenizer or mutate the DOM. pub fn append_to_end(&mut self, chunk: String) { assert!(!self.eof_declared, "cannot append XML input after EOF"); if chunk.is_empty() { return; } self.input.end_segments.push_back(chunk); } /// Declares source EOF. Calling this more than once is a no-op. pub fn declare_eof(&mut self) { if self.eof_declared { return; } self.eof_declared = true; } pub fn has_pending_input(&self) -> bool { self.parser.has_buffered_input() || !self.input.end_segments.is_empty() } pub fn next_input_len(&self) -> usize { if self.parser.has_buffered_input() { self.parser.buffered_input_len() } else { self.input .end_segments .front() .map(String::len) .unwrap_or_default() } } pub fn snapshot_pending_input(&self) -> String { let mut pending = self.parser.snapshot_buffered_input(); for segment in &self.input.end_segments { pending.push_str(segment); } pending } pub fn pump_next_parser_step(&mut self, max_bytes: usize) -> ParserPumpOutcome { let chunk = self.take_next_owned_input(max_bytes); self.parser.pump_parser_step(&chunk) } pub fn pump_next_parser_step_with_runtime_dom_consumer( &mut self, max_bytes: usize, consumer: &mut T, ) -> ParserPumpOutcome where T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer + ParserElementCreationConsumer, { // SAFETY: `consumer` remains exclusively borrowed until the scoped // guard clears every erased callback at the end of this parser step. let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) }; self.parser.enter_runtime_dom_sinks_parse_step(sinks); let step = RuntimeXmlDomSinksParserStep { stream: self }; step.stream.pump_next_parser_step(max_bytes) } pub fn snapshot_parser_stream_document(&self) -> NativeDom { self.parser .sink() .borrow() .common .snapshot_parser_stream_document() } pub fn snapshot_parser_stream_dom_host(&self) -> DomHost { self.parser .sink() .borrow() .common .snapshot_parser_stream_dom_host() } pub fn take_parser_stream_dom_host(&mut self) -> DomHost { self.parser .sink() .borrow_mut() .common .take_parser_stream_document() } pub fn restore_parser_stream_dom_host(&mut self, dom_host: DomHost) { self.parser .sink() .borrow_mut() .common .restore_parser_stream_dom_host(dom_host); } pub fn set_document_content_type(&mut self, content_type: String) { let mut dom_host = self.take_parser_stream_dom_host(); let document_handle = dom_host.document_handle(); let _ = dom_host.set_document_content_type_for_handle(document_handle, content_type); self.restore_parser_stream_dom_host(dom_host); } pub fn with_parser_stream_dom_host_for_bootstrap( &mut self, f: impl FnOnce(DomHost) -> std::result::Result>, ) -> anyhow::Result { let bootstrap_document = self.take_parser_stream_dom_host(); match f(bootstrap_document) { Ok(result) => Ok(result), Err(error) => { let (error, bootstrap_document) = *error; self.restore_parser_stream_dom_host(bootstrap_document); Err(error) } } } pub fn take_parser_stream_null_custom_element_registry_elements( &mut self, ) -> Vec { self.parser .sink() .borrow_mut() .common .take_parser_stream_null_custom_element_registry_elements() } pub fn with_stylesheet_blocking_read_view( &self, f: impl FnOnce(&dyn moli_stylesheet_blocking::StylesheetBlockingReadView) -> R, ) -> R { let target = self.parser.sink().borrow(); f(&target.common) } pub fn drain_discovered_parser_meta_csp_candidates(&mut self) -> Vec { self.parser .sink() .borrow_mut() .common .drain_discovered_parser_meta_csp_candidates() } pub fn mark_script_already_started(&mut self, node_id: NativeNodeId) { self.parser .sink() .borrow_mut() .common .mark_script_already_started(node_id); } pub fn finish_with_runtime_dom_consumer( mut self, consumer: &mut T, ) -> ParserFinishDiscoverySignals where T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer + ParserElementCreationConsumer, { self.declare_eof(); assert!( !self.has_pending_input(), "XML parser must drain all source before finish" ); // SAFETY: `consumer` remains exclusively borrowed for the consuming // finish operation. The unwind guard clears callbacks if finish panics. let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) }; self.parser.enter_runtime_dom_sinks_parse_step(sinks); let mut finish = RuntimeXmlDomSinksParserFinish { parser: Some(self.parser), }; finish.finish() } pub fn finish(mut self) -> NativeDom { self.declare_eof(); while self.has_pending_input() { let _ = self.pump_next_parser_step(0); } self.parser.finish_owned().finish_dom_host().into_dom() } fn take_next_owned_input(&mut self, max_bytes: usize) -> String { if self.parser.has_buffered_input() { return String::new(); } let Some(input) = self.input.end_segments.pop_front() else { return String::new(); }; let (prefix, remainder) = split_xml_parser_input_prefix(input, max_bytes); if let Some(remainder) = remainder { self.input.end_segments.push_front(remainder); } prefix } } impl XmlParserSession { fn new(target: XmlParserStreamTarget, opts: XmlParseOpts) -> Self { let sink = XmlStreamDocumentSink { target: RefCell::new(target), quirks_mode: Cell::new(XmlQuirksMode::NoQuirks), }; let tree_builder = XmlTreeBuilder::new(sink, opts.tree_builder); let tokenizer = XmlTokenizer::new( EmbedderPausingXmlTreeBuilder { inner: tree_builder, }, opts.tokenizer, ); Self { tokenizer, input_buffer: BufferQueue::default(), } } fn sink(&self) -> &RefCell { &self.tokenizer.sink.inner.sink.target } fn has_buffered_input(&self) -> bool { !self.input_buffer.is_empty() } fn buffered_input_len(&self) -> usize { let input = self.input_buffer.clone(); let mut len = 0usize; while let Some(chunk) = input.pop_front() { len = len.saturating_add(chunk.len()); } len } fn snapshot_buffered_input(&self) -> String { let input = self.input_buffer.clone(); let mut buffered = String::new(); while let Some(chunk) = input.pop_front() { buffered.push_str(&chunk); } buffered } fn enter_runtime_dom_sinks_parse_step(&mut self, sinks: ParserRuntimeDomSinks) { self.sink() .borrow_mut() .common .enter_runtime_dom_sinks_parse_step(sinks); } fn clear_runtime_dom_sinks_after_parse_step(&mut self) { self.sink() .borrow_mut() .common .clear_runtime_dom_sinks_after_parse_step(); } fn pump_parser_step(&mut self, chunk: &str) -> ParserPumpOutcome { if !chunk.is_empty() { self.input_buffer.push_back(StrTendril::from(chunk)); } let raw_step = loop { match self.tokenizer.feed(&self.input_buffer) { TokenizerResult::Done => break RawXmlParserStep::InputDrained, TokenizerResult::EncodingIndicator(_) => continue, TokenizerResult::Script(handle) => { let node_id = handle.inner.node_id(); let target = self.sink().borrow(); if target.common.front_pending_blocking_stylesheet_pause() == Some(node_id) { break RawXmlParserStep::BlockingStylesheet(node_id); } if ParserPlanningReadView::parser_script_read(&target.common, node_id).is_some() { break RawXmlParserStep::Script(node_id); } // xml5ever yields on every local-name `script`, including // non-HTML namespaces. Those nodes are not executable and // therefore do not suspend an executable Document parser. } } }; let mut target = self.sink().borrow_mut(); let async_candidate_ids = target.common.drain_discovered_async_prefetch_candidates(); let discovered_async_prefetch_scripts = async_candidate_ids .iter() .filter_map(|node_id| { match prepare_parser_script_handoff_for_static_document( &target.common, *node_id, 0, 0, ) { ParserScriptHandoff::AsyncPostParse { script, .. } => Some(script), _ => None, } }) .collect::>(); let discovered_modulepreload_link_candidates = target .common .drain_discovered_modulepreload_link_candidates(); let discovered_blocking_stylesheet_inputs = target.common.drain_discovered_blocking_stylesheet_inputs(); let result = match raw_step { RawXmlParserStep::Script(node_id) => { let handoff = prepare_parser_script_handoff_for_static_document( &target.common, node_id, 0, 0, ); ParserPumpStep::Yield(ParserYield::Script(Box::new(handoff))) } RawXmlParserStep::BlockingStylesheet(node_id) => { let pending = target.common.pop_pending_blocking_stylesheet_pause(); assert_eq!(pending, Some(node_id)); ParserPumpStep::Yield(ParserYield::BlockingStylesheet( ParserBlockingStylesheetPause { node_id }, )) } RawXmlParserStep::InputDrained => ParserPumpStep::InputDrained, }; ParserPumpOutcome { result, discovered_async_prefetch_scripts, discovered_modulepreload_link_candidates, discovered_blocking_stylesheet_inputs, } } fn finish_owned(self) -> ParserStreamHtmlTreeSinkTarget { let Self { tokenizer, input_buffer, } = self; debug_assert!(input_buffer.is_empty()); tokenizer.end(); let mut target = tokenizer.sink.inner.sink.finish(); target.present_unstyled_top_level_document_if_needed(); target.common } fn finish_live_runtime_dom_sink_parser(self) -> ParserFinishDiscoverySignals { let Self { tokenizer, input_buffer, } = self; debug_assert!(input_buffer.is_empty()); tokenizer.end(); let mut target = tokenizer.sink.inner.sink.target.borrow_mut(); target.present_unstyled_top_level_document_if_needed(); ParserFinishDiscoverySignals { parser_created_null_registry_elements: target .common .take_parser_stream_null_custom_element_registry_elements(), discovered_modulepreload_link_candidates: target .common .drain_discovered_modulepreload_link_candidates(), discovered_parser_meta_csp_candidates: target .common .drain_discovered_parser_meta_csp_candidates(), discovered_blocking_stylesheet_inputs: target .common .drain_discovered_blocking_stylesheet_inputs(), } } } impl TokenSink for EmbedderPausingXmlTreeBuilder { type Handle = XmlStreamParseHandle; fn process_token(&self, token: Token) -> ProcessResult { let result = self.inner.process_token(token); if !matches!(result, ProcessResult::Continue) { return result; } let pending_stylesheet = self .inner .sink .target .borrow() .common .front_pending_blocking_stylesheet_pause(); if let Some(node_id) = pending_stylesheet { return ProcessResult::Script(XmlStreamParseHandle::new(ParseHandle::new( node_id, None, ))); } ProcessResult::Continue } fn end(&self) { self.inner.end(); } } impl XmlParserStreamTarget { fn present_unstyled_top_level_document_if_needed(&mut self) { if self.present_unstyled_top_level_document { transform_parser_target_to_xml_tree_view(&mut self.common); } } } impl XmlStreamParseHandle { fn new(inner: ParseHandle) -> Self { Self { inner, element_name: None, suppress_append: false, } } fn element(inner: ParseHandle, element_name: Rc) -> Self { Self { inner, element_name: Some(element_name), suppress_append: false, } } fn suppressed(inner: ParseHandle) -> Self { Self { inner, element_name: None, suppress_append: true, } } } impl PartialEq for XmlStreamParseHandle { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl Eq for XmlStreamParseHandle {} impl TreeSink for XmlStreamDocumentSink { type Handle = XmlStreamParseHandle; type Output = XmlParserStreamTarget; type ElemName<'a> = ExpandedName<'a> where Self: 'a; fn finish(self) -> Self::Output { self.target.into_inner() } fn parse_error(&self, error: Cow<'static, str>) { self.target .borrow_mut() .common .push_parse_error(error.into_owned()); } fn get_document(&self) -> Self::Handle { XmlStreamParseHandle::new(self.target.borrow().common.document_handle()) } fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> { target .element_name .as_deref() .expect("xml5ever requested the name of a non-element node") .expanded() } fn create_element( &self, name: XmlQualName, attrs: Vec, flags: XmlElementFlags, ) -> Self::Handle { let mut target = self.target.borrow_mut(); let element_name = Rc::new(name.clone()); let html_name = xml_name_to_html(&name); let html_attrs = attrs.into_iter().map(xml_attribute_to_html).collect(); let mut html_flags = HtmlElementFlags::default(); html_flags.template = flags.template; html_flags.mathml_annotation_xml_integration_point = flags.mathml_annotation_xml_integration_point; XmlStreamParseHandle::element( target .common .create_element(html_name, html_attrs, html_flags), element_name, ) } fn create_comment(&self, text: StrTendril) -> Self::Handle { XmlStreamParseHandle::new( self.target .borrow_mut() .common .create_comment(text.to_string()), ) } fn create_pi(&self, target: StrTendril, data: StrTendril) -> Self::Handle { let target_text = target.to_string(); let data_text = data.to_string(); let mut parser_target = self.target.borrow_mut(); let handle = parser_target .common .create_processing_instruction(target_text.clone(), data_text); if target_text.eq_ignore_ascii_case("xml") { XmlStreamParseHandle::suppressed(handle) } else { XmlStreamParseHandle::new(handle) } } fn append(&self, parent: &Self::Handle, child: XmlNodeOrText) { let child = match child { XmlNodeOrText::AppendNode(handle) if handle.suppress_append => return, XmlNodeOrText::AppendNode(handle) => HtmlNodeOrText::AppendNode(handle.inner), XmlNodeOrText::AppendText(text) => { HtmlNodeOrText::AppendText(HtmlStrTendril::from(text.as_ref())) } }; self.mutate_target(|target| target.append(parent.inner.node_id(), child)); } fn append_before_sibling(&self, sibling: &Self::Handle, child: XmlNodeOrText) { let child = match child { XmlNodeOrText::AppendNode(handle) if handle.suppress_append => return, XmlNodeOrText::AppendNode(handle) => HtmlNodeOrText::AppendNode(handle.inner), XmlNodeOrText::AppendText(text) => { HtmlNodeOrText::AppendText(HtmlStrTendril::from(text.as_ref())) } }; self.mutate_target(|target| target.append_before_sibling(sibling.inner.node_id(), child)); } fn append_based_on_parent_node( &self, element: &Self::Handle, prev_element: &Self::Handle, child: XmlNodeOrText, ) { let child = match child { XmlNodeOrText::AppendNode(handle) if handle.suppress_append => return, XmlNodeOrText::AppendNode(handle) => HtmlNodeOrText::AppendNode(handle.inner), XmlNodeOrText::AppendText(text) => { HtmlNodeOrText::AppendText(HtmlStrTendril::from(text.as_ref())) } }; self.mutate_target(|target| { target.append_based_on_parent_node( element.inner.node_id(), prev_element.inner.node_id(), child, ) }); } fn append_doctype_to_document( &self, name: StrTendril, public_id: StrTendril, system_id: StrTendril, ) { self.mutate_target(|target| { target.append_doctype( name.to_string(), public_id.to_string(), system_id.to_string(), ) }); } fn mark_script_already_started(&self, node: &Self::Handle) { self.target .borrow_mut() .common .mark_script_already_started(node.inner.node_id()); } fn pop(&self, node: &Self::Handle) { let is_script = node.inner.element_name.as_ref().is_some_and(|name| { name.local.as_ref() == "script" && matches!( name.ns.as_ref(), "http://www.w3.org/1999/xhtml" | "http://www.w3.org/2000/svg" ) }); self.target .borrow_mut() .common .note_node_closed(node.inner.node_id(), is_script); } fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle { self.target .borrow_mut() .common .template_contents_handle(target.inner.node_id()) .map(XmlStreamParseHandle::new) .unwrap_or_else(|| target.clone()) } fn same_node(&self, left: &Self::Handle, right: &Self::Handle) -> bool { left == right } fn set_quirks_mode(&self, mode: XmlQuirksMode) { self.quirks_mode.set(mode); } fn add_attrs_if_missing(&self, target: &Self::Handle, attrs: Vec) { self.target.borrow_mut().common.add_attrs_if_missing( target.inner.node_id(), attrs.into_iter().map(xml_attribute_to_html).collect(), ); } fn remove_from_parent(&self, target: &Self::Handle) { self.mutate_target(|parser| parser.remove_from_parent(target.inner.node_id())); } fn reparent_children(&self, node: &Self::Handle, new_parent: &Self::Handle) { self.mutate_target(|target| { target.reparent_children(node.inner.node_id(), new_parent.inner.node_id()) }); } } impl xml5ever::tree_builder::XmlTreeSink for XmlStreamDocumentSink { fn create_cdata(&self, text: StrTendril) -> Self::Handle { XmlStreamParseHandle::new( self.target .borrow_mut() .common .create_cdata_section(text.to_string()), ) } } fn xml_name_to_html(name: &XmlQualName) -> HtmlQualName { HtmlQualName::new( name.prefix .as_ref() .map(|prefix| HtmlPrefix::from(prefix.as_ref())), HtmlNamespace::from(name.ns.as_ref()), HtmlLocalName::from(name.local.as_ref()), ) } fn xml_attribute_to_html(attribute: XmlAttribute) -> HtmlAttribute { HtmlAttribute { name: xml_name_to_html(&attribute.name), value: HtmlStrTendril::from(attribute.value.as_ref()), } } fn split_xml_parser_input_prefix(input: String, max_bytes: usize) -> (String, Option) { if max_bytes == 0 || input.len() <= max_bytes { return (input, None); } let mut split = input.len(); for (index, character) in input.char_indices() { let end = index + character.len_utf8(); if end > max_bytes { split = if index == 0 { end } else { index }; break; } } let remainder = input[split..].to_owned(); let prefix = input[..split].to_owned(); (prefix, (!remainder.is_empty()).then_some(remainder)) } struct RuntimeXmlDomSinksParserStep<'a> { stream: &'a mut XmlDocumentStream, } impl Drop for RuntimeXmlDomSinksParserStep<'_> { fn drop(&mut self) { self.stream .parser .clear_runtime_dom_sinks_after_parse_step(); } } struct RuntimeXmlDomSinksParserFinish { parser: Option, } impl RuntimeXmlDomSinksParserFinish { fn finish(&mut self) -> ParserFinishDiscoverySignals { self.parser .take() .map(XmlParserSession::finish_live_runtime_dom_sink_parser) .unwrap_or_default() } } impl Drop for RuntimeXmlDomSinksParserFinish { fn drop(&mut self) { if let Some(parser) = &mut self.parser { parser.clear_runtime_dom_sinks_after_parse_step(); } } } #[cfg(test)] mod tests { use moli_dom::native::{Node, NodeType}; use url::Url; use crate::{ParserPumpStep, ParserYield, XmlDocumentStream}; fn test_url() -> Url { Url::parse("https://example.test/page.xhtml").expect("test URL") } fn element_by_id( document: &moli_dom::native::NativeDom, id: &str, ) -> Option { document .nodes() .find(|node| { node.as_element() .and_then(|element| element.attribute("id")) == Some(id) }) .map(Node::id) } #[test] fn incremental_xml_stops_before_future_dom_at_parser_script() { let mut stream = XmlDocumentStream::new(test_url()); stream.append_to_end( concat!( "", "
", "
" ) .to_owned(), ); stream.declare_eof(); let first = stream.pump_next_parser_step(0); let ParserPumpStep::Yield(ParserYield::Script(handoff)) = first.result else { panic!("expected an executable XML parser script handoff"); }; let crate::ParserScriptHandoff::BlockingClassic { script, .. } = *handoff else { panic!("inline XHTML script must use the parser-blocking classic lane"); }; assert!(matches!( script.source, crate::ScriptSource::Inline(ref source) if source == "globalThis.hit = true;" )); let paused = stream.snapshot_parser_stream_dom_host(); assert!(paused.element_handle_by_id("before").is_some()); assert!( paused.element_handle_by_id("future").is_none(), "XML future nodes must not be materialized past a parser script" ); let second = stream.pump_next_parser_step(0); assert!(matches!(second.result, ParserPumpStep::InputDrained)); let resumed = stream.snapshot_parser_stream_dom_host(); assert!(resumed.element_handle_by_id("future").is_some()); } #[test] fn incremental_xml_preserves_chunked_cdata_and_namespace_declarations() { let mut stream = XmlDocumentStream::new(test_url()); for chunk in [ " & ]]>", ] { stream.append_to_end(chunk.to_owned()); while stream.has_pending_input() { let outcome = stream.pump_next_parser_step(3); assert!(matches!(outcome.result, ParserPumpStep::InputDrained)); } } stream.declare_eof(); while stream.has_pending_input() { let outcome = stream.pump_next_parser_step(3); assert!(matches!(outcome.result, ParserPumpStep::InputDrained)); } let document = stream.finish(); let root = document .document_element_node_id() .expect("document element"); let attributes = document .node(root) .and_then(Node::as_element) .expect("root element") .attributes(); assert_eq!( attributes .iter() .map(|attribute| attribute.name()) .collect::>(), ["data-before", "xmlns:m"] ); let child = document .child_ids(root) .find(|node_id| document.node(*node_id).is_some_and(Node::is_element)) .expect("child element"); let cdata = document.child_ids(child).next().expect("CDATA child"); assert_eq!( document.node(cdata).map(Node::node_type), Some(NodeType::CDataSection) ); assert_eq!( document .node(cdata) .and_then(Node::as_cdata_section) .map(|section| section.data()), Some(" < > & ") ); } #[test] fn incremental_xml_preserves_namespaces_across_many_single_byte_chunks() { let mut source = String::from(""); for index in 0..512 { source.push_str(&format!( "" )); } source.push_str(""); let mut stream = XmlDocumentStream::new(test_url()); for byte in source.bytes() { stream.append_to_end(char::from(byte).to_string()); } let document = stream.finish(); let items = document .nodes() .filter_map(Node::as_element) .filter(|element| element.local_name() == "item") .collect::>(); assert_eq!(items.len(), 512); for (index, item) in items.into_iter().enumerate() { assert_eq!(item.namespace(), format!("urn:item:{index}")); let attributes = item.attributes(); assert_eq!( attributes .iter() .map(|attribute| attribute.name()) .collect::>(), ["xmlns:p", "data-index"] ); assert_eq!(attributes[0].namespace(), "http://www.w3.org/2000/xmlns/"); assert_eq!(attributes[0].value(), format!("urn:item:{index}")); } } #[test] fn incremental_xml_keeps_cdata_syntax_in_comment_and_pi() { let mut stream = XmlDocumentStream::new(test_url()); for chunk in [""] { stream.append_to_end(chunk.to_owned()); while stream.has_pending_input() { let outcome = stream.pump_next_parser_step(2); assert!(matches!(outcome.result, ParserPumpStep::InputDrained)); } } let snapshot = stream.snapshot_parser_stream_document(); assert_eq!( snapshot .nodes() .find_map(Node::as_comment) .map(|comment| comment.data()), Some(" ?>", "", ] { stream.append_to_end(chunk.to_owned()); } let document = stream.finish(); let instructions = document .nodes() .filter_map(Node::as_processing_instruction) .map(|instruction| (instruction.target(), instruction.data())) .collect::>(); assert_eq!( instructions, [("moli-cdata", "0"), ("probe", "")] ); assert_eq!( document .nodes() .find_map(Node::as_cdata_section) .map(|section| section.data()), Some("real") ); } #[test] fn incremental_xml_does_not_suspend_for_non_executable_script_namespace() { let mut stream = XmlDocumentStream::new(test_url()); stream.append_to_end("".to_owned()); stream.declare_eof(); let outcome = stream.pump_next_parser_step(0); assert!(matches!(outcome.result, ParserPumpStep::InputDrained)); assert_eq!( stream .snapshot_parser_stream_document() .elements_by_tag_name( stream.snapshot_parser_stream_document().document_node_id(), "tail", false, ) .len(), 1 ); } #[test] fn incremental_top_level_unstyled_xml_builds_viewer_on_the_same_source_nodes() { let mut stream = XmlDocumentStream::new_top_level_document(test_url()); stream.append_to_end( "ready".to_owned(), ); stream.declare_eof(); while stream.has_pending_input() { let outcome = stream.pump_next_parser_step(2); assert!(matches!(outcome.result, ParserPumpStep::InputDrained)); } let source_root = stream .snapshot_parser_stream_dom_host() .element_handle_by_id("source") .expect("source XML root before viewer conversion"); let document = stream.finish(); let document_element = document .document_element_handle() .and_then(|handle| document.node(handle)) .and_then(Node::as_element) .expect("viewer document element"); assert_eq!(document_element.local_name(), "html"); assert_eq!(document_element.namespace(), "http://www.w3.org/1999/xhtml"); let source_container = element_by_id(&document, "webkit-xml-viewer-source-xml") .expect("viewer source container"); assert_eq!( document .child_ids(source_container) .find(|handle| document.node(*handle).is_some_and(Node::is_element)), Some(source_root), "viewer conversion must move rather than clone the parsed XML source root" ); } #[test] fn incremental_top_level_xhtml_keeps_its_xml_tree() { let mut stream = XmlDocumentStream::new_top_level_document(test_url()); stream.append_to_end( "".to_owned(), ); let document = stream.finish(); assert!(element_by_id(&document, "body").is_some()); assert!(element_by_id(&document, "webkit-xml-viewer-source-xml").is_none()); } #[test] fn incremental_top_level_malformed_xml_does_not_build_viewer() { let mut stream = XmlDocumentStream::new_top_level_document(test_url()); stream.append_to_end("".to_owned()); let document = stream.finish(); assert!(!document.parse_errors().is_empty()); assert_eq!( document .document_element_handle() .and_then(|handle| document.node(handle)) .and_then(Node::as_element) .map(|element| element.local_name()), Some("semantic-root") ); assert!(element_by_id(&document, "webkit-xml-viewer-source-xml").is_none()); } }