use boa_engine::{Context, JsNativeError, JsResult, JsValue, NativeFunction, js_string};
use crate::{
html::css_parse::{CssSelector, selector_chain_matches},
tdom::node::{NodeArena, NodeId, document_base_href},
};
use super::{f64_to_node_id, js_fn, node_id_to_f64, node_wrapper, this_node_id, with_state};
pub(super) fn native_get_parent(
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::null());
};
let st = rc.borrow();
let parent = st.arena.get(node_id).and_then(|n| n.parent);
match parent {
Some(pid) => node_wrapper(context, pid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_node_type(
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::null());
};
let st = rc.borrow();
let Some(node) = st.arena.get(node_id) else {
return Ok(JsValue::null());
};
Ok(JsValue::from(node.node_type_constant() as u32))
}
pub(super) fn native_get_node_name(
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 Some(node) = st.arena.get(node_id) else {
return Ok(js_string!().into());
};
Ok(js_string!(node.node_name()).into())
}
pub(super) fn native_get_tag_name(
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 name = st
.arena
.get(node_id)
.and_then(|n| n.node_type.tag_name())
.map(str::to_uppercase)
.unwrap_or_default();
Ok(js_string!(name).into())
}
pub(super) fn native_get_parent_element(
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::null());
};
let st = rc.borrow();
let parent = st.arena.get(node_id).and_then(|n| n.parent).filter(|&pid| {
st.arena
.get(pid)
.map(|p| p.node_type_constant() == 1)
.unwrap_or(false)
});
match parent {
Some(pid) => node_wrapper(context, pid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_child_nodes(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
call_live_list(context, node_id)
}
fn call_live_list(context: &mut Context, node_id: NodeId) -> JsResult<JsValue> {
let node_key = node_id_to_f64(node_id);
let get_len = js_fn(
context,
NativeFunction::from_copy_closure_with_captures(nodelist_len, node_key),
);
let get_item = js_fn(
context,
NativeFunction::from_copy_closure_with_captures(nodelist_item, node_key),
);
let global = context.global_object();
let helper = global
.get(js_string!("__htuiLiveList"), context)?
.to_object(context)?;
helper.call(
&JsValue::undefined(),
&[get_len.into(), get_item.into()],
context,
)
}
fn nodelist_len(
_this: &JsValue,
_args: &[JsValue],
node_key: &f64,
_context: &mut Context,
) -> JsResult<JsValue> {
let node_id = f64_to_node_id(*node_key);
let len = with_state()
.map(|rc| {
rc.borrow()
.arena
.get(node_id)
.map(|n| n.children.len())
.unwrap_or(0)
})
.unwrap_or(0);
Ok(JsValue::from(len as u32))
}
fn nodelist_item(
_this: &JsValue,
args: &[JsValue],
node_key: &f64,
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = f64_to_node_id(*node_key);
let idx = args
.first()
.and_then(|v| v.to_number(context).ok())
.unwrap_or(-1.0);
if idx < 0.0 {
return Ok(JsValue::null());
}
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let st = rc.borrow();
let child = st
.arena
.get(node_id)
.and_then(|n| n.children.get(idx as usize))
.copied();
match child {
Some(cid) => node_wrapper(context, cid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_first_child(
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::null());
};
let st = rc.borrow();
let child = st
.arena
.get(node_id)
.and_then(|n| n.children.first())
.copied();
match child {
Some(cid) => node_wrapper(context, cid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_last_child(
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::null());
};
let st = rc.borrow();
let child = st
.arena
.get(node_id)
.and_then(|n| n.children.last())
.copied();
match child {
Some(cid) => node_wrapper(context, cid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_prev_sibling(
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::null());
};
let st = rc.borrow();
let prev = st
.arena
.get(node_id)
.and_then(|n| n.parent)
.and_then(|pid| {
let parent = &st.arena[pid];
let mut prev = None;
for &cid in &parent.children {
if cid == node_id {
break;
}
prev = Some(cid);
}
prev
});
match prev {
Some(cid) => node_wrapper(context, cid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_next_sibling(
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::null());
};
let st = rc.borrow();
let next = st
.arena
.get(node_id)
.and_then(|n| n.parent)
.and_then(|pid| {
let parent = &st.arena[pid];
let mut found = false;
let mut next = None;
for &cid in &parent.children {
if found {
next = Some(cid);
break;
}
if cid == node_id {
found = true;
}
}
next
});
match next {
Some(cid) => node_wrapper(context, cid),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_get_owner_document(
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::null());
};
let st = rc.borrow();
if node_id == st.root_id {
return Ok(JsValue::null());
}
node_wrapper(context, st.root_id)
}
pub(super) fn native_get_base_uri(
_this: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<JsValue> {
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let st = rc.borrow();
let uri = document_base_href(&st.arena, st.root_id).unwrap_or_default();
Ok(js_string!(uri).into())
}
pub(super) fn native_has_child_nodes(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let has = with_state()
.map(|rc| {
rc.borrow()
.arena
.get(node_id)
.map(|n| !n.children.is_empty())
.unwrap_or(false)
})
.unwrap_or(false);
Ok(JsValue::from(has))
}
pub(super) fn native_contains(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let Some(other_id) = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context).ok()
else {
return Ok(JsValue::from(false));
};
let Some(rc) = with_state() else {
return Ok(JsValue::from(false));
};
let st = rc.borrow();
let mut cur = Some(other_id);
while let Some(id) = cur {
if id == node_id {
return Ok(JsValue::from(true));
}
cur = st.arena.get(id).and_then(|n| n.parent);
}
Ok(JsValue::from(false))
}
pub(super) fn native_is_same_node(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let other = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context).ok();
Ok(JsValue::from(other == Some(node_id)))
}
pub(super) fn native_is_equal_node(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let Some(other_id) = this_node_id(args.first().unwrap_or(&JsValue::undefined()), context).ok()
else {
return Ok(JsValue::from(false));
};
let Some(rc) = with_state() else {
return Ok(JsValue::from(false));
};
let st = rc.borrow();
Ok(JsValue::from(nodes_equal(&st.arena, node_id, other_id)))
}
fn nodes_equal(arena: &NodeArena, a: NodeId, b: NodeId) -> bool {
let (na, nb) = match (arena.get(a), arena.get(b)) {
(Some(x), Some(y)) => (x, y),
_ => return false,
};
if na.node_type != nb.node_type || na.data != nb.data {
return false;
}
if na.attributes.len() != nb.attributes.len() {
return false;
}
for (k, v) in &na.attributes {
if nb.attributes.get(k) != Some(v) {
return false;
}
}
na.children.len() == nb.children.len()
&& na
.children
.iter()
.zip(&nb.children)
.all(|(&ca, &cb)| nodes_equal(arena, ca, cb))
}
fn parse_query_selector(args: &[JsValue], context: &mut Context) -> JsResult<CssSelector> {
let raw = args
.first()
.map(|v| v.to_string(context).map(|s| s.to_std_string_lossy()))
.transpose()?
.unwrap_or_default();
raw.parse::<CssSelector>().map_err(|e| {
JsNativeError::syntax()
.with_message(format!("invalid selector `{raw}`: {e}"))
.into()
})
}
fn query_selector_nodes(arena: &NodeArena, scope: NodeId, selector: &CssSelector) -> Vec<NodeId> {
let mut out = Vec::new();
let mut stack: Vec<NodeId> = arena[scope].children.iter().rev().copied().collect();
while let Some(id) = stack.pop() {
if selector_chain_matches(arena, id, selector) {
out.push(id);
}
stack.extend(arena[id].children.iter().rev().copied());
}
out
}
pub(super) fn native_query_selector(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let selector = parse_query_selector(args, context)?;
let scope = this_node_id(this, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let st = rc.borrow();
let first = query_selector_nodes(&st.arena, scope, &selector)
.into_iter()
.next();
drop(st);
match first {
Some(node_id) => node_wrapper(context, node_id),
None => Ok(JsValue::null()),
}
}
pub(super) fn native_query_selector_all(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let selector = parse_query_selector(args, context)?;
let scope = this_node_id(this, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let st = rc.borrow();
let ids = query_selector_nodes(&st.arena, scope, &selector);
drop(st);
let arr = boa_engine::object::builtins::JsArray::new(context);
for id in ids {
arr.push(node_wrapper(context, id)?, context)?;
}
Ok(arr.into())
}