fix(style): process parser styles after their children finish

This commit is contained in:
ldm0
2026-09-22 21:47:14 +08:00
parent ae34084619
commit d831793e35
21 changed files with 281 additions and 25 deletions
@@ -148,6 +148,7 @@ pub struct ElementControlState {
indeterminate: bool,
script: Option<Box<ScriptElementState>>,
link_created_by_parser: bool,
style_parser_children_pending: bool,
link_explicitly_enabled: bool,
selection_start: Option<u32>,
selection_end: Option<u32>,
@@ -654,6 +655,18 @@ impl ElementControlState {
true
}
pub fn style_parser_children_pending(&self) -> bool {
self.style_parser_children_pending
}
pub fn note_parser_created_style(&mut self) {
self.style_parser_children_pending = true;
}
pub fn finish_parsing_style_children(&mut self) -> bool {
std::mem::take(&mut self.style_parser_children_pending)
}
pub fn finish_parsing_link_children(&mut self) -> bool {
if !self.link_created_by_parser {
return false;
+21
View File
@@ -155,6 +155,15 @@ impl Element {
{
rare_data.control_state_mut().note_parser_created_link();
}
if creation_source == ElementCreationSource::Parser
&& local_name == "style"
&& matches!(
namespace.as_str(),
"http://www.w3.org/1999/xhtml" | "http://www.w3.org/2000/svg"
)
{
rare_data.control_state_mut().note_parser_created_style();
}
Self {
local_name: LocalName::from(local_name),
namespace: Namespace::from(namespace),
@@ -866,6 +875,18 @@ impl Element {
self.control_state_mut().finish_parsing_link_children()
}
/// A parser-owned style block is processed only after its children finish.
pub fn style_parser_children_pending(&self) -> bool {
self.control_state().style_parser_children_pending()
}
pub fn finish_parsing_style_children(&mut self) -> bool {
if !self.is_inline_style_element() {
return false;
}
self.control_state_mut().finish_parsing_style_children()
}
pub fn parser_associated_form_owner(&self) -> Option<NativeNodeId> {
self.rare_data.parser_associated_form_owner()
}
@@ -1054,6 +1054,31 @@ impl DomHost {
did_change
}
pub fn finish_parsing_style_children(&mut self, handle: DomHandle) -> bool {
let did_change = self
.node_mut(handle)
.and_then(|node| node.data_mut().as_element_mut())
.is_some_and(|element| element.finish_parsing_style_children());
if did_change {
self.record_mutation(MutationScope::LocalState);
}
did_change
}
pub fn finish_parsing_style_children_effects(
&mut self,
handle: DomHandle,
) -> DomMutationEffects {
let mut effects = DomMutationEffects::default();
if self.finish_parsing_style_children(handle) {
effects.mark_stylesheet_owner_contents_change(
handle,
self.dom.stylesheet_candidate_tree_scope_for_node(handle),
);
}
effects
}
pub fn set_cryptographic_nonce(&mut self, handle: DomHandle, nonce: Option<String>) -> bool {
let did_change = {
let Some(element) = self
+20 -2
View File
@@ -521,6 +521,9 @@ pub enum ParserDomMutation {
parent: NativeNodeId,
child: NativeNodeId,
},
FinishParsingStyleChildren {
node: NativeNodeId,
},
}
impl ParserDomMutation {
@@ -533,6 +536,9 @@ impl ParserDomMutation {
reference_child,
} => host.insert_before_effects(parent, child, reference_child),
Self::RemoveChild { parent, child } => host.remove_child_effects(parent, child),
Self::FinishParsingStyleChildren { node } => {
host.finish_parsing_style_children_effects(node)
}
}
}
@@ -554,6 +560,7 @@ impl ParserDomMutation {
Self::RemoveChild { parent, child } => {
host.remove_child_without_mutation_effects(parent, child)
}
Self::FinishParsingStyleChildren { node } => host.finish_parsing_style_children(node),
}
}
}
@@ -2647,7 +2654,18 @@ impl ParserStreamHtmlTreeSinkTarget {
.iter()
.rposition(|candidate| candidate.node_id == node_id)
{
self.open_parser_elements.remove(index);
let element = self.open_parser_elements.remove(index);
if element.name.local.as_ref() == "style"
&& matches!(
element.name.ns.as_ref(),
"http://www.w3.org/1999/xhtml" | "http://www.w3.org/2000/svg"
)
{
self.record_parser_dom_mutation(ParserDomMutation::FinishParsingStyleChildren {
node: node_id,
})
.consume();
}
}
if self.read_is_html_element_named(node_id, "link") {
let _ = self.capture_parser_blocking_stylesheet(node_id);
@@ -3582,10 +3600,10 @@ impl ParserStreamHtmlTreeSinkTarget {
if self.read_is_html_element_named(node_id, "template") {
self.open_template_element_depth = self.open_template_element_depth.saturating_sub(1);
}
self.note_parser_element_popped(node_id);
if self.read_is_html_element_named(node_id, "style") {
self.note_blocking_stylesheet_pause_if_needed(node_id);
}
self.note_parser_element_popped(node_id);
}
pub(super) fn restore_parser_stream_dom_host(&mut self, dom_host: DomHost) {
+41
View File
@@ -2493,6 +2493,47 @@ mod tests {
);
}
#[test]
fn parser_style_children_finish_at_close_and_eof() {
for (opening, closing) in [
("<html><head><style id='sheet'>", "</style>"),
("<html><body><svg><style id='sheet'>", "</style></svg>"),
("<html><head><style id='sheet'>", ""),
] {
let mut stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
Url::parse("https://example.test/page").unwrap(),
);
stream.pump_parser_step(&format!("{opening}p {{ col"));
let document = stream.snapshot_parser_stream_document().into_document();
let style =
document.elements_by_tag_name(document.document_node_id(), "style", false)[0];
assert!(
document
.node(style)
.and_then(Node::as_element)
.unwrap()
.style_parser_children_pending()
);
stream.pump_parser_step(&format!("or: blue; }}{closing}"));
let document = if closing.is_empty() {
stream.finish()
} else {
stream.snapshot_parser_stream_document().into_document()
};
assert!(
!document
.node(style)
.and_then(Node::as_element)
.unwrap()
.style_parser_children_pending()
);
assert_eq!(
document.text_content(style).as_deref(),
Some("p { color: blue; }")
);
}
}
#[test]
fn parser_stream_reports_split_connected_meta_csp_once_and_ignores_template_contents() {
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
+25
View File
@@ -667,6 +667,13 @@ impl<'host> XmlTreeSinkBase for XmlDocumentSink<'host> {
self.target.borrow().document_handle()
}
fn pop(&self, node: &Self::Handle) {
self.target
.borrow_mut()
.dom_host
.finish_parsing_style_children(node.node_id);
}
fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
target
.element_name
@@ -774,6 +781,24 @@ mod tests {
use moli_dom::native::{DomHost, NativeDom, NativeNodeId, Node, NodeType};
use url::Url;
#[test]
fn xml_style_children_finish_at_close() {
for namespace in ["http://www.w3.org/1999/xhtml", "http://www.w3.org/2000/svg"] {
let document = XmlParser.parse(
Url::parse("https://example.test/style.xml").unwrap(),
format!("<style xmlns='{namespace}'>p {{ color: blue; }}</style>"),
);
let root = document.document_element_node_id().unwrap();
assert!(
!document
.node(root)
.and_then(Node::as_element)
.unwrap()
.style_parser_children_pending()
);
}
}
#[test]
fn xml_parser_records_unclosed_and_mismatched_elements() {
for (index, source) in [
@@ -93,9 +93,22 @@ impl DocumentRuntime {
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
effects: DomMutationEffects,
) {
self.apply_parser_stream_mutation_effects_to_live_dom_host_with_options(
scope, host_ptr, effects, RuntimeMutationOptions::parser_tree_sink(),
);
}
pub(super) fn apply_parser_stream_mutation_effects_to_live_dom_host_with_options(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
effects: DomMutationEffects,
options: RuntimeMutationOptions,
) {
self.assert_active_parser_document_incarnation();
apply_parser_mutation_effects(scope, host_ptr, self, &effects);
let mut owner = RuntimeParserMutationEffects { runtime: self, options };
apply_parser_mutation_effects(scope, host_ptr, &mut owner, &effects);
}
pub(crate) fn parser_runtime_dom_node_exists(&mut self, node_id: DomHandle) -> bool {
@@ -818,20 +831,25 @@ impl DocumentRuntime {
}
}
impl ParserMutationEffectsOwner for DocumentRuntime {
struct RuntimeParserMutationEffects<'a> {
runtime: &'a mut DocumentRuntime,
options: RuntimeMutationOptions,
}
impl ParserMutationEffectsOwner for RuntimeParserMutationEffects<'_> {
type Prepared = RuntimeMutationApplyResult;
fn prepare_parser_mutation_effects(&mut self, effects: &DomMutationEffects) -> Self::Prepared {
prepare_runtime_mutation_effects(
self.dom_host(),
self.document.url(),
self.runtime.dom_host(),
self.runtime.document.url(),
effects,
RuntimeMutationOptions::parser_tree_sink(),
self.options,
)
}
fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost) {
self.ensure_parser_custom_element_reaction_queue(host_ptr);
self.runtime.ensure_parser_custom_element_reaction_queue(host_ptr);
}
fn finish_parser_mutation_effects(
@@ -840,7 +858,7 @@ impl ParserMutationEffectsOwner for DocumentRuntime {
host_ptr: *mut JsContextHost,
prepared: Self::Prepared,
) {
let _ = finish_runtime_mutation_effects(self, scope, host_ptr, prepared);
let _ = finish_runtime_mutation_effects(self.runtime, scope, host_ptr, prepared);
}
}
@@ -54,6 +54,17 @@ impl DocumentRuntime {
source_profile: TreeMutationSourceProfile,
) {
match mutation {
ParserDomMutation::FinishParsingStyleChildren { node } => {
let effects = self
.dom_host_mut()
.finish_parsing_style_children_effects(node);
self.apply_parser_stream_mutation_effects_to_live_dom_host_with_options(
scope,
host_ptr,
effects,
mutation_options,
);
}
ParserDomMutation::AppendChild { parent, child } => self
.apply_parser_append_child_to_live_dom_host(
scope,
@@ -389,6 +389,9 @@ impl DocumentRuntime {
let Some(element) = self.dom_host.node(owner).and_then(Node::as_element) else {
return;
};
if element.style_parser_children_pending() {
return;
}
let source = self.dom_host.text_content(owner).unwrap_or_default();
let nonce = element.cryptographic_nonce().map(str::to_owned);
let is_declarative_css_module =
@@ -1272,7 +1272,7 @@ impl JsContextHost {
let element = dom_host.node(owner).and_then(Node::as_element)?;
if !dom_host.is_connected(owner)
|| dom_host.get_attribute(owner, "disabled").is_some()
|| !crate::style_engine::stylesheet_owner_type_is_supported(element)
|| !crate::style_engine::stylesheet_owner_can_have_sheet(element)
{
return None;
}
@@ -41,7 +41,7 @@ pub(super) fn owner_font_face_projection(
let dom_host = host.dom_host();
let element = dom_host.node(owner)?.as_element()?;
if !dom_host.is_connected(owner)
|| !crate::style_engine::stylesheet_owner_type_is_supported(element)
|| !crate::style_engine::stylesheet_owner_can_have_sheet(element)
{
return None;
}
@@ -27,7 +27,7 @@ pub(super) fn sync_document_style_sheets<'s>(
let Some(element) = node.as_element() else {
continue;
};
if !crate::style_engine::stylesheet_owner_type_is_supported(element) {
if !crate::style_engine::stylesheet_owner_can_have_sheet(element) {
continue;
}
let wrapper = if document_is_connected {
@@ -99,7 +99,7 @@ fn sync_shadow_root_style_sheets<'s>(
continue;
};
if !runtime.dom_host().is_connected(sheet_handle)
|| !crate::style_engine::stylesheet_owner_type_is_supported(element)
|| !crate::style_engine::stylesheet_owner_can_have_sheet(element)
{
continue;
}
@@ -132,7 +132,7 @@ fn sync_detached_shadow_root_style_sheets<'s>(
else {
continue;
};
if !crate::style_engine::stylesheet_owner_type_is_supported(element) {
if !crate::style_engine::stylesheet_owner_can_have_sheet(element) {
continue;
}
if element.local_name().eq_ignore_ascii_case("link")
@@ -7,7 +7,7 @@ use crate::{
FullStyleWorldSnapshot, IncrementalStyleWorldUpdate, PreparedStyleWorldUpdate,
StyleSourceId, StyleTreeScopeVersions, StyleViewport, StyleWorldEnvironment,
StyleWorldUpdatePlan, StyloStyleEnvironment, StyloStylesheetSource,
link_rel_qualifies_as_stylesheet, stylesheet_owner_type_is_supported,
link_rel_qualifies_as_stylesheet, stylesheet_owner_can_have_sheet,
},
stylesheet_blocking::link_rel_includes_token,
};
@@ -344,7 +344,7 @@ pub(super) fn active_stylesheet_handles(
element.is_html_element("link") && link_stylesheet_is_enabled(runtime, *handle);
(style || link)
&& (include_detached || stylesheet_is_active_in_scope(runtime, root, *handle))
&& stylesheet_owner_type_is_supported(element)
&& stylesheet_owner_can_have_sheet(element)
})
.collect::<Vec<_>>();
let preferred_title = handles
@@ -133,7 +133,7 @@ pub(crate) fn style_sheet_for_element<'s>(
let is_css_link = element.is_html_element("link")
&& link_rel_qualifies_as_stylesheet(element.attribute("rel"), element.attribute("title"));
if (!is_style && !is_css_link)
|| !crate::style_engine::stylesheet_owner_type_is_supported(element)
|| !crate::style_engine::stylesheet_owner_can_have_sheet(element)
|| (is_css_link
&& runtime
.dom_host()
+1 -1
View File
@@ -119,7 +119,7 @@ impl PageVm {
let dom_host = self.vm().document_runtime.dom_host();
let node = dom_host.node(handle)?;
let element = node.as_element()?;
if !crate::style_engine::stylesheet_owner_type_is_supported(element) {
if !crate::style_engine::stylesheet_owner_can_have_sheet(element) {
return None;
}
@@ -6080,6 +6080,59 @@ globalThis.__outerDocumentWriteScriptContinued = true;
}));
}
#[test]
fn parser_style_csp_checks_complete_contents_once() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
runtime.block_on(tokio::task::LocalSet::new().run_until(async {
for (markup, policy, report_only, expected_violations, expected_sheet) in [
("<style id='sheet'>p {color:blue;}</style>", "style-src 'none'", false, 1, false),
("<style id='sheet'>p {color:blue;}</style>", "style-src 'none'", true, 1, true),
("<style id='sheet'>p {color:blue;}</style>", "style-src 'sha256-rB6kiow2O3eFUeTNyyLeK3wV0+l7vNB90J1aqllKvjg='", false, 0, true),
("<style id='sheet'></style>", "style-src 'none'", false, 1, false),
("<style id='sheet'>p {color:blue;}", "style-src 'none'", false, 1, false),
("<script>document.write(\"<style id='sheet'>p {color:blue;}</style>\")</script>", "style-src 'none'", false, 1, false),
("<script>const s = document.createElement('style'); s.id = 'sheet'; document.head.append(s); s.textContent = 'p {color:blue;}';</script>", "style-src 'none'", false, 2, false),
] {
let env = default_test_page_vm_env_config_with(|env| {
let policies = if report_only {
&mut env.document_policy_container.response_content_security_report_only_policies
} else {
&mut env.document_policy_container.response_content_security_policies
};
*policies = vec![policy.to_owned()];
});
let html = Box::leak(format!("<!doctype html><html><head><script>globalThis.__parserStyleViolations = 0; document.addEventListener('securitypolicyviolation', () => __parserStyleViolations++);</script>{markup}").into_boxed_str());
let mut page_vm = parse_phase_one_html_into_page_vm_for_test_with_env_and_finish(html, env, true).await;
let result = page_vm.evaluate_expression("Boolean(document.getElementById('sheet').sheet)").expect("stylesheet probe");
assert_eq!(result.get("value").and_then(serde_json::Value::as_bool), Some(expected_sheet), "{markup}, {policy}, report-only={report_only}");
let local_executor = page_vm.local_executor.clone();
let page_vm_ptr: *mut PageVm = &mut page_vm;
super::access::run_named_owner_local_task(
local_executor,
"parser style CSP task channel closed",
async move {
let page_vm = unsafe { &mut *page_vm_ptr };
page_vm.page_task_queue.accept_ready_parse_time_wakes();
page_vm.vm_mut().drain_pre_domcontentloaded_content_security_policy_violation_tasks_for_test();
while let Some(task) = page_vm.page_task_queue.parse_time_pop_front() {
let work = PostParsePageOwnedWork::lifecycle_work(
crate::page_task_queue::PostParseLifecycleWork::from_parse_time_page_task(task),
);
execute_page_owned_work_turn_on_local_task(page_vm, work).await?;
}
Ok(())
},
).await.expect("parser style tasks");
let result = page_vm.evaluate_expression("__parserStyleViolations").expect("violation event count");
assert_eq!(result.get("value").and_then(serde_json::Value::as_u64), Some(expected_violations), "{markup}, {policy}, report-only={report_only}");
}
}));
}
#[test]
fn buffered_script_preload_cache_reuses_ready_late_parser_blocking_preload() {
let runtime = tokio::runtime::Builder::new_current_thread()
+1 -3
View File
@@ -131,9 +131,7 @@ use source_id::StyleSourceKind;
use source_id::{StyleInvalidationSourceTarget, StyleScopeId};
pub(crate) use source_lifecycle::OwnedStyleSourceDocumentContext;
use source_lifecycle::StyleSourceDocumentContext;
pub(crate) use source_owner::{
link_rel_qualifies_as_stylesheet, stylesheet_owner_type_is_supported,
};
pub(crate) use source_owner::{link_rel_qualifies_as_stylesheet, stylesheet_owner_can_have_sheet};
pub(crate) use stylesheet_resources::{StylesheetResourceGeneration, StylesheetResourceSnapshot};
#[cfg(test)]
use stylesheet_resources::{
@@ -18,7 +18,10 @@ pub(crate) fn link_rel_qualifies_as_stylesheet(rel: Option<&str>, title: Option<
&& (!includes_token("alternate") || title.is_some_and(|title| !title.is_empty()))
}
pub(crate) fn stylesheet_owner_type_is_supported(element: &Element) -> bool {
pub(crate) fn stylesheet_owner_can_have_sheet(element: &Element) -> bool {
if element.style_parser_children_pending() {
return false;
}
let type_attribute = element.attribute("type");
if element.is_html_element("style")
|| (element.namespace() == "http://www.w3.org/2000/svg" && element.local_name() == "style")
@@ -75,7 +78,7 @@ fn style_element_is_stylesheet_source_enabled(
};
element.is_inline_style_element()
&& host.get_attribute(handle, "disabled").is_none()
&& stylesheet_owner_type_is_supported(element)
&& stylesheet_owner_can_have_sheet(element)
&& stylesheet_source_media_matches(media_text, emulated_media, viewport)
}
@@ -1444,6 +1444,14 @@ impl DocumentRuntime {
let mut canceled_load_event_bindings = Vec::new();
let mut prepared = Vec::new();
for (owner, should_queue) in transitions {
if self
.dom_host
.node(owner)
.and_then(Node::as_element)
.is_some_and(|element| element.style_parser_children_pending())
{
continue;
}
canceled_load_event_bindings.extend(self.invalidate_style_related_state(owner));
if !should_queue {
continue;
@@ -1875,10 +1883,13 @@ fn connected_modulepreload_has_non_matching_media(
fn connected_style_owner_kind(
element: &crate::dom::native::Element,
) -> Option<ConnectedStyleOwnerKind> {
if element.style_parser_children_pending() {
return None;
}
if super::is_inline_style_element(element) {
return if is_declarative_css_module_style_element(element) {
Some(ConnectedStyleOwnerKind::DeclarativeCssModule)
} else if crate::style_engine::stylesheet_owner_type_is_supported(element) {
} else if crate::style_engine::stylesheet_owner_can_have_sheet(element) {
Some(ConnectedStyleOwnerKind::ClassicStyle)
} else {
None
+17 -1
View File
@@ -101,6 +101,7 @@ pub struct StylesheetElementRead {
is_html_element: bool,
local_name: String,
parser_blocking_eligible: bool,
style_parser_children_pending: bool,
rel: Option<String>,
href: Option<String>,
as_attr: Option<String>,
@@ -121,6 +122,7 @@ impl StylesheetElementRead {
Some(Self {
is_html_element: element.namespace() == "http://www.w3.org/1999/xhtml",
local_name: element.local_name().to_owned(),
style_parser_children_pending: element.style_parser_children_pending(),
parser_blocking_eligible: if element.is_html_element("link") {
element.link_created_by_parser()
} else {
@@ -154,6 +156,7 @@ impl StylesheetElementRead {
is_html_element: true,
local_name: "link".to_owned(),
parser_blocking_eligible: true,
style_parser_children_pending: false,
rel: Some("stylesheet".to_owned()),
href: Some(href.to_owned()),
as_attr: None,
@@ -690,7 +693,10 @@ fn parser_created_style_import_urls(
) -> Option<Vec<Url>> {
let native_node_id = NativeNodeId::new(node_id.index());
let element = document.stylesheet_element(native_node_id)?;
if !element.is_html_element("style") || !element.parser_blocking_eligible {
if !element.is_html_element("style")
|| !element.parser_blocking_eligible
|| element.style_parser_children_pending
{
return None;
}
if !media_blocks_scripts(element.media.as_deref()) {
@@ -793,6 +799,16 @@ mod tests {
assert!(host.append_child(style, text));
assert!(host.append_child(host.document_handle(), style));
assert!(
document_owned_blocking_stylesheet_candidate_for_node(
&host,
moli_dom::NodeId::new(style.index()),
)
.is_none(),
"an open parser style must not start imports"
);
assert!(host.finish_parsing_style_children(style));
let candidate = document_owned_blocking_stylesheet_candidate_for_node(
&host,
moli_dom::NodeId::new(style.index()),