use boa_engine::{Context, JsNativeError, JsResult, JsValue, js_string};
use crate::tdom::node::{Node, NodeId, NodeType};
use super::{
ScriptState, collect_text, mark_dirty, node_wrapper, this_node_id, with_state,
wrapper_is_cached,
};
pub(super) fn native_get_value(
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)
.map(|n| {
if matches!(n.node_type, NodeType::Output | NodeType::Textarea) {
n.text_content(&st.arena)
} else {
n.attributes.get("value").cloned().unwrap_or_default()
}
})
.unwrap_or_default();
Ok(js_string!(value).into())
}
pub(super) fn native_set_value(
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 st
.arena
.get(node_id)
.is_some_and(|n| matches!(n.node_type, NodeType::Output | NodeType::Textarea))
{
return set_text_content(&mut st, node_id, &value);
}
let existing = st
.arena
.get(node_id)
.and_then(|n| n.attributes.get("value"))
.cloned()
.unwrap_or_default();
if existing == value {
return Ok(JsValue::undefined());
}
if let Some(node) = st.arena.get_mut(node_id) {
node.attributes.insert("value".to_string(), value);
mark_dirty(&mut st, node_id);
}
Ok(JsValue::undefined())
}
pub(super) fn native_get_text(
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 text = collect_text(&st.arena, node_id);
Ok(js_string!(text).into())
}
pub(super) fn native_set_text(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let text = 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();
set_text_content(&mut st, node_id, &text)
}
fn set_text_content(st: &mut ScriptState, node_id: NodeId, text: &str) -> JsResult<JsValue> {
let is_text_like = matches!(
st.arena.get(node_id).map(|n| n.node_type),
Some(NodeType::Text | NodeType::Comment)
);
if is_text_like {
let existing = st
.arena
.get(node_id)
.and_then(|n| n.data.clone())
.unwrap_or_default();
if existing == text {
return Ok(JsValue::undefined());
}
if let Some(n) = st.arena.get_mut(node_id) {
n.data = Some(text.to_string());
}
mark_dirty(st, node_id);
return Ok(JsValue::undefined());
}
let reuse_child = st
.arena
.get(node_id)
.filter(|n| n.children.len() == 1)
.and_then(|n| n.children.first().copied())
.filter(|&c| matches!(st.arena.get(c).map(|n| n.node_type), Some(NodeType::Text)));
if let Some(child_id) = reuse_child {
let existing = st
.arena
.get(child_id)
.and_then(|c| c.data.clone())
.unwrap_or_default();
if existing == text {
return Ok(JsValue::undefined());
}
if let Some(c) = st.arena.get_mut(child_id) {
c.data = Some(text.to_string());
}
mark_dirty(st, node_id);
return Ok(JsValue::undefined());
}
let Some(node) = st.arena.get_mut(node_id) else {
return Ok(JsValue::undefined());
};
let old_children = std::mem::take(&mut node.children);
for child in old_children {
if let Some(c) = st.arena.get_mut(child) {
c.parent = None;
}
}
let text_id = st.arena.insert(Node::text(text));
if let Some(parent) = st.arena.get_mut(node_id) {
parent.children.push(text_id);
}
if let Some(t) = st.arena.get_mut(text_id) {
t.parent = Some(node_id);
}
mark_dirty(st, node_id);
Ok(JsValue::undefined())
}
pub(super) fn native_get_inner_html(
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 html = crate::html::html_parse::serialize_inner_html(&st.arena, node_id);
Ok(js_string!(html).into())
}
pub(super) fn native_get_outer_html(
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 html = crate::html::html_parse::serialize_outer_html(&st.arena, node_id);
Ok(js_string!(html).into())
}
pub(super) fn native_set_inner_html(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let html = 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();
set_inner_html(&mut st, node_id, &html, context)?;
}
crate::script::bindings::compile_attribute_listeners_for_subtree(context, &rc, node_id)?;
Ok(JsValue::undefined())
}
fn set_inner_html(
st: &mut ScriptState,
node_id: NodeId,
html: &str,
context: &mut Context,
) -> JsResult<JsValue> {
let is_element = st
.arena
.get(node_id)
.is_some_and(|n| n.node_type_constant() == 1);
if !is_element {
return Ok(JsValue::undefined());
}
let replaced = st
.arena
.get(node_id)
.map(|n| n.children.clone())
.unwrap_or_default();
let mut css_sources = Vec::new();
crate::html::html_parse::parse_fragment_into(&mut st.arena, node_id, html, &mut css_sources);
if !css_sources.is_empty() {
st.dirty
.injected_css
.insert(node_id, css_sources.join("\n"));
}
sweep_replaced_subtrees(st, &replaced, context)?;
mark_dirty(st, node_id);
Ok(JsValue::undefined())
}
fn sweep_replaced_subtrees(
st: &mut ScriptState,
roots: &[NodeId],
context: &mut Context,
) -> JsResult<()> {
for &root in roots {
let mut doomed: Vec<NodeId> = Vec::new();
let mut stack = vec![root];
while let Some(id) = stack.pop() {
if wrapper_is_cached(context, id)? {
continue;
}
doomed.push(id);
if let Some(node) = st.arena.get(id) {
stack.extend(node.children.iter().copied());
}
}
for id in doomed {
st.dirty.injected_css.swap_remove(&id);
crate::script::bindings::clear_node_attribute_listeners(context, id)?;
st.arena.remove(id);
}
}
Ok(())
}
pub(super) fn native_get_node_value(
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());
};
match node.node_type {
NodeType::Text | NodeType::Comment => {
Ok(js_string!(node.data.as_deref().unwrap_or("")).into())
}
_ => Ok(JsValue::null()),
}
}
pub(super) fn native_set_node_value(
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();
let Some(node) = st.arena.get_mut(node_id) else {
return Ok(JsValue::undefined());
};
if matches!(node.node_type, NodeType::Text | NodeType::Comment) {
node.data = Some(value);
mark_dirty(&mut st, node_id);
}
Ok(JsValue::undefined())
}
pub(super) fn native_get_data(
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 data = st
.arena
.get(node_id)
.and_then(|n| n.data.clone())
.unwrap_or_default();
Ok(js_string!(data).into())
}
pub(super) fn native_set_data(
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(n) = st.arena.get_mut(node_id) {
if n.data.as_deref() == Some(value.as_str()) {
return Ok(JsValue::undefined());
}
n.data = Some(value);
mark_dirty(&mut st, node_id);
}
Ok(JsValue::undefined())
}
fn checked_char_offset(data: &str, offset: u32) -> JsResult<usize> {
let len = data.chars().count();
let offset = offset as usize;
if offset > len {
return Err(JsNativeError::range()
.with_message(format!(
"IndexSizeError: offset {offset} exceeds data length {len}"
))
.into());
}
Ok(offset)
}
fn replace_char_range(data: &str, offset: usize, count: u32, insert: &str) -> String {
let mut chars: Vec<char> = data.chars().collect();
let end = (offset + count as usize).min(chars.len());
chars.splice(offset..end, insert.chars());
chars.into_iter().collect()
}
fn uint_arg(args: &[JsValue], index: usize, context: &mut Context) -> JsResult<u32> {
match args.get(index) {
Some(v) => v.to_u32(context),
None => Ok(0),
}
}
fn string_arg(args: &[JsValue], index: usize, context: &mut Context) -> JsResult<String> {
match args.get(index) {
Some(v) => Ok(v.to_string(context)?.to_std_string_lossy()),
None => Ok(String::new()),
}
}
fn text_data(arena: &crate::tdom::node::NodeArena, node_id: NodeId) -> String {
arena
.get(node_id)
.and_then(|n| n.data.clone())
.unwrap_or_default()
}
fn store_data(st: &mut ScriptState, node_id: NodeId, new: String) {
let Some(node) = st.arena.get_mut(node_id) else {
return;
};
let changed = node.data.as_deref() != Some(new.as_str());
node.data = Some(new);
if changed {
mark_dirty(st, node_id);
}
}
pub(super) fn native_substring_data(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let offset = uint_arg(args, 0, context)?;
let count = uint_arg(args, 1, context)?;
let Some(rc) = with_state() else {
return Ok(js_string!().into());
};
let st = rc.borrow();
let data = text_data(&st.arena, node_id);
let offset = checked_char_offset(&data, offset)?;
Ok(js_string!(
data.chars()
.skip(offset)
.take(count as usize)
.collect::<String>()
)
.into())
}
pub(super) fn native_append_data(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let text = string_arg(args, 0, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
let data = text_data(&st.arena, node_id);
let new = format!("{data}{text}");
store_data(&mut st, node_id, new);
Ok(JsValue::undefined())
}
pub(super) fn native_insert_data(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let offset = uint_arg(args, 0, context)?;
let text = string_arg(args, 1, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
let data = text_data(&st.arena, node_id);
let offset = checked_char_offset(&data, offset)?;
let new = replace_char_range(&data, offset, 0, &text);
store_data(&mut st, node_id, new);
Ok(JsValue::undefined())
}
pub(super) fn native_delete_data(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let offset = uint_arg(args, 0, context)?;
let count = uint_arg(args, 1, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
let data = text_data(&st.arena, node_id);
let offset = checked_char_offset(&data, offset)?;
let new = replace_char_range(&data, offset, count, "");
store_data(&mut st, node_id, new);
Ok(JsValue::undefined())
}
pub(super) fn native_replace_data(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let offset = uint_arg(args, 0, context)?;
let count = uint_arg(args, 1, context)?;
let text = string_arg(args, 2, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::undefined());
};
let mut st = rc.borrow_mut();
let data = text_data(&st.arena, node_id);
let offset = checked_char_offset(&data, offset)?;
let new = replace_char_range(&data, offset, count, &text);
store_data(&mut st, node_id, new);
Ok(JsValue::undefined())
}
pub(super) fn native_get_data_length(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let len = with_state()
.map(|rc| {
let st = rc.borrow();
text_data(&st.arena, node_id).chars().count()
})
.unwrap_or(0);
Ok(JsValue::from(len as u32))
}
pub(super) fn native_split_text(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let node_id = this_node_id(this, context)?;
let offset = uint_arg(args, 0, context)?;
let Some(rc) = with_state() else {
return Ok(JsValue::null());
};
let mut st = rc.borrow_mut();
let data = text_data(&st.arena, node_id);
let offset = checked_char_offset(&data, offset)?;
let head: String = data.chars().take(offset).collect();
let tail: String = data.chars().skip(offset).collect();
let parent = st.arena.get(node_id).and_then(|n| n.parent);
store_data(&mut st, node_id, head);
let tail_id = st.arena.insert(Node::text(&tail));
if let Some(pid) = parent {
if let Some(pnode) = st.arena.get_mut(pid) {
let pos = pnode
.children
.iter()
.position(|&c| c == node_id)
.map_or(pnode.children.len(), |p| p + 1);
pnode.children.insert(pos, tail_id);
}
if let Some(t) = st.arena.get_mut(tail_id) {
t.parent = Some(pid);
}
mark_dirty(&mut st, pid);
}
drop(st);
node_wrapper(context, tail_id)
}