fix(dom): expose reflected token list attributes

This commit is contained in:
ldm0
2026-09-28 01:46:59 +08:00
parent b8be4d1154
commit ae4fe9ef2f
10 changed files with 303 additions and 61 deletions
@@ -88,11 +88,7 @@ impl NativeDomBridge {
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
) -> Option<v8::Local<'s, v8::Object>> {
self.wrap_bridge_handle(
scope,
runtime_ptr,
BridgeHandle::ClassList(handle, DomTokenListKind::Class),
)
self.wrap_dom_token_list(scope, runtime_ptr, handle, DomTokenListKind::Class)
}
pub(crate) fn wrap_part_list<'s, 'i>(
@@ -101,24 +97,17 @@ impl NativeDomBridge {
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
) -> Option<v8::Local<'s, v8::Object>> {
self.wrap_bridge_handle(
scope,
runtime_ptr,
BridgeHandle::ClassList(handle, DomTokenListKind::Part),
)
self.wrap_dom_token_list(scope, runtime_ptr, handle, DomTokenListKind::Part)
}
pub(crate) fn wrap_rel_list<'s, 'i>(
pub(in crate::native_bridge) fn wrap_dom_token_list<'s, 'i>(
&mut self,
scope: &mut v8::PinScope<'s, 'i>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
kind: DomTokenListKind,
) -> Option<v8::Local<'s, v8::Object>> {
self.wrap_bridge_handle(
scope,
runtime_ptr,
BridgeHandle::ClassList(handle, DomTokenListKind::Rel),
)
self.wrap_bridge_handle(scope, runtime_ptr, BridgeHandle::ClassList(handle, kind))
}
pub(crate) fn wrap_dataset<'s, 'i>(
+32 -7
View File
@@ -171,7 +171,9 @@ pub(crate) use canvas::{
pub(crate) use class_list::install_dom_token_list_prototype_bindings;
pub(super) use class_list::{
build_dom_token_list_wrapper_template, html_rel_list_getter_function,
html_rel_list_setter_function,
html_rel_list_setter_function, iframe_sandbox_getter_function, iframe_sandbox_setter_function,
link_sizes_getter_function, link_sizes_setter_function, output_html_for_getter_function,
output_html_for_setter_function, svg_rel_list_setter_function,
};
pub(super) use content::{
node_direct_text_content, node_get_html_callback, node_inner_html_getter_function,
@@ -461,10 +463,9 @@ pub(super) use global_attributes::{
node_hidden_setter_function, node_inert_getter_function, node_inert_setter_function,
node_input_mode_getter_function, node_input_mode_setter_function,
node_is_content_editable_getter_function, node_lang_getter_function, node_lang_setter_function,
node_sandbox_getter_function, node_sandbox_setter_function, node_spellcheck_getter_function,
node_spellcheck_setter_function, node_tab_index_getter_function,
node_tab_index_setter_function, node_title_getter_function, node_title_setter_function,
node_translate_getter_function, node_translate_setter_function,
node_spellcheck_getter_function, node_spellcheck_setter_function,
node_tab_index_getter_function, node_tab_index_setter_function, node_title_getter_function,
node_title_setter_function, node_translate_getter_function, node_translate_setter_function,
node_writing_suggestions_getter_function, node_writing_suggestions_setter_function,
null_to_empty_dom_string_reflection_getter_function,
null_to_empty_dom_string_reflection_setter_function, object_archive_getter_function,
@@ -4112,8 +4113,8 @@ struct HtmlIFrameElementPrototypeDeclaration {
referrer_policy: (),
#[webapi(
accessor_property,
getter = node_sandbox_getter_function,
setter = node_sandbox_setter_function
getter = iframe_sandbox_getter_function,
setter = iframe_sandbox_setter_function
)]
sandbox: (),
#[webapi(
@@ -4252,6 +4253,12 @@ struct HtmlLinkElementUrlPrototypeDeclaration {
setter_data = DomStringReflection::LinkHreflang
)]
hreflang: (),
#[webapi(
accessor_property,
getter = link_sizes_getter_function,
setter = link_sizes_setter_function
)]
sizes: (),
#[webapi(
accessor_property,
getter = html_charset_getter_function,
@@ -5086,6 +5093,12 @@ struct HtmlInputElementValuePrototypeDeclaration {
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::HTMLOutputElement, enumerable)]
struct HtmlOutputElementValuePrototypeDeclaration {
#[webapi(
accessor_property = "htmlFor",
getter = output_html_for_getter_function,
setter = output_html_for_setter_function
)]
html_for: (),
#[webapi(
accessor_property,
getter = output_default_value_getter_function,
@@ -5102,6 +5115,17 @@ struct HtmlOutputElementValuePrototypeDeclaration {
type_: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::SVGAElement, enumerable)]
struct SvgAElementRelListPrototypeDeclaration {
#[webapi(
accessor_property = "relList",
getter = html_rel_list_getter_function,
setter = svg_rel_list_setter_function
)]
rel_list: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::HTMLMeterElement, enumerable)]
struct HtmlMeterElementPrototypeDeclaration {
@@ -7374,6 +7398,7 @@ pub(crate) fn install_element_template_bindings<'s>(
"HTMLScriptElement" => install!(HtmlScriptElementPrototypeDeclaration),
"SVGScriptElement" => install!(SvgScriptElementPrototypeDeclaration),
"SVGImageElement" => install!(SvgImageElementPrototypeDeclaration),
"SVGAElement" => install!(SvgAElementRelListPrototypeDeclaration),
"HTMLStyleElement" => install!(HtmlStyleElementPrototypeDeclaration),
"SVGStyleElement" => install!(SvgStyleElementPrototypeDeclaration),
"HTMLTableElement" => install!(HtmlTableElementPrototypeDeclaration),
@@ -15,7 +15,9 @@ mod template;
mod tokens;
pub(in crate::native_bridge) use self::accessors::{
html_rel_list_getter_function, html_rel_list_setter_function,
html_rel_list_getter_function, html_rel_list_setter_function, iframe_sandbox_getter_function,
iframe_sandbox_setter_function, link_sizes_getter_function, link_sizes_setter_function,
output_html_for_getter_function, output_html_for_setter_function, svg_rel_list_setter_function,
};
pub(in crate::native_bridge) use self::template::build_dom_token_list_wrapper_template;
pub(crate) use self::template::install_dom_token_list_prototype_bindings;
@@ -1,9 +1,10 @@
use super::*;
pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>(
fn token_list_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
kind: DomTokenListKind,
) {
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
@@ -16,30 +17,41 @@ pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>(
return;
}
let runtime = unsafe { &mut *runtime_ptr };
match runtime
.native_bridge_mut()
.wrap_rel_list(scope, runtime_ptr, handle)
{
Some(rel_list) => rv.set(rel_list.into()),
let token_list =
runtime
.native_bridge_mut()
.wrap_dom_token_list(scope, runtime_ptr, handle, kind);
match token_list {
Some(token_list) => rv.set(token_list.into()),
None => rv.set_null(),
}
}
fn set_html_rel_list_for_receiver<'s>(
fn set_token_list_for_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
owner: &'static str,
property: &'static str,
attribute: &'static str,
) {
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, receiver)
else {
return;
};
let Some(value) = property_dom_string_value(scope, value, owner, "relList") else {
let Some(value) = property_dom_string_value(scope, value, owner, property) else {
return;
};
set_reflected_attribute(scope, runtime_ptr, handle, "rel", &value);
set_reflected_attribute(scope, runtime_ptr, handle, attribute, &value);
}
pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
token_list_getter(scope, args, rv, DomTokenListKind::Rel);
}
pub(in crate::native_bridge) fn html_rel_list_setter_function<'s>(
@@ -50,7 +62,102 @@ pub(in crate::native_bridge) fn html_rel_list_setter_function<'s>(
if let Some(interface) =
super::super::reflection::ElementReflectionInterface::from_callback_data(scope, args.data())
{
set_html_rel_list_for_receiver(scope, args.this(), args.get(0), interface.name());
set_token_list_for_receiver(
scope,
args.this(),
args.get(0),
interface.name(),
"relList",
"rel",
);
}
rv.set_undefined();
}
pub(in crate::native_bridge) fn svg_rel_list_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
set_token_list_for_receiver(
scope,
args.this(),
args.get(0),
"SVGAElement",
"relList",
"rel",
);
rv.set_undefined();
}
pub(in crate::native_bridge) fn output_html_for_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
token_list_getter(scope, args, rv, DomTokenListKind::HtmlFor);
}
pub(in crate::native_bridge) fn output_html_for_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
set_token_list_for_receiver(
scope,
args.this(),
args.get(0),
"HTMLOutputElement",
"htmlFor",
"for",
);
rv.set_undefined();
}
pub(in crate::native_bridge) fn iframe_sandbox_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
token_list_getter(scope, args, rv, DomTokenListKind::Sandbox);
}
pub(in crate::native_bridge) fn iframe_sandbox_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
set_token_list_for_receiver(
scope,
args.this(),
args.get(0),
"HTMLIFrameElement",
"sandbox",
"sandbox",
);
rv.set_undefined();
}
pub(in crate::native_bridge) fn link_sizes_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
token_list_getter(scope, args, rv, DomTokenListKind::Sizes);
}
pub(in crate::native_bridge) fn link_sizes_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
set_token_list_for_receiver(
scope,
args.this(),
args.get(0),
"HTMLLinkElement",
"sizes",
"sizes",
);
rv.set_undefined();
}
@@ -72,12 +72,34 @@ const LINK_REL_LIST_SUPPORTED_TOKENS: &[&str] = &[
const NAVIGATION_REL_LIST_SUPPORTED_TOKENS: &[&str] = &["noreferrer", "noopener", "opener"];
const SANDBOX_SUPPORTED_TOKENS: &[&str] = &[
"allow-downloads",
"allow-forms",
"allow-modals",
"allow-orientation-lock",
"allow-pointer-lock",
"allow-popups",
"allow-popups-to-escape-sandbox",
"allow-presentation",
"allow-same-origin",
"allow-scripts",
"allow-storage-access-by-user-activation",
"allow-top-navigation",
"allow-top-navigation-by-user-activation",
"allow-top-navigation-to-custom-protocols",
];
fn rel_list_supports_token(runtime: &JsContextHost, handle: DomHandle, token: &str) -> bool {
let supported_tokens = if runtime.dom_host().is_html_element_named(handle, "link") {
LINK_REL_LIST_SUPPORTED_TOKENS
} else if ["a", "area", "form"]
.into_iter()
.any(|name| runtime.dom_host().is_html_element_named(handle, name))
|| runtime
.dom_host()
.node(handle)
.and_then(|node| node.as_element())
.is_some_and(|element| element.is_svg_element("a"))
{
NAVIGATION_REL_LIST_SUPPORTED_TOKENS
} else {
@@ -88,6 +110,12 @@ fn rel_list_supports_token(runtime: &JsContextHost, handle: DomHandle, token: &s
.any(|supported| token.eq_ignore_ascii_case(supported))
}
fn sandbox_supports_token(token: &str) -> bool {
SANDBOX_SUPPORTED_TOKENS
.iter()
.any(|supported| token.eq_ignore_ascii_case(supported))
}
pub(super) fn class_list_item_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -314,11 +342,18 @@ pub(super) fn class_list_supports_callback<'s>(
return;
};
match kind {
DomTokenListKind::Class | DomTokenListKind::Part => {
DomTokenListKind::Class
| DomTokenListKind::Part
| DomTokenListKind::HtmlFor
| DomTokenListKind::Sizes => {
throw_type_error(scope, "DOMTokenList has no supported tokens.");
return;
}
DomTokenListKind::Rel => {}
DomTokenListKind::Sandbox => {
rv.set_bool(sandbox_supports_token(&parsed.token));
return;
}
}
rv.set_bool(rel_list_supports_token(
unsafe { &*runtime_ptr },
@@ -40,5 +40,8 @@ pub(super) fn token_list_attribute_name(kind: DomTokenListKind) -> &'static str
DomTokenListKind::Class => "class",
DomTokenListKind::Part => "part",
DomTokenListKind::Rel => "rel",
DomTokenListKind::HtmlFor => "for",
DomTokenListKind::Sandbox => "sandbox",
DomTokenListKind::Sizes => "sizes",
}
}
@@ -1030,30 +1030,6 @@ pub(in crate::native_bridge) fn html_color_getter_function<'s>(
attribute_property_getter_from_object_or_detached(scope, args.this(), "color", rv);
}
pub(in crate::native_bridge) fn node_sandbox_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
rv: v8::ReturnValue<'s, v8::Value>,
) {
attribute_property_getter_from_object_or_detached(scope, args.this(), "sandbox", rv);
}
pub(in crate::native_bridge) fn node_sandbox_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
set_dom_string_attribute_property_on_object(
scope,
args.this(),
"sandbox",
args.get(0),
"HTMLIFrameElement",
"sandbox",
);
rv.set_undefined();
}
fn set_dom_string_treat_null_as_empty_on_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -150,6 +150,9 @@ pub(super) enum DomTokenListKind {
Class,
Part,
Rel,
HtmlFor,
Sandbox,
Sizes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -1034,10 +1034,14 @@ fn detached_resource_template_accessors_use_owner_prototypes() {
}
assert(iframe.csp === "", "iframe csp default");
iframe.csp = 123456;
const sandbox = iframe.sandbox;
assert(Object.prototype.toString.call(sandbox) === "[object DOMTokenList]", "iframe sandbox type");
assert(sandbox === iframe.sandbox, "iframe sandbox SameObject");
iframe.sandbox = "allow-scripts";
iframe.allowFullscreen = true;
assert(iframe.csp === "123456" && iframe.getAttribute("csp") === "123456", "iframe csp");
assert(iframe.sandbox === "allow-scripts" && iframe.getAttribute("sandbox") === "allow-scripts", "iframe sandbox");
assert(sandbox.value === "allow-scripts" && iframe.getAttribute("sandbox") === "allow-scripts", "iframe sandbox");
assert(sandbox.supports("ALLOW-SCRIPTS"), "iframe sandbox supported token");
assert(iframe.allowFullscreen === true && iframe.getAttribute("allowfullscreen") === "", "iframe allowFullscreen");
for (const name of ["csp", "sandbox", "allowFullscreen"]) {
assert(!own(iframe, name), `iframe.${name} should stay inherited after set`);
@@ -1046,7 +1050,7 @@ fn detached_resource_template_accessors_use_owner_prototypes() {
}
iframe.setAttribute("csp", "default-src 'self'");
assert(iframe.csp === "default-src 'self'", "iframe csp after delete");
assert(iframe.sandbox === "allow-scripts", "iframe sandbox after delete");
assert(iframe.sandbox === sandbox && sandbox.value === "allow-scripts", "iframe sandbox after delete");
assert(iframe.allowFullscreen === true, "iframe allowFullscreen after delete");
}
@@ -2732,3 +2732,101 @@ fn element_heading_reflections_drive_flat_tree_heading_matching() {
r#"{"owner":[true,false,true,true],"initial":[0,false,true],"parentOffset":["3",true],"reset":[true,true],"clamped":["20",8,true],"slotted":true,"modalState":[false,true,true],"badGetter":"throw:TypeError","badSetter":"throw:TypeError"}"#
);
}
#[test]
fn reflected_dom_token_list_attributes_are_live_same_object_and_owner_scoped() {
let mut vm = new_storage_test_vm("https://reflected-token-lists.test/");
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const svg = "http://www.w3.org/2000/svg";
const detached = document.implementation.createHTMLDocument("");
const descriptors = [
[HTMLIFrameElement.prototype, "sandbox"],
[HTMLLinkElement.prototype, "sizes"],
[HTMLOutputElement.prototype, "htmlFor"],
[SVGAElement.prototype, "relList"]
];
for (const [prototype, name] of descriptors) {
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
assert(!!descriptor, `${prototype.constructor.name}.${name} descriptor`);
assert(typeof descriptor.get === "function", `${name} getter`);
assert(typeof descriptor.set === "function", `${name} PutForwards setter`);
assert(descriptor.enumerable && descriptor.configurable, `${name} descriptor flags`);
}
const noSupportedTokens = list => {
try {
list.supports("anything");
return false;
} catch (error) {
return error && error.name === "TypeError";
}
};
const exercise = ownerDocument => {
const iframe = ownerDocument.createElement("iframe");
const sandbox = iframe.sandbox;
assert(Object.prototype.toString.call(sandbox) === "[object DOMTokenList]", "sandbox type");
assert(sandbox === iframe.sandbox, "sandbox SameObject");
iframe.sandbox = "allow-scripts allow-forms allow-scripts";
assert(sandbox.length === 2 && sandbox.contains("allow-forms"), "sandbox tokens");
assert(iframe.getAttribute("sandbox") === "allow-scripts allow-forms allow-scripts", "sandbox PutForwards");
assert(sandbox.supports("ALLOW-SCRIPTS"), "sandbox supports ASCII case-insensitively");
assert(sandbox.supports("allow-storage-access-by-user-activation"), "sandbox storage-access token");
assert(!sandbox.supports("unknown"), "sandbox rejects unknown token");
sandbox.add("allow-popups");
assert(iframe.getAttribute("sandbox") === "allow-scripts allow-forms allow-popups", "sandbox mutation reflects");
const output = ownerDocument.createElement("output");
const htmlFor = output.htmlFor;
assert(Object.prototype.toString.call(htmlFor) === "[object DOMTokenList]", "htmlFor type");
assert(htmlFor === output.htmlFor, "htmlFor SameObject");
output.htmlFor = "first second first";
htmlFor.add("third");
assert(output.getAttribute("for") === "first second third", "htmlFor reflects for");
assert(noSupportedTokens(htmlFor), "htmlFor has no supported tokens");
const link = ownerDocument.createElement("link");
const sizes = link.sizes;
assert(Object.prototype.toString.call(sizes) === "[object DOMTokenList]", "sizes type");
assert(sizes === link.sizes, "sizes SameObject");
assert(sizes !== link.relList, "link token lists have distinct identity");
link.sizes = "16x16 32x32 16x16";
sizes.remove("16x16");
assert(link.getAttribute("sizes") === "32x32", "sizes mutation reflects");
assert(noSupportedTokens(sizes), "sizes has no supported tokens");
const anchor = ownerDocument.createElementNS(svg, "a");
const relList = anchor.relList;
assert(Object.prototype.toString.call(relList) === "[object DOMTokenList]", "SVG relList type");
assert(relList === anchor.relList, "SVG relList SameObject");
anchor.relList = "noopener noreferrer";
relList.add("opener");
assert(anchor.getAttribute("rel") === "noopener noreferrer opener", "SVG relList reflects rel");
assert(relList.supports("NOOPENER"), "SVG relList supported tokens");
};
exercise(document);
exercise(detached);
const div = document.createElement("div");
for (const name of ["htmlFor", "sandbox", "sizes", "relList"]) {
assert(div[name] === undefined, `div.${name} should be undefined`);
}
assert(document.createElementNS(svg, "link").sizes === undefined, "SVG link.sizes");
assert(document.createElementNS(svg, "output").htmlFor === undefined, "SVG output.htmlFor");
assert(document.createElementNS(svg, "iframe").sandbox === undefined, "SVG iframe.sandbox");
assert(document.createElement("svg").relList === undefined, "HTML svg.relList");
return "ok";
})()
"#,
)
.expect("reflected DOMTokenList attributes should evaluate");
assert_eq!(result, "ok");
}