mirror of
https://github.com/lexmount/moli.git
synced 2026-09-29 00:01:38 +00:00
refactor(parser): share mutation effects and isolate tokenizer pauses
Share parser mutation notifications between the main document and child frames, with each owner retaining its resource and lifecycle work. Update stylesheet owners incrementally so parser mutations preserve CSSOM edits. Keep tokenizer pause reasons separate from DOM handle identity. Add main/iframe regressions for reaction and observer ordering, form association, stylesheet visibility, CSSOM edits, and nested frame identity. Validation: cargo fmt --all; strict workspace clippy; cargo nextest run --no-fail-fast (18,118 passed, 13 skipped).
This commit is contained in:
+1
-17
@@ -338,9 +338,6 @@ pub(super) struct ParseHandle {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ParseHandleIdentity {
|
||||
DomNode(NativeNodeId),
|
||||
// html5ever exposes only Script(handle) as an embedder yield. This marker
|
||||
// is translated to OwnerInterrupted before any DOM handle is inspected.
|
||||
OwnerInterrupted,
|
||||
// Standalone fragment parsing, like `Element.innerHTML` staging, only has a
|
||||
// context element name. Chromium keeps a real `context_element` next to the
|
||||
// `DocumentFragment` target; our detached staging parser uses this
|
||||
@@ -1363,18 +1360,6 @@ impl Drop for ParserInputContext {
|
||||
}
|
||||
|
||||
impl ParseHandle {
|
||||
pub(super) fn owner_interrupted() -> Self {
|
||||
Self {
|
||||
identity: ParseHandleIdentity::OwnerInterrupted,
|
||||
element_name: None,
|
||||
parser_flags: ParserElementFlags::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_owner_interrupted(&self) -> bool {
|
||||
self.identity == ParseHandleIdentity::OwnerInterrupted
|
||||
}
|
||||
|
||||
pub(super) fn new(node_id: NativeNodeId, element_name: Option<Rc<QualName>>) -> Self {
|
||||
Self {
|
||||
identity: ParseHandleIdentity::DomNode(node_id),
|
||||
@@ -1406,8 +1391,7 @@ impl ParseHandle {
|
||||
pub(super) fn dom_node_id(&self) -> Option<NativeNodeId> {
|
||||
match self.identity {
|
||||
ParseHandleIdentity::DomNode(node_id) => Some(node_id),
|
||||
ParseHandleIdentity::SyntheticFragmentContext
|
||||
| ParseHandleIdentity::OwnerInterrupted => None,
|
||||
ParseHandleIdentity::SyntheticFragmentContext => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,13 @@ pub(super) enum HtmlParserSessionResult {
|
||||
Script(ParseHandle),
|
||||
}
|
||||
|
||||
// html5ever carries tokenizer pauses through Script(Handle). Keep the pause
|
||||
// reason separate from the tree builder's DOM handles at this adapter boundary.
|
||||
enum TokenizerPause {
|
||||
Handoff(ParseHandle),
|
||||
OwnerInterrupted,
|
||||
}
|
||||
|
||||
struct EmbedderPausingTreeBuilder {
|
||||
inner: TreeBuilder<ParseHandle, DocumentSink>,
|
||||
}
|
||||
@@ -128,16 +135,24 @@ impl EmbedderPausingTreeBuilder {
|
||||
}
|
||||
|
||||
impl TokenSink for EmbedderPausingTreeBuilder {
|
||||
type Handle = ParseHandle;
|
||||
type Handle = TokenizerPause;
|
||||
|
||||
fn process_token(&self, token: Token, line_number: u64) -> TokenSinkResult<Self::Handle> {
|
||||
let result = self.process_token_before_callbacks(token, line_number);
|
||||
if self.sink().finish_parser_dom_mutations().is_break() {
|
||||
// A nested parser invocation already handed its blocker to the
|
||||
// owner. Stop this outer feed before it consumes another token.
|
||||
return TokenSinkResult::Script(ParseHandle::owner_interrupted());
|
||||
return TokenSinkResult::Script(TokenizerPause::OwnerInterrupted);
|
||||
}
|
||||
match result {
|
||||
TokenSinkResult::Continue => TokenSinkResult::Continue,
|
||||
TokenSinkResult::Script(handle) => {
|
||||
TokenSinkResult::Script(TokenizerPause::Handoff(handle))
|
||||
}
|
||||
TokenSinkResult::Plaintext => TokenSinkResult::Plaintext,
|
||||
TokenSinkResult::RawData(kind) => TokenSinkResult::RawData(kind),
|
||||
TokenSinkResult::EncodingIndicator(label) => TokenSinkResult::EncodingIndicator(label),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn end(&self) {
|
||||
@@ -325,12 +340,11 @@ fn feed_with_definitive_encoding(
|
||||
// advisory notification without exposing a false parser pause.
|
||||
TokenizerResult::EncodingIndicator(_) => {}
|
||||
TokenizerResult::Done => return HtmlParserSessionResult::InputDrained,
|
||||
TokenizerResult::Script(handle) => {
|
||||
return if handle.is_owner_interrupted() {
|
||||
HtmlParserSessionResult::OwnerInterrupted
|
||||
} else {
|
||||
HtmlParserSessionResult::Script(handle)
|
||||
};
|
||||
TokenizerResult::Script(TokenizerPause::Handoff(handle)) => {
|
||||
return HtmlParserSessionResult::Script(handle);
|
||||
}
|
||||
TokenizerResult::Script(TokenizerPause::OwnerInterrupted) => {
|
||||
return HtmlParserSessionResult::OwnerInterrupted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::mutation_commands::{
|
||||
apply_runtime_mutation_effects_to_dom_host, finish_runtime_mutation_effects,
|
||||
RuntimeMutationApplyResult, finish_runtime_mutation_effects, prepare_runtime_mutation_effects,
|
||||
};
|
||||
use super::*;
|
||||
use crate::parser::{ParserPlanningReadView, ParserScriptRead};
|
||||
use crate::parser_mutation_effects::{ParserMutationEffectsOwner, apply_parser_mutation_effects};
|
||||
use crate::stylesheet_blocking::StylesheetElementRead;
|
||||
use html5ever::tree_builder::QuirksMode;
|
||||
// This slice collects the remaining low-level facade methods that mostly forward into `DomHost`
|
||||
@@ -97,39 +98,8 @@ impl DocumentRuntime {
|
||||
host_ptr: *mut JsContextHost,
|
||||
effects: DomMutationEffects,
|
||||
) {
|
||||
let connected_roots = effects.tree().connected_roots().to_vec();
|
||||
let form_owner_effects = effects.clone();
|
||||
self.assert_active_parser_document_incarnation();
|
||||
let result = {
|
||||
let dom_host = self.dom_host.borrow_mut();
|
||||
apply_runtime_mutation_effects_to_dom_host(
|
||||
&mut self.mutations,
|
||||
&self.document,
|
||||
self.script_lifecycle.scripts_mut(),
|
||||
&mut self.events,
|
||||
scope,
|
||||
host_ptr,
|
||||
dom_host,
|
||||
effects,
|
||||
RuntimeMutationOptions::parser_tree_sink(),
|
||||
)
|
||||
};
|
||||
let _ = finish_runtime_mutation_effects(self, scope, host_ptr, result);
|
||||
if !connected_roots.is_empty() {
|
||||
self.ensure_parser_custom_element_reaction_queue(host_ptr);
|
||||
crate::custom_elements::enqueue_connected_and_form_callbacks_for_already_upgraded_subtrees(
|
||||
scope,
|
||||
host_ptr,
|
||||
&connected_roots,
|
||||
);
|
||||
}
|
||||
if crate::custom_elements::form_owner_mutation_effects_touch_html_form(
|
||||
self.dom_host(),
|
||||
&form_owner_effects,
|
||||
) {
|
||||
self.ensure_parser_custom_element_reaction_queue(host_ptr);
|
||||
crate::custom_elements::enqueue_form_association_callbacks_for_all(scope, host_ptr);
|
||||
}
|
||||
apply_parser_mutation_effects(scope, host_ptr, self, &effects);
|
||||
}
|
||||
|
||||
pub(crate) fn parser_runtime_dom_node_exists(&mut self, node_id: DomHandle) -> bool {
|
||||
@@ -843,14 +813,30 @@ impl DocumentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn sync_style_sources_from_dom_mutation_effects(
|
||||
host_ptr: *mut JsContextHost,
|
||||
effects: &DomMutationEffects,
|
||||
) {
|
||||
if effects.stylesheet_owners().changes().is_empty() {
|
||||
return;
|
||||
impl ParserMutationEffectsOwner for DocumentRuntime {
|
||||
type Prepared = RuntimeMutationApplyResult;
|
||||
|
||||
fn prepare_parser_mutation_effects(&mut self, effects: &DomMutationEffects) -> Self::Prepared {
|
||||
prepare_runtime_mutation_effects(
|
||||
self.dom_host(),
|
||||
self.document.url(),
|
||||
effects,
|
||||
RuntimeMutationOptions::parser_tree_sink(),
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost) {
|
||||
self.ensure_parser_custom_element_reaction_queue(host_ptr);
|
||||
}
|
||||
|
||||
fn finish_parser_mutation_effects(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
prepared: Self::Prepared,
|
||||
) {
|
||||
let _ = finish_runtime_mutation_effects(self, scope, host_ptr, prepared);
|
||||
}
|
||||
unsafe { &mut *host_ptr }.apply_stylesheet_owner_changes(effects.stylesheet_owners().changes());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::{
|
||||
};
|
||||
use moli_dom::native::{Element, Node, NodeType};
|
||||
|
||||
use super::dom_facade::sync_style_sources_from_dom_mutation_effects;
|
||||
use super::*;
|
||||
|
||||
mod details;
|
||||
@@ -1709,7 +1708,7 @@ impl DocumentRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct RuntimeMutationApplyResult {
|
||||
pub(crate) struct RuntimeMutationApplyResult {
|
||||
changed: bool,
|
||||
meta_refresh_candidates: Vec<MetaRefreshNavigation>,
|
||||
devtools_dom_mutations: Vec<super::devtools_mutations::DevToolsDomMutationFact>,
|
||||
@@ -1919,25 +1918,17 @@ fn execute_committed_inline_classic_script(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_runtime_mutation_effects_to_dom_host(
|
||||
mutations: &mut MutationCoordinator,
|
||||
document: &HostDocumentState,
|
||||
scripts: &mut HostScriptScheduler,
|
||||
events: &mut HostEventTargetRegistry,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
dom_host: &mut DomHost,
|
||||
effects: DomMutationEffects,
|
||||
pub(super) fn prepare_runtime_mutation_effects(
|
||||
dom_host: &DomHost,
|
||||
document_url: &Url,
|
||||
effects: &DomMutationEffects,
|
||||
options: RuntimeMutationOptions,
|
||||
) -> RuntimeMutationApplyResult {
|
||||
let cpu_profile_enabled = moli_trace::cpu_profile_enabled();
|
||||
let total_started = cpu_profile_enabled.then(Instant::now);
|
||||
let style_sources_started = cpu_profile_enabled.then(Instant::now);
|
||||
let stylesheet_owner_changes = effects.stylesheet_owners().changes().to_vec();
|
||||
let meta_refresh_candidates = super::meta_refresh::meta_refresh_navigations_from_mutation(
|
||||
dom_host,
|
||||
&effects,
|
||||
document.url(),
|
||||
effects,
|
||||
document_url,
|
||||
);
|
||||
let inline_style_attribute_csp_mutations = if options.check_inline_style_csp {
|
||||
effects
|
||||
@@ -1975,11 +1966,36 @@ pub(super) fn apply_runtime_mutation_effects_to_dom_host(
|
||||
Vec::new()
|
||||
};
|
||||
let devtools_dom_mutations =
|
||||
super::devtools_mutations::capture_devtools_dom_mutation_facts(dom_host, &effects);
|
||||
if effects.did_change() {
|
||||
sync_style_sources_from_dom_mutation_effects(host_ptr, &effects);
|
||||
super::devtools_mutations::capture_devtools_dom_mutation_facts(dom_host, effects);
|
||||
RuntimeMutationApplyResult {
|
||||
changed: effects.did_change(),
|
||||
meta_refresh_candidates,
|
||||
devtools_dom_mutations,
|
||||
runtime_script_start_candidates: Vec::new(),
|
||||
removed_open_popovers: effects.tree().removed_open_popovers().to_vec(),
|
||||
changed_slots: effects.slots().changed_slots().to_vec(),
|
||||
stylesheet_owner_changes,
|
||||
inline_style_attribute_csp_mutations,
|
||||
connected_style_csp_roots,
|
||||
}
|
||||
let style_sources_us = style_sources_started
|
||||
}
|
||||
|
||||
pub(super) fn apply_runtime_mutation_effects_to_dom_host(
|
||||
mutations: &mut MutationCoordinator,
|
||||
document: &HostDocumentState,
|
||||
scripts: &mut HostScriptScheduler,
|
||||
events: &mut HostEventTargetRegistry,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
dom_host: &mut DomHost,
|
||||
effects: DomMutationEffects,
|
||||
options: RuntimeMutationOptions,
|
||||
) -> RuntimeMutationApplyResult {
|
||||
let cpu_profile_enabled = moli_trace::cpu_profile_enabled();
|
||||
let total_started = cpu_profile_enabled.then(Instant::now);
|
||||
let preparation_started = cpu_profile_enabled.then(Instant::now);
|
||||
let mut result = prepare_runtime_mutation_effects(dom_host, document.url(), &effects, options);
|
||||
let preparation_us = preparation_started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
let started = dom_binding_timing_started();
|
||||
@@ -1992,24 +2008,16 @@ pub(super) fn apply_runtime_mutation_effects_to_dom_host(
|
||||
tracing::info!(
|
||||
target: "moli_cpu_profile",
|
||||
stage = "apply_runtime_mutation_effects",
|
||||
style_sources_us,
|
||||
coordinator_us = total_us.saturating_sub(style_sources_us),
|
||||
preparation_us,
|
||||
coordinator_us = total_us.saturating_sub(preparation_us),
|
||||
total_us,
|
||||
);
|
||||
}
|
||||
}
|
||||
record_dom_binding_timing("mutation.apply", started);
|
||||
RuntimeMutationApplyResult {
|
||||
changed: mutation_result.changed,
|
||||
meta_refresh_candidates,
|
||||
devtools_dom_mutations,
|
||||
runtime_script_start_candidates: mutation_result.runtime_script_start_candidates,
|
||||
removed_open_popovers: mutation_result.removed_open_popovers,
|
||||
changed_slots: mutation_result.changed_slots,
|
||||
stylesheet_owner_changes,
|
||||
inline_style_attribute_csp_mutations,
|
||||
connected_style_csp_roots,
|
||||
}
|
||||
result.changed = mutation_result.changed;
|
||||
result.runtime_script_start_candidates = mutation_result.runtime_script_start_candidates;
|
||||
result
|
||||
}
|
||||
|
||||
fn should_dispatch_attribute_changed_for_set(
|
||||
|
||||
@@ -80,6 +80,7 @@ mod page_resource_completion;
|
||||
mod page_task_queue;
|
||||
mod parser_module_evaluation;
|
||||
mod parser_module_pending;
|
||||
mod parser_mutation_effects;
|
||||
mod parser_script;
|
||||
mod permissions_policy;
|
||||
mod queue_microtask;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::dom::native::{DomHost, DomMutationEffects, NativeNodeId, ScriptPrepareTriggerKind};
|
||||
use crate::style_engine::StyleMutationEffect;
|
||||
|
||||
mod notifications;
|
||||
pub(crate) use notifications::notify_dom_mutation;
|
||||
|
||||
use super::{
|
||||
host::{
|
||||
@@ -9,7 +11,6 @@ use super::{
|
||||
RuntimeScriptStartDecision, ScriptElementLoader, ScriptElementLoaderOptions,
|
||||
},
|
||||
native_bridge::{self, JsContextHost},
|
||||
observer_runtime,
|
||||
util::v8str,
|
||||
};
|
||||
|
||||
@@ -19,8 +20,6 @@ pub(super) struct MutationCoordinator;
|
||||
pub(super) struct MutationCoordinatorApplyResult {
|
||||
pub(super) changed: bool,
|
||||
pub(super) runtime_script_start_candidates: Vec<RuntimeScriptStartCandidate>,
|
||||
pub(super) removed_open_popovers: Vec<NativeNodeId>,
|
||||
pub(super) changed_slots: Vec<NativeNodeId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -175,14 +174,11 @@ impl MutationCoordinator {
|
||||
return MutationCoordinatorApplyResult {
|
||||
changed: false,
|
||||
runtime_script_start_candidates: Vec::new(),
|
||||
removed_open_popovers: Vec::new(),
|
||||
changed_slots: Vec::new(),
|
||||
};
|
||||
}
|
||||
if options.source == DomMutationSource::JsDomApi {
|
||||
Self::note_script_children_changed_by_api(dom_host, &effects);
|
||||
}
|
||||
unsafe { &mut *host_ptr }.note_app_manifest_link_mutation(dom_host, &effects);
|
||||
let cpu_profile_enabled = moli_trace::cpu_profile_enabled();
|
||||
let total_started = cpu_profile_enabled.then(Instant::now);
|
||||
tracing::trace!(
|
||||
@@ -194,19 +190,12 @@ impl MutationCoordinator {
|
||||
dispatch_atomic_move_callbacks = options.dispatch_atomic_move_callbacks,
|
||||
"applying runtime mutation effects"
|
||||
);
|
||||
let style_effects_started = cpu_profile_enabled.then(Instant::now);
|
||||
let style_effects = StyleMutationEffect::from_dom_mutation_effects(dom_host, &effects);
|
||||
let style_effect_count = style_effects.len();
|
||||
let style_effects_us = style_effects_started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
let style_invalidation_started = cpu_profile_enabled.then(Instant::now);
|
||||
if !style_effects.is_empty() {
|
||||
unsafe { &mut *host_ptr }.note_style_mutation_effects(&style_effects);
|
||||
}
|
||||
let style_invalidation_us = style_invalidation_started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
let notifications::MutationNotificationTimings {
|
||||
style_effect_count,
|
||||
style_effects_us,
|
||||
style_invalidation_us,
|
||||
observer_us,
|
||||
} = notify_dom_mutation(scope, host_ptr, dom_host, &effects);
|
||||
|
||||
let timing_started = moli_trace::cdp_nav_timing_enabled().then(Instant::now);
|
||||
let prepare_connected_scripts = options.prepares_connected_scripts();
|
||||
@@ -262,12 +251,6 @@ impl MutationCoordinator {
|
||||
let script_planning_us = script_planning_started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
let observer_started = cpu_profile_enabled.then(Instant::now);
|
||||
observer_runtime::queue_mutation_records(scope, host_ptr, dom_host, &effects);
|
||||
let observer_us = observer_started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
|
||||
let script_start_started = cpu_profile_enabled.then(Instant::now);
|
||||
let mut runtime_script_start_candidates = Vec::new();
|
||||
for request in script_start_requests {
|
||||
@@ -328,8 +311,6 @@ impl MutationCoordinator {
|
||||
MutationCoordinatorApplyResult {
|
||||
changed: true,
|
||||
runtime_script_start_candidates,
|
||||
removed_open_popovers: effects.tree().removed_open_popovers().to_vec(),
|
||||
changed_slots: effects.slots().changed_slots().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
dom::native::{DomHost, DomMutationEffects},
|
||||
native_bridge::JsContextHost,
|
||||
observer_runtime,
|
||||
style_engine::StyleMutationEffect,
|
||||
};
|
||||
|
||||
pub(crate) struct MutationNotificationTimings {
|
||||
pub(super) style_effect_count: usize,
|
||||
pub(super) style_effects_us: u128,
|
||||
pub(super) style_invalidation_us: u128,
|
||||
pub(super) observer_us: u128,
|
||||
}
|
||||
|
||||
/// Notifications derived entirely from the committed DOM changes. This layer
|
||||
/// does not need a DocumentRuntime or prepare scripts for any document owner.
|
||||
pub(crate) fn notify_dom_mutation(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
dom_host: &DomHost,
|
||||
effects: &DomMutationEffects,
|
||||
) -> MutationNotificationTimings {
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
if !effects.stylesheet_owners().changes().is_empty() {
|
||||
host.apply_stylesheet_owner_changes(effects.stylesheet_owners().changes());
|
||||
}
|
||||
host.note_app_manifest_link_mutation(dom_host, effects);
|
||||
|
||||
let profile = moli_trace::cpu_profile_enabled();
|
||||
let started = profile.then(Instant::now);
|
||||
let style_effects = StyleMutationEffect::from_dom_mutation_effects(dom_host, effects);
|
||||
let style_effect_count = style_effects.len();
|
||||
let style_effects_us = started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
let started = profile.then(Instant::now);
|
||||
if !style_effects.is_empty() {
|
||||
host.note_style_mutation_effects(&style_effects);
|
||||
}
|
||||
let style_invalidation_us = started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
|
||||
let started = profile.then(Instant::now);
|
||||
observer_runtime::queue_mutation_records(scope, host_ptr, dom_host, effects);
|
||||
let observer_us = started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
MutationNotificationTimings {
|
||||
style_effect_count,
|
||||
style_effects_us,
|
||||
style_invalidation_us,
|
||||
observer_us,
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ use crate::{
|
||||
ParserElementCreationConsumer, ParserElementCreationRequest, ParserMutationEffectConsumer,
|
||||
ParserPlanningReadView, ParserScriptHandoff, ParserScriptRead, PreparedImportMapSource,
|
||||
},
|
||||
parser_mutation_effects::{ParserMutationEffectsOwner, apply_parser_mutation_effects},
|
||||
planning::ScriptSource,
|
||||
types::{ScriptKind, ScriptSourceKind},
|
||||
};
|
||||
@@ -60,9 +61,36 @@ struct ChildFrameLiveParserOwner<'a, 'scope, 'pin> {
|
||||
child_document_handle: DomHandle,
|
||||
document_owner: Option<FrameDocumentOwner>,
|
||||
parser_control: Option<crate::live_document_parser::DocumentParserSessionControlHandle>,
|
||||
mutation_effects: ChildParserMutationEffects,
|
||||
}
|
||||
|
||||
struct ChildParserMutationEffects {
|
||||
document_handle: DomHandle,
|
||||
reaction_queue_active: bool,
|
||||
}
|
||||
|
||||
impl ParserMutationEffectsOwner for ChildParserMutationEffects {
|
||||
type Prepared = ();
|
||||
|
||||
fn prepare_parser_mutation_effects(&mut self, _effects: &DomMutationEffects) {}
|
||||
|
||||
fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost) {
|
||||
if !self.reaction_queue_active {
|
||||
custom_elements::push_parser_custom_element_reaction_queue(host_ptr);
|
||||
self.reaction_queue_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_parser_mutation_effects(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
_prepared: (),
|
||||
) {
|
||||
unsafe { &mut *host_ptr }.sync_child_browsing_context_subtree(scope, self.document_handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'scope, 'pin> ChildFrameLiveParserOwner<'a, 'scope, 'pin> {
|
||||
fn new(
|
||||
host: &'a mut JsContextHost,
|
||||
@@ -81,7 +109,10 @@ impl<'a, 'scope, 'pin> ChildFrameLiveParserOwner<'a, 'scope, 'pin> {
|
||||
child_document_handle,
|
||||
document_owner,
|
||||
parser_control,
|
||||
reaction_queue_active: false,
|
||||
mutation_effects: ChildParserMutationEffects {
|
||||
document_handle: child_document_handle,
|
||||
reaction_queue_active: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,44 +126,6 @@ impl<'a, 'scope, 'pin> ChildFrameLiveParserOwner<'a, 'scope, 'pin> {
|
||||
})
|
||||
== self.document_owner
|
||||
}
|
||||
|
||||
fn ensure_reaction_queue(&mut self) {
|
||||
if !self.reaction_queue_active {
|
||||
custom_elements::push_parser_custom_element_reaction_queue(self.host);
|
||||
self.reaction_queue_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_child_parser_mutation_effects(&mut self, effects: &DomMutationEffects) {
|
||||
if !effects.did_change() {
|
||||
return;
|
||||
}
|
||||
let host_ptr = self.host as *mut JsContextHost;
|
||||
crate::observer_runtime::queue_mutation_records(
|
||||
self.scope,
|
||||
host_ptr,
|
||||
self.host.dom_host(),
|
||||
effects,
|
||||
);
|
||||
for &root in effects.tree().disconnected_roots() {
|
||||
custom_elements::enqueue_disconnected_callbacks_for_subtree(self.scope, host_ptr, root);
|
||||
}
|
||||
custom_elements::enqueue_connected_and_form_callbacks_for_already_upgraded_subtrees(
|
||||
self.scope,
|
||||
host_ptr,
|
||||
effects.tree().connected_roots(),
|
||||
);
|
||||
if custom_elements::form_owner_mutation_effects_touch_html_form(
|
||||
self.host.dom_host(),
|
||||
effects,
|
||||
) {
|
||||
custom_elements::enqueue_form_association_callbacks_for_all(self.scope, host_ptr);
|
||||
}
|
||||
self.host
|
||||
.sync_owner_style_sheet_texts_for_document_tree_scopes(self.child_document_handle);
|
||||
self.host
|
||||
.sync_child_browsing_context_subtree(self.scope, self.child_document_handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveDocumentParserOwner for ChildFrameLiveParserOwner<'_, '_, '_> {}
|
||||
@@ -190,12 +183,11 @@ impl ParserMutationEffectConsumer for ChildFrameLiveParserOwner<'_, '_, '_> {
|
||||
if !self.targets_current_document() {
|
||||
return;
|
||||
}
|
||||
self.ensure_reaction_queue();
|
||||
self.apply_child_parser_mutation_effects(&effects);
|
||||
apply_parser_mutation_effects(self.scope, self.host, &mut self.mutation_effects, &effects);
|
||||
}
|
||||
|
||||
fn finish_parser_dom_mutations(&mut self) -> std::ops::ControlFlow<()> {
|
||||
if std::mem::take(&mut self.reaction_queue_active) {
|
||||
if std::mem::take(&mut self.mutation_effects.reaction_queue_active) {
|
||||
custom_elements::flush_parser_custom_element_reaction_queue(self.scope, self.host);
|
||||
}
|
||||
if !self.targets_current_document()
|
||||
@@ -337,9 +329,10 @@ impl ParserDomMutationConsumer for ChildFrameLiveParserOwner<'_, '_, '_> {
|
||||
// The tree sink invokes this queue after releasing its structural
|
||||
// borrow, before returning to the tree builder. Nested document.write
|
||||
// can then use the insertion point preceding this element's children.
|
||||
self.ensure_reaction_queue();
|
||||
self.mutation_effects
|
||||
.ensure_parser_reaction_queue(self.host);
|
||||
let effects = mutation.apply_to_dom_host(self.host.dom_host_mut());
|
||||
self.apply_child_parser_mutation_effects(&effects);
|
||||
self.consume_parser_mutation_effects(effects);
|
||||
}
|
||||
|
||||
fn create_parser_element_without_attributes(
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::{
|
||||
custom_elements, dom::native::DomMutationEffects, mutation_coordinator::notify_dom_mutation,
|
||||
native_bridge::JsContextHost,
|
||||
};
|
||||
|
||||
/// Document-specific resource and lifecycle work surrounding the shared parser
|
||||
/// notifications. Owners keep their runtime state; they do not choose which
|
||||
/// observer or custom-element notifications a mutation produces.
|
||||
pub(crate) trait ParserMutationEffectsOwner {
|
||||
type Prepared;
|
||||
|
||||
fn prepare_parser_mutation_effects(&mut self, effects: &DomMutationEffects) -> Self::Prepared;
|
||||
|
||||
fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost);
|
||||
|
||||
fn finish_parser_mutation_effects(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
prepared: Self::Prepared,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn apply_parser_mutation_effects(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
owner: &mut impl ParserMutationEffectsOwner,
|
||||
effects: &DomMutationEffects,
|
||||
) {
|
||||
if !effects.did_change() {
|
||||
return;
|
||||
}
|
||||
let prepared = owner.prepare_parser_mutation_effects(effects);
|
||||
notify_dom_mutation(scope, host_ptr, unsafe { &*host_ptr }.dom_host(), effects);
|
||||
|
||||
let form_owner_changed = custom_elements::form_owner_mutation_effects_touch_html_form(
|
||||
unsafe { &*host_ptr }.dom_host(),
|
||||
effects,
|
||||
);
|
||||
let tree = effects.tree();
|
||||
if !tree.disconnected_roots().is_empty()
|
||||
|| !tree.connected_roots().is_empty()
|
||||
|| form_owner_changed
|
||||
{
|
||||
owner.ensure_parser_reaction_queue(host_ptr);
|
||||
for &root in tree.disconnected_roots() {
|
||||
custom_elements::enqueue_disconnected_callbacks_for_subtree(scope, host_ptr, root);
|
||||
}
|
||||
custom_elements::enqueue_connected_and_form_callbacks_for_already_upgraded_subtrees(
|
||||
scope,
|
||||
host_ptr,
|
||||
tree.connected_roots(),
|
||||
);
|
||||
if form_owner_changed {
|
||||
custom_elements::enqueue_form_association_callbacks_for_all(scope, host_ptr);
|
||||
}
|
||||
}
|
||||
owner.finish_parser_mutation_effects(scope, host_ptr, prepared);
|
||||
}
|
||||
@@ -1,5 +1,114 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn main_and_child_parser_mutation_notifications_preserve_order_and_identity() {
|
||||
let mut vm = new_storage_test_vm("https://parser-mutation-parity.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
function exercise(w) {
|
||||
const d = w.document;
|
||||
d.open(); d.write('<!doctype html><body>');
|
||||
const log = [], records = [];
|
||||
const collect = entries => records.push(...entries.map(r => [
|
||||
r.target.nodeName, Array.from(r.addedNodes, n => n.nodeName), r.removedNodes.length
|
||||
]));
|
||||
const observer = new w.MutationObserver(collect);
|
||||
observer.observe(d.body, {childList:true, subtree:true});
|
||||
class Probe extends w.HTMLElement {
|
||||
static get observedAttributes() { return ['title']; }
|
||||
constructor() { super(); log.push('construct'); }
|
||||
attributeChangedCallback(name, oldValue, value) { log.push(name + ':' + value); }
|
||||
connectedCallback() { log.push('connected:' + this.childNodes.length); }
|
||||
}
|
||||
let internals;
|
||||
class Face extends w.HTMLElement {
|
||||
static formAssociated = true;
|
||||
constructor() { super(); internals = this.attachInternals(); }
|
||||
connectedCallback() { log.push('face:' + (internals.form?.id || 'null')); }
|
||||
formAssociatedCallback(form) { log.push('form:' + (form?.id || 'null')); }
|
||||
}
|
||||
w.customElements.define('x-mutation-probe', Probe);
|
||||
w.customElements.define('x-mutation-face', Face);
|
||||
d.write('<x-mutation-probe title=parsed>text</x-mutation-probe>' +
|
||||
'<x-mutation-face form=owner></x-mutation-face><form id=owner></form><iframe id=nested></iframe>');
|
||||
collect(observer.takeRecords());
|
||||
observer.disconnect();
|
||||
const nested = d.getElementById('nested');
|
||||
const result = {log, records, form:internals.form === d.getElementById('owner'),
|
||||
childIdentity:nested.contentDocument === nested.contentWindow.document};
|
||||
d.close();
|
||||
return result;
|
||||
}
|
||||
const main = exercise(window);
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
const child = exercise(frame.contentWindow);
|
||||
frame.remove();
|
||||
return JSON.stringify([main, child]);
|
||||
})()
|
||||
"#).unwrap();
|
||||
let expected = serde_json::json!({
|
||||
"log": ["construct", "title:parsed", "connected:0", "face:null", "form:owner"],
|
||||
"records": [
|
||||
["BODY", ["X-MUTATION-PROBE"], 0],
|
||||
["X-MUTATION-PROBE", ["#text"], 0],
|
||||
["BODY", ["X-MUTATION-FACE"], 0],
|
||||
["BODY", ["FORM"], 0],
|
||||
["BODY", ["IFRAME"], 0]
|
||||
],
|
||||
"form": true,
|
||||
"childIdentity": true
|
||||
});
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!([expected, expected]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_and_child_parser_styles_are_current_in_reactions_and_preserve_cssom_edits() {
|
||||
let mut vm = new_storage_test_vm("https://parser-style-mutation-parity.test/");
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
function exercise(w) {
|
||||
const d = w.document;
|
||||
d.open(); d.write('<!doctype html><body><style id=first>.first { color: red; }</style>');
|
||||
const first = d.getElementById('first').sheet;
|
||||
first.insertRule('.kept { color: green; }', first.cssRules.length);
|
||||
const log = [];
|
||||
class Probe extends w.HTMLElement {
|
||||
connectedCallback() {
|
||||
log.push({color:w.getComputedStyle(this).color,
|
||||
selectors:Array.from(d.getElementById('second').sheet.cssRules, r => r.selectorText)});
|
||||
}
|
||||
}
|
||||
w.customElements.define('x-style-probe', Probe);
|
||||
d.write('<style id=second>x-style-probe { color: rgb(1, 2, 3); }</style><x-style-probe></x-style-probe>');
|
||||
d.write('<div>unrelated insertion</div>');
|
||||
const result = {log, identity:first === d.getElementById('first').sheet,
|
||||
firstSelectors:Array.from(first.cssRules, r => r.selectorText)};
|
||||
d.close();
|
||||
return result;
|
||||
}
|
||||
const main = exercise(window);
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
const child = exercise(frame.contentWindow);
|
||||
frame.remove();
|
||||
return JSON.stringify([main, child]);
|
||||
})()
|
||||
"#).unwrap();
|
||||
let expected = serde_json::json!({
|
||||
"log": [{"color": "rgb(1, 2, 3)", "selectors": ["x-style-probe"]}],
|
||||
"identity": true,
|
||||
"firstSelectors": [".first", ".kept"]
|
||||
});
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!([expected, expected]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_parser_write_constructs_elements_and_runs_reactions_synchronously() {
|
||||
let mut vm = new_storage_test_vm("https://child-parser-custom-elements.test/");
|
||||
|
||||
Reference in New Issue
Block a user