mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
Flush parser insertion reactions after the tree builder completes its current token so connected callbacks run before children without retaining parser borrows across JavaScript execution. Keep one DocumentParserSession and expose a restricted insertion handle for nested writes. Share input, suspension, cancellation, and close state so blocked writes remain queued and outer feeds stop when nested parsing blocks or replaces the document. Let the owning session finish deferred close and dispatch readiness transitions once. Reuse form-owner mutation detection for child parser insertions and add regressions for reaction order, form association, nested writes, gated external scripts, document replacement, and deferred close. Validation: cargo fmt --all; strict workspace Clippy; cargo nextest run --no-fail-fast (18,104 passed, 13 existing skips); release build.
1135 lines
38 KiB
Rust
1135 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::{
|
|
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<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 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<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.mutate_target(|target| target.append(parent.inner.node_id(), child));
|
|
}
|
|
|
|
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.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<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.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<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.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<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());
|
|
}
|
|
}
|