From bc2002c419f6d83603548fe58bcf0ddee347fc76 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 31 Aug 2026 01:31:26 +0800 Subject: [PATCH] fix(svg): reflect conditional processing string lists --- .../src/context_bootstrap/runtime_state.rs | 1 + .../src/context_bootstrap/specs/registry.rs | 4 + .../context_bootstrap/svg_runtime/bindings.rs | 81 ++++ .../context_bootstrap/svg_runtime/builders.rs | 183 ++++++++ .../svg_runtime/callbacks.rs | 440 ++++++++++++++++++ .../src/context_bootstrap/svg_runtime/mod.rs | 14 + .../tests/dom_elements/live_document.rs | 188 ++++++++ moli-renderer-v8/src/web_api_interfaces.rs | 1 + 8 files changed, 912 insertions(+) diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 06c3e89f76..decaae8ef1 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -1734,6 +1734,7 @@ pub(crate) fn finish_context_bootstrap( ("SVGAnimatedLengthList", "SVGAnimatedLengthList"), ("SVGAnimatedNumber", "SVGAnimatedNumber"), ("SVGNumberList", "SVGNumberList"), + ("SVGStringList", "SVGStringList"), ("SVGAnimatedNumberList", "SVGAnimatedNumberList"), ("SVGAnimatedBoolean", "SVGAnimatedBoolean"), ("SVGAnimatedEnumeration", "SVGAnimatedEnumeration"), diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index fe83e27f2d..1308232779 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -120,6 +120,10 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[ interface: web_api_interfaces::SVGNumberList::DESCRIPTOR, kind: ConstructorKind::Illegal, }, + ConstructorSpec { + interface: web_api_interfaces::SVGStringList::DESCRIPTOR, + kind: ConstructorKind::Illegal, + }, ConstructorSpec { interface: web_api_interfaces::SVGAnimatedNumberList::DESCRIPTOR, kind: ConstructorKind::Illegal, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs index f874697f0a..3e2253ef45 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -148,6 +148,51 @@ struct SvgNumberListTemplateMethodsDeclaration { append_item: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGStringList, enumerable)] +struct SvgStringListTemplateMethodsDeclaration { + #[webapi(method = "clear", length = 0, callback = svg_string_list_clear_callback)] + clear: (), + + #[webapi( + method = "initialize", + length = 1, + callback = svg_string_list_initialize_callback + )] + initialize: (), + + #[webapi(method = "getItem", length = 1, callback = svg_string_list_get_item_callback)] + get_item: (), + + #[webapi( + method = "insertItemBefore", + length = 2, + callback = svg_string_list_insert_item_before_callback + )] + insert_item_before: (), + + #[webapi( + method = "replaceItem", + length = 2, + callback = svg_string_list_replace_item_callback + )] + replace_item: (), + + #[webapi( + method = "removeItem", + length = 1, + callback = svg_string_list_remove_item_callback + )] + remove_item: (), + + #[webapi( + method = "appendItem", + length = 1, + callback = svg_string_list_append_item_callback + )] + append_item: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGTransformList, enumerable)] struct SvgTransformListTemplateMethodsDeclaration { @@ -721,6 +766,19 @@ struct SvgNumberListTemplateAccessorsDeclaration { number_of_items: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGStringList, enumerable)] +struct SvgStringListTemplateAccessorsDeclaration { + #[webapi(accessor_property = "length", getter = svg_string_list_length_getter)] + length: (), + + #[webapi( + accessor_property = "numberOfItems", + getter = svg_string_list_length_getter + )] + number_of_items: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGAnimatedTransformList, enumerable)] struct SvgAnimatedTransformListTemplateAccessorsDeclaration { @@ -844,6 +902,20 @@ struct SvgElementPrototypeAccessorsDeclaration { struct SvgGraphicsElementPrototypeAccessorsDeclaration { #[webapi(accessor_property = "transform", getter = svg_graphics_transform_getter)] transform: (), + + #[webapi( + accessor_property = "requiredExtensions", + getter = svg_graphics_test_string_list_getter, + data = callback_data_index_value(scope, 0) + )] + required_extensions: (), + + #[webapi( + accessor_property = "systemLanguage", + getter = svg_graphics_test_string_list_getter, + data = callback_data_index_value(scope, 1) + )] + system_language: (), } #[derive(WebApiFunctionTemplate)] @@ -1171,6 +1243,15 @@ pub(super) fn install_svg_number_list_bindings<'s>( install_svg_value_list_bindings(scope, template, SvgListKind::Number); } +pub(super) fn install_svg_string_list_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + template: v8::Local<'s, v8::FunctionTemplate>, +) { + let proto = template.prototype_template(scope); + SvgStringListTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto); + SvgStringListTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); +} + pub(super) fn install_svg_animated_enumeration_bindings<'s>( scope: &mut v8::PinScope<'s, '_, ()>, template: v8::Local<'s, v8::FunctionTemplate>, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs index b2decb1383..611ce04829 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/builders.rs @@ -215,6 +215,13 @@ struct SvgNumberListObjectDeclaration<'scope> { append_item: (), } +#[derive(WebApiObject)] +#[webapi(interface = web_api_interfaces::SVGStringList, own_to_string_tag = "SVGStringList")] +struct SvgStringListObjectDeclaration<'scope> { + #[webapi(slot = SVG_STRING_LIST_ITEMS_SLOT)] + items: Vec>, +} + #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::SVGTransformList, own_to_string_tag = "SVGTransformList")] struct SvgTransformListObjectDeclaration<'scope> { @@ -668,6 +675,182 @@ pub(super) fn build_svg_number_list<'s>( .expect("SVGNumberList declaration should bind") } +pub(super) fn build_svg_string_list_for_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, + attribute: &str, +) -> v8::Local<'s, v8::Object> { + let template = v8::ObjectTemplate::new(scope); + configure_svg_string_list_indexed_property_handler(template); + let object = template + .new_instance(scope) + .expect("SVGStringList object template should instantiate"); + SvgStringListObjectDeclaration::new(Vec::new()) + .bind_into(scope, object) + .expect("SVGStringList declaration should bind"); + set_private_value( + scope, + object, + SVG_STRING_LIST_OWNER_ELEMENT_SLOT, + owner.into(), + ); + set_private_value( + scope, + object, + SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT, + v8_string(scope, attribute) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + sync_svg_string_list_from_owner_attribute(scope, object); + object +} + +pub(super) fn svg_string_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, list, SVG_STRING_LIST_ITEMS_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) +} + +pub(super) fn set_svg_string_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + items: v8::Local<'s, v8::Array>, +) { + set_private_value(scope, list, SVG_STRING_LIST_ITEMS_SLOT, items.into()); +} + +pub(super) fn sync_svg_string_list_from_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) { + let Some((owner, attribute)) = svg_string_list_owner_attribute(scope, list) else { + return; + }; + let raw = svg_owner_attribute_value(scope, owner, &attribute); + let synced_value = get_private_value(scope, list, SVG_STRING_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT) + .and_then(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)); + let synced_present = + get_private_value(scope, list, SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT) + .map(|value| value.is_true()); + if synced_present == Some(raw.is_some()) + && synced_value.as_deref() == Some(raw.as_deref().unwrap_or_default()) + { + return; + } + + let values = parse_svg_string_list_attribute(&attribute, raw.as_deref()); + let length = i32::try_from(values.len()).unwrap_or(i32::MAX); + let items = v8::Array::new(scope, length); + for (index, value) in values.into_iter().enumerate() { + let Ok(index) = u32::try_from(index) else { + break; + }; + if let Some(value) = v8_string(scope, &value) { + let _ = items.set_index(scope, index, value.into()); + } + } + set_svg_string_list_items(scope, list, items); + set_svg_string_list_synced_attribute(scope, list, raw.as_deref()); +} + +pub(super) fn reflect_svg_string_list_to_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) { + let Some((owner, attribute)) = svg_string_list_owner_attribute(scope, list) else { + return; + }; + let value = serialize_svg_string_list(scope, list, &attribute); + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object(scope, owner) + else { + return; + }; + let runtime = unsafe { &mut *runtime_ptr }; + let _ = runtime.set_attribute(scope, runtime_ptr, handle, &attribute, &value); + set_svg_string_list_synced_attribute(scope, list, Some(&value)); +} + +fn svg_string_list_owner_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option<(v8::Local<'s, v8::Object>, String)> { + let owner = get_private_value(scope, list, SVG_STRING_LIST_OWNER_ELEMENT_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + let attribute = get_private_value(scope, list, SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT)? + .to_string(scope)? + .to_rust_string_lossy(scope); + (!attribute.is_empty()).then_some((owner, attribute)) +} + +fn set_svg_string_list_synced_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + raw: Option<&str>, +) { + set_private_value( + scope, + list, + SVG_STRING_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT, + v8_string(scope, raw.unwrap_or_default()) + .unwrap_or_else(|| v8str(scope, "")) + .into(), + ); + set_private_value( + scope, + list, + SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT, + v8::Boolean::new(scope, raw.is_some()).into(), + ); +} + +fn parse_svg_string_list_attribute(attribute: &str, raw: Option<&str>) -> Vec { + let Some(raw) = raw else { + return Vec::new(); + }; + if attribute == "systemLanguage" { + return raw + .split(',') + .map(|token| token.trim_matches(is_html_ascii_whitespace).to_owned()) + .collect(); + } + raw.split(is_html_ascii_whitespace) + .filter(|token| !token.is_empty()) + .map(str::to_owned) + .collect() +} + +fn serialize_svg_string_list<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + attribute: &str, +) -> String { + let Some(items) = svg_string_list_items(scope, list) else { + return String::new(); + }; + let values = (0..items.length()) + .filter_map(|index| items.get_index(scope, index)) + .filter_map(|value| value.to_string(scope)) + .map(|value| value.to_rust_string_lossy(scope)) + .collect::>(); + values.join(if attribute == "systemLanguage" { + "," + } else { + " " + }) +} + +fn is_html_ascii_whitespace(character: char) -> bool { + matches!( + character, + '\u{0009}' | '\u{000A}' | '\u{000C}' | '\u{000D}' | '\u{0020}' + ) +} + pub(super) fn build_svg_transform_list<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs index 27d0a89f4c..ace2b58e05 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -2,6 +2,7 @@ use super::builders::*; use super::*; use crate::util::serialize_v8_array; use crate::web_api_interfaces; +use moli_webapi_declare::DataPropertyDescriptorDeclaration; const SVG_RECT_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["x", "y", "width", "height", "rx", "ry"]; const SVG_CIRCLE_ANIMATED_LENGTH_ATTRIBUTES: &[&str] = &["cx", "cy", "r"]; @@ -18,6 +19,21 @@ const SVG_ANIMATED_ACCESSOR_NAMES: &[&str] = &["baseVal", "animVal"]; const SVG_TRANSFORM_ACCESSOR_NAMES: &[&str] = &["type", "matrix", "angle"]; const SVG_MATRIX_ACCESSOR_NAMES: &[&str] = &["a", "b", "c", "d", "e", "f"]; +pub(super) fn configure_svg_string_list_indexed_property_handler( + template: v8::Local<'_, v8::ObjectTemplate>, +) { + template.set_indexed_property_handler( + v8::IndexedPropertyHandlerConfiguration::new() + .getter(svg_string_list_indexed_getter) + .setter(svg_string_list_indexed_setter) + .query(svg_string_list_indexed_query) + .deleter(svg_string_list_indexed_deleter) + .enumerator(svg_string_list_indexed_enumerator) + .definer(svg_string_list_indexed_definer) + .descriptor(svg_string_list_indexed_descriptor), + ); +} + fn svg_dom_matrix_2d_init_arg<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, @@ -380,6 +396,82 @@ pub(super) fn svg_graphics_transform_getter<'s>( svg_transform_attribute_getter(scope, args, rv, SVG_GRAPHICS_TRANSFORM_SLOT, "transform"); } +pub(super) fn svg_graphics_test_string_list_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some((attribute, slot)) = callback_data_item( + scope, + &args, + SVG_TEST_STRING_LIST_ATTRIBUTES, + "SVGTests string list attributes", + ) else { + rv.set_undefined(); + return; + }; + let receiver = args.this(); + if !require_svg_graphics_element_receiver(scope, receiver, attribute) { + return; + } + if let Some(value) = get_private_value(scope, receiver, slot) { + if let Ok(list) = v8::Local::::try_from(value) { + sync_svg_string_list_from_owner_attribute(scope, list); + } + rv.set(value); + return; + } + let list = build_svg_string_list_for_attribute(scope, receiver, attribute); + set_private_value(scope, receiver, slot, list.into()); + rv.set(list.into()); +} + +fn require_svg_graphics_element_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + member: &str, +) -> bool { + let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, receiver) + else { + webidl::throw_type_error( + scope, + &format!("SVGGraphicsElement.{member} called on incompatible receiver."), + ); + return false; + }; + let runtime = unsafe { &*runtime_ptr }; + let Some(mut interface_name) = runtime + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .map(|element| element.wrapper_prototype_name()) + else { + webidl::throw_type_error( + scope, + &format!("SVGGraphicsElement.{member} called on incompatible receiver."), + ); + return false; + }; + loop { + if interface_name == "SVGGraphicsElement" { + return true; + } + let Some(parent) = + crate::context_bootstrap::bridge_descriptor::node_bridge_descriptor(interface_name) + .and_then(|descriptor| descriptor.parent_constructor) + else { + break; + }; + interface_name = parent; + } + webidl::throw_type_error( + scope, + &format!("SVGGraphicsElement.{member} called on incompatible receiver."), + ); + false +} + pub(super) fn svg_pattern_transform_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -1105,6 +1197,354 @@ pub(super) fn svg_animated_transform_list_getter<'s>( ); } +pub(super) fn svg_string_list_length_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "length getter") else { + return; + }; + rv.set_uint32(items.length()); +} + +pub(super) fn svg_string_list_clear_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_string_list_items(scope, args.this(), "clear").is_none() { + return; + } + set_svg_string_list_items(scope, args.this(), v8::Array::new(scope, 0)); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set_undefined(); +} + +pub(super) fn svg_string_list_initialize_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if require_svg_string_list_items(scope, args.this(), "initialize").is_none() { + return; + } + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_string_list_dom_string(scope, parsed.item, "SVGStringList.initialize", 1) + else { + return; + }; + let items = v8::Array::new(scope, 1); + let _ = items.set_index(scope, 0, item); + set_svg_string_list_items(scope, args.this(), items); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +pub(super) fn svg_string_list_get_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "getItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_list_item_or_throw(scope, items, parsed.index) else { + return; + }; + rv.set(item); +} + +pub(super) fn svg_string_list_insert_item_before_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "insertItemBefore") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = + svg_string_list_dom_string(scope, parsed.item, "SVGStringList.insertItemBefore", 1) + else { + return; + }; + let length = items.length(); + let index = parsed.index.min(length); + let array_length = i32::try_from(length.saturating_add(1)).unwrap_or(i32::MAX); + let next = v8::Array::new(scope, array_length); + for old_index in 0..length { + let new_index = if old_index < index { + old_index + } else { + old_index + 1 + }; + if let Some(value) = items.get_index(scope, old_index) { + let _ = next.set_index(scope, new_index, value); + } + } + let _ = next.set_index(scope, index, item); + set_svg_string_list_items(scope, args.this(), next); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +pub(super) fn svg_string_list_replace_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "replaceItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_string_list_dom_string(scope, parsed.item, "SVGStringList.replaceItem", 1) + else { + return; + }; + if parsed.index >= items.length() { + webidl::throw_index_size_error(scope); + return; + } + let _ = items.set_index(scope, parsed.index, item); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +pub(super) fn svg_string_list_remove_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "removeItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let length = items.length(); + let Some(removed) = svg_list_item_or_throw(scope, items, parsed.index) else { + return; + }; + let next_length = i32::try_from(length.saturating_sub(1)).unwrap_or(i32::MAX); + let next = v8::Array::new(scope, next_length); + for old_index in 0..length { + if old_index == parsed.index { + continue; + } + let new_index = if old_index < parsed.index { + old_index + } else { + old_index - 1 + }; + if let Some(value) = items.get_index(scope, old_index) { + let _ = next.set_index(scope, new_index, value); + } + } + set_svg_string_list_items(scope, args.this(), next); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(removed); +} + +pub(super) fn svg_string_list_append_item_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(items) = require_svg_string_list_items(scope, args.this(), "appendItem") else { + return; + }; + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Some(item) = svg_string_list_dom_string(scope, parsed.item, "SVGStringList.appendItem", 1) + else { + return; + }; + let _ = items.set_index(scope, items.length(), item); + reflect_svg_string_list_to_owner_attribute(scope, args.this()); + rv.set(item); +} + +fn require_svg_string_list_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, + member: &str, +) -> Option> { + if svg_string_list_items(scope, list).is_none() { + webidl::throw_type_error( + scope, + &format!("SVGStringList.{member} called on incompatible receiver."), + ); + return None; + } + sync_svg_string_list_from_owner_attribute(scope, list); + svg_string_list_items(scope, list) +} + +fn svg_string_list_dom_string<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + operation: &'static str, + argument_index: usize, +) -> Option> { + let value = match webidl::convert::( + scope, + value, + webidl::Context::argument(operation, argument_index), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return None; + } + }; + v8_string(scope, &value).map(Into::into) +} + +fn svg_string_list_intercepted_items<'s>( + scope: &mut v8::PinScope<'s, '_>, + list: v8::Local<'s, v8::Object>, +) -> Option> { + svg_string_list_items(scope, list)?; + sync_svg_string_list_from_owner_attribute(scope, list); + svg_string_list_items(scope, list) +} + +fn svg_string_list_indexed_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_string_list_intercepted_items(scope, args.holder()) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + rv.set(value); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_setter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + value: v8::Local<'s, v8::Value>, + args: v8::PropertyCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let list = args.holder(); + let Some(items) = svg_string_list_intercepted_items(scope, list) else { + return v8::Intercepted::kNo; + }; + let Some(value) = svg_string_list_dom_string(scope, value, "SVGStringList indexed setter", 2) + else { + return v8::Intercepted::kYes; + }; + if index >= items.length() { + webidl::throw_index_size_error(scope); + return v8::Intercepted::kYes; + } + let _ = items.set_index(scope, index, value); + reflect_svg_string_list_to_owner_attribute(scope, list); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_query<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Integer>, +) -> v8::Intercepted { + let Some(items) = svg_string_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_int32(v8::PropertyAttribute::NONE.as_u32() as i32); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_deleter<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + let Some(items) = svg_string_list_intercepted_items(scope, args.holder()) else { + return v8::Intercepted::kNo; + }; + if index >= items.length() { + return v8::Intercepted::kNo; + } + rv.set_bool(false); + v8::Intercepted::kYes +} + +fn svg_string_list_indexed_definer<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + descriptor: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Boolean>, +) -> v8::Intercepted { + if descriptor.has_get() || descriptor.has_set() { + rv.set_bool(false); + return v8::Intercepted::kYes; + } + let value = if descriptor.has_value() { + v8::Local::new(scope, descriptor.value()) + } else { + v8::undefined(scope).into() + }; + svg_string_list_indexed_setter(scope, index, value, args, rv) +} + +fn svg_string_list_indexed_enumerator<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let length = svg_string_list_intercepted_items(scope, args.holder()) + .map(|items| items.length()) + .unwrap_or(0); + let keys = (0..length) + .map(|index| v8::Integer::new_from_unsigned(scope, index).into()) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + +fn svg_string_list_indexed_descriptor<'s>( + scope: &mut v8::PinScope<'s, '_>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) -> v8::Intercepted { + let Some(value) = svg_string_list_intercepted_items(scope, args.holder()) + .filter(|items| index < items.length()) + .and_then(|items| items.get_index(scope, index)) + else { + return v8::Intercepted::kNo; + }; + let Ok(descriptor) = DataPropertyDescriptorDeclaration::new(value, true, true).bind(scope) + else { + return v8::Intercepted::kNo; + }; + rv.set(descriptor.into()); + v8::Intercepted::kYes +} + pub(super) fn svg_length_list_length_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs index b8fbc96971..529b133f88 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -19,6 +19,8 @@ mod callbacks; mod rect; const SVG_GRAPHICS_TRANSFORM_SLOT: &str = "__moliSvgGraphicsTransform"; +const SVG_GRAPHICS_REQUIRED_EXTENSIONS_SLOT: &str = "__moliSvgGraphicsRequiredExtensions"; +const SVG_GRAPHICS_SYSTEM_LANGUAGE_SLOT: &str = "__moliSvgGraphicsSystemLanguage"; const SVG_PATTERN_TRANSFORM_SLOT: &str = "__moliSvgPatternTransform"; const SVG_GRADIENT_TRANSFORM_SLOT: &str = "__moliSvgGradientTransform"; const SVG_GEOMETRY_PATH_LENGTH_SLOT: &str = "__moliSvgGeometryPathLength"; @@ -52,6 +54,12 @@ const SVG_ANIMATED_NUMBER_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedNumberAnimVal" const SVG_ANIMATED_NUMBER_LIST_BASE_VAL_SLOT: &str = "__moliSvgAnimatedNumberListBaseVal"; const SVG_ANIMATED_NUMBER_LIST_ANIM_VAL_SLOT: &str = "__moliSvgAnimatedNumberListAnimVal"; const SVG_NUMBER_LIST_ITEMS_SLOT: &str = "__moliSvgNumberListItems"; +const SVG_STRING_LIST_ITEMS_SLOT: &str = "__moliSvgStringListItems"; +const SVG_STRING_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgStringListOwnerElement"; +const SVG_STRING_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgStringListOwnerAttribute"; +const SVG_STRING_LIST_SYNCED_ATTRIBUTE_VALUE_SLOT: &str = "__moliSvgStringListSyncedAttributeValue"; +const SVG_STRING_LIST_SYNCED_ATTRIBUTE_PRESENT_SLOT: &str = + "__moliSvgStringListSyncedAttributePresent"; const SVG_VALUE_LIST_OWNER_ELEMENT_SLOT: &str = "__moliSvgValueListOwnerElement"; const SVG_VALUE_LIST_OWNER_ATTRIBUTE_SLOT: &str = "__moliSvgValueListOwnerAttribute"; const SVG_VALUE_LIST_ITEM_OWNER_LIST_SLOT: &str = "__moliSvgValueListItemOwnerList"; @@ -96,6 +104,11 @@ const SVG_TEXT_POSITIONING_DX_SLOT: &str = "__moliSvgTextPositioningDx"; const SVG_TEXT_POSITIONING_DY_SLOT: &str = "__moliSvgTextPositioningDy"; const SVG_TEXT_POSITIONING_ROTATE_SLOT: &str = "__moliSvgTextPositioningRotate"; +const SVG_TEST_STRING_LIST_ATTRIBUTES: &[(&str, &str)] = &[ + ("requiredExtensions", SVG_GRAPHICS_REQUIRED_EXTENSIONS_SLOT), + ("systemLanguage", SVG_GRAPHICS_SYSTEM_LANGUAGE_SLOT), +]; + #[derive(Clone, Copy)] enum SvgListKind { Length, @@ -382,6 +395,7 @@ pub(in crate::context_bootstrap) fn install_svg_template_bindings<'s>( } "SVGAnimatedNumber" => bindings::install_svg_animated_number_bindings(scope, template), "SVGNumberList" => bindings::install_svg_number_list_bindings(scope, template), + "SVGStringList" => bindings::install_svg_string_list_bindings(scope, template), "SVGAnimatedNumberList" => { bindings::install_svg_animated_number_list_bindings(scope, template) } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs index 631c4a2584..ed3aece69d 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs @@ -1923,6 +1923,194 @@ fn svg_animated_boolean_reflects_preserve_alpha() { assert_eq!(result, "ok"); } +#[test] +fn svg_string_lists_reflect_conditional_processing_attributes() { + let mut vm = new_parsed_test_vm( + "https://svg-string-list.test/", + "", + ); + + let result = vm + .eval( + r##" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const ns = "http://www.w3.org/2000/svg"; + const text = document.createElementNS(ns, "text"); + + assert(typeof SVGStringList === "function", "SVGStringList constructor"); + let illegalConstructor = false; + try { + new SVGStringList(); + } catch (error) { + illegalConstructor = error instanceof TypeError; + } + assert(illegalConstructor, "SVGStringList illegal constructor"); + + for (const name of ["requiredExtensions", "systemLanguage"]) { + const descriptor = Object.getOwnPropertyDescriptor( + SVGGraphicsElement.prototype, + name, + ); + assert(typeof descriptor.get === "function" && descriptor.set === undefined, + `${name} readonly descriptor`); + assert(descriptor.enumerable && descriptor.configurable, `${name} flags`); + } + for (const name of ["length", "numberOfItems"]) { + const descriptor = Object.getOwnPropertyDescriptor(SVGStringList.prototype, name); + assert(typeof descriptor.get === "function" && descriptor.set === undefined, + `${name} descriptor`); + assert(descriptor.enumerable && descriptor.configurable, `${name} flags`); + } + const methodLengths = { + clear: 0, + initialize: 1, + getItem: 1, + insertItemBefore: 2, + replaceItem: 2, + removeItem: 1, + appendItem: 1, + }; + for (const [name, length] of Object.entries(methodLengths)) { + const descriptor = Object.getOwnPropertyDescriptor(SVGStringList.prototype, name); + assert(typeof descriptor.value === "function", `${name} method`); + assert(descriptor.value.length === length, `${name} arity`); + assert(descriptor.enumerable && descriptor.configurable && descriptor.writable, + `${name} flags`); + } + + const languages = text.systemLanguage; + assert(languages instanceof SVGStringList, "systemLanguage interface"); + assert(Object.prototype.toString.call(languages) === "[object SVGStringList]", + "SVGStringList tag"); + assert(text.systemLanguage === languages, "systemLanguage SameObject"); + assert(languages.length === 0 && languages.numberOfItems === 0, + "absent attribute empty list"); + assert(!text.hasAttribute("systemLanguage"), "getter does not create attribute"); + + const parsingCases = [ + ["en,fr,de", ["en", "fr", "de"]], + ["en, fr, de", ["en", "fr", "de"]], + ["en ,fr ,de", ["en", "fr", "de"]], + ["en , fr , de", ["en", "fr", "de"]], + [" en, fr ", ["en", "fr"]], + [" \t\nen, fr\t\n ", ["en", "fr"]], + ["en", ["en"]], + ["en-US, zh-Hans, pt-BR", ["en-US", "zh-Hans", "pt-BR"]], + ["en,,fr", ["en", "", "fr"]], + ["", [""]], + [",", ["", ""]], + ["123, 456", ["123", "456"]], + ["not-a-lang, ???, @#$", ["not-a-lang", "???", "@#$"]], + ]; + for (const [raw, expected] of parsingCases) { + text.setAttribute("systemLanguage", raw); + assert(text.systemLanguage === languages, `SameObject after ${raw}`); + assert(languages.length === expected.length, `length for ${raw}`); + assert(languages.numberOfItems === expected.length, + `numberOfItems for ${raw}`); + assert(Object.keys(languages).join() === expected.map((_, index) => index).join(), + `supported indices for ${raw}`); + for (let index = 0; index < expected.length; index++) { + assert(languages.getItem(index) === expected[index], + `getItem ${index} for ${raw}`); + assert(languages[index] === expected[index], `index ${index} for ${raw}`); + } + } + + text.removeAttribute("systemLanguage"); + assert(languages.length === 0, "removed attribute empty list"); + const extensions = text.requiredExtensions; + assert(text.requiredExtensions === extensions, "requiredExtensions SameObject"); + text.setAttribute("requiredExtensions", " one\t two\nthree "); + assert(extensions.length === 3 && extensions[0] === "one" && + extensions[1] === "two" && extensions[2] === "three", + "requiredExtensions space-separated parsing"); + + assert(languages.initialize("en") === "en", "initialize return"); + assert(text.getAttribute("systemLanguage") === "en", "initialize reflection"); + assert(languages.appendItem("fr") === "fr", "append return"); + assert(text.getAttribute("systemLanguage") === "en,fr", "append reflection"); + assert(languages.insertItemBefore("de", 1) === "de", "insert return"); + assert(text.getAttribute("systemLanguage") === "en,de,fr", "insert reflection"); + assert(languages.insertItemBefore("it", 99) === "it", "clamped insert return"); + assert(text.getAttribute("systemLanguage") === "en,de,fr,it", + "clamped insert reflection"); + assert(languages.replaceItem("zh", 1) === "zh", "replace return"); + assert(text.getAttribute("systemLanguage") === "en,zh,fr,it", + "replace reflection"); + languages[2] = "pt-BR"; + assert(text.getAttribute("systemLanguage") === "en,zh,pt-BR,it", + "indexed setter reflection"); + Object.defineProperty(languages, "0", {value: "es"}); + assert(text.getAttribute("systemLanguage") === "es,zh,pt-BR,it", + "indexed definer reflection"); + const indexDescriptor = Object.getOwnPropertyDescriptor(languages, "0"); + assert(indexDescriptor.value === "es" && indexDescriptor.writable && + indexDescriptor.enumerable && indexDescriptor.configurable, + "indexed property descriptor"); + assert(delete languages[0] === false, "supported index cannot be deleted"); + assert(languages.removeItem(1) === "zh", "remove return"); + assert(text.getAttribute("systemLanguage") === "es,pt-BR,it", + "remove reflection"); + languages.clear(); + assert(text.getAttribute("systemLanguage") === "" && languages.length === 0, + "clear reflection"); + + extensions.initialize("alpha"); + extensions.appendItem("beta"); + assert(text.getAttribute("requiredExtensions") === "alpha beta", + "requiredExtensions space serialization"); + assert(extensions.appendItem(null) === "null", "DOMString conversion"); + assert(text.getAttribute("requiredExtensions") === "alpha beta null", + "converted string reflection"); + + text.setAttribute("systemLanguage", "en,fr"); + for (const operation of [ + () => languages.getItem(9), + () => languages.replaceItem("x", 9), + () => languages.removeItem(9), + () => { languages[9] = "x"; }, + ]) { + let indexError = false; + try { + operation(); + } catch (error) { + indexError = error instanceof DOMException && error.name === "IndexSizeError"; + } + assert(indexError, "out-of-range operation"); + } + + let incompatibleListReceiver = false; + try { + SVGStringList.prototype.getItem.call({}, 0); + } catch (error) { + incompatibleListReceiver = error instanceof TypeError; + } + assert(incompatibleListReceiver, "SVGStringList receiver brand"); + + const systemLanguageGetter = Object.getOwnPropertyDescriptor( + SVGGraphicsElement.prototype, + "systemLanguage", + ).get; + let incompatibleElementReceiver = false; + try { + systemLanguageGetter.call(document.createElementNS(ns, "filter")); + } catch (error) { + incompatibleElementReceiver = error instanceof TypeError; + } + assert(incompatibleElementReceiver, "SVGGraphicsElement receiver brand"); + return "ok"; + })() + "##, + ) + .expect("SVG string list probe should evaluate"); + + assert_eq!(result, "ok"); +} + #[test] fn svg_geometry_queries_use_computed_paths_live_tree_and_kurbo_bounds() { let mut vm = new_parsed_test_vm( diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index c52d4422b5..e9785258e5 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -415,6 +415,7 @@ interfaces! { SVGTextPositioningElement: SVGTextContentElement; SVGTitleElement: SVGElement; SVGUnitTypes; + SVGStringList; SVGTransform; SVGTransformList; SVGUseElement: SVGGraphicsElement;