use std::ffi::CString;
use std::ops::ControlFlow;
use std::ptr::{self, NonNull};
use std::sync::LazyLock;
use cssparser::{Parser, ParserInput};
use dom_struct::dom_struct;
use js::context::JSContext;
use js::conversions::{
ConversionResult, FromJSValConvertible, ToJSValConvertible, jsstr_to_string,
};
use js::gc::{HandleValue, RootedVec};
use js::jsapi::{HandleId, Heap, JS_GetPropertyById, JSITER_OWNONLY, JSObject, JSPROP_ENUMERATE};
use js::jsval::{ObjectValue, UndefinedValue};
use js::rust::wrappers2::{GetPropertyKeys, JS_DefineProperty, JS_IdToValue, JS_NewObject};
use js::rust::{
ForOfIterationFailure, HandleObject, IdVector, IntoHandle, IntoMutableHandle, for_of,
};
use rustc_hash::FxHashMap;
use script_bindings::cell::DomRefCell;
use script_bindings::codegen::GenericBindings::KeyframeEffectBinding::{
BaseKeyframe, CompositeOperationOrAuto,
};
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use script_bindings::codegen::GenericUnionTypes::UnrestrictedDoubleOrKeyframeEffectOptions;
use script_bindings::conversions::StringificationBehavior;
use script_bindings::error::{Error, Fallible};
use script_bindings::inheritance::Castable;
use script_bindings::num::Finite;
use script_bindings::reflector::reflect_dom_object_with_proto;
use script_bindings::root::DomRoot;
use script_bindings::str::DOMString;
use style::parser::ParserContext;
use style::properties::generated::PropertyDeclaration;
use style::properties::{
Importance, LonghandId, NonCustomPropertyId, PropertyDeclarationBlock, PropertyId,
SourcePropertyDeclaration,
};
use style::stylesheets::CssRuleType;
use style_traits::{CssWriter, ParsingMode, ToCss};
use crate::css::parser_context_for_document;
use crate::dom::Document;
use crate::dom::animationeffect::AnimationEffect;
use crate::dom::bindings::codegen::Bindings::KeyframeEffectBinding::{
BaseComputedKeyframe, KeyframeEffectMethods,
};
use crate::dom::bindings::root::MutNullableDom;
use crate::dom::element::Element;
use crate::dom::window::Window;
#[dom_struct]
pub(crate) struct KeyframeEffect {
animationeffect: AnimationEffect,
target_element: MutNullableDom<Element>,
keyframes: DomRefCell<Vec<Keyframe>>,
}
impl KeyframeEffect {
pub(crate) fn new_inherited(window: &Window) -> Self {
Self {
animationeffect: AnimationEffect::new_inherited(window),
target_element: Default::default(),
keyframes: Default::default(),
}
}
fn new_with_proto_and_cx(
cx: &mut JSContext,
window: &Window,
proto: Option<HandleObject>,
) -> DomRoot<Self> {
reflect_dom_object_with_proto(cx, Box::new(Self::new_inherited(window)), window, proto)
}
pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<Self> {
Self::new_with_proto_and_cx(cx, window, None)
}
}
impl KeyframeEffectMethods<crate::DomTypeHolder> for KeyframeEffect {
fn Constructor(
cx: &mut JSContext,
window: &Window,
_: Option<HandleObject>,
target: Option<&Element>,
keyframes: *mut JSObject,
_options: UnrestrictedDoubleOrKeyframeEffectOptions,
) -> DomRoot<KeyframeEffect> {
let effect = KeyframeEffect::new(cx, window);
effect.target_element.set(target);
effect.SetKeyframes(cx, keyframes);
effect
}
#[expect(unsafe_code)]
fn GetKeyframes(
&self,
cx: &mut JSContext,
result: &mut RootedVec<'_, Box<Heap<*mut JSObject>>>,
) -> Fallible<()> {
let mut layout = self.upcast::<AnimationEffect>().window().layout_mut();
let stylist = layout.stylist_mut();
debug_assert!(result.is_empty());
let keyframes = self.keyframes.borrow();
for keyframe in keyframes.iter() {
let base_keyframe = BaseComputedKeyframe {
composite: keyframe.composite,
offset: keyframe.offset,
computedOffset: keyframe.offset,
easing: keyframe.easing_function.clone(),
};
rooted!(&in(cx) let mut output_keyframe = unsafe { JS_NewObject(cx, ptr::null()) });
base_keyframe.to_jsobject(cx, output_keyframe.handle_mut());
for property_value_pair in &keyframe.declarations {
debug_assert!(property_value_pair.property_id.is_animatable());
let mut property_name = String::new();
let mut writer = CssWriter::new(&mut property_name);
if property_value_pair.property_id.to_css(&mut writer).is_err() {
continue;
}
let property_name = animation_property_name_to_idl_attribute_name(&property_name);
let mut value_string = String::new();
if property_value_pair
.block
.single_value_to_css(
&property_value_pair.property_id,
&mut value_string,
None,
stylist,
)
.is_err()
{
continue;
}
rooted!(&in(cx) let mut value = UndefinedValue());
value_string.safe_to_jsval(cx, value.handle_mut());
let Ok(property_name) = CString::new(property_name) else {
continue;
};
let success = unsafe {
JS_DefineProperty(
cx,
output_keyframe.handle(),
property_name.as_ptr(),
value.handle(),
JSPROP_ENUMERATE as u32,
)
};
if !success {
if cfg!(debug_assertions) {
unreachable!("Setting a property on output_keyframe should never fail");
}
return Err(Error::Operation(None));
}
}
result.push(Heap::boxed(output_keyframe.get()))
}
Ok(())
}
fn SetKeyframes(&self, cx: &mut JSContext, keyframes: *mut JSObject) {
let document = self.upcast::<AnimationEffect>().window().Document();
let Ok(keyframes) = process_a_keyframes_argument(cx, &document, keyframes) else {
return;
};
*self.keyframes.safe_borrow_mut(cx.no_gc()) = keyframes;
}
}
#[expect(unsafe_code)]
fn process_a_keyframes_argument(
cx: &mut JSContext,
document: &Document,
keyframes: *mut JSObject,
) -> Fallible<Vec<Keyframe>> {
if keyframes.is_null() {
return Ok(Vec::new());
}
rooted!(&in(cx) let iterable = ObjectValue(keyframes));
let mut keyframes = Vec::new();
let result = for_of(
unsafe { cx.raw_cx() },
iterable.handle(),
|iterator_element| {
if !iterator_element.is_object() {
return Err(ForOfIterationFailure::Other(Error::Type(
c"Keyframe must be an object".to_owned(),
)));
}
keyframes.push(keyframe_from_value(cx, document, iterator_element)?);
Ok(ControlFlow::Continue(()))
},
);
match result {
Ok(()) => Ok(keyframes),
Err(ForOfIterationFailure::ValueIsNotIterable) => {
Err(Error::Operation(None))
},
Err(ForOfIterationFailure::JSFailed) => Err(Error::JSFailed),
Err(ForOfIterationFailure::Other(error)) => Err(error),
}
}
#[derive(JSTraceable, MallocSizeOf)]
struct Keyframe {
offset: Option<Finite<f64>>,
easing_function: DOMString,
composite: CompositeOperationOrAuto,
declarations: Vec<KeyframePropertyDeclaration>,
}
#[derive(JSTraceable, MallocSizeOf)]
struct KeyframePropertyDeclaration {
#[no_trace]
property_id: PropertyId,
#[no_trace]
block: PropertyDeclarationBlock,
}
fn keyframe_from_value(
cx: &mut JSContext,
document: &Document,
value: HandleValue<'_>,
) -> Fallible<Keyframe> {
if !value.is_null_or_undefined() && !value.is_object() {
return Err(Error::Type(c"Invalid keyframe value".to_owned()));
}
process_a_keyframe_like_object(cx, document, value)
}
fn process_a_keyframe_like_object(
cx: &mut JSContext,
document: &Document,
value: HandleValue,
) -> Fallible<Keyframe> {
let Ok(keyframe_output) = BaseKeyframe::safe_from_jsval(cx, value, ()) else {
return Err(Error::JSFailed);
};
let ConversionResult::Success(keyframe_output) = keyframe_output else {
return Err(Error::Operation(None));
};
let urlextradata = document.url().into_url().into();
let parser_context = parser_context_for_document(
document,
CssRuleType::Style,
ParsingMode::DEFAULT,
&urlextradata,
);
rooted!(&in(cx) let object = value.to_object());
let declarations = get_property_declarations(cx, object.handle(), &parser_context)?;
Ok(Keyframe {
offset: keyframe_output.offset,
easing_function: keyframe_output.easing,
composite: keyframe_output.composite,
declarations,
})
}
#[expect(unsafe_code)]
fn get_property_declarations(
cx: &mut JSContext,
object: HandleObject,
parser_context: &ParserContext<'_>,
) -> Fallible<Vec<KeyframePropertyDeclaration>> {
let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
if !unsafe { GetPropertyKeys(cx, object, JSITER_OWNONLY, ids.handle_mut()) } {
return Ok(Vec::new());
}
let mut declarations = Vec::with_capacity(ids.len());
for id in ids.iter() {
rooted!(&in(cx) let id = *id);
if !id.is_string() {
continue;
}
rooted!(&in(cx) let mut key_value = UndefinedValue());
let raw_id: HandleId = id.handle().into();
if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
continue;
}
rooted!(&in(cx) let js_string = key_value.to_string());
let Some(js_string) = NonNull::new(js_string.get()) else {
continue;
};
let property_name = unsafe { jsstr_to_string(cx, js_string) };
let Some(property_id) = lookup_css_property_by_idl_attribute_name(&property_name) else {
continue;
};
debug_assert!(property_id.is_animatable());
rooted!(&in(cx) let mut property_value = UndefinedValue());
if !unsafe {
JS_GetPropertyById(
cx.raw_cx(),
object.into_handle(),
id.handle().into_handle(),
property_value.handle_mut().into_handle_mut(),
)
} {
continue;
}
let property_value = match DOMString::safe_from_jsval(
cx,
property_value.handle(),
StringificationBehavior::Default,
) {
Ok(ConversionResult::Success(property_value)) => property_value,
Ok(ConversionResult::Failure(error_message)) => {
return Err(Error::Operation(
error_message
.to_str()
.ok()
.map(|message| message.to_owned()),
));
},
Err(_) => return Err(Error::JSFailed),
};
let Some(declaration) =
parse_single_property_declaration(property_id, &property_value.str(), parser_context)
else {
continue;
};
declarations.push(declaration);
}
Ok(declarations)
}
fn parse_single_property_declaration(
property: NonCustomPropertyId,
input: &str,
parser_context: &ParserContext<'_>,
) -> Option<KeyframePropertyDeclaration> {
let mut declaration = SourcePropertyDeclaration::default();
let mut input = ParserInput::new(input);
let mut parser = Parser::new(&mut input);
parser
.parse_entirely(|parser| {
PropertyDeclaration::parse_into(
&mut declaration,
PropertyId::NonCustom(property),
parser_context,
parser,
)
})
.ok()?;
let mut block = PropertyDeclarationBlock::new();
block.extend(declaration.drain(), Importance::Normal);
Some(KeyframePropertyDeclaration {
property_id: PropertyId::NonCustom(property),
block,
})
}
fn lookup_css_property_by_idl_attribute_name(attribute_name: &str) -> Option<NonCustomPropertyId> {
if attribute_name == "cssFloat" {
return Some(LonghandId::Float.into());
}
static IDL_ATTRIBUTE_TO_ANIMATED_PROPERTY_LOOKUP_TABLE: LazyLock<
FxHashMap<String, NonCustomPropertyId>,
> = LazyLock::new(|| {
log::debug!("Initializing map from IDL attribute names to CSS properties");
NonCustomPropertyId::iter()
.filter(|non_custom_property| non_custom_property.is_animatable())
.filter(|non_custom_property| {
non_custom_property
.to_property_id()
.enabled_for_all_content()
})
.map(|non_custom_property| {
let idl_attribute_name =
animation_property_name_to_idl_attribute_name(non_custom_property.name());
(idl_attribute_name, non_custom_property)
})
.collect()
});
IDL_ATTRIBUTE_TO_ANIMATED_PROPERTY_LOOKUP_TABLE
.get(attribute_name)
.copied()
}
fn animation_property_name_to_idl_attribute_name(property_name: &str) -> String {
let mut idl_attribute_name = String::with_capacity(property_name.len());
let mut chunks = property_name.split('-');
let Some(first_chunk) = chunks.next() else {
unreachable!("CSS property name should not consist only of dashes");
};
idl_attribute_name.push_str(first_chunk);
for chunk in chunks {
let mut characters = chunk.chars();
let Some(to_capitalize) = characters.next() else {
continue;
};
idl_attribute_name.push(to_capitalize.to_ascii_uppercase());
idl_attribute_name.push_str(characters.as_str());
}
idl_attribute_name
}