From dfd3fe7569a3d490b717cf7f6ee47432f7943ff6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 17 Jul 2026 06:09:50 +0800 Subject: [PATCH] fix(domparser): preserve HTML parse modes --- .../wpt-cross-current/failed-cases.txt | 1 - .../wpt-cross-current/passed-cases.txt | 1 + moli-dom/src/native/host/document.rs | 19 ++++++++++ moli-parser/src/stream.rs | 26 ++++++------- .../detached_objects/builders/documents.rs | 28 +++++++++++--- .../detached_surface/document_state.rs | 8 +++- .../script_vm/tests/dom_elements/detached.rs | 38 +++++++++++++++++++ 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index afd61a596..b2999fe27 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2549,7 +2549,6 @@ dom/ranges/tentative/OpaqueRange-range-updates.html dom/ranges/tentative/OpaqueRange-supported-elements.html dom/ranges/tentative/OpaqueRange-unsupported-elements.html dom/ranges/tentative/OpaqueRange-validation.html -domparsing/DOMParser-parseFromString-html.html domparsing/DOMParser-parseFromString-url-base-pushstate.html domparsing/DOMParser-parseFromString-url-base.html domparsing/DOMParser-parseFromString-url-moretests.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 98c31ab1a..06982ce09 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -4103,6 +4103,7 @@ dom/traversal/TreeWalker.html dom/window-extends-event-target.html dom/xpath-result-single-node-value-nullable.html domparsing/DOMParser-parseFromString-encoding.html +domparsing/DOMParser-parseFromString-html.html domparsing/DOMParser-parseFromString-stylesheets.html domparsing/DOMParser-parseFromString-xml-doctype.html domparsing/DOMParser-parseFromString-xml-parsererror.html diff --git a/moli-dom/src/native/host/document.rs b/moli-dom/src/native/host/document.rs index 1d251c3a9..a0adc6f9c 100644 --- a/moli-dom/src/native/host/document.rs +++ b/moli-dom/src/native/host/document.rs @@ -1134,6 +1134,25 @@ impl DomHost { true } + pub fn set_document_quirks_mode_for_handle( + &mut self, + document_handle: DomHandle, + quirks_mode: selectors::matching::QuirksMode, + ) -> bool { + let Some(document) = self + .node_mut(document_handle) + .and_then(|node| node.data_mut().as_document_mut()) + else { + return false; + }; + if document.quirks_mode() == quirks_mode { + return false; + } + document.set_quirks_mode(quirks_mode); + self.record_mutation(MutationScope::QueryState); + true + } + pub fn set_document_default_language_for_handle( &mut self, document_handle: DomHandle, diff --git a/moli-parser/src/stream.rs b/moli-parser/src/stream.rs index a8a3937a8..23a27b665 100644 --- a/moli-parser/src/stream.rs +++ b/moli-parser/src/stream.rs @@ -29,8 +29,8 @@ use super::{ }, live_target::{ParserRuntimeDomSinks, ParserStreamHtmlTreeSinkTarget}, session::{ - HtmlParserSession, HtmlParserSessionResult, new_fragment_html_tree_sink_session, - new_html_tree_sink_session, + HtmlParserSession, HtmlParserSessionResult, HtmlTreeSinkSession, + new_fragment_html_tree_sink_session, new_html_tree_sink_session, }, }; @@ -429,11 +429,7 @@ pub(crate) fn prepare_parser_script_handoff_for_static_document( } impl HtmlTreeSinkStream { - pub(super) fn from_target_with_scripting( - target: ParserStreamHtmlTreeSinkTarget, - scripting_enabled: bool, - ) -> Self { - let session = new_html_tree_sink_session(target, scripting_enabled); + fn from_session(session: HtmlTreeSinkSession) -> Self { Self { parser: session.parser, script_input: session.script_input, @@ -442,6 +438,13 @@ impl HtmlTreeSinkStream { } } + pub(super) fn from_target_with_scripting( + target: ParserStreamHtmlTreeSinkTarget, + scripting_enabled: bool, + ) -> Self { + Self::from_session(new_html_tree_sink_session(target, scripting_enabled)) + } + pub(super) fn from_fragment_target( target: ParserStreamHtmlTreeSinkTarget, context_handle: NativeNodeId, @@ -456,12 +459,7 @@ impl HtmlTreeSinkStream { context_local_name, scripting_enabled, ); - Self { - parser: session.parser, - script_input: session.script_input, - parser_script_positions: RefCell::default(), - next_parser_script_position: Cell::new(0), - } + Self::from_session(session) } fn parser_script_position(&self, node_id: NativeNodeId) -> usize { @@ -1721,7 +1719,7 @@ mod tests { #[test] fn parser_script_handoff_only_exposes_nonceable_nonces() { fn handoff_nonce(markup: &str) -> Option { - let mut stream = DocumentStream::new_parser_stream_for_testing( + let mut stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing( Url::parse("https://example.test/page.html").expect("test url"), ); let outcome = stream.pump_parser_step(markup); diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs index a52958314..e4f6651d0 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs @@ -201,16 +201,34 @@ pub(crate) fn build_detached_document_object_from_dom_host_with_content_type<'s> character_set: Option<&str>, ) -> Option> { let url = detached_document_url(&parsed); + let quirks_mode = parsed.dom().document()?.quirks_mode(); + let compat_mode = if quirks_mode == selectors::matching::QuirksMode::Quirks { + "BackCompat" + } else { + "CSS1Compat" + }; let content_type = content_type.unwrap_or(parsed.dom().document()?.content_type()); let document = new_detached_document_shell(scope, kind, content_type, url)?; - if let Some(state) = detached_state_object(scope, document) - && let Some(character_set) = character_set - { + if let Some(state) = detached_state_object(scope, document) { let _ = state.set( scope, - v8str(scope, "characterSet").into(), - v8_string(scope, character_set)?.into(), + v8str(scope, "compatMode").into(), + v8_string(scope, compat_mode)?.into(), ); + if let Some(character_set) = character_set { + let _ = state.set( + scope, + v8str(scope, "characterSet").into(), + v8_string(scope, character_set)?.into(), + ); + } + } + if let Some(document_handle) = detached_native_handle(scope, document) + && let Some(runtime_ptr) = context_host_ptr_from_global_bridge(scope) + { + let _ = unsafe { &mut *runtime_ptr } + .dom_host_mut() + .set_document_quirks_mode_for_handle(document_handle, quirks_mode); } import_detached_document_children_from_host(scope, document, &parsed)?; Some(document) diff --git a/moli-renderer-v8/src/native_bridge/document/detached_surface/document_state.rs b/moli-renderer-v8/src/native_bridge/document/detached_surface/document_state.rs index 7b02b5d1b..b1df185e3 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_surface/document_state.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_surface/document_state.rs @@ -267,10 +267,14 @@ pub(in crate::native_bridge) fn bridge_detached_document_character_set_callback< pub(in crate::native_bridge) fn bridge_detached_document_compat_mode_callback<'a>( scope: &mut v8::PinScope<'a, '_>, - _args: v8::FunctionCallbackArguments<'a>, + args: v8::FunctionCallbackArguments<'a>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - set_string_return_value(scope, &mut rv, "CSS1Compat"); + let compat_mode = v8::Local::::try_from(args.get(0)) + .ok() + .map(|document| detached_document_state_string(scope, document, "compatMode", "CSS1Compat")) + .unwrap_or_else(|| "CSS1Compat".to_owned()); + set_string_return_value(scope, &mut rv, &compat_mode); } pub(in crate::native_bridge) fn bridge_detached_document_referrer_callback<'a>( diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs index 486d1d601..5243b78e9 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs @@ -272,6 +272,44 @@ fn adopted_xml_cdata_uses_html_fragment_serialization() { ); } +#[test] +fn domparser_html_preserves_quirks_mode_and_parses_with_scripting_disabled() { + let mut vm = new_storage_test_vm("https://domparser-html-mode.test/"); + + let result = vm + .eval( + r#" +(() => { + const parser = new DOMParser(); + const quirks = parser.parseFromString( + "", + "text/html" + ); + const standards = parser.parseFromString( + "", + "text/html" + ); + const noscript = parser.parseFromString( + "", + "text/html" + ); + return JSON.stringify({ + quirks: quirks.compatMode, + standards: standards.compatMode, + noscriptChildren: Array.from(noscript.querySelector("noscript").children) + .map(element => element.id) + }); +})() +"#, + ) + .expect("DOMParser HTML parse mode probe should evaluate"); + + assert_eq!( + result, + r#"{"quirks":"BackCompat","standards":"CSS1Compat","noscriptChildren":["first","second"]}"# + ); +} + #[test] fn detached_query_brand_checks_accept_standard_prototype_methods() { let mut vm = new_storage_test_vm("https://detached-query-brand-check.test/");