mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 16:01:42 +00:00
1137 lines
38 KiB
Rust
1137 lines
38 KiB
Rust
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::{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<String>,
|
|
}
|
|
|
|
struct XmlParserSession {
|
|
tokenizer: XmlTokenizer<EmbedderPausingXmlTreeBuilder>,
|
|
input_buffer: BufferQueue,
|
|
}
|
|
|
|
struct EmbedderPausingXmlTreeBuilder {
|
|
inner: XmlTreeBuilder<XmlStreamParseHandle, XmlStreamDocumentSink>,
|
|
}
|
|
|
|
struct XmlStreamDocumentSink {
|
|
target: RefCell<XmlParserStreamTarget>,
|
|
quirks_mode: Cell<XmlQuirksMode>,
|
|
}
|
|
|
|
struct XmlParserStreamTarget {
|
|
common: ParserStreamHtmlTreeSinkTarget,
|
|
present_unstyled_top_level_document: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct XmlStreamParseHandle {
|
|
inner: ParseHandle,
|
|
element_name: Option<Rc<XmlQualName>>,
|
|
suppress_append: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RawXmlParserStep {
|
|
Script(NativeNodeId),
|
|
BlockingStylesheet(NativeNodeId),
|
|
InputDrained,
|
|
}
|
|
|
|
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<T>(
|
|
&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<R>(
|
|
&mut self,
|
|
f: impl FnOnce(DomHost) -> std::result::Result<R, Box<(anyhow::Error, DomHost)>>,
|
|
) -> anyhow::Result<R> {
|
|
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<NativeNodeId> {
|
|
self.parser
|
|
.sink()
|
|
.borrow_mut()
|
|
.common
|
|
.take_parser_stream_null_custom_element_registry_elements()
|
|
}
|
|
|
|
pub fn with_stylesheet_blocking_read_view<R>(
|
|
&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<NativeNodeId> {
|
|
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<T>(
|
|
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<XmlParserStreamTarget> {
|
|
&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::<Vec<PreparedScript>>();
|
|
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<Self::Handle> {
|
|
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<XmlQualName>) -> 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<XmlAttribute>,
|
|
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<Self::Handle>) {
|
|
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.target
|
|
.borrow_mut()
|
|
.common
|
|
.append(parent.inner.node_id(), child)
|
|
.consume();
|
|
}
|
|
|
|
fn append_before_sibling(&self, sibling: &Self::Handle, child: XmlNodeOrText<Self::Handle>) {
|
|
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.target
|
|
.borrow_mut()
|
|
.common
|
|
.append_before_sibling(sibling.inner.node_id(), child)
|
|
.consume();
|
|
}
|
|
|
|
fn append_based_on_parent_node(
|
|
&self,
|
|
element: &Self::Handle,
|
|
prev_element: &Self::Handle,
|
|
child: XmlNodeOrText<Self::Handle>,
|
|
) {
|
|
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.target
|
|
.borrow_mut()
|
|
.common
|
|
.append_based_on_parent_node(
|
|
element.inner.node_id(),
|
|
prev_element.inner.node_id(),
|
|
child,
|
|
)
|
|
.consume();
|
|
}
|
|
|
|
fn append_doctype_to_document(
|
|
&self,
|
|
name: StrTendril,
|
|
public_id: StrTendril,
|
|
system_id: StrTendril,
|
|
) {
|
|
self.target
|
|
.borrow_mut()
|
|
.common
|
|
.append_doctype(
|
|
name.to_string(),
|
|
public_id.to_string(),
|
|
system_id.to_string(),
|
|
)
|
|
.consume();
|
|
}
|
|
|
|
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<XmlAttribute>) {
|
|
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.target
|
|
.borrow_mut()
|
|
.common
|
|
.remove_from_parent(target.inner.node_id())
|
|
.consume();
|
|
}
|
|
|
|
fn reparent_children(&self, node: &Self::Handle, new_parent: &Self::Handle) {
|
|
self.target
|
|
.borrow_mut()
|
|
.common
|
|
.reparent_children(node.inner.node_id(), new_parent.inner.node_id())
|
|
.consume();
|
|
}
|
|
}
|
|
|
|
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<String>) {
|
|
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<XmlParserSession>,
|
|
}
|
|
|
|
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<moli_dom::native::NativeNodeId> {
|
|
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!(
|
|
"<html xmlns='http://www.w3.org/1999/xhtml'><body>",
|
|
"<div id='before'/><script>globalThis.hit = true;</script>",
|
|
"<div id='future'/></body></html>"
|
|
)
|
|
.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 [
|
|
"<root data-before='1' xml",
|
|
"ns:m='urn:meta'><child><![CDA",
|
|
"TA[ < > & ]]></child></root>",
|
|
] {
|
|
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::<Vec<_>>(),
|
|
["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("<root>");
|
|
for index in 0..512 {
|
|
source.push_str(&format!(
|
|
"<p:item xmlns:p='urn:item:{index}' data-index='{index}'/>"
|
|
));
|
|
}
|
|
source.push_str("</root>");
|
|
|
|
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::<Vec<_>>();
|
|
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::<Vec<_>>(),
|
|
["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 ["<root><!-- <![CDA", "TA[not closed -->"] {
|
|
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(" <![CDATA[not closed "),
|
|
"a fake CDATA opener inside a closed comment must not retain later input"
|
|
);
|
|
|
|
for chunk in [
|
|
"<?moli-cdata 0?><?probe <![CDATA[pi]]>?>",
|
|
"<child><![CDA",
|
|
"TA[real]]></child></root>",
|
|
] {
|
|
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::<Vec<_>>();
|
|
assert_eq!(
|
|
instructions,
|
|
[("moli-cdata", "0"), ("probe", "<![CDATA[pi]]>")]
|
|
);
|
|
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("<root><script>not executable</script><tail/></root>".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(
|
|
"<semantic-root id='source'><child>ready</child></semantic-root>".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(
|
|
"<html xmlns='http://www.w3.org/1999/xhtml'><body id='body'/></html>".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("<semantic-root>".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());
|
|
}
|
|
}
|