From 130c968f87b19c643b362040273f346cf84f0fdb Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 5 Oct 2026 17:51:59 +0800 Subject: [PATCH] fix(parser): preserve fragment context form submission state --- moli-parser/src/live_target.rs | 39 ++++++++++++++++++++++++++++++++++ moli-parser/src/session.rs | 15 ++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/moli-parser/src/live_target.rs b/moli-parser/src/live_target.rs index df57ad594d..d291962c5a 100644 --- a/moli-parser/src/live_target.rs +++ b/moli-parser/src/live_target.rs @@ -3970,6 +3970,45 @@ fn parser_stream_marks_only_eof_unclosed_form_controls_as_submission_blocking() } } +#[test] +fn live_fragment_parser_preserves_the_contexts_submission_blocking_state() { + for (tag_name, html) in [("select", ""), ("textarea", "value")] { + for blocked in [false, true] { + let url = Url::parse("https://fragment.test/").expect("test url"); + let mut dom_host = DomHost::from_dom(NativeDom::new_html(url.clone())); + let document = dom_host.document_handle(); + let context = dom_host.create_element(tag_name); + dom_host.set_blocks_form_submission(context, blocked); + let fragment = dom_host.create_document_fragment(); + let mut effects = DomMutationEffects::default(); + let mut collector = TestMutationEffectCollector { + host: &mut dom_host, + effects: &mut effects, + panic_on_mutation: false, + }; + crate::HtmlParser::with_scripting_enabled(true).parse_fragment_into_live_dom( + url, + fragment, + document, + context, + "http://www.w3.org/1999/xhtml", + tag_name, + html, + &mut collector, + false, + ); + assert_eq!( + dom_host + .node(context) + .and_then(Node::as_element) + .is_some_and(|element| element.blocks_form_submission()), + blocked, + "fragment parsing must preserve {tag_name}'s pre-existing EOF state" + ); + } + } +} + #[test] fn parser_stream_async_prefetch_uses_shared_script_type_classification() { let html = concat!( diff --git a/moli-parser/src/session.rs b/moli-parser/src/session.rs index 5bb637492e..ece58eeb58 100644 --- a/moli-parser/src/session.rs +++ b/moli-parser/src/session.rs @@ -47,6 +47,7 @@ enum TokenizerPause { struct EmbedderPausingTreeBuilder { inner: TreeBuilder, + fragment_context: Option, // Retain a callback's Break until html5ever accepts a pause result. This is // an undelivered notification, not a copy of the owner's current run state; // it must survive both later Continue callbacks and input chunk boundaries. @@ -56,6 +57,7 @@ struct EmbedderPausingTreeBuilder { #[derive(Default)] struct OpenFormControlTracer { handles: RefCell>, + fragment_context: Option, } impl Tracer for OpenFormControlTracer { @@ -72,6 +74,11 @@ impl Tracer for OpenFormControlTracer { let Some(node_id) = node.dom_node_id() else { return; }; + // html5ever also traces the fragment context for GC. It is an existing + // element, not an unclosed control created by this parser. + if Some(node_id) == self.fragment_context { + return; + } let mut handles = self.handles.borrow_mut(); if !handles.contains(&node_id) { handles.push(node_id); @@ -83,6 +90,7 @@ impl EmbedderPausingTreeBuilder { fn new(sink: DocumentSink, opts: TreeBuilderOpts) -> Self { Self { inner: TreeBuilder::new(sink, opts), + fragment_context: None, deferred_owner_interruption: Cell::new(false), } } @@ -92,8 +100,10 @@ impl EmbedderPausingTreeBuilder { context_handle: ParseHandle, opts: TreeBuilderOpts, ) -> Self { + let fragment_context = context_handle.dom_node_id(); Self { inner: TreeBuilder::new_for_fragment(sink, context_handle, None, opts), + fragment_context, deferred_owner_interruption: Cell::new(false), } } @@ -103,7 +113,10 @@ impl EmbedderPausingTreeBuilder { } fn begin_tree_builder_finish(&self) { - let tracer = OpenFormControlTracer::default(); + let tracer = OpenFormControlTracer { + fragment_context: self.fragment_context, + ..OpenFormControlTracer::default() + }; self.inner.trace_handles(&tracer); self.inner .sink