fix(dom): align insertAdjacentHTML sibling contexts

This commit is contained in:
ldm0
2026-10-02 03:28:07 +08:00
parent 4f79a50468
commit e2db4592e0
3 changed files with 110 additions and 11 deletions
@@ -36,6 +36,7 @@ enum DocumentWriteParserPumpInput<'a> {
enum HtmlFragmentParserContextMode {
Standard,
RangeCreateContextualFragment,
SiblingInsertion,
}
struct DocumentWriteParserMutationOwner<'a, 'scope, 'pin> {
@@ -553,8 +554,11 @@ impl DocumentRuntime {
.and_then(Node::local_name)
.unwrap_or("body")
.to_owned();
if context_mode == HtmlFragmentParserContextMode::RangeCreateContextualFragment
&& context_namespace == "http://www.w3.org/1999/xhtml"
if matches!(
context_mode,
HtmlFragmentParserContextMode::RangeCreateContextualFragment
| HtmlFragmentParserContextMode::SiblingInsertion
) && context_namespace == "http://www.w3.org/1999/xhtml"
&& context_local_name.eq_ignore_ascii_case("html")
{
context_local_name = "body".to_owned();
@@ -951,7 +955,13 @@ impl DocumentRuntime {
html: &str,
insert: impl FnOnce(&mut Self, &mut v8::PinScope<'_, '_>, *mut JsContextHost, DomHandle) -> bool,
) -> bool {
let Some(fragment) = self.build_fragment_from_html(
let scripting_enabled = unsafe { &*host_ptr }.document_scripting_enabled(document_handle);
let context_mode = if context_handle == target {
HtmlFragmentParserContextMode::Standard
} else {
HtmlFragmentParserContextMode::SiblingInsertion
};
let Some(fragment) = self.build_fragment_from_html_with_context_mode(
scope,
host_ptr,
document_handle,
@@ -959,11 +969,13 @@ impl DocumentRuntime {
html,
true,
HtmlFragmentCustomElementUpgradeTiming::AfterInsertion,
context_mode,
scripting_enabled,
false,
) else {
return false;
};
let added_children = self.dom_host().child_handles(fragment).collect::<Vec<_>>();
let _ = target;
let changed = insert(self, scope, host_ptr, fragment);
if changed
&& !self.upgrade_inserted_html_fragment_custom_elements(
@@ -297,15 +297,26 @@ pub(in crate::native_bridge) fn node_insert_adjacent_html_callback<'s>(
position,
InsertAdjacentPosition::BeforeBegin | InsertAdjacentPosition::AfterEnd
);
if needs_parent
&& unsafe { &*runtime_ptr }
if needs_parent {
let parent = unsafe { &*runtime_ptr }
.dom_host()
.node(target)
.and_then(Node::parent_node)
.is_none()
{
rv.set_undefined();
return;
.and_then(Node::parent_node);
let has_no_modifiable_parent = parent.is_none_or(|parent| {
unsafe { &*runtime_ptr }
.dom_host()
.node(parent)
.is_none_or(Node::is_document)
});
if has_no_modifiable_parent {
throw_dom_exception(
scope,
"NoModificationAllowedError",
7,
"The element has no modifiable parent for sibling insertion.",
);
return;
}
}
let Some(document_handle) =
insert_adjacent_document_handle(unsafe { &*runtime_ptr }, target, position)
@@ -419,6 +419,82 @@ fn element_insert_adjacent_methods_parse_webidl_arguments() {
);
}
#[test]
fn insert_adjacent_html_enforces_sibling_context_rules() {
let mut vm = new_storage_test_vm("https://insert-adjacent-sibling-context.test/");
let result = vm
.eval(
r#"
(() => {
const probe = callback => {
try {
callback();
return 'missing';
} catch (error) {
return `${error.name}:${error.code}`;
}
};
const sources = ['', 'text', '<!--comment-->', '<div></div>'];
const positions = ['beforebegin', 'afterend'];
const detached = document.createElement('div');
const root = document.documentElement ||
document.appendChild(document.createElement('html'));
const detachedErrors = positions.flatMap(position =>
sources.map(source => probe(() => detached.insertAdjacentHTML(position, source)))
);
const documentErrors = positions.flatMap(position =>
sources.map(source => probe(() => root.insertAdjacentHTML(position, source)))
);
while (root.firstChild) {
root.removeChild(root.firstChild);
}
root.insertAdjacentHTML(
'afterbegin',
'<head id="inside-head"></head><body id="inside-body"></body>'
);
const preservedInnerHtmlContext =
document.head?.id === 'inside-head' &&
document.body?.id === 'inside-body' &&
root.firstChild === document.head &&
root.lastChild === document.body;
const head = document.head ||
root.insertBefore(document.createElement('head'), root.firstChild);
const body = document.body || root.appendChild(document.createElement('body'));
head.insertAdjacentHTML('beforebegin', '<p id="before-head"></p>');
body.insertAdjacentHTML('afterend', '<p id="after-body"></p>');
const beforeHead = document.getElementById('before-head');
const afterBody = document.getElementById('after-body');
return JSON.stringify({
detachedErrors,
documentErrors,
preservedInnerHtmlContext,
counts: [
document.getElementsByTagName('html').length,
document.getElementsByTagName('head').length,
document.getElementsByTagName('body').length
],
placement: [
beforeHead.nextSibling === head,
body.nextSibling === afterBody,
beforeHead.parentNode === root,
afterBody.parentNode === root
]
});
})()
"#,
)
.expect("insertAdjacentHTML sibling context rules should evaluate");
assert_eq!(
result,
r#"{"detachedErrors":["NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7"],"documentErrors":["NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7","NoModificationAllowedError:7"],"preservedInnerHtmlContext":true,"counts":[1,1,1],"placement":[true,true,true,true]}"#
);
}
#[test]
fn document_fragment_and_shadow_root_get_element_by_id_match_browser_lookup_boundaries() {
let mut vm = new_storage_test_vm("https://fragment-shadow-get-by-id.test/");