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