mirror of
https://github.com/lexmount/moli.git
synced 2026-09-25 08:01:28 +00:00
fix(dom): preserve windowless document parser streams
This commit is contained in:
@@ -4421,6 +4421,7 @@ dom/nodes/Node-childNodes-cache-2.html
|
||||
dom/nodes/Node-childNodes-cache.html
|
||||
dom/nodes/Node-childNodes.html
|
||||
dom/nodes/Node-cloneNode-XMLDocument.html
|
||||
dom/nodes/Node-cloneNode-document-allow-declarative-shadow-roots.window.js?moli-wpt-script=window
|
||||
dom/nodes/Node-cloneNode-document-with-doctype.html
|
||||
dom/nodes/Node-cloneNode-on-inactive-document-crash.html
|
||||
dom/nodes/Node-cloneNode-svg.html
|
||||
|
||||
@@ -50,6 +50,7 @@ pub struct Document {
|
||||
url: Url,
|
||||
content_type: Box<str>,
|
||||
character_set: Box<str>,
|
||||
allow_declarative_shadow_roots: bool,
|
||||
ready_state: DocumentReadyState,
|
||||
// Retained with the Document when its Window is replaced during navigation.
|
||||
visibility_hidden: bool,
|
||||
@@ -81,6 +82,7 @@ impl Document {
|
||||
url,
|
||||
content_type: "text/html".into(),
|
||||
character_set: "UTF-8".into(),
|
||||
allow_declarative_shadow_roots: false,
|
||||
ready_state: DocumentReadyState::Complete,
|
||||
visibility_hidden: false,
|
||||
quirks_mode: QuirksMode::NoQuirks,
|
||||
@@ -99,6 +101,7 @@ impl Document {
|
||||
url,
|
||||
content_type: "application/xml".into(),
|
||||
character_set: "UTF-8".into(),
|
||||
allow_declarative_shadow_roots: false,
|
||||
ready_state: DocumentReadyState::Complete,
|
||||
visibility_hidden: false,
|
||||
quirks_mode: QuirksMode::NoQuirks,
|
||||
@@ -123,6 +126,14 @@ impl Document {
|
||||
&self.character_set
|
||||
}
|
||||
|
||||
pub fn allow_declarative_shadow_roots(&self) -> bool {
|
||||
self.allow_declarative_shadow_roots
|
||||
}
|
||||
|
||||
pub fn set_allow_declarative_shadow_roots(&mut self, allow: bool) {
|
||||
self.allow_declarative_shadow_roots = allow;
|
||||
}
|
||||
|
||||
pub fn set_character_set(&mut self, character_set: impl Into<String>) {
|
||||
self.character_set = character_set.into().into_boxed_str();
|
||||
}
|
||||
|
||||
@@ -1003,6 +1003,19 @@ impl DomHost {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_document_allow_declarative_shadow_roots_for_handle(
|
||||
&mut self,
|
||||
document_handle: DomHandle,
|
||||
allow: bool,
|
||||
) {
|
||||
if let Some(document) = self
|
||||
.node_mut(document_handle)
|
||||
.and_then(|node| node.data_mut().as_document_mut())
|
||||
{
|
||||
document.set_allow_declarative_shadow_roots(allow);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn document_scripting_enabled_for_handle(
|
||||
&self,
|
||||
document_handle: DomHandle,
|
||||
|
||||
+39
-2
@@ -488,7 +488,25 @@ impl HtmlParser {
|
||||
final_url: Url,
|
||||
document_handle: NativeNodeId,
|
||||
) -> DocumentStream {
|
||||
DocumentStream::new_live_document_root(final_url, document_handle, self.scripting_enabled)
|
||||
self.start_live_document_root_with_declarative_shadow_roots(
|
||||
final_url,
|
||||
document_handle,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn start_live_document_root_with_declarative_shadow_roots(
|
||||
&self,
|
||||
final_url: Url,
|
||||
document_handle: NativeNodeId,
|
||||
allow_declarative_shadow_roots: bool,
|
||||
) -> DocumentStream {
|
||||
DocumentStream::new_live_document_root(
|
||||
final_url,
|
||||
document_handle,
|
||||
self.scripting_enabled,
|
||||
allow_declarative_shadow_roots,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse_fragment_without_declarative_shadow_roots(
|
||||
@@ -609,12 +627,14 @@ impl DocumentStream {
|
||||
final_url: Url,
|
||||
document_handle: NativeNodeId,
|
||||
scripting_enabled: bool,
|
||||
allow_declarative_shadow_roots: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: new_live_document_root_html_tree_sink_stream(
|
||||
final_url,
|
||||
document_handle,
|
||||
scripting_enabled,
|
||||
allow_declarative_shadow_roots,
|
||||
),
|
||||
input: RefCell::default(),
|
||||
}
|
||||
@@ -662,7 +682,7 @@ impl DocumentStream {
|
||||
final_url: Url,
|
||||
document_handle: NativeNodeId,
|
||||
) -> Self {
|
||||
Self::new_live_document_root(final_url, document_handle, true)
|
||||
Self::new_live_document_root(final_url, document_handle, true, true)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -733,6 +753,23 @@ impl DocumentStream {
|
||||
self.inner.feed(chunk)
|
||||
}
|
||||
|
||||
/// Feeds an inert document parser while retaining its tokenizer and tree
|
||||
/// builder state between writes. Scripts are not handed off for execution.
|
||||
pub fn feed_with_runtime_dom_consumer<T>(&self, chunk: &str, consumer: &mut T)
|
||||
where
|
||||
T: ParserDomReadConsumer
|
||||
+ ParserDomMutationConsumer
|
||||
+ ParserMutationEffectConsumer
|
||||
+ ParserElementCreationConsumer,
|
||||
{
|
||||
// SAFETY: the parser-step guard clears the callbacks before the
|
||||
// exclusively borrowed consumer leaves this call.
|
||||
let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) };
|
||||
self.inner.enter_runtime_dom_sinks_parse_step(sinks);
|
||||
let step = RuntimeDomSinksParserStep { stream: self };
|
||||
step.feed(chunk);
|
||||
}
|
||||
|
||||
fn feed_with_runtime_dom_consumer_without_element_creation<T>(
|
||||
&self,
|
||||
chunk: &str,
|
||||
|
||||
@@ -2108,11 +2108,15 @@ impl ParserStreamHtmlTreeSinkTarget {
|
||||
allow_declarative_shadow_roots: bool,
|
||||
scripting_enabled: bool,
|
||||
) -> Self {
|
||||
let dom_host = DomHost::from_dom(NativeDom::new_html_with_scripting(
|
||||
let mut dom_host = DomHost::from_dom(NativeDom::new_html_with_scripting(
|
||||
final_url.clone(),
|
||||
scripting_enabled,
|
||||
));
|
||||
let document_handle = dom_host.document_handle();
|
||||
dom_host.set_document_allow_declarative_shadow_roots_for_handle(
|
||||
document_handle,
|
||||
allow_declarative_shadow_roots,
|
||||
);
|
||||
Self {
|
||||
owned_dom_host: Some(dom_host),
|
||||
parser_root_handle: Some(document_handle),
|
||||
@@ -3690,9 +3694,18 @@ pub(super) fn new_live_document_root_html_tree_sink_stream(
|
||||
final_url: Url,
|
||||
document_handle: NativeNodeId,
|
||||
scripting_enabled: bool,
|
||||
allow_declarative_shadow_roots: bool,
|
||||
) -> HtmlTreeSinkStream {
|
||||
HtmlTreeSinkStream::from_target_with_scripting(
|
||||
ParserStreamHtmlTreeSinkTarget::new_live_document_root(final_url, document_handle),
|
||||
if allow_declarative_shadow_roots {
|
||||
ParserStreamHtmlTreeSinkTarget::new_live_document_root(final_url, document_handle)
|
||||
} else {
|
||||
ParserStreamHtmlTreeSinkTarget::new_live_document_root_with_declarative_shadow_roots(
|
||||
final_url,
|
||||
document_handle,
|
||||
false,
|
||||
)
|
||||
},
|
||||
scripting_enabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -745,6 +745,7 @@ pub(super) struct DocumentRuntime {
|
||||
destructive_write_counters: destructive_writes::DocumentWriteCounters,
|
||||
document_unload_counters: destructive_writes::DocumentWriteCounters,
|
||||
root_document_parser: Option<DocumentParserSession>,
|
||||
windowless_document_parsers: HashMap<DomHandle, document_write::WindowlessDocumentParserState>,
|
||||
post_parse_schedule_invalidated: bool,
|
||||
stylesheet_lifecycle: StylesheetLifecycleState,
|
||||
main_parser_continuation: main_parser_continuation::MainParserContinuationState,
|
||||
|
||||
@@ -22,6 +22,9 @@ use moli_parser::{
|
||||
use std::collections::HashSet;
|
||||
use tracing::debug;
|
||||
|
||||
mod windowless;
|
||||
pub(super) use windowless::WindowlessDocumentParserState;
|
||||
|
||||
struct DocumentWriteParserPumpStep {
|
||||
outcome: ParserPumpOutcome,
|
||||
}
|
||||
@@ -49,6 +52,7 @@ struct DocumentWriteParserMutationOwner<'a, 'scope, 'pin> {
|
||||
#[derive(Clone, Copy)]
|
||||
enum DocumentWriteParserMutationTarget {
|
||||
LiveDocument,
|
||||
WindowlessDocument { owner_document: DomHandle },
|
||||
DetachedFragment { owner_document: DomHandle },
|
||||
}
|
||||
|
||||
@@ -56,7 +60,8 @@ impl DocumentWriteParserMutationOwner<'_, '_, '_> {
|
||||
fn owner_document_handle(&self) -> DomHandle {
|
||||
match self.target {
|
||||
DocumentWriteParserMutationTarget::LiveDocument => self.runtime.document_handle(),
|
||||
DocumentWriteParserMutationTarget::DetachedFragment { owner_document } => {
|
||||
DocumentWriteParserMutationTarget::WindowlessDocument { owner_document }
|
||||
| DocumentWriteParserMutationTarget::DetachedFragment { owner_document } => {
|
||||
owner_document
|
||||
}
|
||||
}
|
||||
@@ -71,7 +76,10 @@ impl LiveDocumentParserOwner for DocumentWriteParserMutationOwner<'_, '_, '_> {}
|
||||
|
||||
impl ParserMutationEffectConsumer for DocumentWriteParserMutationOwner<'_, '_, '_> {
|
||||
fn consume_parser_mutation_effects(&mut self, effects: DomMutationEffects) {
|
||||
if !self.targets_live_document() {
|
||||
if matches!(
|
||||
self.target,
|
||||
DocumentWriteParserMutationTarget::DetachedFragment { .. }
|
||||
) {
|
||||
return;
|
||||
}
|
||||
self.runtime
|
||||
@@ -183,7 +191,10 @@ impl ParserDomReadConsumer for DocumentWriteParserMutationOwner<'_, '_, '_> {
|
||||
|
||||
impl ParserDomMutationConsumer for DocumentWriteParserMutationOwner<'_, '_, '_> {
|
||||
fn apply_parser_dom_mutation(&mut self, mutation: ParserDomMutation) {
|
||||
if !self.targets_live_document() {
|
||||
if matches!(
|
||||
self.target,
|
||||
DocumentWriteParserMutationTarget::DetachedFragment { .. }
|
||||
) {
|
||||
let _ = mutation
|
||||
.apply_to_detached_dom_host(self.runtime.dom_host_mut_for_active_parser_step());
|
||||
return;
|
||||
@@ -236,10 +247,12 @@ impl ParserDomMutationConsumer for DocumentWriteParserMutationOwner<'_, '_, '_>
|
||||
// new body/frameset Window attributes when they are added, even if the
|
||||
// fragment is never connected. Existing attributes must not reactivate
|
||||
// an event handler that script has cleared through its IDL property.
|
||||
let window_handlers = if !self.targets_live_document()
|
||||
&& self.runtime.dom_host().node(node_id).is_some_and(|node| {
|
||||
node.is_html_element_named("body") || node.is_html_element_named("frameset")
|
||||
}) {
|
||||
let window_handlers = if matches!(
|
||||
self.target,
|
||||
DocumentWriteParserMutationTarget::DetachedFragment { .. }
|
||||
) && self.runtime.dom_host().node(node_id).is_some_and(|node| {
|
||||
node.is_html_element_named("body") || node.is_html_element_named("frameset")
|
||||
}) {
|
||||
attrs
|
||||
.iter()
|
||||
.filter(|attr| {
|
||||
@@ -334,6 +347,17 @@ impl ParserDomMutationConsumer for DocumentWriteParserMutationOwner<'_, '_, '_>
|
||||
if self.targets_live_document() {
|
||||
self.runtime
|
||||
.set_html_quirks_mode_for_parser_in_live_dom_host(quirks_mode);
|
||||
} else if let DocumentWriteParserMutationTarget::WindowlessDocument { owner_document } =
|
||||
self.target
|
||||
{
|
||||
let quirks_mode = match quirks_mode {
|
||||
QuirksMode::NoQuirks => selectors::matching::QuirksMode::NoQuirks,
|
||||
QuirksMode::LimitedQuirks => selectors::matching::QuirksMode::LimitedQuirks,
|
||||
QuirksMode::Quirks => selectors::matching::QuirksMode::Quirks,
|
||||
};
|
||||
self.runtime
|
||||
.dom_host_mut()
|
||||
.set_document_quirks_mode_for_handle(owner_document, quirks_mode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
use super::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub(in crate::document_runtime) struct WindowlessDocumentParserState {
|
||||
stream: Option<DocumentStream>,
|
||||
token: Rc<()>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WindowlessDocumentParserState {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
.debug_struct("WindowlessDocumentParserState")
|
||||
.field("has_stream", &self.stream.is_some())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentRuntime {
|
||||
pub(crate) fn has_windowless_document_parser(&self, document: DomHandle) -> bool {
|
||||
self.windowless_document_parsers
|
||||
.get(&document)
|
||||
.is_some_and(|state| state.stream.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn start_windowless_document_parser(&mut self, document: DomHandle) -> Option<()> {
|
||||
let source = self.dom_host().node(document)?.as_document()?;
|
||||
let stream = HtmlParser::with_scripting_enabled(false)
|
||||
.start_live_document_root_with_declarative_shadow_roots(
|
||||
source.url().clone(),
|
||||
document,
|
||||
source.allow_declarative_shadow_roots(),
|
||||
);
|
||||
self.dom_host_mut().set_document_quirks_mode_for_handle(
|
||||
document,
|
||||
selectors::matching::QuirksMode::NoQuirks,
|
||||
);
|
||||
self.dom_host_mut()
|
||||
.set_document_scripting_enabled_for_handle(document, false);
|
||||
self.dom_host_mut()
|
||||
.set_document_ready_state_for_handle(document, DocumentReadyState::Loading);
|
||||
self.windowless_document_parsers.insert(
|
||||
document,
|
||||
WindowlessDocumentParserState {
|
||||
stream: Some(stream),
|
||||
token: Rc::new(()),
|
||||
},
|
||||
);
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_windowless_document(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
document: DomHandle,
|
||||
html: &str,
|
||||
) -> Option<()> {
|
||||
let state = self.windowless_document_parsers.get_mut(&document)?;
|
||||
let stream = state.stream.take()?;
|
||||
let token = state.token.clone();
|
||||
self.with_dom_host_parse_step(|runtime| {
|
||||
let mut owner = DocumentWriteParserMutationOwner {
|
||||
runtime,
|
||||
scope,
|
||||
host_ptr,
|
||||
target: DocumentWriteParserMutationTarget::WindowlessDocument {
|
||||
owner_document: document,
|
||||
},
|
||||
};
|
||||
stream.feed_with_runtime_dom_consumer(html, &mut owner);
|
||||
});
|
||||
custom_elements::apply_parser_created_null_registry_associations(
|
||||
host_ptr,
|
||||
&stream.take_parser_stream_null_custom_element_registry_elements(),
|
||||
);
|
||||
if self.windowless_document_parser_is_current(document, &token) {
|
||||
self.windowless_document_parsers.get_mut(&document)?.stream = Some(stream);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub(crate) fn finish_windowless_document_parser(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
document: DomHandle,
|
||||
) -> Option<Rc<()>> {
|
||||
let state = self.windowless_document_parsers.get_mut(&document)?;
|
||||
let stream = state.stream.take()?;
|
||||
let token = state.token.clone();
|
||||
let finish_signals = self.with_dom_host_parse_step(|runtime| {
|
||||
let mut owner = DocumentWriteParserMutationOwner {
|
||||
runtime,
|
||||
scope,
|
||||
host_ptr,
|
||||
target: DocumentWriteParserMutationTarget::WindowlessDocument {
|
||||
owner_document: document,
|
||||
},
|
||||
};
|
||||
stream.finish_with_runtime_dom_consumer(&mut owner)
|
||||
});
|
||||
custom_elements::apply_parser_created_null_registry_associations(
|
||||
host_ptr,
|
||||
&finish_signals.parser_created_null_registry_elements,
|
||||
);
|
||||
Some(token)
|
||||
}
|
||||
|
||||
pub(crate) fn windowless_document_parser_is_current(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
token: &Rc<()>,
|
||||
) -> bool {
|
||||
self.windowless_document_parsers
|
||||
.get(&document)
|
||||
.is_some_and(|state| Rc::ptr_eq(&state.token, token))
|
||||
}
|
||||
|
||||
pub(crate) fn release_finished_windowless_document_parser(
|
||||
&mut self,
|
||||
document: DomHandle,
|
||||
token: &Rc<()>,
|
||||
) {
|
||||
if self.windowless_document_parser_is_current(document, token) {
|
||||
self.windowless_document_parsers.remove(&document);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,8 @@ impl DocumentRuntime {
|
||||
// initial "loading". Without this, the DomHost keeps its default "complete"
|
||||
// (from NativeDom Document::new) and scripts see the wrong readyState.
|
||||
let _ = dom_host.set_document_ready_state(document.ready_state());
|
||||
let document_handle = dom_host.document_handle();
|
||||
dom_host.set_document_allow_declarative_shadow_roots_for_handle(document_handle, true);
|
||||
let parser_boundary_lifecycle_tx = page_task_parser_boundary_injection_tx.clone();
|
||||
Self {
|
||||
dom_host,
|
||||
@@ -86,6 +88,7 @@ impl DocumentRuntime {
|
||||
destructive_write_counters: Default::default(),
|
||||
document_unload_counters: Default::default(),
|
||||
root_document_parser: None,
|
||||
windowless_document_parsers: HashMap::new(),
|
||||
post_parse_schedule_invalidated: false,
|
||||
stylesheet_lifecycle,
|
||||
main_parser_continuation:
|
||||
@@ -220,6 +223,7 @@ impl DocumentRuntime {
|
||||
destructive_write_counters: _,
|
||||
document_unload_counters: _,
|
||||
root_document_parser: _,
|
||||
windowless_document_parsers: _,
|
||||
post_parse_schedule_invalidated: _,
|
||||
stylesheet_lifecycle: _,
|
||||
main_parser_continuation: _,
|
||||
|
||||
@@ -207,6 +207,8 @@ impl JsContextHost {
|
||||
// observe them. Keep the generic detached/initial-empty default complete.
|
||||
self.dom_host_mut()
|
||||
.set_document_character_set_for_handle(document_handle, &snapshot.character_set);
|
||||
self.dom_host_mut()
|
||||
.set_document_allow_declarative_shadow_roots_for_handle(document_handle, true);
|
||||
let _ = self
|
||||
.set_dom_document_ready_state_for_handle(document_handle, DocumentReadyState::Loading);
|
||||
let document_url = self.document_url_for_handle(document_handle);
|
||||
|
||||
@@ -1778,6 +1778,8 @@ impl JsContextHost {
|
||||
let document_handle = self
|
||||
.dom_host_mut()
|
||||
.create_detached_html_document_with_url(document_url);
|
||||
self.dom_host_mut()
|
||||
.set_document_allow_declarative_shadow_roots_for_handle(document_handle, true);
|
||||
if let Some(content_type) = content_type {
|
||||
let _ = self.set_dom_document_content_type_for_handle(document_handle, content_type);
|
||||
}
|
||||
|
||||
@@ -1172,10 +1172,6 @@ fn document_ready_state_getter_function<'s>(
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let receiver = args.this();
|
||||
if let Some(ready_state) = detached_state_string(scope, receiver, "readyState") {
|
||||
set_document_string_return_value(scope, &mut rv, &ready_state);
|
||||
return;
|
||||
}
|
||||
let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, receiver) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
|
||||
@@ -137,6 +137,7 @@ fn set_detached_document_parse_metadata<'s>(
|
||||
document: v8::Local<'s, v8::Object>,
|
||||
quirks_mode: selectors::matching::QuirksMode,
|
||||
character_set: &str,
|
||||
allow_declarative_shadow_roots: bool,
|
||||
) -> Option<()> {
|
||||
let compat_mode = if quirks_mode == selectors::matching::QuirksMode::Quirks {
|
||||
"BackCompat"
|
||||
@@ -159,6 +160,10 @@ fn set_detached_document_parse_metadata<'s>(
|
||||
let dom_host = unsafe { &mut *runtime_ptr }.dom_host_mut();
|
||||
dom_host.set_document_quirks_mode_for_handle(handle, quirks_mode);
|
||||
dom_host.set_document_character_set_for_handle(handle, character_set);
|
||||
dom_host.set_document_allow_declarative_shadow_roots_for_handle(
|
||||
handle,
|
||||
allow_declarative_shadow_roots,
|
||||
);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -182,11 +187,18 @@ pub(in crate::native_bridge::document) fn build_detached_document_clone_shell<'s
|
||||
let content_type = document.content_type().to_owned();
|
||||
let quirks_mode = document.quirks_mode();
|
||||
let character_set = document.character_set().to_owned();
|
||||
let allow_declarative_shadow_roots = document.allow_declarative_shadow_roots();
|
||||
|
||||
// Start with an empty, inert Document. Only the metadata required by the
|
||||
// DOM cloning algorithm is inherited, before any cloned children are added.
|
||||
let cloned = new_detached_document_shell(scope, &kind, &content_type, url, false)?;
|
||||
set_detached_document_parse_metadata(scope, cloned, quirks_mode, &character_set)?;
|
||||
set_detached_document_parse_metadata(
|
||||
scope,
|
||||
cloned,
|
||||
quirks_mode,
|
||||
&character_set,
|
||||
allow_declarative_shadow_roots,
|
||||
)?;
|
||||
inherit_detached_document_origin(scope, cloned, source);
|
||||
Some(cloned)
|
||||
}
|
||||
@@ -271,8 +283,15 @@ pub(crate) fn build_detached_document_object_from_dom_host_with_content_type<'s>
|
||||
let scripting_enabled = parsed.dom().document()?.scripting_enabled();
|
||||
let content_type = content_type.unwrap_or(parsed.dom().document()?.content_type());
|
||||
let character_set = character_set.unwrap_or(parsed.dom().document()?.character_set());
|
||||
let allow_declarative_shadow_roots = parsed.dom().document()?.allow_declarative_shadow_roots();
|
||||
let document = new_detached_document_shell(scope, kind, content_type, url, scripting_enabled)?;
|
||||
set_detached_document_parse_metadata(scope, document, quirks_mode, character_set)?;
|
||||
set_detached_document_parse_metadata(
|
||||
scope,
|
||||
document,
|
||||
quirks_mode,
|
||||
character_set,
|
||||
allow_declarative_shadow_roots,
|
||||
)?;
|
||||
import_detached_document_children_from_host(scope, document, &parsed)?;
|
||||
Some(document)
|
||||
}
|
||||
|
||||
@@ -168,7 +168,12 @@ pub(in crate::native_bridge) fn bridge_detached_document_ready_state_callback<'a
|
||||
rv.set_empty_string();
|
||||
return;
|
||||
};
|
||||
let value = detached_document_state_string(scope, document, "readyState", "complete");
|
||||
let value =
|
||||
crate::native_bridge::document::document_receiver_runtime_and_handle(scope, document)
|
||||
.map(|(runtime_ptr, handle)| {
|
||||
unsafe { &*runtime_ptr }.document_ready_state_for_handle(handle)
|
||||
})
|
||||
.unwrap_or_else(|| "complete".to_owned());
|
||||
set_string_return_value(scope, &mut rv, &value);
|
||||
}
|
||||
|
||||
@@ -272,7 +277,20 @@ pub(in crate::native_bridge) fn bridge_detached_document_compat_mode_callback<'a
|
||||
) {
|
||||
let compat_mode = v8::Local::<v8::Object>::try_from(args.get(0))
|
||||
.ok()
|
||||
.map(|document| detached_document_state_string(scope, document, "compatMode", "CSS1Compat"))
|
||||
.and_then(|document| {
|
||||
crate::native_bridge::document::document_receiver_runtime_and_handle(scope, document)
|
||||
})
|
||||
.map(|(runtime_ptr, handle)| {
|
||||
let quirks = unsafe { &*runtime_ptr }
|
||||
.dom_host()
|
||||
.document_quirks_mode_for_handle(handle);
|
||||
if quirks == Some(selectors::matching::QuirksMode::Quirks) {
|
||||
"BackCompat"
|
||||
} else {
|
||||
"CSS1Compat"
|
||||
}
|
||||
.to_owned()
|
||||
})
|
||||
.unwrap_or_else(|| "CSS1Compat".to_owned());
|
||||
set_string_return_value(scope, &mut rv, &compat_mode);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use super::super::node::{
|
||||
remove_child_in_reaction_scope,
|
||||
};
|
||||
use super::{
|
||||
JsContextHost, detached_native_handle_for_runtime, is_html_document, throw_dom_exception,
|
||||
JsContextHost, document_has_browsing_context,
|
||||
is_html_document, throw_dom_exception,
|
||||
};
|
||||
use crate::native_bridge::element::{
|
||||
TextEditInputType, contenteditable_editing_host, dispatch_text_control_event, document_copy_command_supported,
|
||||
@@ -15,20 +16,17 @@ use crate::native_bridge::element::{
|
||||
use crate::{
|
||||
context_bootstrap::WINDOW_EVENT_HANDLER_PROPERTIES,
|
||||
custom_elements,
|
||||
document_runtime::DomHandle,
|
||||
dom::native::{NativeDom, NodeData},
|
||||
document_runtime::{DomHandle, EventTargetHandle},
|
||||
dom::native::{DocumentReadyState, NativeDom, NodeData},
|
||||
parser::HtmlParser,
|
||||
util::{
|
||||
call_object_method, get_private_value, node_wrapper_from_handle, set_private_value,
|
||||
utf16_next_scalar_boundary, utf16_previous_scalar_boundary,
|
||||
utf16_replace_units_range_lossy, utf16_scalar_boundary_at_or_after, utf16_units, v8_string,
|
||||
v8str,
|
||||
call_object_method, node_wrapper_from_handle, utf16_next_scalar_boundary,
|
||||
utf16_previous_scalar_boundary, utf16_replace_units_range_lossy,
|
||||
utf16_scalar_boundary_at_or_after, utf16_units, v8_string, v8str,
|
||||
},
|
||||
webidl,
|
||||
};
|
||||
|
||||
const DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT: &str = "__moliDetachedDocumentWriteStreamOpen";
|
||||
|
||||
struct DocumentWriteInput {
|
||||
text: String,
|
||||
is_trusted: bool,
|
||||
@@ -121,24 +119,25 @@ fn node_document_write_or_writeln_callback<'s>(
|
||||
);
|
||||
return;
|
||||
}
|
||||
if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() {
|
||||
let document = args.this();
|
||||
let stream_was_open = detached_document_write_stream_is_open(scope, document);
|
||||
if !document_has_browsing_context(unsafe { &*runtime_ptr }, handle) {
|
||||
let stream_was_open = unsafe { &*runtime_ptr }.has_windowless_document_parser(handle);
|
||||
if !stream_was_open && unsafe { &*runtime_ptr }.has_document_unload_counter(handle) {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
if !stream_was_open {
|
||||
set_detached_document_write_stream_open(scope, document, true);
|
||||
}
|
||||
let wrote = if stream_was_open {
|
||||
append_detached_html_document_body_html(scope, runtime_ptr, handle, &html)
|
||||
} else {
|
||||
set_detached_html_document_body_html(scope, runtime_ptr, handle, &html)
|
||||
};
|
||||
if !wrote && !stream_was_open {
|
||||
set_detached_document_write_stream_open(scope, document, false);
|
||||
unsafe { &mut *runtime_ptr }.prepare_windowless_document_replacement(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
);
|
||||
}
|
||||
let _ = unsafe { &mut *runtime_ptr }.write_windowless_document(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
&html,
|
||||
);
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
@@ -253,12 +252,12 @@ pub(in crate::native_bridge) fn node_document_open_callback<'s>(
|
||||
rv.set(args.this().into());
|
||||
return;
|
||||
}
|
||||
if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() {
|
||||
let document = args.this();
|
||||
set_detached_document_write_stream_open(scope, document, true);
|
||||
if !set_detached_html_document_body_html(scope, runtime_ptr, handle, "") {
|
||||
set_detached_document_write_stream_open(scope, document, false);
|
||||
}
|
||||
if !document_has_browsing_context(unsafe { &*runtime_ptr }, handle) {
|
||||
unsafe { &mut *runtime_ptr }.prepare_windowless_document_replacement(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
return;
|
||||
}
|
||||
@@ -280,6 +279,20 @@ fn clear_window_event_handlers(scope: &mut v8::PinScope<'_, '_>) {
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
fn prepare_windowless_document_replacement(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
document: DomHandle,
|
||||
) {
|
||||
self.clear_event_callbacks_for_document_replacement(document, false);
|
||||
custom_elements::with_custom_element_reaction_scope(scope, host_ptr, |scope| {
|
||||
let runtime = unsafe { &mut *host_ptr };
|
||||
runtime.remove_all_children_for_document_replacement(scope, host_ptr, document);
|
||||
let _ = runtime.start_windowless_document_parser(document);
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_root_document_replacement(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -376,8 +389,8 @@ pub(in crate::native_bridge) fn node_document_close_callback<'s>(
|
||||
);
|
||||
return;
|
||||
}
|
||||
if detached_native_handle_for_runtime(scope, runtime_ptr, args.this()).is_some() {
|
||||
set_detached_document_write_stream_open(scope, args.this(), false);
|
||||
if !document_has_browsing_context(unsafe { &*runtime_ptr }, handle) {
|
||||
close_windowless_document(scope, runtime_ptr, handle, args.this());
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
@@ -386,25 +399,55 @@ pub(in crate::native_bridge) fn node_document_close_callback<'s>(
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
fn detached_document_write_stream_is_open<'s>(
|
||||
fn close_windowless_document<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
document: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
get_private_value(scope, document, DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT)
|
||||
.is_some_and(|value| value.boolean_value(scope))
|
||||
}
|
||||
|
||||
fn set_detached_document_write_stream_open(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
document: v8::Local<'_, v8::Object>,
|
||||
open: bool,
|
||||
) {
|
||||
set_private_value(
|
||||
scope,
|
||||
document,
|
||||
DETACHED_DOCUMENT_WRITE_STREAM_OPEN_SLOT,
|
||||
v8::Boolean::new(scope, open).into(),
|
||||
);
|
||||
// Borrowing close() from another realm does not change the realm of the
|
||||
// Document's lifecycle events.
|
||||
let context = crate::native_bridge::node_relevant_context(scope, document)
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let Some(token) =
|
||||
unsafe { &mut *runtime_ptr }.finish_windowless_document_parser(scope, runtime_ptr, handle)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for (ready_state, event_type) in [
|
||||
(Some(DocumentReadyState::Interactive), "readystatechange"),
|
||||
(None, "DOMContentLoaded"),
|
||||
(Some(DocumentReadyState::Complete), "readystatechange"),
|
||||
] {
|
||||
// A lifecycle listener can open a replacement stream on the same
|
||||
// Document. The old parser must not complete that replacement.
|
||||
if !unsafe { &*runtime_ptr }.windowless_document_parser_is_current(handle, &token) {
|
||||
return;
|
||||
}
|
||||
if let Some(state) = ready_state {
|
||||
unsafe { &mut *runtime_ptr }
|
||||
.dom_host_mut()
|
||||
.set_document_ready_state_for_handle(handle, state);
|
||||
}
|
||||
if let Ok(event) = crate::host::create_host_event(
|
||||
scope,
|
||||
event_type,
|
||||
document.into(),
|
||||
document.into(),
|
||||
event_type == "DOMContentLoaded",
|
||||
false,
|
||||
) {
|
||||
let _ = unsafe { &mut *runtime_ptr }.dispatch_public_event_best_effort(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
EventTargetHandle::Node(handle),
|
||||
event,
|
||||
"windowless document lifecycle event",
|
||||
);
|
||||
}
|
||||
}
|
||||
unsafe { &mut *runtime_ptr }.release_finished_windowless_document_parser(handle, &token);
|
||||
}
|
||||
|
||||
fn detached_html_document_body_handle(
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn windowless_documents_use_independent_incremental_parser_streams() {
|
||||
let mut vm = new_storage_test_vm("https://windowless-document-stream.test/page.html");
|
||||
vm.eval(
|
||||
"document.appendChild(document.createElement('html')).appendChild(document.createElement('body'));",
|
||||
)
|
||||
.expect("the shared browser fixture needs an initial page body");
|
||||
let fixture = include_str!("../../../../tests/fixtures/windowless-document-stream.js");
|
||||
let result = vm
|
||||
.eval(&format!("JSON.stringify({fixture})"))
|
||||
.expect("windowless document stream regression probe");
|
||||
let result: serde_json::Value = serde_json::from_str(&result).unwrap();
|
||||
assert_eq!(result["failures"], serde_json::json!([]), "{result}");
|
||||
assert_eq!(result["checks"], 261);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_clones_preserve_internal_metadata_and_url_resolution() {
|
||||
let mut vm = new_storage_test_vm("https://document-clone-metadata.test/path/page.html");
|
||||
@@ -182,6 +198,7 @@ fn detached_document_write_preserves_existing_noscript_text() {
|
||||
(() => {
|
||||
const doc = document.implementation.createHTMLDocument("");
|
||||
doc.open();
|
||||
doc.write("<body>");
|
||||
const noscript = doc.createElement("noscript");
|
||||
noscript.textContent = "<em>fallback&</em>";
|
||||
doc.body.append(noscript);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
(() => {
|
||||
const EventConstructor = Event;
|
||||
const mainBody = document.body;
|
||||
const parser = new DOMParser();
|
||||
const parsed = () => parser.parseFromString('<title>old</title><p>old</p>', 'text/html');
|
||||
const unsafe = () => Document.parseHTMLUnsafe('<p>old</p>');
|
||||
const sources = [
|
||||
['implementation', document.implementation.createHTMLDocument('old'), false],
|
||||
['DOMParser', parsed(), false],
|
||||
['parseHTMLUnsafe', unsafe(), true],
|
||||
['live shallow clone', document.cloneNode(), true],
|
||||
['live deep clone', document.cloneNode(true), true],
|
||||
['DOMParser shallow clone', parsed().cloneNode(), false],
|
||||
['DOMParser deep clone', parsed().cloneNode(true), false],
|
||||
['unsafe shallow clone', unsafe().cloneNode(), true],
|
||||
['unsafe deep clone', unsafe().cloneNode(true), true],
|
||||
];
|
||||
const failures = [];
|
||||
let checks = 0;
|
||||
function equal(label, actual, expected) {
|
||||
checks++;
|
||||
if (actual !== expected) failures.push({label, actual, expected});
|
||||
}
|
||||
for (const [name, doc, allowShadow] of sources) {
|
||||
try {
|
||||
const originalURL = doc.URL;
|
||||
let erasedListenerCalls = 0;
|
||||
doc.addEventListener('probe', () => erasedListenerCalls++);
|
||||
equal(name + ' open receiver', doc.open() === doc, true);
|
||||
equal(name + ' open clears all children', doc.childNodes.length, 0);
|
||||
equal(name + ' open readyState', doc.readyState, 'loading');
|
||||
equal(name + ' open resets mode', doc.compatMode, 'CSS1Compat');
|
||||
equal(name + ' open keeps URL', doc.URL, originalURL);
|
||||
doc.dispatchEvent(new EventConstructor('probe'));
|
||||
equal(name + ' open erases listeners', erasedListenerCalls, 0);
|
||||
const events = [];
|
||||
for (const type of ['readystatechange', 'DOMContentLoaded', 'load']) {
|
||||
doc.addEventListener(type, event => events.push([
|
||||
event.type, doc.readyState, event.bubbles, event.isTrusted,
|
||||
event.target === doc, event.currentTarget === doc,
|
||||
]));
|
||||
}
|
||||
doc.write('<!doctype html><html><head><title>written</title></head><body><section id="host"><template shadowrootmode="open"><span>shadow</span></template><b id="keep">light</b>');
|
||||
equal(name + ' document title', doc.title, 'written');
|
||||
equal(name + ' document doctype', doc.doctype?.name, 'html');
|
||||
equal(name + ' write retains loading', doc.readyState, 'loading');
|
||||
equal(name + ' write has no lifecycle events yet', events.length, 0);
|
||||
const host = doc.getElementById('host');
|
||||
const keep = doc.getElementById('keep');
|
||||
equal(name + ' declarative shadow root', host?.shadowRoot?.textContent ?? null, allowShadow ? 'shadow' : null);
|
||||
equal(name + ' ordinary template when disabled', !!host?.querySelector('template'), !allowShadow);
|
||||
let listenerCalls = 0;
|
||||
keep.addEventListener('probe', () => listenerCalls++);
|
||||
const observer = new MutationObserver(() => {});
|
||||
observer.observe(host, {childList: true});
|
||||
doc.write('<i id="ta');
|
||||
doc.write('il">tail</i></section>');
|
||||
const tail = doc.getElementById('tail');
|
||||
equal(name + ' streamed token', tail?.textContent, 'tail');
|
||||
equal(name + ' tree builder insertion point', tail?.parentNode === host, true);
|
||||
equal(name + ' existing node identity', doc.getElementById('keep') === keep, true);
|
||||
keep.dispatchEvent(new EventConstructor('probe'));
|
||||
equal(name + ' existing listener identity', listenerCalls, 1);
|
||||
equal(name + ' parser insert is observable', observer.takeRecords().some(record => Array.from(record.addedNodes).includes(tail)), true);
|
||||
observer.disconnect();
|
||||
const priorEvent = globalThis.Event;
|
||||
globalThis.Event = function() { throw new Error('author Event constructor called'); };
|
||||
try { doc.close(); } finally { globalThis.Event = priorEvent; }
|
||||
equal(name + ' close readiness', doc.readyState, 'complete');
|
||||
equal(name + ' close lifecycle', JSON.stringify(events), JSON.stringify([
|
||||
['readystatechange', 'interactive', false, true, true, true],
|
||||
['DOMContentLoaded', 'interactive', true, true, true, true],
|
||||
['readystatechange', 'complete', false, true, true, true],
|
||||
]));
|
||||
const closedRoot = doc.documentElement;
|
||||
doc.close();
|
||||
equal(name + ' repeated close keeps tree', doc.documentElement === closedRoot, true);
|
||||
equal(name + ' repeated close has no events', events.length, 3);
|
||||
doc.writeln('<p id="replacement">new</p>');
|
||||
equal(name + ' implicit open replaces previous tree', doc.getElementById('keep'), null);
|
||||
equal(name + ' implicit open creates body', doc.body?.textContent, 'new\n');
|
||||
equal(name + ' no doctype enters quirks mode', doc.compatMode, 'BackCompat');
|
||||
doc.close();
|
||||
equal(name + ' implicit open clears previous listeners', events.length, 3);
|
||||
doc.open();
|
||||
doc.write('<script>globalThis.__windowlessStreamScriptRan = true;</scr' + 'ipt>');
|
||||
doc.close();
|
||||
equal(name + ' scripts remain inert', globalThis.__windowlessStreamScriptRan, undefined);
|
||||
equal(name + ' main Document unchanged', document.body === mainBody, true);
|
||||
equal(name + ' no browsing context', doc.defaultView, null);
|
||||
} catch (error) {
|
||||
equal(name + ' unexpected exception', error.name + ': ' + error.message, null);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const a = document.implementation.createHTMLDocument('');
|
||||
const b = document.implementation.createHTMLDocument('');
|
||||
a.write('<div id="a">');
|
||||
b.write('<div id="b">');
|
||||
const rootA = a.getElementById('a');
|
||||
const rootB = b.getElementById('b');
|
||||
a.write('<b>A</b>');
|
||||
b.write('<i>B</i>');
|
||||
a.close();
|
||||
b.close();
|
||||
equal('independent parser A', rootA?.firstChild?.textContent, 'A');
|
||||
equal('independent parser B', rootB?.firstChild?.textContent, 'B');
|
||||
equal('independent node identities', a.getElementById('a') === rootA && b.getElementById('b') === rootB, true);
|
||||
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.open();
|
||||
let oldDOMContentLoaded = 0;
|
||||
doc.addEventListener('DOMContentLoaded', () => oldDOMContentLoaded++);
|
||||
doc.addEventListener('readystatechange', () => {
|
||||
if (doc.readyState === 'interactive') {
|
||||
doc.open();
|
||||
doc.write('<p id="new-stream">replacement</p>');
|
||||
}
|
||||
});
|
||||
doc.write('<p>old stream</p>');
|
||||
doc.close();
|
||||
equal('reentrant open retains replacement parser', doc.readyState, 'loading');
|
||||
equal('reentrant open retains replacement content', doc.getElementById('new-stream')?.textContent, 'replacement');
|
||||
equal('old close does not dispatch after replacement', oldDOMContentLoaded, 0);
|
||||
doc.close();
|
||||
equal('replacement can close independently', doc.readyState, 'complete');
|
||||
doc.open();
|
||||
doc.close();
|
||||
equal('empty stream produces document structure', doc.documentElement?.localName + '/' + doc.head?.localName + '/' + doc.body?.localName, 'html/head/body');
|
||||
} catch (error) {
|
||||
equal('stream lifecycle unexpected exception', error.name + ': ' + error.message, null);
|
||||
}
|
||||
try {
|
||||
const frame = document.body.appendChild(document.createElement('iframe'));
|
||||
try {
|
||||
const other = frame.contentWindow;
|
||||
const doc = other.document.implementation.createHTMLDocument('');
|
||||
Document.prototype.open.call(doc);
|
||||
const events = [];
|
||||
for (const type of ['readystatechange', 'DOMContentLoaded']) {
|
||||
doc.addEventListener(type, event => events.push([
|
||||
event.type, event instanceof other.Event, event instanceof Event,
|
||||
]));
|
||||
}
|
||||
Document.prototype.write.call(doc, '<p>content</p>');
|
||||
Document.prototype.close.call(doc);
|
||||
equal('lifecycle events use the Document realm', JSON.stringify(events), JSON.stringify([
|
||||
['readystatechange', true, false],
|
||||
['DOMContentLoaded', true, false],
|
||||
['readystatechange', true, false],
|
||||
]));
|
||||
} finally {
|
||||
frame.remove();
|
||||
}
|
||||
} catch (error) {
|
||||
equal('cross-realm lifecycle unexpected exception', error.name + ': ' + error.message, null);
|
||||
}
|
||||
return {checks, failures};
|
||||
})()
|
||||
Reference in New Issue
Block a user