mod collections;
pub(crate) mod custom_elements;
pub(crate) mod document;
pub(crate) mod doma;
pub(crate) mod element;
pub(crate) mod event;
pub(crate) mod event_interfaces;
mod event_target;
mod geometry;
mod html_element;
pub(crate) mod inline_handlers;
pub(crate) mod interfaces;
pub(crate) mod node;
mod parsing;
pub(crate) mod range;
#[cfg(feature = "shadow-dom")]
pub(crate) mod shadow;
#[cfg(feature = "shadow-dom")]
pub(crate) mod shadow_event;
#[cfg(feature = "shadow-dom")]
pub(crate) mod sheets;
pub(crate) mod style;
use blitz_dom::NodeId;
use blitz_dom::node::NodeData;
use blitz_dom::{LocalName, Namespace, QualName};
use boa_engine::object::{FunctionObjectBuilder, JsObject, WeakJsObject};
use boa_engine::property::{PropertyDescriptor, PropertyKey};
use boa_engine::value::JsValue;
use boa_engine::{
Context, Finalize, JsData, JsNativeError, JsResult, JsString, NativeFunction, Trace,
};
use crate::state::{DomCtx, DomProtos};
#[derive(Trace, Finalize, JsData)]
pub(crate) struct NodeRef {
#[unsafe_ignore_trace]
pub node_id: NodeId,
}
pub(crate) fn dom_ctx(context: &mut Context) -> JsResult<DomCtx> {
context.get_data::<DomCtx>().cloned().ok_or_else(|| {
JsNativeError::typ()
.with_message("DOM context missing")
.into()
})
}
pub(crate) fn node_id_of_value(value: &JsValue) -> Option<NodeId> {
value.as_object().and_then(|obj| {
obj.downcast_ref::<NodeRef>()
.map(|node_ref| node_ref.node_id)
})
}
pub(crate) fn this_node_id(this: &JsValue) -> JsResult<NodeId> {
node_id_of_value(this).ok_or_else(|| {
JsNativeError::typ()
.with_message("`this` is not a DOM node")
.into()
})
}
pub(crate) fn node_wrapper(ctx: &DomCtx, node_id: NodeId, context: &mut Context) -> JsObject {
let cached = ctx
.state
.borrow()
.node_wrappers
.get(&node_id)
.and_then(WeakJsObject::upgrade);
if let Some(wrapper) = cached {
pin_detached_document(ctx, node_id, &wrapper, context);
return wrapper;
}
let custom_prototype = custom_elements::element_prototype(ctx, node_id, context);
let proto = {
let doc = ctx.doc.borrow();
let state = ctx.state.borrow();
let node = doc.get_node(node_id);
let data = node.map(|node| &node.data);
let is_element = matches!(
data,
Some(NodeData::Element(_)) | Some(NodeData::AnonymousBlock(_))
);
match node.and_then(|node| node.markup.as_deref()) {
Some(blitz_dom::node::MarkupNode::Doctype { .. }) => {
interfaces::prototype("DocumentType", context)
}
Some(blitz_dom::node::MarkupNode::ProcessingInstruction { .. }) => {
interfaces::prototype("ProcessingInstruction", context)
}
None => match custom_prototype {
Some(custom) if is_element => custom,
#[cfg(feature = "shadow-dom")]
_ if matches!(data, Some(NodeData::ShadowRoot(_))) => shadow::root_proto(context),
_ if matches!(data, Some(NodeData::Document(data)) if data.content_type != "text/html") => {
interfaces::prototype("XMLDocument", context)
}
_ => interfaces::node_prototype(data, state.protos(), context),
},
}
};
let wrapper = JsObject::from_proto_and_data(Some(proto), NodeRef { node_id });
let connected = ctx
.doc
.borrow()
.get_node(node_id)
.is_some_and(|node| node.flags.is_in_document());
let mut state = ctx.state.borrow_mut();
state.node_wrappers.insert(node_id, wrapper.downgrade());
if connected {
state.connected_wrappers.insert(node_id, wrapper.clone());
}
drop(state);
inline_handlers::seed(ctx, node_id, &wrapper, context);
pin_detached_document(ctx, node_id, &wrapper, context);
wrapper
}
fn pin_detached_document(ctx: &DomCtx, node_id: NodeId, wrapper: &JsObject, context: &mut Context) {
const OWNER_KEY: &str = "__blitz_internal_owner_document__";
let owner = {
let doc = ctx.doc.borrow();
doc.get_node(node_id)
.and_then(|node| node.owner_document)
.filter(|owner| *owner != doc.root_node().id)
};
let previous = wrapper
.get(JsString::from(OWNER_KEY), context)
.ok()
.and_then(|value| value.as_object());
let previous_id = previous
.as_ref()
.and_then(|object| object.downcast_ref::<NodeRef>().map(|node| node.node_id));
let owned_key = format!("__blitz_internal_owned_node_{}", node_id.as_u64());
if previous_id != owner {
if let Some(previous) = previous {
let _ = previous.delete_property_or_throw(JsString::from(owned_key.as_str()), context);
}
define_value(wrapper, OWNER_KEY, JsValue::null(), context);
}
if let Some(owner) = owner {
let document = node_wrapper(ctx, owner, context);
define_value(wrapper, OWNER_KEY, document.clone().into(), context);
define_value(&document, &owned_key, wrapper.clone().into(), context);
}
}
pub(crate) fn remove_and_free_node(ctx: &DomCtx, node_id: NodeId, context: &mut Context) {
node::unroot_detached_listener_subtree(ctx, node_id, context);
ctx.mutate_doc().mutate().remove_node(node_id);
ctx.state.borrow_mut().detached_nodes.push(node_id);
}
pub(crate) fn mark_node_reattached(ctx: &DomCtx, node_id: NodeId) {
let ids = {
let doc = ctx.doc.borrow();
let mut ids = Vec::new();
let mut stack = vec![node_id];
while let Some(id) = stack.pop() {
if let Some(node) = doc.get_node(id) {
if node.flags.is_in_document() {
ids.push(id);
}
stack.extend(node.children.iter().copied());
if let Some(contents) = node
.element_data()
.and_then(|element| element.template_contents)
{
stack.push(contents);
}
#[cfg(feature = "shadow-dom")]
if let Some(root_id) = node.shadow_root_id() {
stack.push(root_id);
}
}
}
ids
};
let mut state = ctx.state.borrow_mut();
state
.detached_nodes
.retain(|candidate| *candidate != node_id);
for id in ids {
if let Some(wrapper) = state
.node_wrappers
.get(&id)
.and_then(|wrapper| wrapper.upgrade())
{
state.connected_wrappers.insert(id, wrapper);
}
}
}
pub(crate) fn sweep_detached_nodes(ctx: &DomCtx) {
const DETACHED_NODE_GC_THRESHOLD: usize = 256;
let fresh = {
let state = ctx.state.borrow();
state
.detached_nodes
.len()
.saturating_sub(state.detached_kept)
};
if fresh >= DETACHED_NODE_GC_THRESHOLD {
boa_gc::force_collect();
}
let candidates = std::mem::take(&mut ctx.state.borrow_mut().detached_nodes);
if candidates.is_empty() {
return;
}
let mut keep = Vec::new();
let mut freed = Vec::new();
for node_id in candidates {
let held = {
let state = ctx.state.borrow();
let doc = ctx.doc.borrow();
if doc.get_node(node_id).is_none() {
continue;
}
let mut stack = vec![node_id];
let mut held = false;
while let Some(id) = stack.pop() {
let wrapper_alive = state
.node_wrappers
.get(&id)
.and_then(WeakJsObject::upgrade)
.is_some();
if wrapper_alive {
held = true;
break;
}
if let Some(node) = doc.get_node(id) {
stack.extend(node.children.iter().copied());
if let Some(contents) = node
.element_data()
.and_then(|element| element.template_contents)
{
stack.push(contents);
}
#[cfg(feature = "shadow-dom")]
if let Some(root_id) = node.shadow_root_id() {
stack.push(root_id);
}
}
}
held
};
if held {
keep.push(node_id);
continue;
}
ctx.mutate_doc()
.mutate()
.remove_and_drop_node_with(node_id, &mut |id| freed.push(id));
}
let mut state = ctx.state.borrow_mut();
state.detached_kept = keep.len();
state.detached_nodes = keep;
for id in freed {
state.node_wrappers.remove(&id);
state.dataset_wrappers.remove(&id);
state.class_list_wrappers.remove(&id);
state.node_listeners.remove(&id);
state.connected_wrappers.remove(&id);
}
}
pub(crate) fn node_or_null(
ctx: &DomCtx,
node_id: Option<NodeId>,
context: &mut Context,
) -> JsValue {
match node_id {
Some(node_id) => node_wrapper(ctx, node_id, context).into(),
None => JsValue::null(),
}
}
pub(crate) fn qual_name(local: &str) -> QualName {
QualName::new(None, markup5ever::ns!(html), LocalName::from(local))
}
pub(crate) fn qual_name_ns(local: &str, ns: &str) -> QualName {
QualName::new(None, Namespace::from(ns), LocalName::from(local))
}
pub(crate) fn js_str(s: &str) -> JsValue {
JsString::from(s).into()
}
pub(crate) fn to_rust_string(value: &JsValue, context: &mut Context) -> JsResult<String> {
let string = value.to_string(context)?.to_std_string_lossy();
crate::script_stats::record_boundary_string(string.len());
Ok(string)
}
type NativeFnPtr = fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>;
pub(crate) fn define_method(
obj: &JsObject,
name: &str,
length: usize,
body: NativeFnPtr,
context: &mut Context,
) {
let function = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure(move |this, args, context| {
custom_elements::native_scope(this, args, context, body)
}),
)
.name(JsString::from(name))
.length(length)
.build();
obj.define_property_or_throw(
PropertyKey::from(JsString::from(name)),
PropertyDescriptor::builder()
.value(function)
.writable(true)
.enumerable(false)
.configurable(true)
.build(),
context,
)
.expect("failed to define DOM method");
}
pub(crate) fn define_accessor(
obj: &JsObject,
name: &str,
getter: Option<NativeFnPtr>,
setter: Option<NativeFnPtr>,
context: &mut Context,
) {
let getter = getter.map(|g| {
FunctionObjectBuilder::new(context.realm(), NativeFunction::from_fn_ptr(g))
.name(JsString::from(format!("get {name}")))
.length(0)
.build()
});
let setter = setter.map(|s| {
FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure(move |this, args, context| {
custom_elements::native_scope(this, args, context, s)
}),
)
.name(JsString::from(format!("set {name}")))
.length(1)
.build()
});
let mut builder = PropertyDescriptor::builder()
.enumerable(false)
.configurable(true);
if let Some(getter) = getter {
builder = builder.get(getter);
}
if let Some(setter) = setter {
builder = builder.set(setter);
}
obj.define_property_or_throw(
PropertyKey::from(JsString::from(name)),
builder.build(),
context,
)
.expect("failed to define DOM accessor");
}
pub(crate) fn define_value(obj: &JsObject, name: &str, value: JsValue, context: &mut Context) {
if event::set_event_field(obj, name, &value) {
return;
}
obj.define_property_or_throw(
PropertyKey::from(JsString::from(name)),
PropertyDescriptor::builder()
.value(value)
.writable(true)
.enumerable(false)
.configurable(true)
.build(),
context,
)
.expect("failed to define DOM property");
}
pub(crate) const ON_EVENT_TYPES: &[&str] = &[
"click",
"dblclick",
"contextmenu",
"mousedown",
"mouseup",
"mousemove",
"mouseenter",
"mouseleave",
"mouseover",
"mouseout",
"pointerdown",
"pointerup",
"pointermove",
"pointercancel",
"pointerenter",
"pointerleave",
"pointerover",
"pointerout",
"touchstart",
"touchmove",
"touchend",
"touchcancel",
"keydown",
"keyup",
"keypress",
"input",
"change",
"focus",
"blur",
"focusin",
"focusout",
"submit",
"scroll",
"wheel",
"error",
"load",
"slotchange",
];
pub(crate) const ON_HANDLER_PREFIX: &str = "__blitz_internal_on_";
pub(crate) fn define_on_event_accessor(
proto: &JsObject,
event_type: &'static str,
context: &mut Context,
) {
let getter = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure(move |this, _args, context| {
inline_handlers::get(this, event_type, context)
}),
)
.name(JsString::from(format!("get on{event_type}")))
.length(0)
.build();
let setter = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_copy_closure(move |this, args, context| {
inline_handlers::set(this, args, event_type, context)
}),
)
.name(JsString::from(format!("set on{event_type}")))
.length(1)
.build();
proto
.define_property_or_throw(
PropertyKey::from(JsString::from(format!("on{event_type}"))),
PropertyDescriptor::builder()
.get(getter)
.set(setter)
.enumerable(false)
.configurable(true)
.build(),
context,
)
.expect("failed to define an on<event> accessor");
}
pub(crate) fn init_protos(ctx: &DomCtx, context: &mut Context) {
let object_proto = context.intrinsics().constructors().object().prototype();
let node_proto = JsObject::with_object_proto(context.intrinsics());
node::init_node_proto(&node_proto, context);
for event_type in ON_EVENT_TYPES {
define_on_event_accessor(&node_proto, event_type, context);
}
let element_proto = JsObject::with_object_proto(context.intrinsics());
element_proto.set_prototype(Some(node_proto.clone()));
element::init_element_proto(&element_proto, context);
let character_data_proto = JsObject::with_object_proto(context.intrinsics());
character_data_proto.set_prototype(Some(node_proto.clone()));
node::init_character_data_proto(&character_data_proto, context);
let document_proto = JsObject::with_object_proto(context.intrinsics());
document_proto.set_prototype(Some(node_proto.clone()));
document::init_document_proto(&document_proto, context);
let event_proto = JsObject::with_object_proto(context.intrinsics());
event::init_event_proto(&event_proto, context);
let style_proto = JsObject::with_object_proto(context.intrinsics());
style::init_style_proto(&style_proto, context);
let _ = object_proto;
ctx.state.borrow_mut().protos = Some(DomProtos {
node: node_proto,
element: element_proto,
character_data: character_data_proto.clone(),
document: document_proto,
event: event_proto.clone(),
style: style_proto,
});
event::register_event_constructor(&event_proto, context);
document::register_text_constructor(&character_data_proto, context);
interfaces::init(context);
doma::install(ctx, context);
#[cfg(feature = "shadow-dom")]
shadow::init(ctx, context);
range::init(context);
parsing::install(ctx, context);
inline_handlers::install(context);
}