use blitz_dom::NodeId;
use blitz_dom::local_name;
use boa_engine::object::builtins::JsArray;
use boa_engine::object::{JsObject, ObjectInitializer};
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, "implementation", Some(implementation), None, context);
define_accessor(proto, "title", Some(title), Some(set_title), context);
define_accessor(proto, "defaultView", Some(default_view), None, context);
define_accessor(
proto,
"location",
Some(get_location),
Some(set_location),
context,
);
define_accessor(proto, "domain", Some(get_domain), Some(set_domain), 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, "createRange", 0, super::range::create_range, context);
define_method(
proto,
"getSelection",
0,
super::range::document_selection,
context,
);
define_method(proto, "createComment", 1, create_comment, context);
define_method(
proto,
"createDocumentFragment",
0,
create_document_fragment,
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_within(ctx: &DomCtx, root_id: NodeId, tag: blitz_dom::LocalName) -> Option<NodeId> {
let doc = ctx.doc.borrow();
let root_element = doc
.get_node(root_id)?
.children
.iter()
.copied()
.find(|id| doc.get_node(*id).is_some_and(|child| child.is_element()));
if let Some(element_id) = root_element {
let element = doc.get_node(element_id)?;
if element.data.is_element_with_tag_name(&tag) {
return Some(element_id);
}
if let Some(found) = element.children.iter().copied().find(|child_id| {
doc.get_node(*child_id)
.is_some_and(|child| child.data.is_element_with_tag_name(&tag))
}) {
return Some(found);
}
}
let mut stack = vec![root_id];
while let Some(node_id) = stack.pop() {
let Some(node) = doc.get_node(node_id) else {
continue;
};
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_id = this_node_id(this)?;
let root_id = ctx.doc.borrow().try_root_element().map(|root| root.id);
let root_id = if Some(this_id) == ctx.doc.borrow().root_node().id.into() {
root_id
} else {
let doc = ctx.doc.borrow();
doc.get_node(this_id).and_then(|node| {
node.children
.iter()
.copied()
.find(|id| doc.get_node(*id).is_some_and(|c| c.is_element()))
})
};
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_id = this_node_id(this)?;
let body_id = if ctx
.doc
.borrow()
.get_node(this_id)
.is_some_and(|node| node.is_html_document())
{
find_tag_within(&ctx, this_id, local_name!("body"))
} else {
None
};
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_id = this_node_id(this)?;
let head_id = if ctx
.doc
.borrow()
.get_node(this_id)
.is_some_and(|node| node.is_html_document())
{
find_tag_within(&ctx, this_id, local_name!("head"))
} else {
None
};
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_id = this_node_id(this)?;
let focus_id = if this_id == ctx.doc.borrow().root_node().id {
ctx.doc.borrow().get_focussed_node_id()
} else {
None
};
Ok(node_or_null(&ctx, focus_id, context))
}
fn default_view(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let this_id = this_node_id(this)?;
if this_id == ctx.doc.borrow().root_node().id {
Ok(context.global_object().into())
} else {
Ok(JsValue::null())
}
}
fn get_location(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let this_id = this_node_id(this)?;
if this_id != ctx.doc.borrow().root_node().id {
return Ok(JsValue::null());
}
context.global_object().get(js_string!("location"), context)
}
fn set_location(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let this_id = this_node_id(this)?;
if this_id != ctx.doc.borrow().root_node().id {
return Ok(JsValue::undefined());
}
let location = context
.global_object()
.get(js_string!("location"), context)?;
if let Some(location) = location.as_object() {
location.set(
js_string!("href"),
args.first().cloned().unwrap_or_default(),
true,
context,
)?;
}
Ok(JsValue::undefined())
}
fn get_domain(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = this_node_id(this)?;
let location = context
.global_object()
.get(js_string!("location"), context)?;
match location.as_object() {
Some(location) => location.get(js_string!("hostname"), context),
None => Ok(js_string!("").into()),
}
}
fn set_domain(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let requested = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?
.to_ascii_lowercase();
let current = get_domain(this, &[], context)?
.to_string(context)?
.to_std_string_escaped()
.to_ascii_lowercase();
let allowed = !requested.is_empty()
&& (current == requested
|| (current.ends_with(&requested)
&& current[..current.len() - requested.len()].ends_with('.')
&& requested.contains('.')));
if !allowed {
return Err(crate::dom_exception::error(
"SecurityError",
&format!("'{requested}' is not a suffix of '{current}'"),
context,
));
}
Ok(JsValue::undefined())
}
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 document_id = this_node_id(this)?;
let html = ctx
.doc
.borrow()
.get_node(document_id)
.is_some_and(|node| node.is_html_document());
let tag = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let tag = if html { tag.to_ascii_lowercase() } else { tag };
let node_id = {
let mut doc = ctx.doc.borrow_mut();
let mut mutr = doc.mutate();
let name = if html {
qual_name(&tag)
} else {
qual_name_ns(&tag, "")
};
let id = mutr.create_element(name, Vec::new());
mutr.adopt_node(id, document_id);
id
};
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 document_id = this_node_id(this)?;
let ns_value = &args.first().cloned().unwrap_or_default();
let ns = if ns_value.is_null_or_undefined() {
String::new()
} else {
to_rust_string(ns_value, context)?
};
let tag = to_rust_string(args.get(1).unwrap_or(&JsValue::undefined()), context)?;
let (prefix, local) = match tag.split_once(':') {
Some((prefix, local)) => (Some(markup5ever::Prefix::from(prefix)), local),
None => (None, tag.as_str()),
};
let node_id = {
let mut doc = ctx.doc.borrow_mut();
let mut mutr = doc.mutate();
let name = blitz_dom::QualName::new(
prefix,
blitz_dom::Namespace::from(ns.as_str()),
blitz_dom::LocalName::from(local),
);
let id = mutr.create_element(name, Vec::new());
mutr.adopt_node(id, document_id);
id
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
fn import_node(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let document_id = this_node_id(this)?;
let node = args.first().cloned().unwrap_or_else(JsValue::undefined);
let source_id = this_node_id(&node)?;
if ctx.doc.borrow().get_node(source_id).is_some_and(|node| {
matches!(
node.data,
blitz_dom::node::NodeData::Document(_) | blitz_dom::node::NodeData::ShadowRoot(_)
)
}) {
return Err(super::doma::dom_error(
"NotSupportedError",
"Cannot import this node",
context,
));
}
let deep = args.get(1).cloned().unwrap_or_else(JsValue::undefined);
let cloned = super::node::clone_node(&node, &[deep], context)?;
let id = this_node_id(&cloned)?;
ctx.doc.borrow_mut().mutate().adopt_node(id, document_id);
Ok(node_wrapper(&ctx, id, context).into())
}
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 document_id = 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();
let mut mutr = doc.mutate();
let id = mutr.create_text_node(&text);
mutr.adopt_node(id, document_id);
id
};
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 document_id = 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();
let mut mutr = doc.mutate();
let id = mutr.create_comment_node(&text);
mutr.adopt_node(id, document_id);
id
};
Ok(node_wrapper(&ctx, node_id, context).into())
}
fn create_document_fragment(
this: &JsValue,
_args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let document_id = this_node_id(this)?;
let node_id = {
let mut doc = ctx.doc.borrow_mut();
let mut mutr = doc.mutate();
let id = mutr.create_document_fragment();
mutr.adopt_node(id, document_id);
id
};
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 document_id = 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_in(document_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 document_id = this_node_id(this)?;
let selector = to_rust_string(args.first().unwrap_or(&JsValue::undefined()), context)?;
let node_id = ctx
.doc
.borrow()
.query_selector_in(document_id, &selector)
.map_err(|_| super::element::invalid_selector(&selector, context))?;
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 document_id = 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_in(document_id, &selector)
.map(|matches| matches.into_iter().collect())
.map_err(|_| super::element::invalid_selector(&selector, context))?;
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())
}
fn implementation(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let _ = this_node_id(this)?;
let object = ObjectInitializer::new(context)
.function(
NativeFunction::from_fn_ptr(create_html_document),
js_string!("createHTMLDocument"),
1,
)
.function(
NativeFunction::from_fn_ptr(create_xml_document),
js_string!("createDocument"),
2,
)
.build();
Ok(object.into())
}
fn title(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let this_id = this_node_id(this)?;
let text = find_tag_within(&ctx, this_id, local_name!("title"))
.and_then(|title_id| {
let doc = ctx.doc.borrow();
let title = doc.get_node(title_id)?;
Some(
title
.children
.iter()
.filter_map(|child_id| doc.get_node(*child_id))
.filter_map(|child| child.text_data().map(|text| text.content.clone()))
.collect::<String>(),
)
})
.unwrap_or_default();
Ok(JsString::from(text.as_str()).into())
}
const SVG_NS: &str = "http://www.w3.org/2000/svg";
fn set_title(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args
.first()
.cloned()
.unwrap_or_default()
.to_string(context)?;
let ctx = dom_ctx(context)?;
let this_id = this_node_id(this)?;
let Some(document) = this.as_object() else {
return Ok(JsValue::undefined());
};
let root = document.get(js_string!("documentElement"), context)?;
let Some(root) = root.as_object() else {
return Ok(JsValue::undefined());
};
let root_id = this_node_id(&root.clone().into())?;
let root_is_svg = {
let doc = ctx.doc.borrow();
doc.get_node(root_id)
.and_then(|node| node.element_data())
.is_some_and(|element| &*element.name.ns == SVG_NS)
};
let existing = if root_is_svg {
let doc = ctx.doc.borrow();
doc.get_node(root_id).and_then(|node| {
node.children.iter().copied().find(|id| {
doc.get_node(*id)
.and_then(|child| child.element_data())
.is_some_and(|element| {
element.name.local == local_name!("title") && &*element.name.ns == SVG_NS
})
})
})
} else {
find_tag_within(&ctx, this_id, local_name!("title"))
};
let element = match existing {
Some(id) => node_wrapper(&ctx, id, context),
None if root_is_svg => {
let created = call(
&document,
js_string!("createElementNS"),
&[js_string!(SVG_NS).into(), js_string!("title").into()],
context,
)?;
let first = root.get(js_string!("firstChild"), context)?;
call(
&root,
js_string!("insertBefore"),
&[created.clone(), first],
context,
)?;
created.as_object().ok_or_else(|| {
boa_engine::JsNativeError::typ().with_message("createElementNS returned no element")
})?
}
None => {
let head = document.get(js_string!("head"), context)?;
let Some(head) = head.as_object() else {
return Ok(JsValue::undefined());
};
let created = call(
&document,
js_string!("createElement"),
&[js_string!("title").into()],
context,
)?;
call(
&head,
js_string!("appendChild"),
&[created.clone()],
context,
)?;
created.as_object().ok_or_else(|| {
boa_engine::JsNativeError::typ().with_message("createElement returned no element")
})?
}
};
element.set(js_string!("textContent"), value, true, context)?;
Ok(JsValue::undefined())
}
fn call(
object: &JsObject,
name: JsString,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let method = object.get(name, context)?;
let method = method.as_callable().ok_or_else(|| {
boa_engine::JsNativeError::typ().with_message("document.title: missing DOM method")
})?;
method.call(&object.clone().into(), args, context)
}
fn create_xml_document(
_this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let namespace = args.first().cloned().unwrap_or_default();
let qualified = match args.get(1) {
None => {
return Err(boa_engine::JsNativeError::typ()
.with_message("createDocument: qualifiedName is required")
.into());
}
Some(value) if value.is_null() => String::new(),
Some(value) => to_rust_string(value, context)?,
};
if args
.get(2)
.is_some_and(|doctype| !doctype.is_null_or_undefined())
{
return Err(boa_engine::JsNativeError::typ()
.with_message("createDocument: parameter 3 is not of type 'DocumentType'")
.into());
}
let ns = if namespace.is_null_or_undefined() {
String::new()
} else {
to_rust_string(&namespace, context)?
};
let content_type = match ns.as_str() {
"http://www.w3.org/1999/xhtml" => "application/xhtml+xml",
"http://www.w3.org/2000/svg" => "image/svg+xml",
_ => "application/xml",
};
let document_id = ctx.doc.borrow_mut().mutate().create_document(content_type);
ctx.state.borrow_mut().detached_nodes.push(document_id);
let document: JsValue = node_wrapper(&ctx, document_id, context).into();
if !qualified.is_empty() {
let element =
create_element_ns(&document, &[namespace, super::js_str(&qualified)], context)?;
let element_id = this_node_id(&element)?;
ctx.doc
.borrow_mut()
.mutate()
.append_children(document_id, &[element_id]);
}
Ok(document)
}
fn create_html_document(
_this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let ctx = dom_ctx(context)?;
let title = match args.first() {
None => String::new(),
Some(value) => to_rust_string(value, context)?,
};
let node_id = {
let mut doc = ctx.doc.borrow_mut();
doc.mutate().create_html_document(&title)
};
ctx.state.borrow_mut().detached_nodes.push(node_id);
Ok(node_wrapper(&ctx, node_id, context).into())
}