use std::collections::BTreeMap;
use boa_cat::Value;
use boa_cat::fuel::Fuel;
use boa_cat::heap::Heap;
use boa_cat::outcome::{EvalResult, Outcome};
use boa_cat::value::{Object, ObjectId};
pub const LISTENERS_KEY: &str = "__listeners__";
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn add_event_listener_impl(
args: Vec<Value>,
this: Value,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let event_type = string_arg(&args, 0);
let callback = args.get(1).cloned().unwrap_or(Value::Undefined);
let capture = parse_capture_arg(&args, &heap);
let new_heap = append_listener(&this, &event_type, callback, capture, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn remove_event_listener_impl(
args: Vec<Value>,
this: Value,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let event_type = string_arg(&args, 0);
let callback = args.get(1).cloned().unwrap_or(Value::Undefined);
let capture = parse_capture_arg(&args, &heap);
let new_heap = drop_listener(&this, &event_type, &callback, capture, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
#[allow(clippy::needless_pass_by_value)]
pub fn dispatch_event_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let event = args.first().cloned().unwrap_or(Value::Undefined);
let Some(target_id) = object_id_of(&this) else {
return Ok((Outcome::Normal(Value::Boolean(true)), heap, fuel));
};
let heap = decorate_event_in_place(&event, target_id, heap);
let decorated = event;
let event_type = read_event_type(&decorated, &heap);
let chain = build_bubble_chain(&this, &heap);
let ancestors: Vec<Value> = chain.iter().skip(1).cloned().collect();
let capture_chain: Vec<Value> = ancestors.iter().rev().cloned().collect();
let (heap, fuel) = walk_chain(
&capture_chain,
&decorated,
&event_type,
ListenerFilter::Capture,
heap,
fuel,
)?;
let (heap, fuel) = if read_bool_flag(&decorated, PROPAGATION_STOPPED_KEY, &heap)
|| read_bool_flag(&decorated, IMMEDIATE_STOPPED_KEY, &heap)
{
(heap, fuel)
} else {
let heap = set_current_target(&decorated, target_id, heap);
invoke_level_listeners(
&this,
&decorated,
&event_type,
ListenerFilter::Any,
heap,
fuel,
)?
};
let (heap, fuel) = walk_chain(
&ancestors,
&decorated,
&event_type,
ListenerFilter::Bubble,
heap,
fuel,
)?;
let default_prevented = read_bool_flag(&decorated, DEFAULT_PREVENTED_KEY, &heap);
Ok((
Outcome::Normal(Value::Boolean(!default_prevented)),
heap,
fuel,
))
}
#[derive(Clone, Copy)]
enum ListenerFilter {
Capture,
Bubble,
Any,
}
fn walk_chain(
chain: &[Value],
event: &Value,
event_type: &str,
filter: ListenerFilter,
heap: Heap,
fuel: Fuel,
) -> Result<(Heap, Fuel), boa_cat::Error> {
chain.iter().try_fold((heap, fuel), |(heap, fuel), level| {
if read_bool_flag(event, PROPAGATION_STOPPED_KEY, &heap)
|| read_bool_flag(event, IMMEDIATE_STOPPED_KEY, &heap)
{
Ok((heap, fuel))
} else {
let heap = if let Some(id) = object_id_of(level) {
set_current_target(event, id, heap)
} else {
heap
};
invoke_level_listeners(level, event, event_type, filter, heap, fuel)
}
})
}
pub const DEFAULT_PREVENTED_KEY: &str = "defaultPrevented";
const PROPAGATION_STOPPED_KEY: &str = "__propagation_stopped__";
const IMMEDIATE_STOPPED_KEY: &str = "__immediate_propagation_stopped__";
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn prevent_default_impl(_args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let new_heap = set_bool_flag(&this, DEFAULT_PREVENTED_KEY, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn stop_propagation_impl(_args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let new_heap = set_bool_flag(&this, PROPAGATION_STOPPED_KEY, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn stop_immediate_propagation_impl(
_args: Vec<Value>,
this: Value,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let heap = set_bool_flag(&this, PROPAGATION_STOPPED_KEY, heap);
let heap = set_bool_flag(&this, IMMEDIATE_STOPPED_KEY, heap);
Ok((Outcome::Normal(Value::Undefined), heap, fuel))
}
fn decorate_event_in_place(event: &Value, target_id: ObjectId, heap: Heap) -> Heap {
let Some(event_id) = object_id_of(event) else {
return heap;
};
let Some(obj) = heap.object(event_id).cloned() else {
return heap;
};
let updated = obj
.with("target".to_owned(), Value::Object(target_id))
.with("currentTarget".to_owned(), Value::Object(target_id))
.with(DEFAULT_PREVENTED_KEY.to_owned(), Value::Boolean(false))
.with(PROPAGATION_STOPPED_KEY.to_owned(), Value::Boolean(false))
.with(IMMEDIATE_STOPPED_KEY.to_owned(), Value::Boolean(false))
.with(
"preventDefault".to_owned(),
Value::Native(prevent_default_impl),
)
.with(
"stopPropagation".to_owned(),
Value::Native(stop_propagation_impl),
)
.with(
"stopImmediatePropagation".to_owned(),
Value::Native(stop_immediate_propagation_impl),
);
heap.store_object(event_id, updated).unwrap_or_else(|h| h)
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn event_constructor_impl(
args: Vec<Value>,
_this: Value,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let event_type = string_arg(&args, 0);
let options = args.get(1).cloned().unwrap_or(Value::Undefined);
let (bubbles, cancelable, composed) = parse_event_options(&options, &heap);
let (event_value, heap) = build_event_object_value(
&event_type,
bubbles,
cancelable,
composed,
Value::Null,
heap,
);
Ok((Outcome::Normal(event_value), heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn custom_event_constructor_impl(
args: Vec<Value>,
_this: Value,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let event_type = string_arg(&args, 0);
let options = args.get(1).cloned().unwrap_or(Value::Undefined);
let (bubbles, cancelable, composed) = parse_event_options(&options, &heap);
let detail = read_options_field(&options, "detail", &heap).unwrap_or(Value::Null);
let (event_value, heap) =
build_event_object_value(&event_type, bubbles, cancelable, composed, detail, heap);
Ok((Outcome::Normal(event_value), heap, fuel))
}
fn build_event_object_value(
event_type: &str,
bubbles: bool,
cancelable: bool,
composed: bool,
detail: Value,
heap: Heap,
) -> (Value, Heap) {
let mut props = BTreeMap::new();
let _ = props.insert("type".to_owned(), Value::String(event_type.to_owned()));
let _ = props.insert("bubbles".to_owned(), Value::Boolean(bubbles));
let _ = props.insert("cancelable".to_owned(), Value::Boolean(cancelable));
let _ = props.insert("composed".to_owned(), Value::Boolean(composed));
let _ = props.insert(DEFAULT_PREVENTED_KEY.to_owned(), Value::Boolean(false));
let _ = props.insert("target".to_owned(), Value::Null);
let _ = props.insert("currentTarget".to_owned(), Value::Null);
let _ = props.insert("detail".to_owned(), detail);
let _ = props.insert(PROPAGATION_STOPPED_KEY.to_owned(), Value::Boolean(false));
let _ = props.insert(IMMEDIATE_STOPPED_KEY.to_owned(), Value::Boolean(false));
let _ = props.insert(
"preventDefault".to_owned(),
Value::Native(prevent_default_impl),
);
let _ = props.insert(
"stopPropagation".to_owned(),
Value::Native(stop_propagation_impl),
);
let _ = props.insert(
"stopImmediatePropagation".to_owned(),
Value::Native(stop_immediate_propagation_impl),
);
let (id, heap) = heap.alloc_object(Object::from_properties(props));
(Value::Object(id), heap)
}
fn parse_event_options(options: &Value, heap: &Heap) -> (bool, bool, bool) {
let Some(obj_id) = object_id_of(options) else {
return (false, false, false);
};
let Some(obj) = heap.object(obj_id) else {
return (false, false, false);
};
(
read_bool_field(obj, "bubbles"),
read_bool_field(obj, "cancelable"),
read_bool_field(obj, "composed"),
)
}
fn read_bool_field(obj: &Object, key: &str) -> bool {
obj.get(key)
.and_then(|v| match v {
Value::Boolean(b) => Some(*b),
Value::Undefined
| Value::Null
| Value::Number(_)
| Value::String(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})
.unwrap_or(false)
}
fn read_options_field(options: &Value, key: &str, heap: &Heap) -> Option<Value> {
let obj_id = object_id_of(options)?;
let obj = heap.object(obj_id)?;
obj.get(key).cloned()
}
fn set_current_target(event: &Value, level_id: ObjectId, heap: Heap) -> Heap {
let Some(event_id) = object_id_of(event) else {
return heap;
};
let Some(obj) = heap.object(event_id).cloned() else {
return heap;
};
let updated = obj.with("currentTarget".to_owned(), Value::Object(level_id));
heap.store_object(event_id, updated).unwrap_or_else(|h| h)
}
fn set_bool_flag(this: &Value, key: &str, heap: Heap) -> Heap {
let Some(id) = object_id_of(this) else {
return heap;
};
let Some(obj) = heap.object(id).cloned() else {
return heap;
};
let updated = obj.with(key.to_owned(), Value::Boolean(true));
heap.store_object(id, updated).unwrap_or_else(|h| h)
}
fn read_bool_flag(value: &Value, key: &str, heap: &Heap) -> bool {
object_id_of(value)
.and_then(|id| heap.object(id))
.and_then(|obj| obj.get(key))
.and_then(|v| match v {
Value::Boolean(b) => Some(*b),
Value::Undefined
| Value::Null
| Value::Number(_)
| Value::String(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})
.unwrap_or(false)
}
fn append_listener(
this: &Value,
event_type: &str,
callback: Value,
capture: bool,
heap: Heap,
) -> Heap {
let Some(element_id) = object_id_of(this) else {
return heap;
};
let Some(element) = heap.object(element_id).cloned() else {
return heap;
};
let (listeners_id, heap) = resolve_or_create_listeners_map(element_id, &element, heap);
let Some(listeners) = heap.object(listeners_id).cloned() else {
return heap;
};
let (array_id, heap) = resolve_or_create_type_array(listeners_id, &listeners, event_type, heap);
let (entry_value, heap) = make_listener_entry(callback, capture, heap);
let Some(array) = heap.object(array_id).cloned() else {
return heap;
};
let length = array_length(&array);
let updated = array
.with(format!("{length}"), entry_value)
.with("length".to_owned(), Value::Number(f64::from(length + 1)));
heap.store_object(array_id, updated).unwrap_or_else(|h| h)
}
fn drop_listener(
this: &Value,
event_type: &str,
callback: &Value,
capture: bool,
heap: Heap,
) -> Heap {
let Some(element_id) = object_id_of(this) else {
return heap;
};
let Some(element) = heap.object(element_id) else {
return heap;
};
let Some(listeners_id) = element.get(LISTENERS_KEY).and_then(object_id_from_value) else {
return heap;
};
let Some(listeners) = heap.object(listeners_id) else {
return heap;
};
let Some(array_id) = listeners.get(event_type).and_then(object_id_from_value) else {
return heap;
};
let Some(array) = heap.object(array_id).cloned() else {
return heap;
};
let length = array_length(&array);
let remaining: Vec<Value> = (0..length)
.filter_map(|i| array.get(&format!("{i}")).cloned())
.filter(|entry| {
let (entry_cb, entry_capture) = unwrap_listener_entry(entry, &heap);
!(entry_cb == *callback && entry_capture == capture)
})
.collect();
let new_length = u32::try_from(remaining.len()).unwrap_or(u32::MAX);
let pairs: BTreeMap<String, Value> = remaining
.into_iter()
.enumerate()
.map(|(i, v)| (format!("{i}"), v))
.chain(std::iter::once((
"length".to_owned(),
Value::Number(f64::from(new_length)),
)))
.collect();
heap.store_object(array_id, Object::from_properties(pairs))
.unwrap_or_else(|h| h)
}
fn invoke_level_listeners(
level: &Value,
event: &Value,
event_type: &str,
filter: ListenerFilter,
heap: Heap,
fuel: Fuel,
) -> Result<(Heap, Fuel), boa_cat::Error> {
let listeners = collect_listeners(level, event_type, &heap);
listeners
.into_iter()
.try_fold((heap, fuel), |(heap, fuel), (callback, capture)| {
if read_bool_flag(event, IMMEDIATE_STOPPED_KEY, &heap)
|| !filter_matches(filter, capture)
{
Ok((heap, fuel))
} else {
let (_outcome, heap, fuel) = boa_cat::expression::call_function(
&callback,
level,
vec![event.clone()],
heap,
fuel,
)?;
Ok((heap, fuel))
}
})
}
fn filter_matches(filter: ListenerFilter, capture: bool) -> bool {
match filter {
ListenerFilter::Capture => capture,
ListenerFilter::Bubble => !capture,
ListenerFilter::Any => true,
}
}
fn collect_listeners(level: &Value, event_type: &str, heap: &Heap) -> Vec<(Value, bool)> {
let Some(element_id) = object_id_of(level) else {
return Vec::new();
};
let Some(element) = heap.object(element_id) else {
return Vec::new();
};
let Some(listeners_id) = element.get(LISTENERS_KEY).and_then(object_id_from_value) else {
return Vec::new();
};
let Some(listeners) = heap.object(listeners_id) else {
return Vec::new();
};
let Some(array_id) = listeners.get(event_type).and_then(object_id_from_value) else {
return Vec::new();
};
let Some(array) = heap.object(array_id) else {
return Vec::new();
};
let length = array_length(array);
(0..length)
.filter_map(|i| array.get(&format!("{i}")).cloned())
.map(|entry| unwrap_listener_entry(&entry, heap))
.collect()
}
fn make_listener_entry(callback: Value, capture: bool, heap: Heap) -> (Value, Heap) {
let props: BTreeMap<String, Value> = [
("callback".to_owned(), callback),
("capture".to_owned(), Value::Boolean(capture)),
]
.into_iter()
.collect();
let (id, heap) = heap.alloc_object(Object::from_properties(props));
(Value::Object(id), heap)
}
fn unwrap_listener_entry(entry: &Value, heap: &Heap) -> (Value, bool) {
let Some(entry_id) = object_id_of(entry) else {
return (entry.clone(), false);
};
let Some(obj) = heap.object(entry_id) else {
return (entry.clone(), false);
};
let callback = obj.get("callback").cloned().unwrap_or(Value::Undefined);
let capture = obj
.get("capture")
.and_then(|v| match v {
Value::Boolean(b) => Some(*b),
Value::Undefined
| Value::Null
| Value::Number(_)
| Value::String(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})
.unwrap_or(false);
(callback, capture)
}
fn parse_capture_arg(args: &[Value], heap: &Heap) -> bool {
args.get(2).is_some_and(|v| match v {
Value::Boolean(b) => *b,
Value::Object(id) => heap
.object(*id)
.and_then(|obj| obj.get("capture"))
.and_then(|val| match val {
Value::Boolean(b) => Some(*b),
Value::Undefined
| Value::Null
| Value::Number(_)
| Value::String(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})
.unwrap_or(false),
Value::Undefined
| Value::Null
| Value::Number(_)
| Value::String(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => false,
})
}
fn build_bubble_chain(target: &Value, heap: &Heap) -> Vec<Value> {
std::iter::successors(Some(target.clone()), |current| read_parent(current, heap)).collect()
}
fn read_parent(value: &Value, heap: &Heap) -> Option<Value> {
let id = object_id_of(value)?;
let obj = heap.object(id)?;
obj.get("__parent__").and_then(|v| match v {
Value::Object(_) => Some(v.clone()),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::String(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})
}
fn resolve_or_create_listeners_map(
element_id: ObjectId,
element: &Object,
heap: Heap,
) -> (ObjectId, Heap) {
if let Some(id) = element.get(LISTENERS_KEY).and_then(object_id_from_value) {
(id, heap)
} else {
let (id, heap) = heap.alloc_object(Object::from_properties(BTreeMap::new()));
let updated = element
.clone()
.with(LISTENERS_KEY.to_owned(), Value::Object(id));
let heap = heap.store_object(element_id, updated).unwrap_or_else(|h| h);
(id, heap)
}
}
fn resolve_or_create_type_array(
listeners_id: ObjectId,
listeners: &Object,
event_type: &str,
heap: Heap,
) -> (ObjectId, Heap) {
if let Some(id) = listeners.get(event_type).and_then(object_id_from_value) {
(id, heap)
} else {
let empty = Object::from_properties(
std::iter::once(("length".to_owned(), Value::Number(0.0))).collect(),
);
let (id, heap) = heap.alloc_object(empty);
let updated = listeners
.clone()
.with(event_type.to_owned(), Value::Object(id));
let heap = heap
.store_object(listeners_id, updated)
.unwrap_or_else(|h| h);
(id, heap)
}
}
fn read_event_type(event: &Value, heap: &Heap) -> String {
object_id_of(event)
.and_then(|id| heap.object(id))
.and_then(|obj| obj.get("type").cloned())
.and_then(|v| match v {
Value::String(s) => Some(s),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})
.unwrap_or_default()
}
fn array_length(array: &Object) -> u32 {
match array.get("length") {
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let length = *n as u32;
length
}
Some(_) | None => 0,
}
}
fn object_id_of(value: &Value) -> Option<ObjectId> {
object_id_from_value(value)
}
fn object_id_from_value(value: &Value) -> Option<ObjectId> {
match value {
Value::Object(id) => Some(*id),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::String(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
}
}
fn string_arg(args: &[Value], idx: usize) -> String {
match args.get(idx) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => format!("{n}"),
Some(Value::Boolean(b)) => format!("{b}"),
Some(Value::Null) => "null".to_owned(),
Some(Value::Undefined) | None => String::new(),
Some(Value::Object(_) | Value::Function(_) | Value::Native(_) | Value::Promise(_)) => {
"[object]".to_owned()
}
}
}