use crate::css_values::calc::resolve_computed_value;
use crate::css_values::types::length::CalcContext;
use crate::dom::node::NodeId;
use crate::dom::DomElement;
use crate::js_runtime::native_fns::{install_native_fp_tostring, IframeRealmStore};
use crate::js_runtime::state::DomState;
use crate::js_runtime::utils::tokens_to_string;
use deno_core::op2;
use deno_core::v8;
use deno_core::JsRuntime;
use deno_core::OpState;
use std::collections::HashMap;
fn calc_context_from(state: &DomState) -> CalcContext {
let mut ctx = CalcContext::default();
if let Some(p) = state.stealth_profile.as_ref() {
ctx.viewport_w = p.inner_width as f64;
ctx.viewport_h = p.inner_height as f64;
ctx.container_w = p.inner_width as f64;
ctx.container_h = p.inner_height as f64;
ctx.root_font_size_px = 16.0;
ctx.font_size_px = 16.0;
}
ctx
}
#[op2(fast)]
#[smi]
pub fn op_dom_document_node() -> i32 {
NodeId::DOCUMENT.to_raw() as i32
}
#[op2]
#[string]
pub fn op_dom_get_tag_name(state: &mut OpState, #[smi] node_id: i32) -> String {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
state
.dom
.get(id)
.and_then(|n| n.as_element())
.map(|e| e.name.local.clone())
.unwrap_or_default()
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_node_type(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
state.dom.node_type(NodeId::from_raw(node_id as u32)) as i32
}
#[op2]
#[string]
pub fn op_dom_get_text_content(state: &mut OpState, #[smi] node_id: i32) -> String {
let state = state.borrow::<DomState>();
state.dom.text_content(NodeId::from_raw(node_id as u32))
}
#[op2]
#[string]
pub fn op_dom_get_inner_html(state: &mut OpState, #[smi] node_id: i32) -> String {
let state = state.borrow::<DomState>();
state
.dom
.serialize_inner_html(NodeId::from_raw(node_id as u32))
}
#[op2]
#[string]
pub fn op_dom_get_outer_html(state: &mut OpState, #[smi] node_id: i32) -> String {
let state = state.borrow::<DomState>();
state.dom.serialize_html(NodeId::from_raw(node_id as u32))
}
#[op2]
#[string]
pub fn op_dom_get_attribute(
state: &mut OpState,
#[smi] node_id: i32,
#[string] name: &str,
) -> String {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
state
.dom
.get(id)
.and_then(|n| n.as_element())
.and_then(|e| {
e.attrs
.iter()
.find(|a| a.name.local.eq_ignore_ascii_case(name))
.map(|a| a.value.clone())
})
.unwrap_or_default()
}
#[op2(fast)]
pub fn op_dom_has_attribute(
state: &mut OpState,
#[smi] node_id: i32,
#[string] name: &str,
) -> bool {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
state
.dom
.get(id)
.and_then(|n| n.as_element())
.is_some_and(|e| {
e.attrs
.iter()
.any(|a| a.name.local.eq_ignore_ascii_case(name))
})
}
#[op2]
#[serde]
pub fn op_dom_get_attribute_names(state: &mut OpState, #[smi] node_id: i32) -> Vec<String> {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
state
.dom
.get(id)
.and_then(|n| n.as_element())
.map(|e| e.attrs.iter().map(|a| a.name.local.clone()).collect())
.unwrap_or_default()
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_parent(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
state
.dom
.get(id)
.and_then(|n| n.parent)
.map(|p| p.to_raw() as i32)
.unwrap_or(-1)
}
#[op2]
#[serde]
pub fn op_dom_get_children(state: &mut OpState, #[smi] node_id: i32) -> Vec<i32> {
let state = state.borrow::<DomState>();
state
.dom
.children(NodeId::from_raw(node_id as u32))
.iter()
.map(|id| id.to_raw() as i32)
.collect()
}
#[op2]
#[serde]
pub fn op_dom_get_children_with_types(state: &mut OpState, #[smi] node_id: i32) -> Vec<i32> {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let children = state.dom.children(id);
let mut res = Vec::with_capacity(children.len() * 2);
for cid in children {
res.push(cid.to_raw() as i32);
res.push(state.dom.node_type(cid) as i32);
}
res
}
#[op2]
#[serde]
pub fn op_dom_get_child_elements(state: &mut OpState, #[smi] node_id: i32) -> Vec<i32> {
let state = state.borrow::<DomState>();
state
.dom
.child_elements(NodeId::from_raw(node_id as u32))
.iter()
.map(|id| id.to_raw() as i32)
.collect()
}
#[op2]
#[serde]
pub fn op_dom_get_child_elements_with_types(state: &mut OpState, #[smi] node_id: i32) -> Vec<i32> {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let children = state.dom.child_elements(id);
let mut res = Vec::with_capacity(children.len() * 2);
for cid in children {
res.push(cid.to_raw() as i32);
res.push(state.dom.node_type(cid) as i32);
}
res
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_first_child(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
state
.dom
.get(NodeId::from_raw(node_id as u32))
.and_then(|n| n.first_child)
.map(|id| id.to_raw() as i32)
.unwrap_or(-1)
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_last_child(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
state
.dom
.get(NodeId::from_raw(node_id as u32))
.and_then(|n| n.last_child)
.map(|id| id.to_raw() as i32)
.unwrap_or(-1)
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_next_sibling(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
state
.dom
.get(NodeId::from_raw(node_id as u32))
.and_then(|n| n.next_sibling)
.map(|id| id.to_raw() as i32)
.unwrap_or(-1)
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_prev_sibling(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
state
.dom
.get(NodeId::from_raw(node_id as u32))
.and_then(|n| n.prev_sibling)
.map(|id| id.to_raw() as i32)
.unwrap_or(-1)
}
#[op2(fast)]
#[smi]
pub fn op_dom_query_selector(
state: &mut OpState,
#[smi] node_id: i32,
#[string] selector: &str,
) -> i32 {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let element = match DomElement::new(&state.dom, id) {
Some(el) => el,
None => {
let children = state.dom.child_elements(id);
if children.is_empty() {
return -1;
}
match DomElement::new(&state.dom, children[0]) {
Some(el) => {
if let Ok(Some(found)) = crate::css_selectors::query_selector(&el, selector) {
return found.node_id().to_raw() as i32;
}
return -1;
}
None => return -1,
}
}
};
match crate::css_selectors::query_selector(&element, selector) {
Ok(Some(found)) => found.node_id().to_raw() as i32,
_ => -1,
}
}
#[op2]
#[serde]
pub fn op_dom_query_selector_all(
state: &mut OpState,
#[smi] node_id: i32,
#[string] selector: String,
) -> Vec<i32> {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let root_el = DomElement::new(&state.dom, id).or_else(|| {
let children = state.dom.child_elements(id);
children
.first()
.and_then(|&c| DomElement::new(&state.dom, c))
});
match root_el {
Some(el) => crate::css_selectors::query_selector_all(&el, &selector)
.unwrap_or_default()
.iter()
.map(|e| e.node_id().to_raw() as i32)
.collect(),
None => vec![],
}
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_element_by_id(state: &mut OpState, #[string] id: &str) -> i32 {
let state = state.borrow::<DomState>();
state
.dom
.get_element_by_id(id)
.map(|n| n.to_raw() as i32)
.unwrap_or(-1)
}
#[op2]
#[serde]
pub fn op_dom_get_elements_by_tag_name(
state: &mut OpState,
#[smi] node_id: i32,
#[string] tag: String,
) -> Vec<i32> {
let state = state.borrow::<DomState>();
state
.dom
.get_elements_by_tag_name(NodeId::from_raw(node_id as u32), &tag)
.iter()
.map(|id| id.to_raw() as i32)
.collect()
}
#[op2]
#[serde]
pub fn op_dom_get_elements_by_class_name(
state: &mut OpState,
#[smi] node_id: i32,
#[string] class: String,
) -> Vec<i32> {
let state = state.borrow::<DomState>();
state
.dom
.get_elements_by_class_name(NodeId::from_raw(node_id as u32), &class)
.iter()
.map(|id| id.to_raw() as i32)
.collect()
}
#[op2(fast)]
#[smi]
pub fn op_dom_create_element(state: &mut OpState, #[string] tag: &str) -> i32 {
let state = state.borrow_mut::<DomState>();
state
.dom
.create_element(crate::dom::node::QualName::new(tag), vec![])
.to_raw() as i32
}
#[op2(fast)]
#[smi]
pub fn op_dom_create_text_node(state: &mut OpState, #[string] text: &str) -> i32 {
let state = state.borrow_mut::<DomState>();
state.dom.create_text(text.to_string()).to_raw() as i32
}
#[op2(fast)]
#[smi]
pub fn op_dom_create_document_fragment(state: &mut OpState) -> i32 {
let state = state.borrow_mut::<DomState>();
state.dom.create_document_fragment().to_raw() as i32
}
#[op2(fast)]
pub fn op_dom_append_child(state: &mut OpState, #[smi] parent: i32, #[smi] child: i32) {
let state = state.borrow_mut::<DomState>();
state.dom.append_child(
NodeId::from_raw(parent as u32),
NodeId::from_raw(child as u32),
);
state.layout_engine.mark_dirty();
}
#[op2(fast)]
pub fn op_dom_insert_before(
state: &mut OpState,
#[smi] parent: i32,
#[smi] child: i32,
#[smi] reference: i32,
) {
let state = state.borrow_mut::<DomState>();
state.dom.insert_before(
NodeId::from_raw(parent as u32),
NodeId::from_raw(child as u32),
NodeId::from_raw(reference as u32),
);
state.layout_engine.mark_dirty();
}
#[op2(fast)]
pub fn op_dom_remove_child(state: &mut OpState, #[smi] _parent: i32, #[smi] child: i32) {
let state = state.borrow_mut::<DomState>();
state.dom.detach(NodeId::from_raw(child as u32));
state.layout_engine.mark_dirty();
}
#[op2(fast)]
pub fn op_dom_set_attribute(
state: &mut OpState,
#[smi] node_id: i32,
#[string] name: &str,
#[string] value: &str,
) {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
if let Some(node) = state.dom.get_mut(id) {
if let Some(elem) = node.as_element_mut() {
if let Some(attr) = elem
.attrs
.iter_mut()
.find(|a| a.name.local.eq_ignore_ascii_case(name))
{
attr.value = value.to_string();
} else {
elem.attrs.push(crate::dom::node::Attribute {
name: crate::dom::node::QualName::new(name),
value: value.to_string(),
});
}
}
}
if name.eq_ignore_ascii_case("style") || name.eq_ignore_ascii_case("class") {
state.layout_engine.mark_dirty();
}
}
#[op2(fast)]
pub fn op_dom_remove_attribute(state: &mut OpState, #[smi] node_id: i32, #[string] name: &str) {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
if let Some(node) = state.dom.get_mut(id) {
if let Some(elem) = node.as_element_mut() {
elem.attrs
.retain(|a| !a.name.local.eq_ignore_ascii_case(name));
}
}
if name.eq_ignore_ascii_case("style") || name.eq_ignore_ascii_case("class") {
state.layout_engine.mark_dirty();
}
}
#[op2(fast)]
pub fn op_dom_set_text_content(state: &mut OpState, #[smi] node_id: i32, #[string] text: &str) {
let state = state.borrow_mut::<DomState>();
state
.dom
.set_text_content(NodeId::from_raw(node_id as u32), text);
state.layout_engine.mark_dirty();
}
#[op2(fast)]
pub fn op_dom_set_inner_html(state: &mut OpState, #[smi] node_id: i32, #[string] html: &str) {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let fragment_dom = crate::html_parser::parse_html(&format!("<body>{}</body>", html));
let body = fragment_dom
.get_elements_by_tag_name(NodeId::DOCUMENT, "body")
.into_iter()
.next();
let old_children: Vec<NodeId> = state.dom.children(id);
for child in old_children {
state.dom.remove(child);
}
if let Some(body_id) = body {
for child_id in fragment_dom.children(body_id) {
let new_child = state.dom.merge_subtree(&fragment_dom, child_id);
state.dom.append_child(id, new_child);
}
}
state.layout_engine.mark_dirty();
}
#[op2(fast)]
#[smi]
pub fn op_dom_clone_node(state: &mut OpState, #[smi] node_id: i32, deep: bool) -> i32 {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
if deep {
let cloned = {
clone_subtree_deep(&mut state.dom, id)
};
cloned.to_raw() as i32
} else {
let node = match state.dom.get(id) {
Some(n) => n,
None => return -1,
};
let new_id = match &node.data {
crate::dom::node::NodeData::Element(elem) => state
.dom
.create_element(elem.name.clone(), elem.attrs.clone()),
crate::dom::node::NodeData::Text(t) => state.dom.create_text(t.clone()),
crate::dom::node::NodeData::Comment(t) => state.dom.create_comment(t.clone()),
_ => state.dom.create_document_fragment(),
};
new_id.to_raw() as i32
}
}
fn clone_subtree_deep(dom: &mut crate::dom::Dom, root: NodeId) -> NodeId {
let snapshot = collect_subtree(dom, root);
rebuild_from_snapshot(dom, &snapshot)
}
#[derive(Debug)]
enum SnapshotNode {
Element {
name: crate::dom::node::QualName,
attrs: Vec<crate::dom::node::Attribute>,
children: Vec<SnapshotNode>,
},
Text(String),
Comment(String),
Fragment(Vec<SnapshotNode>),
}
fn collect_subtree(dom: &crate::dom::Dom, id: NodeId) -> SnapshotNode {
let node = match dom.get(id) {
Some(n) => n,
None => return SnapshotNode::Fragment(vec![]),
};
let children: Vec<SnapshotNode> = dom
.children(id)
.iter()
.map(|&child_id| collect_subtree(dom, child_id))
.collect();
match &node.data {
crate::dom::node::NodeData::Element(elem) => SnapshotNode::Element {
name: elem.name.clone(),
attrs: elem.attrs.clone(),
children,
},
crate::dom::node::NodeData::Text(t) => SnapshotNode::Text(t.clone()),
crate::dom::node::NodeData::Comment(t) => SnapshotNode::Comment(t.clone()),
_ => SnapshotNode::Fragment(children),
}
}
fn rebuild_from_snapshot(dom: &mut crate::dom::Dom, snapshot: &SnapshotNode) -> NodeId {
match snapshot {
SnapshotNode::Element {
name,
attrs,
children,
} => {
let id = dom.create_element(name.clone(), attrs.clone());
for child in children {
let child_id = rebuild_from_snapshot(dom, child);
dom.append_child(id, child_id);
}
id
}
SnapshotNode::Text(t) => dom.create_text(t.clone()),
SnapshotNode::Comment(t) => dom.create_comment(t.clone()),
SnapshotNode::Fragment(children) => {
let id = dom.create_document_fragment();
for child in children {
let child_id = rebuild_from_snapshot(dom, child);
dom.append_child(id, child_id);
}
id
}
}
}
#[op2(fast)]
pub fn op_dom_insert_adjacent_html(
state: &mut OpState,
#[smi] node_id: i32,
#[string] position: &str,
#[string] html: &str,
) {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let fragment_dom = crate::html_parser::parse_html(&format!("<body>{}</body>", html));
let frag_body = fragment_dom
.get_elements_by_tag_name(NodeId::DOCUMENT, "body")
.into_iter()
.next();
let frag_children: Vec<NodeId> = frag_body
.map(|b| fragment_dom.children(b))
.unwrap_or_default();
if frag_children.is_empty() {
return;
}
match position {
"beforebegin" => {
if let Some(parent) = state.dom.get(id).and_then(|n| n.parent) {
for &child_id in &frag_children {
let new_child = state.dom.merge_subtree(&fragment_dom, child_id);
state.dom.insert_before(parent, new_child, id);
}
}
}
"afterbegin" => {
let first = state.dom.get(id).and_then(|n| n.first_child);
for child_id in frag_children.iter().rev() {
let new_child = state.dom.merge_subtree(&fragment_dom, *child_id);
if let Some(ref_child) = first {
state.dom.insert_before(id, new_child, ref_child);
} else {
state.dom.append_child(id, new_child);
}
}
}
"beforeend" => {
for &child_id in &frag_children {
let new_child = state.dom.merge_subtree(&fragment_dom, child_id);
state.dom.append_child(id, new_child);
}
}
"afterend" => {
if let Some(parent) = state.dom.get(id).and_then(|n| n.parent) {
let next = state.dom.get(id).and_then(|n| n.next_sibling);
for &child_id in &frag_children {
let new_child = state.dom.merge_subtree(&fragment_dom, child_id);
if let Some(ref_child) = next {
state.dom.insert_before(parent, new_child, ref_child);
} else {
state.dom.append_child(parent, new_child);
}
}
}
}
_ => {}
}
state.layout_engine.mark_dirty();
}
#[op2]
#[serde]
pub fn op_dom_document_write(state: &mut OpState, #[string] html: &str) -> Vec<i32> {
let state = state.borrow_mut::<DomState>();
let body_id = state
.dom
.get_elements_by_tag_name(NodeId::DOCUMENT, "body")
.into_iter()
.next();
let body_id = match body_id {
Some(id) => id,
None => return vec![],
};
let fragment_dom = crate::html_parser::parse_html(&format!("<body>{}</body>", html));
let frag_body = fragment_dom
.get_elements_by_tag_name(NodeId::DOCUMENT, "body")
.into_iter()
.next();
let mut new_ids = Vec::new();
if let Some(frag_body_id) = frag_body {
for child_id in fragment_dom.children(frag_body_id) {
let new_child = state.dom.merge_subtree(&fragment_dom, child_id);
state.dom.append_child(body_id, new_child);
new_ids.push(new_child.to_raw() as i32);
}
}
state.layout_engine.mark_dirty();
new_ids
}
#[op2(fast)]
pub fn op_dom_class_list_add(state: &mut OpState, #[smi] node_id: i32, #[string] class: &str) {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
if let Some(node) = state.dom.get_mut(id) {
if let Some(elem) = node.as_element_mut() {
let current = elem
.attrs
.iter()
.find(|a| a.name.local == "class")
.map(|a| a.value.clone())
.unwrap_or_default();
if !current.split_whitespace().any(|c| c == class) {
let new_val = if current.is_empty() {
class.to_string()
} else {
format!("{} {}", current, class)
};
if let Some(attr) = elem.attrs.iter_mut().find(|a| a.name.local == "class") {
attr.value = new_val;
} else {
elem.attrs.push(crate::dom::node::Attribute {
name: crate::dom::node::QualName::new("class"),
value: new_val,
});
}
}
}
}
}
#[op2(fast)]
pub fn op_dom_class_list_remove(state: &mut OpState, #[smi] node_id: i32, #[string] class: &str) {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
if let Some(node) = state.dom.get_mut(id) {
if let Some(elem) = node.as_element_mut() {
if let Some(attr) = elem.attrs.iter_mut().find(|a| a.name.local == "class") {
let new_val: String = attr
.value
.split_whitespace()
.filter(|c| *c != class)
.collect::<Vec<_>>()
.join(" ");
attr.value = new_val;
}
}
}
}
#[op2]
#[serde]
#[allow(
clippy::explicit_counter_loop,
reason = "explicit CSS source-order counter"
)]
pub fn op_dom_get_all_computed_styles(
state: &mut OpState,
#[smi] node_id: i32,
) -> HashMap<String, String> {
let state = state.borrow_mut::<DomState>();
if state.cached_rules.is_empty() && !state.stylesheets.is_empty() {
state.update_cached_rules();
}
let id = NodeId::from_raw(node_id as u32);
let dom_el = if let Some(el) = DomElement::new(&state.dom, id) {
el
} else {
return HashMap::new();
};
let mut declarations: HashMap<String, (u32, u32, String)> = HashMap::new();
let mut source_order: u32 = 0;
for rule in &state.cached_rules {
for sel in &rule.selectors {
if crate::css_selectors::matches_selector(&dom_el, sel) {
let s = crate::css_selectors::compute_specificity(sel);
let spec = s.a * 10000 + s.b * 100 + s.c;
for (name, val) in &rule.declarations {
let entry = declarations
.entry(name.clone())
.or_insert((0, 0, String::new()));
if spec > entry.0 || (spec == entry.0 && source_order >= entry.1) {
*entry = (spec, source_order, val.clone());
}
}
}
}
source_order += 1;
}
if let Some(el) = state.dom.get(id).and_then(|n| n.as_element()) {
if let Some(style) = el
.attrs
.iter()
.find(|a| a.name.local.eq_ignore_ascii_case("style"))
{
for decl in style.value.split(';') {
if let Some(colon) = decl.find(':') {
let name = decl[..colon].trim().to_string();
let val = decl[colon + 1..].trim().to_string();
declarations.insert(name, (999999, 999999, val));
}
}
}
}
let ctx = calc_context_from(state);
let res: HashMap<String, String> = declarations
.into_iter()
.map(|(k, v)| (k, resolve_computed_value(&v.2, &ctx)))
.collect();
res
}
#[op2]
#[string]
pub fn op_dom_get_computed_style(
state: &mut OpState,
#[smi] node_id: i32,
#[string] property: &str,
) -> String {
let state = state.borrow_mut::<DomState>();
if state.cached_rules.is_empty() && !state.stylesheets.is_empty() {
state.update_cached_rules();
}
let id = NodeId::from_raw(node_id as u32);
let ctx = calc_context_from(state);
let inline_val = get_inline_style_value(&state.dom, id, property);
if let Some(val) = &inline_val {
if !val.is_empty() {
return resolve_computed_value(val, &ctx);
}
}
if let Some(val) = get_stylesheet_value(state, id, property) {
return resolve_computed_value(&val, &ctx);
}
const INHERITED: &[&str] = &[
"color",
"font-family",
"font-size",
"font-style",
"font-weight",
"font-variant",
"line-height",
"letter-spacing",
"word-spacing",
"text-align",
"text-indent",
"text-transform",
"white-space",
"direction",
"visibility",
"cursor",
"list-style-type",
"list-style-position",
"list-style-image",
"list-style",
"border-collapse",
"border-spacing",
"caption-side",
"empty-cells",
"quotes",
"orphans",
"widows",
"text-decoration-color",
];
if INHERITED.contains(&property) {
let mut current = id;
while let Some(parent_id) = state.dom.get(current).and_then(|n| n.parent) {
if let Some(val) = get_inline_style_value(&state.dom, parent_id, property) {
if !val.is_empty() {
return resolve_computed_value(&val, &ctx);
}
}
if let Some(val) = get_stylesheet_value(state, parent_id, property) {
return resolve_computed_value(&val, &ctx);
}
current = parent_id;
}
}
crate::js_runtime::extensions::layout_ext::css_default(property)
}
fn get_inline_style_value(dom: &crate::dom::Dom, id: NodeId, property: &str) -> Option<String> {
let style_attr = dom.get(id).and_then(|n| n.as_element()).and_then(|e| {
e.attrs
.iter()
.find(|a| a.name.local.eq_ignore_ascii_case("style"))
.map(|a| a.value.clone())
})?;
for decl in style_attr.split(';') {
let decl = decl.trim();
if decl.is_empty() {
continue;
}
if let Some(colon) = decl.find(':') {
let prop = decl[..colon].trim();
let val = decl[colon + 1..].trim();
if prop.eq_ignore_ascii_case(property) {
return Some(val.to_string());
}
}
}
None
}
#[allow(clippy::explicit_counter_loop, reason = "CSS source-order counter")]
fn get_stylesheet_value(state: &DomState, id: NodeId, property: &str) -> Option<String> {
let dom_el = DomElement::new(&state.dom, id)?;
let mut matches: Vec<(u32, u32, String)> = Vec::new();
let mut source_order: u32 = 0;
for rule in &state.cached_rules {
let mut matched = false;
let mut best_spec: u32 = 0;
for sel in &rule.selectors {
if crate::css_selectors::matches_selector(&dom_el, sel) {
matched = true;
let s = crate::css_selectors::compute_specificity(sel);
let spec = s.a * 10000 + s.b * 100 + s.c;
if spec > best_spec {
best_spec = spec;
}
}
}
if matched {
if let Some(val) = rule.declarations.get(property) {
matches.push((best_spec, source_order, val.clone()));
}
}
source_order += 1;
}
if matches.is_empty() {
return None;
}
matches.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
matches.last().map(|(_, _, val)| val.clone())
}
#[op2(fast)]
#[smi]
pub fn op_dom_attach_shadow(state: &mut OpState, #[smi] node_id: i32, #[string] mode: &str) -> i32 {
let state = state.borrow_mut::<DomState>();
let id = NodeId::from_raw(node_id as u32);
let shadow_mode = match mode {
"closed" => crate::dom::node::ShadowRootMode::Closed,
_ => crate::dom::node::ShadowRootMode::Open,
};
let shadow_id = state.dom.create_shadow_root(id, shadow_mode);
shadow_id.to_raw() as i32
}
#[op2(fast)]
#[smi]
pub fn op_dom_get_shadow_root(state: &mut OpState, #[smi] node_id: i32) -> i32 {
let state = state.borrow::<DomState>();
let id = NodeId::from_raw(node_id as u32);
state
.dom
.get(id)
.and_then(|n| n.as_element())
.and_then(|e| e.shadow_root)
.map(|sr| sr.to_raw() as i32)
.unwrap_or(-1)
}
#[op2(fast)]
pub fn op_dom_get_stylesheet_count(state: &mut OpState) -> i32 {
let state = state.borrow::<DomState>();
state.stylesheets.len() as i32
}
#[derive(serde::Serialize)]
pub struct CSSRuleJson {
pub selector_text: String,
pub css_text: String,
pub rule_type: u8,
}
#[op2]
#[serde]
pub fn op_dom_get_stylesheet_rules(state: &mut OpState, #[smi] index: i32) -> Vec<CSSRuleJson> {
let state = state.borrow::<DomState>();
let idx = index as usize;
if idx >= state.stylesheets.len() {
return vec![];
}
let (stylesheet, _errors) = crate::css_parser::parse_stylesheet(&state.stylesheets[idx]);
let mut rules = Vec::new();
for rule in &stylesheet.rules {
if let crate::css_parser::ast::Rule::Qualified(qr) = rule {
let selector_text = tokens_to_string(&qr.prelude);
if selector_text.is_empty() {
continue;
}
let decl_parts: Vec<String> = qr
.declarations
.iter()
.map(|d| {
let val = tokens_to_string(&d.value).trim().to_string();
if d.important {
format!("{}: {} !important", d.name, val)
} else {
format!("{}: {}", d.name, val)
}
})
.collect();
let css_text = format!("{} {{ {} }}", selector_text, decl_parts.join("; "));
rules.push(CSSRuleJson {
selector_text: selector_text.trim().to_string(),
css_text,
rule_type: 1, });
}
}
rules
}
#[op2]
#[string]
pub fn op_dom_get_base_url(state: &mut OpState) -> String {
let state = state.borrow::<DomState>();
state
.base_url
.as_ref()
.map(|u| u.to_string())
.unwrap_or_else(|| "about:blank".to_string())
}
#[op2]
#[string]
pub fn op_dom_storage_get(
state: &mut OpState,
#[string] area: String,
#[string] key: String,
) -> Option<String> {
let state = state.borrow::<DomState>();
state.storage.get(&area).and_then(|m| m.get(&key)).cloned()
}
#[op2(fast)]
pub fn op_dom_storage_set(
state: &mut OpState,
#[string] area: String,
#[string] key: String,
#[string] value: String,
) {
let state = state.borrow_mut::<DomState>();
if let Some(m) = state.storage.get_mut(&area) {
m.insert(key, value);
}
}
#[op2(fast)]
pub fn op_dom_storage_remove(state: &mut OpState, #[string] area: String, #[string] key: String) {
let state = state.borrow_mut::<DomState>();
if let Some(m) = state.storage.get_mut(&area) {
m.remove(&key);
}
}
#[op2(fast)]
pub fn op_dom_storage_clear(state: &mut OpState, #[string] area: String) {
let state = state.borrow_mut::<DomState>();
if let Some(m) = state.storage.get_mut(&area) {
m.clear();
}
}
#[op2]
#[serde]
pub fn op_dom_storage_keys(state: &mut OpState, #[string] area: String) -> Vec<String> {
let state = state.borrow::<DomState>();
state
.storage
.get(&area)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default()
}
fn _window_ctor_cb(
scope: &mut v8::PinScope,
_args: v8::FunctionCallbackArguments,
mut _rv: v8::ReturnValue,
) {
if let Some(msg) = v8::String::new(scope, "Illegal constructor") {
let e = v8::Exception::type_error(scope, msg);
scope.throw_exception(e);
}
}
#[op2]
pub fn op_create_child_realm<'s>(
scope: &mut v8::PinScope<'s, '_>,
#[smi] realm_id: i32,
) -> v8::Local<'s, v8::Value> {
let rid = realm_id as u32;
let op_state_rc = JsRuntime::op_state_from(scope);
{
let op_state = op_state_rc.borrow();
if let Some(store) = op_state.try_borrow::<IframeRealmStore>() {
if let Some(global) = store.globals.get(&rid) {
return v8::Local::new(scope, global).into();
}
}
}
let orig_fpt: Option<v8::Global<v8::Function>>;
let native_tag_sym: Option<v8::Global<v8::Symbol>>;
{
let op_state = op_state_rc.borrow();
if let Some(store) = op_state.try_borrow::<IframeRealmStore>() {
orig_fpt = store.orig_fp_tostring.as_ref().map(|g| {
let local = v8::Local::new(scope, g);
v8::Global::new(scope, local)
});
native_tag_sym = store.native_tag_sym.as_ref().map(|g| {
let local = v8::Local::new(scope, g);
v8::Global::new(scope, local)
});
} else {
orig_fpt = None;
native_tag_sym = None;
}
}
let child_ctx = v8::Context::new(scope, v8::ContextOptions::default());
let parent_ctx = scope.get_current_context();
let parent_tok = parent_ctx.get_security_token(scope);
child_ctx.set_security_token(parent_tok);
let child_global_g: Option<v8::Global<v8::Object>> = {
let cs = &mut v8::ContextScope::new(scope, child_ctx);
let window_tmpl = v8::FunctionTemplate::new(cs, _window_ctor_cb);
if let Some(n) = v8::String::new(cs, "Window") {
window_tmpl.set_class_name(n);
}
let window_fn = match window_tmpl.get_function(cs) {
Some(f) => f,
None => return v8::undefined(cs).into(),
};
if let Some(n) = v8::String::new(cs, "Window") {
window_fn.set_name(n);
}
if let Some(pk) = v8::String::new(cs, "prototype") {
if let Some(proto_val) = window_fn.get(cs, pk.into()) {
let child_global = child_ctx.global(cs);
child_global.set_prototype(cs, proto_val);
}
}
let child_global = child_ctx.global(cs);
if let Some(k) = v8::String::new(cs, "Window") {
child_global.set(cs, k.into(), window_fn.into());
}
for key in &["window", "self", "globalThis", "frames"] {
if let Some(k) = v8::String::new(cs, key) {
child_global.set(cs, k.into(), child_global.into());
}
}
if let Some(k) = v8::String::new(cs, "length") {
let zero = v8::Integer::new(cs, 0);
child_global.set(cs, k.into(), zero.into());
}
if let Some(k) = v8::String::new(cs, "opener") {
let null = v8::null(cs);
child_global.set(cs, k.into(), null.into());
}
if let Some(ref orig) = orig_fpt {
install_native_fp_tostring(cs, orig, native_tag_sym.as_ref());
}
Some(v8::Global::new(cs, child_global))
};
let child_global_g = match child_global_g {
Some(g) => g,
None => return v8::undefined(scope).into(),
};
let local: v8::Local<'s, v8::Value> = v8::Local::new(scope, &child_global_g).into();
{
let mut op_state = op_state_rc.borrow_mut();
if let Some(store) = op_state.try_borrow_mut::<IframeRealmStore>() {
store
.contexts
.insert(rid, v8::Global::new(scope, child_ctx));
store.globals.insert(rid, child_global_g);
}
}
local
}
#[op2]
pub fn op_set_child_realm_prop<'s>(
scope: &mut v8::PinScope<'s, '_>,
#[smi] realm_id: i32,
key: v8::Local<v8::Value>,
value: v8::Local<v8::Value>,
) -> v8::Local<'s, v8::Value> {
let rid = realm_id as u32;
let op_state_rc = JsRuntime::op_state_from(scope);
let child_ctx_g: Option<v8::Global<v8::Context>> = {
let op_state = op_state_rc.borrow();
op_state.try_borrow::<IframeRealmStore>().and_then(|store| {
store.contexts.get(&rid).map(|g| {
let local = v8::Local::new(scope, g);
v8::Global::new(scope, local)
})
})
};
let child_ctx_g = match child_ctx_g {
Some(g) => g,
None => return v8::undefined(scope).into(),
};
let child_ctx = v8::Local::new(scope, &child_ctx_g);
let cs = &mut v8::ContextScope::new(scope, child_ctx);
let child_proxy = child_ctx.global(cs);
if let Some(inner) = child_proxy
.get_prototype(cs)
.and_then(|p| v8::Local::<v8::Object>::try_from(p).ok())
{
if let Ok(k) = v8::Local::<v8::Name>::try_from(key) {
inner.create_data_property(cs, k, value);
}
}
child_proxy.set(cs, key, value);
v8::undefined(cs).into()
}
#[op2]
#[string]
pub fn op_eval_in_child_realm<'s>(
scope: &mut v8::PinScope<'s, '_>,
#[smi] realm_id: i32,
#[string] code: String,
) -> Option<String> {
let rid = realm_id as u32;
let op_state_rc = JsRuntime::op_state_from(scope);
let child_ctx_g: Option<v8::Global<v8::Context>> = {
let op_state = op_state_rc.borrow();
op_state.try_borrow::<IframeRealmStore>().and_then(|store| {
store.contexts.get(&rid).map(|g| {
let local = v8::Local::new(scope, g);
v8::Global::new(scope, local)
})
})
};
let child_ctx_g = child_ctx_g?;
let child_ctx = v8::Local::new(scope, &child_ctx_g);
let cs = &mut v8::ContextScope::new(scope, child_ctx);
let src = v8::String::new(cs, &code)?;
v8::tc_scope!(let tc, cs);
let ok = match v8::Script::compile(tc, src, None) {
Some(script) => script.run(tc).is_some(),
None => false,
};
if !ok && std::env::var("BROWSER_OXIDE_DEBUG_CHILD_REALM").is_ok() {
let msg = tc
.exception()
.and_then(|e| e.to_string(tc))
.map(|s| s.to_rust_string_lossy(tc))
.unwrap_or_else(|| "<no exception object>".to_string());
let snippet: String = code.chars().take(160).collect();
eprintln!("[child-realm:{rid}] eval error: {msg} | code[..160]={snippet:?}");
}
None
}
deno_core::extension!(
dom_extension,
ops = [
op_dom_document_node,
op_dom_get_tag_name,
op_dom_get_node_type,
op_dom_get_text_content,
op_dom_get_inner_html,
op_dom_get_outer_html,
op_dom_get_attribute,
op_dom_has_attribute,
op_dom_get_attribute_names,
op_dom_get_parent,
op_dom_get_children,
op_dom_get_children_with_types,
op_dom_get_child_elements,
op_dom_get_child_elements_with_types,
op_dom_get_first_child,
op_dom_get_last_child,
op_dom_get_next_sibling,
op_dom_get_prev_sibling,
op_dom_query_selector,
op_dom_query_selector_all,
op_dom_get_element_by_id,
op_dom_get_elements_by_tag_name,
op_dom_get_elements_by_class_name,
op_dom_create_element,
op_dom_create_text_node,
op_dom_create_document_fragment,
op_dom_append_child,
op_dom_insert_before,
op_dom_remove_child,
op_dom_set_attribute,
op_dom_remove_attribute,
op_dom_set_text_content,
op_dom_set_inner_html,
op_dom_document_write,
op_dom_clone_node,
op_dom_insert_adjacent_html,
op_dom_class_list_add,
op_dom_class_list_remove,
op_dom_get_computed_style,
op_dom_get_all_computed_styles,
op_dom_get_stylesheet_count,
op_dom_get_stylesheet_rules,
op_dom_attach_shadow,
op_dom_get_shadow_root,
op_dom_get_base_url,
op_dom_storage_get,
op_dom_storage_set,
op_dom_storage_remove,
op_dom_storage_clear,
op_dom_storage_keys,
op_create_child_realm,
op_set_child_realm_prop,
op_eval_in_child_realm,
],
);