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};
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::unnecessary_wraps)]
pub fn get_attribute_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let name = string_arg(&args, 0);
let outcome = read_attribute(&this, &name, &heap).map_or(Outcome::Normal(Value::Null), |v| {
Outcome::Normal(Value::String(v))
});
Ok((outcome, heap, fuel))
}
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::unnecessary_wraps)]
pub fn has_attribute_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let name = string_arg(&args, 0);
let present = read_attribute(&this, &name, &heap).is_some();
Ok((Outcome::Normal(Value::Boolean(present)), heap, fuel))
}
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::unnecessary_wraps)]
pub fn set_attribute_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let name = string_arg(&args, 0);
let value = string_arg(&args, 1);
let new_heap = write_attribute(&this, &name, &value, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
fn object_id_of(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 attributes_id_of(element: &Object) -> Option<ObjectId> {
match element.get("__attributes") {
Some(value) => object_id_of(value),
None => None,
}
}
fn read_attribute(this: &Value, name: &str, heap: &Heap) -> Option<String> {
let element_id = object_id_of(this)?;
let element = heap.object(element_id)?;
let attrs_id = attributes_id_of(element)?;
let attrs = heap.object(attrs_id)?;
match attrs.get(name) {
Some(Value::String(s)) => Some(s.clone()),
Some(_) | None => None,
}
}
fn write_attribute(this: &Value, name: &str, value: &str, 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 Some(attrs_id) = attributes_id_of(&element) else {
return heap;
};
let Some(attrs) = heap.object(attrs_id).cloned() else {
return heap;
};
let updated_attrs = attrs.with(name.to_owned(), Value::String(value.to_owned()));
let heap = heap
.store_object(attrs_id, updated_attrs)
.unwrap_or_else(|h| h);
let mirror_key = match name {
"id" => Some("id"),
"class" => Some("className"),
_other => None,
};
if let Some(key) = mirror_key {
let updated_element = element.with(key.to_owned(), Value::String(value.to_owned()));
heap.store_object(element_id, updated_element)
.unwrap_or_else(|h| h)
} else {
heap
}
}
fn string_arg(args: &[Value], idx: usize) -> String {
match args.get(idx) {
Some(Value::String(s)) => s.clone(),
Some(other) => format!("{other}"),
None => String::new(),
}
}
#[must_use]
pub fn build_attributes_object(pairs: &[(String, String)], heap: Heap) -> (Value, Heap) {
let map: BTreeMap<String, Value> = pairs
.iter()
.map(|(k, v)| (k.clone(), Value::String(v.clone())))
.collect();
let (id, heap) = heap.alloc_object(Object::from_properties(map));
(Value::Object(id), heap)
}
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::unnecessary_wraps)]
pub fn query_selector_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let selector = string_arg(&args, 0);
let pattern = parse_simple_selector(&selector);
let outcome =
find_matching(&this, &pattern, &heap).map_or(Outcome::Normal(Value::Null), Outcome::Normal);
Ok((outcome, heap, fuel))
}
#[derive(Debug, Clone, Default)]
struct SelectorPattern {
tag: Option<String>,
id: Option<String>,
class: Option<String>,
}
fn parse_simple_selector(source: &str) -> SelectorPattern {
let trimmed = source.trim();
parse_selector_recursive(trimmed, 0, SelectorPattern::default())
}
fn parse_selector_recursive(source: &str, start: usize, acc: SelectorPattern) -> SelectorPattern {
let bytes = source.as_bytes();
if start >= bytes.len() {
acc
} else {
match bytes.get(start) {
Some(b'#') => {
let end = scan_ident(bytes, start + 1);
let name = source.get(start + 1..end).unwrap_or("").to_owned();
parse_selector_recursive(
source,
end,
SelectorPattern {
id: Some(name),
..acc
},
)
}
Some(b'.') => {
let end = scan_ident(bytes, start + 1);
let name = source.get(start + 1..end).unwrap_or("").to_owned();
parse_selector_recursive(
source,
end,
SelectorPattern {
class: Some(name),
..acc
},
)
}
Some(_) => {
let end = scan_ident(bytes, start);
let name = source.get(start..end).unwrap_or("").to_ascii_lowercase();
if name.is_empty() {
acc
} else {
parse_selector_recursive(
source,
end,
SelectorPattern {
tag: Some(name),
..acc
},
)
}
}
None => acc,
}
}
}
fn scan_ident(bytes: &[u8], start: usize) -> usize {
bytes
.iter()
.enumerate()
.skip(start)
.find(|(_, b)| !is_ident_byte(**b))
.map_or(bytes.len(), |(i, _)| i)
}
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'-' || b == b'_'
}
fn find_matching(this: &Value, pattern: &SelectorPattern, heap: &Heap) -> Option<Value> {
let root_id = object_id_of(this)?;
walk_descendants(root_id, pattern, heap)
}
fn walk_descendants(node_id: ObjectId, pattern: &SelectorPattern, heap: &Heap) -> Option<Value> {
let children = element_children(node_id, heap);
children.iter().find_map(|child_id| {
if matches_pattern(*child_id, pattern, heap) {
Some(Value::Object(*child_id))
} else {
walk_descendants(*child_id, pattern, heap)
}
})
}
fn element_children(node_id: ObjectId, heap: &Heap) -> Vec<ObjectId> {
let Some(object) = heap.object(node_id) else {
return Vec::new();
};
let Some(children_id) = object.get("children").and_then(object_id_of) else {
return Vec::new();
};
let Some(children) = heap.object(children_id) else {
return Vec::new();
};
let length = array_length(children);
(0..length)
.filter_map(|i| children.get(&format!("{i}")).and_then(object_id_of))
.collect()
}
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 len = *n as u32;
len
}
Some(_) | None => 0,
}
}
fn matches_pattern(node_id: ObjectId, pattern: &SelectorPattern, heap: &Heap) -> bool {
let Some(object) = heap.object(node_id) else {
return false;
};
let tag_ok = pattern
.tag
.as_ref()
.is_none_or(|want| string_property(object, "tagName").eq_ignore_ascii_case(want));
let id_ok = pattern
.id
.as_ref()
.is_none_or(|want| string_property(object, "id") == *want);
let class_ok = pattern.class.as_ref().is_none_or(|want| {
string_property(object, "className")
.split_ascii_whitespace()
.any(|c| c == want)
});
tag_ok && id_ok && class_ok
}
fn string_property(object: &Object, key: &str) -> String {
match object.get(key) {
Some(Value::String(s)) => s.clone(),
Some(_) | None => String::new(),
}
}
#[must_use]
pub fn find_first_descendant(root: ObjectId, pattern_source: &str, heap: &Heap) -> Option<Value> {
let pattern = parse_simple_selector(pattern_source);
walk_descendants(root, &pattern, heap)
}
#[must_use]
pub fn find_by_id(root: ObjectId, id: &str, heap: &Heap) -> Option<Value> {
let pattern = SelectorPattern {
id: Some(id.to_owned()),
..SelectorPattern::default()
};
walk_descendants(root, &pattern, heap)
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn append_child_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let child = args.first().cloned().unwrap_or(Value::Undefined);
let outcome_heap = append_child_to_parent(&this, &child, heap);
Ok((Outcome::Normal(child), outcome_heap, fuel))
}
fn append_child_to_parent(this: &Value, child: &Value, heap: Heap) -> Heap {
if let Some((children_id, children_obj)) = children_object_of(this, &heap) {
let length = array_length(&children_obj);
let updated_children = children_obj
.with(format!("{length}"), child.clone())
.with("length".to_owned(), Value::Number(f64::from(length + 1)));
heap.store_object(children_id, updated_children)
.unwrap_or_else(|h| h)
} else {
heap
}
}
fn children_object_of(this: &Value, heap: &Heap) -> Option<(ObjectId, Object)> {
let parent_id = object_id_of(this)?;
let parent = heap.object(parent_id)?;
let children_id = parent.get("children").and_then(|v| match v {
Value::Object(id) => Some(*id),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::String(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})?;
let children_obj = heap.object(children_id)?.clone();
Some((children_id, children_obj))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn remove_child_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let child = args.first().cloned().unwrap_or(Value::Undefined);
let outcome_heap = remove_child_from_parent(&this, &child, heap);
Ok((Outcome::Normal(child), outcome_heap, fuel))
}
fn remove_child_from_parent(this: &Value, child: &Value, heap: Heap) -> Heap {
let Some((children_id, children_obj)) = children_object_of(this, &heap) else {
return heap;
};
let Some(child_id) = object_id_of(child) else {
return heap;
};
let length = array_length(&children_obj);
let Some(removal_index) = (0..length)
.find(|i| children_obj.get(&format!("{i}")).and_then(object_id_of) == Some(child_id))
else {
return heap;
};
let new_length = length.saturating_sub(1);
let pairs: BTreeMap<String, Value> = (0..length)
.filter(|i| *i != removal_index)
.enumerate()
.map(|(new_idx, old_idx)| {
let value = children_obj
.get(&format!("{old_idx}"))
.cloned()
.unwrap_or(Value::Null);
(format!("{new_idx}"), value)
})
.chain(std::iter::once((
"length".to_owned(),
Value::Number(f64::from(new_length)),
)))
.collect();
let new_children = Object::from_properties(pairs);
heap.store_object(children_id, new_children)
.unwrap_or_else(|h| h)
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn insert_before_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let new_node = args.first().cloned().unwrap_or(Value::Undefined);
let ref_node = args.get(1).cloned().unwrap_or(Value::Null);
let outcome_heap = insert_before_into_parent(&this, &new_node, &ref_node, heap);
Ok((Outcome::Normal(new_node), outcome_heap, fuel))
}
fn insert_before_into_parent(this: &Value, new_node: &Value, ref_node: &Value, heap: Heap) -> Heap {
let ref_id_opt = object_id_of(ref_node);
if ref_id_opt.is_none() {
append_child_to_parent(this, new_node, heap)
} else {
let Some((children_id, children_obj)) = children_object_of(this, &heap) else {
return heap;
};
let Some(ref_id) = ref_id_opt else {
return heap;
};
let length = array_length(&children_obj);
let Some(insert_index) = (0..length)
.find(|i| children_obj.get(&format!("{i}")).and_then(object_id_of) == Some(ref_id))
else {
return heap;
};
let new_length = length + 1;
let pairs: BTreeMap<String, Value> = (0..new_length)
.map(|new_idx| {
let value = match new_idx.cmp(&insert_index) {
std::cmp::Ordering::Less => children_obj
.get(&format!("{new_idx}"))
.cloned()
.unwrap_or(Value::Null),
std::cmp::Ordering::Equal => new_node.clone(),
std::cmp::Ordering::Greater => {
let old_idx = new_idx - 1;
children_obj
.get(&format!("{old_idx}"))
.cloned()
.unwrap_or(Value::Null)
}
};
(format!("{new_idx}"), value)
})
.chain(std::iter::once((
"length".to_owned(),
Value::Number(f64::from(new_length)),
)))
.collect();
let new_children = Object::from_properties(pairs);
heap.store_object(children_id, new_children)
.unwrap_or_else(|h| h)
}
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn class_list_add_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let token = string_arg(&args, 0);
let new_heap = add_class_token(&this, &token, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn class_list_remove_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let token = string_arg(&args, 0);
let new_heap = remove_class_token(&this, &token, heap);
Ok((Outcome::Normal(Value::Undefined), new_heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn class_list_contains_impl(
args: Vec<Value>,
this: Value,
heap: Heap,
fuel: Fuel,
) -> EvalResult {
let token = string_arg(&args, 0);
let present = element_of_class_list(&this, &heap)
.is_some_and(|(_, element)| class_list_tokens(&element).iter().any(|t| t == &token));
Ok((Outcome::Normal(Value::Boolean(present)), heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
pub fn class_list_toggle_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let token = string_arg(&args, 0);
let (present, new_heap) = toggle_class_token(&this, &token, heap);
Ok((Outcome::Normal(Value::Boolean(present)), new_heap, fuel))
}
fn element_of_class_list(this: &Value, heap: &Heap) -> Option<(ObjectId, Object)> {
let list_id = object_id_of(this)?;
let list = heap.object(list_id)?;
let element_id = list.get("__element__").and_then(|v| match v {
Value::Object(id) => Some(*id),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::String(_)
| Value::Function(_)
| Value::Native(_)
| Value::Promise(_) => None,
})?;
let element = heap.object(element_id)?.clone();
Some((element_id, element))
}
fn class_list_tokens(element: &Object) -> Vec<String> {
match element.get("className") {
Some(Value::String(s)) => s.split_ascii_whitespace().map(str::to_owned).collect(),
Some(_) | None => Vec::new(),
}
}
fn add_class_token(this: &Value, token: &str, heap: Heap) -> Heap {
let Some((element_id, element)) = element_of_class_list(this, &heap) else {
return heap;
};
let tokens = class_list_tokens(&element);
let new_tokens: Vec<String> = if tokens.iter().any(|t| t == token) {
tokens
} else {
tokens
.into_iter()
.chain(std::iter::once(token.to_owned()))
.collect()
};
let new_class = new_tokens.join(" ");
let element_value = Value::Object(element_id);
write_attribute(&element_value, "class", &new_class, heap)
}
fn remove_class_token(this: &Value, token: &str, heap: Heap) -> Heap {
let Some((element_id, element)) = element_of_class_list(this, &heap) else {
return heap;
};
let tokens = class_list_tokens(&element);
let new_tokens: Vec<String> = tokens.into_iter().filter(|t| t != token).collect();
let new_class = new_tokens.join(" ");
let element_value = Value::Object(element_id);
write_attribute(&element_value, "class", &new_class, heap)
}
fn toggle_class_token(this: &Value, token: &str, heap: Heap) -> (bool, Heap) {
let Some((element_id, element)) = element_of_class_list(this, &heap) else {
return (false, heap);
};
let tokens = class_list_tokens(&element);
let was_present = tokens.iter().any(|t| t == token);
let new_tokens: Vec<String> = if was_present {
tokens.into_iter().filter(|t| t != token).collect()
} else {
tokens
.into_iter()
.chain(std::iter::once(token.to_owned()))
.collect()
};
let new_class = new_tokens.join(" ");
let element_value = Value::Object(element_id);
let new_heap = write_attribute(&element_value, "class", &new_class, heap);
(!was_present, new_heap)
}