use blitz_dom::NodeId;
use blitz_dom::local_name;
use boa_engine::object::builtins::JsArray;
use boa_engine::object::{JsObject, ObjectInitializer};
use boa_engine::property::Attribute;
use boa_engine::value::JsValue;
use boa_engine::{Context, JsResult, JsString, NativeFunction, js_string};
use super::{
define_accessor, define_method, dom_ctx, node_or_null, node_wrapper, qual_name, qual_name_ns,
this_node_id, to_rust_string,
};
use crate::state::DomCtx;
pub(crate) fn init_document_proto(proto: &JsObject, context: &mut Context) {
define_accessor(
proto,
"documentElement",
Some(document_element),
None,
context,
);
define_accessor(proto, "body", Some(body), None, context);
define_accessor(proto, "head", Some(head), None, context);
define_accessor(proto, "activeElement", Some(active_element), None, context);
define_accessor(proto, "defaultView", Some(default_view), None, context);
define_method(proto, "createElement", 1, create_element, context);
define_method(proto, "createElementNS", 2, create_element_ns, context);
define_method(proto, "createTextNode", 1, create_text_node, context);
define_method(proto, "importNode", 2, import_node, context);
define_method(proto, "getSelection", 0, get_selection, context);
define_method(proto, "createComment", 1, create_comment, context);
define_method(proto, "getElementById", 1, get_element_by_id, context);
define_method(proto, "querySelector", 1, query_selector, context);
define_method(proto, "querySelectorAll", 1, query_selector_all, context);
}
fn find_tag(ctx: &DomCtx, tag: blitz_dom::LocalName) -> Option<NodeId> {
let doc = ctx.doc.borrow();
let root = doc.try_root_element()?;
if root.data.is_element_with_tag_name(&tag) {
return Some(root.id);
}
root.children
.iter()
.copied()
.find(|child_id| {
doc.get_node(*child_id)
.is_some_and(|child| child.data.is_element_with_tag_name(&tag))
})
.or_else(|| {
let mut stack = vec![doc.root_node().id];
while let Some(node_id) = stack.pop() {
let node = doc.get_node(node_id)?;
if node.data.is_element_with_tag_name(&tag) {
return Some(node_id);
}
stack.extend(node.children.iter().rev().copied());
}
None
})
}
fn document_element(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let root_id = ctx.doc.borrow().try_root_element().map(|root| root.id);
Ok(node_or_null(&ctx, root_id, context))
}
fn body(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let body_id = find_tag(&ctx, local_name!("body"));
Ok(node_or_null(&ctx, body_id, context))
}
fn head(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let head_id = find_tag(&ctx, local_name!("head"));
Ok(node_or_null(&ctx, head_id, context))
}
fn active_element(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let focus_id = ctx.doc.borrow().get_focussed_node_id();
Ok(node_or_null(&ctx, focus_id, context))
}
fn default_view(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = this_node_id(this)?;
Ok(context.global_object().into())
}
fn create_element(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _t = crate::script_stats::Timed::new(&ctx, "dom:createElement");
let _ = this_node_id(this)?;
let tag = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?
.to_ascii_lowercase();
let node_id = {
let mut doc = ctx.doc.borrow_mut();
doc.mutate().create_element(qual_name(&tag), Vec::new())
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
fn create_element_ns(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let ns = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let tag = to_rust_string(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
let node_id = {
let mut doc = ctx.doc.borrow_mut();
doc.mutate()
.create_element(qual_name_ns(&tag, &ns), Vec::new())
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
fn import_node(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = this_node_id(this)?;
let node = args.first().cloned().unwrap_or_else(JsValue::undefined);
let deep = args.get(1).cloned().unwrap_or_else(JsValue::undefined);
super::node::clone_node(&node, &[deep], context)
}
fn get_selection(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let text = ctx.doc.borrow().get_selected_text().unwrap_or_default();
let range_count = i32::from(!text.is_empty());
let selection = ObjectInitializer::new(context)
.function(
NativeFunction::from_fn_ptr(selection_to_string),
js_string!("toString"),
0,
)
.function(
NativeFunction::from_fn_ptr(selection_get_range_at),
js_string!("getRangeAt"),
1,
)
.function(
NativeFunction::from_fn_ptr(selection_remove_all_ranges),
js_string!("removeAllRanges"),
0,
)
.function(
NativeFunction::from_fn_ptr(selection_add_range),
js_string!("addRange"),
1,
)
.property(js_string!("rangeCount"), range_count, Attribute::all())
.property(js_string!("isCollapsed"), text.is_empty(), Attribute::all())
.build();
Ok(selection.into())
}
fn selection_to_string(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let text = ctx.doc.borrow().get_selected_text().unwrap_or_default();
Ok(JsValue::from(JsString::from(text)))
}
fn selection_get_range_at(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::null())
}
fn selection_remove_all_ranges(
_: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
dom_ctx(context)?.doc.borrow_mut().clear_text_selection();
Ok(JsValue::undefined())
}
fn selection_add_range(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn create_text_node(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _t = crate::script_stats::Timed::new(&ctx, "dom:createTextNode");
let _ = this_node_id(this)?;
let text = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let node_id = {
let mut doc = ctx.doc.borrow_mut();
doc.mutate().create_text_node(&text)
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
pub(crate) fn register_text_constructor(proto: &JsObject, context: &mut Context) {
context
.register_global_callable(
boa_engine::js_string!("Text"),
1,
boa_engine::NativeFunction::from_fn_ptr(text_constructor),
)
.expect("failed to register Text constructor");
let global = context.global_object().clone();
let constructor = global
.get(boa_engine::js_string!("Text"), context)
.expect("Text constructor missing")
.as_object()
.expect("Text is not an object");
constructor
.set(
boa_engine::js_string!("prototype"),
proto.clone(),
true,
context,
)
.expect("failed to set Text prototype");
}
fn text_constructor(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let text = match args.first() {
Some(value) if !value.is_undefined() => to_rust_string(value, context)?,
_ => String::new(),
};
let node_id = {
let mut doc = ctx.doc.borrow_mut();
doc.mutate().create_text_node(&text)
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
fn create_comment(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let text = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let node_id = {
let mut doc = ctx.doc.borrow_mut();
doc.mutate().create_comment_node(&text)
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
fn get_element_by_id(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let id = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let node_id = ctx.doc.borrow().get_element_by_id(&id);
Ok(node_or_null(&ctx, node_id, context))
}
fn query_selector(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let node_id = ctx.doc.borrow().query_selector(&selector).ok().flatten();
Ok(node_or_null(&ctx, node_id, context))
}
fn query_selector_all(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let _ = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let matches: Vec<NodeId> = ctx
.doc
.borrow()
.query_selector_all(&selector)
.map(|matches| matches.into_iter().collect())
.unwrap_or_default();
let wrappers: Vec<JsValue> = matches
.into_iter()
.map(|match_id| node_wrapper(&ctx, match_id, context).into())
.collect();
Ok(JsArray::from_iter(wrappers, context).into())
}