use boa_engine::{
Context, JsNativeError, JsResult, JsValue, js_string,
object::{JsObject, ObjectInitializer},
property::Attribute,
};
use super::{mark_dirty, this_node_id, with_state};
pub(super) fn native_get_attribute(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let name = args
.first()
.and_then(|v| v.as_string())
.map(|s| s.to_std_string_lossy().to_ascii_lowercase());
let Some(name) = name else {
return Err(JsNativeError::typ()
.with_message("getAttribute expects a string")
.into());
};
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let st = rc.borrow();
let value = st
.arena
.get(node_id)
.and_then(|n| n.attributes.get(&name).cloned());
Ok(value
.map(|s| js_string!(s).into())
.unwrap_or(JsValue::null()))
}
pub(super) fn native_set_attribute(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let name = args
.first()
.and_then(|v| v.as_string())
.map(|s| s.to_std_string_lossy().to_ascii_lowercase());
let value = args
.get(1)
.and_then(|v| v.as_string())
.map(|s| s.to_std_string_lossy());
let (Some(name), Some(value)) = (name, value) else {
return Err(JsNativeError::typ()
.with_message("setAttribute expects two strings")
.into());
};
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let wires_handler = crate::script::bindings::attribute_event_type(&name).is_some();
{
let mut st = rc.borrow_mut();
if let Some(node) = st.arena.get_mut(node_id) {
node.attributes.insert(name, value);
mark_dirty(&mut st, node_id);
}
}
if wires_handler {
crate::script::bindings::compile_node_attribute_listeners(context, &rc, node_id)?;
}
Ok(JsValue::undefined())
}
pub(super) fn native_remove_attribute(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let name = args
.first()
.and_then(|v| v.as_string())
.map(|s| s.to_std_string_lossy().to_ascii_lowercase());
let Some(name) = name else {
return Err(JsNativeError::typ()
.with_message("removeAttribute expects a string")
.into());
};
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let unwired_handler = crate::script::bindings::attribute_event_type(&name);
{
let mut st = rc.borrow_mut();
if let Some(node) = st.arena.get_mut(node_id) {
node.attributes.shift_remove(&name);
mark_dirty(&mut st, node_id);
}
}
if let Some(event_type) = unwired_handler {
super::events::clear_attr_handler(context, node_id, event_type)?;
}
Ok(JsValue::undefined())
}
fn attr_name_arg(args: &[JsValue], method: &str) -> JsResult<String> {
args.first()
.and_then(|v| v.as_string())
.map(|s| s.to_std_string_lossy().to_ascii_lowercase())
.ok_or_else(|| {
JsNativeError::typ()
.with_message(format!("{method} expects a string"))
.into()
})
}
pub(super) fn native_has_attribute(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let name = attr_name_arg(args, "hasAttribute")?;
let has = with_state()
.map(|rc| {
rc.borrow()
.arena
.get(node_id)
.map(|n| n.attributes.contains_key(&name))
.unwrap_or(false)
})
.unwrap_or(false);
Ok(JsValue::from(has))
}
pub(super) fn native_toggle_attribute(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let name = attr_name_arg(args, "toggleAttribute")?;
let force = args.get(1).and_then(|v| v.as_boolean());
let Some(rc) = with_state() else {
return Ok(JsValue::from(false));
};
let added;
{
let mut st = rc.borrow_mut();
let Some(node) = st.arena.get_mut(node_id) else {
return Ok(JsValue::from(false));
};
let present = node.attributes.contains_key(&name);
added = force.unwrap_or(!present);
if added == present {
return Ok(JsValue::from(present));
}
if added {
node.attributes.insert(name.clone(), String::new());
} else {
node.attributes.shift_remove(&name);
}
mark_dirty(&mut st, node_id);
}
match crate::script::bindings::attribute_event_type(&name) {
Some(_) if added => {
crate::script::bindings::compile_node_attribute_listeners(context, &rc, node_id)?;
}
Some(event_type) => {
super::events::clear_attr_handler(context, node_id, event_type)?;
}
None => {}
}
Ok(JsValue::from(added))
}
pub(super) fn native_get_id(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let Some(rc) = with_state() else {
return Ok(js_string!().into());
};
let st = rc.borrow();
let value = st
.arena
.get(node_id)
.and_then(|n| n.attributes.get("id").cloned())
.unwrap_or_default();
Ok(js_string!(value).into())
}
pub(super) fn native_set_id(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let value = args
.first()
.map(|v| v.to_string(context).map(|s| s.to_std_string_lossy()))
.transpose()?
.unwrap_or_default();
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
if let Some(node) = st.arena.get_mut(node_id) {
node.attributes.insert("id".to_string(), value);
mark_dirty(&mut st, node_id);
}
Ok(JsValue::undefined())
}
pub(super) fn native_get_attributes(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let st = rc.borrow();
let Some(node) = st.arena.get(node_id) else {
return Ok(JsValue::undefined());
};
let entries: Vec<(String, String)> = node
.attributes
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
drop(st);
let get_named_item = super::js_fn(
context,
boa_engine::NativeFunction::from_fn_ptr(named_item_lookup),
);
let mut records = Vec::with_capacity(entries.len());
for (name, value) in entries.iter() {
records.push((
attr_record(context, name, value)?,
js_string!(value.as_str()),
));
}
let mut init = ObjectInitializer::new(context);
init.property(js_string!("length"), entries.len() as u32, Attribute::all());
init.property(js_string!("getNamedItem"), get_named_item, Attribute::all());
for (idx, (record, value_str)) in records.into_iter().enumerate() {
let name = &entries[idx].0;
init.property(idx, record, Attribute::all());
init.property(js_string!(name.as_str()), value_str, Attribute::all());
}
Ok(init.build().into())
}
fn attr_record(context: &mut Context, name: &str, value: &str) -> JsResult<JsObject> {
let mut rec = ObjectInitializer::new(context);
rec.property(js_string!("name"), js_string!(name), Attribute::all());
rec.property(js_string!("value"), js_string!(value), Attribute::all());
Ok(rec.build())
}
fn named_item_lookup(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let obj = this
.to_object(context)
.map_err(|_| JsNativeError::typ().with_message("expected an attributes object"))?;
let name = args
.first()
.map(|v| v.to_string(context).map(|s| s.to_std_string_lossy()))
.transpose()?
.unwrap_or_default()
.to_ascii_lowercase();
let length = obj.get(js_string!("length"), context)?.to_u32(context)?;
for i in 0..length {
let record = obj.get(i, context)?;
if let Some(rec) = record.as_object() {
let rec_name = rec
.get(js_string!("name"), context)?
.to_string(context)?
.to_std_string_lossy()
.to_ascii_lowercase();
if rec_name == name {
return Ok(record);
}
}
}
Ok(JsValue::null())
}