use serde_json::Value;
use crate::protocol::{
eval_json, CdpError, HandlerResult, ERR_INVALID_PARAMS, ERR_NOT_SUPPORTED,
};
use crate::servo_bridge::BridgeSender;
const IDENTITY_JS: &str = r#"
var __baoResolve = function(id) {
if (!(typeof id === 'number' && Number.isSafeInteger(id) && id >= 1)) return null;
if (id === 1) return document;
var q = id - 1;
var parent = __baoResolve(Math.floor(q / 100));
if (!parent) return null;
return parent.childNodes[q % 100] || null;
};
var __baoCanonical = function(node) {
var chain = [];
var n = node;
while (n && n !== document) {
var p = n.parentNode;
if (!p) return null;
var i = Array.prototype.indexOf.call(p.childNodes, n);
if (i < 0) return null;
chain.push(i);
n = p;
}
if (n !== document) return null;
var id = 1;
for (var k = chain.length - 1; k >= 0; k--) {
id = id * 100 + chain[k] + 1;
if (!Number.isSafeInteger(id)) return null;
}
return id;
};
var __baoDecls = function(style) {
var css = [];
for (var i = 0; i < style.length; i++) {
var name = style[i];
css.push({
name: name,
value: style.getPropertyValue(name),
important: style.getPropertyPriority(name) === "important"
});
}
return { cssProperties: css, shorthandEntries: [] };
};
var __baoNodeFields = function(node, id) {
var e = {
nodeId: id,
backendNodeId: id,
nodeType: node.nodeType,
nodeName: node.nodeName,
localName: node.localName || "",
nodeValue: node.nodeValue || "",
childNodeCount: node.childNodes.length
};
if (node.nodeType === 1) {
var attrs = [];
for (var a = 0; a < node.attributes.length; a++) {
attrs.push(node.attributes[a].name);
attrs.push(node.attributes[a].value);
}
e.attributes = attrs;
}
return e;
};
"#;
pub(crate) fn require_node_id(params: &Option<Value>) -> Result<i64, CdpError> {
params
.as_ref()
.and_then(|p| p.get("nodeId"))
.and_then(|v| v.as_i64())
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: nodeId".into(),
})
}
pub(crate) fn require_object_id(params: &Option<Value>) -> Result<String, CdpError> {
params
.as_ref()
.and_then(|p| p.get("objectId"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: objectId".into(),
})
}
pub(crate) fn eval_dom(
bridge: Option<&BridgeSender>,
target_id: &str,
body: &str,
) -> HandlerResult {
let expression = format!("(function() {{{IDENTITY_JS}{body}\n}})()");
let doc = eval_json(bridge, target_id, &expression)?;
if let Some(reason) = doc.get("__bao_err").and_then(|e| e.as_str()) {
return Err(CdpError {
code: ERR_NOT_SUPPORTED,
message: reason.to_string(),
});
}
Ok(doc)
}
pub(crate) fn css_element_body(node_id: i64, on_element: &str) -> String {
format!(
r#"var el = __baoResolve({node_id});
if (!el) return JSON.stringify({{__bao_err: "No node with given id found"}});
if (el.nodeType !== 1) return JSON.stringify({{__bao_err: "node is not an element (styles require an Element node)"}});
{on_element}"#,
node_id = node_id,
on_element = on_element,
)
}
pub(crate) fn computed_style_body(node_id: i64) -> String {
css_element_body(
node_id,
r#"var styles = window.getComputedStyle(el);
var out = [];
for (var i = 0; i < styles.length; i++) {
var name = styles[i];
out.push({ name: name, value: styles.getPropertyValue(name) });
}
return JSON.stringify({ computedStyle: out });"#,
)
}
pub(crate) fn inline_styles_body(node_id: i64) -> String {
css_element_body(
node_id,
r#"var inline = null;
if (el.style && el.style.length > 0) inline = __baoDecls(el.style);
return JSON.stringify({ inlineStyle: inline, attributesStyle: null });"#,
)
}
pub(crate) fn matched_styles_body(node_id: i64) -> String {
css_element_body(
node_id,
r#"var rules = [];
var sheets = document.styleSheets;
for (var s = 0; s < sheets.length; s++) {
var sheetRules = null;
try { sheetRules = sheets[s].cssRules; } catch (e) { sheetRules = null; }
if (!sheetRules) continue;
for (var r = 0; r < sheetRules.length; r++) {
var rule = sheetRules[r];
if (!rule || typeof rule.selectorText !== "string" || !rule.style) continue;
var matching = [];
var selectors = rule.selectorText.split(",");
for (var si = 0; si < selectors.length; si++) {
var sel = selectors[si].replace(/^\s+|\s+$/g, "");
if (!sel) continue;
try { if (el.matches(sel)) matching.push(si); } catch (e2) {}
}
if (matching.length === 0) continue;
rules.push({
rule: {
selectorList: { selectors: [{ text: rule.selectorText }] },
style: __baoDecls(rule.style),
origin: "regular",
sourceURL: sheets[s].href || "",
styleSheetId: String(s)
},
matchingSelectors: matching
});
}
}
var inline = null;
if (el.style && el.style.length > 0) inline = __baoDecls(el.style);
return JSON.stringify({ matchedCSSRules: rules, inlineStyle: inline, attributesStyle: null });"#,
)
}
pub(crate) fn flattened_document_body(depth: i64) -> String {
format!(
r#"var depth = {depth};
var nodes = [];
function push(node, parentId, level) {{
var id = __baoCanonical(node);
if (id === null) throw "node-id-overflow";
var e = __baoNodeFields(node, id);
if (parentId !== 0) e.parentId = parentId;
nodes.push(e);
var within = (depth < 0) || (level < depth);
if (e.childNodeCount > 0 && within) {{
e.children = [];
for (var i = 0; i < node.childNodes.length; i++) {{
e.children.push(push(node.childNodes[i], id, level + 1));
}}
}}
return e;
}}
var root;
try {{ root = push(document, 0, 0); }}
catch (e) {{
if (e === "node-id-overflow") return JSON.stringify({{__bao_err: "subtree exceeds addressable node-id precision (positional ids are safe through ~7 nested levels)"}});
throw e;
}}
return JSON.stringify({{ nodes: nodes }});"#,
depth = depth,
)
}
pub(crate) fn node_for_owner_body(object_id: &str) -> String {
let oid_json = serde_json::to_string(object_id).unwrap_or_default();
format!(
r#"var oid = {oid_json};
var v = null;
if (oid.lastIndexOf("node-", 0) === 0) {{
var n = parseInt(oid.slice(5), 10);
v = (n <= 1) ? document
: (n === 2 ? document.documentElement
: (document.body ? document.body.childNodes[n - 3] : null));
}} else {{
if (!window.__bao_cdp) return JSON.stringify({{__bao_err: "object registry not present: obtain objectIds via Runtime.evaluate(returnByValue:false) on this document"}});
v = window.__bao_cdp.get(oid);
}}
if (v === null || v === undefined) return JSON.stringify({{__bao_err: "No object with given id found"}});
if (typeof Node === "undefined" || !(v instanceof Node)) return JSON.stringify({{__bao_err: "object is not a DOM Node"}});
var id = __baoCanonical(v);
if (id === null) return JSON.stringify({{__bao_err: "node is detached from the document or exceeds addressable node-id precision"}});
return JSON.stringify({{ nodeId: id, backendNodeId: id }});"#,
oid_json = oid_json,
)
}
pub(crate) enum NodeRef {
NodeId(i64),
ObjectId(String),
}
pub(crate) fn require_node_ref(params: &Option<Value>) -> Result<NodeRef, CdpError> {
let node_id = params
.as_ref()
.and_then(|p| p.get("nodeId"))
.and_then(|v| v.as_i64());
let object_id = params
.as_ref()
.and_then(|p| p.get("objectId"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty());
match (node_id, object_id) {
(Some(id), _) => Ok(NodeRef::NodeId(id)),
(None, Some(oid)) => Ok(NodeRef::ObjectId(oid.to_string())),
_ => Err(CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: nodeId or objectId".into(),
}),
}
}
pub(crate) fn require_selector(params: &Option<Value>) -> Result<String, CdpError> {
params
.as_ref()
.and_then(|p| p.get("selector"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: selector".into(),
})
}
fn node_ref_prelude(r#ref: &NodeRef) -> String {
match r#ref {
NodeRef::NodeId(id) => format!(
r#"var el = __baoResolve({id});
if (!el) return JSON.stringify({{__bao_err: "No node with given id found"}});"#
),
NodeRef::ObjectId(oid) => {
let oid_json = serde_json::to_string(oid).unwrap_or_default();
format!(
r#"if (!window.__bao_cdp) return JSON.stringify({{__bao_err: "object registry not present: obtain objectIds via Runtime.evaluate(returnByValue:false) on this document"}});
var el = window.__bao_cdp.get({oid_json});
if (el === null || el === undefined) return JSON.stringify({{__bao_err: "No object with given id found"}});"#
)
}
}
}
pub(crate) fn query_selector_body(selector: &str) -> String {
let sel_json = serde_json::to_string(selector).unwrap_or_default();
format!(
r#"var el = document.querySelector({sel_json});
if (!el) return JSON.stringify({{ nodeId: 0 }});
var id = __baoCanonical(el);
if (id === null) return JSON.stringify({{__bao_err: "node is detached from the document or exceeds addressable node-id precision"}});
return JSON.stringify({{ nodeId: id, backendNodeId: id }});"#,
sel_json = sel_json,
)
}
pub(crate) fn query_selector_all_body(selector: &str) -> String {
let sel_json = serde_json::to_string(selector).unwrap_or_default();
format!(
r#"var list = document.querySelectorAll({sel_json});
var ids = [];
for (var i = 0; i < list.length; i++) {{
var id = __baoCanonical(list[i]);
if (id === null) return JSON.stringify({{__bao_err: "matched node is detached or exceeds addressable node-id precision"}});
ids.push(id);
}}
return JSON.stringify({{ nodeIds: ids }});"#,
sel_json = sel_json,
)
}
pub(crate) fn describe_node_body(r#ref: &NodeRef, depth: i64) -> String {
format!(
r#"{}var depth = {depth};
if (__baoCanonical(el) === null) return JSON.stringify({{__bao_err: "node is detached from the document or exceeds addressable node-id precision"}});
function build(node, parentId, depthLeft) {{
var id = __baoCanonical(node);
if (id === null) throw "node-id-overflow";
var e = __baoNodeFields(node, id);
if (parentId !== 0) e.parentId = parentId;
if (e.childNodeCount > 0 && (depthLeft < 0 || depthLeft > 0)) {{
e.children = [];
for (var i = 0; i < node.childNodes.length; i++) {{
e.children.push(build(node.childNodes[i], id, depthLeft < 0 ? -1 : depthLeft - 1));
}}
}}
return e;
}}
var node;
try {{ node = build(el, 0, depth); }}
catch (e) {{
if (e === "node-id-overflow") return JSON.stringify({{__bao_err: "subtree exceeds addressable node-id precision (positional ids are safe through ~7 nested levels)"}});
throw e;
}}
return JSON.stringify({{ node: node }});"#,
node_ref_prelude(r#ref),
depth = depth,
)
}
pub(crate) fn box_model_body(r#ref: &NodeRef) -> String {
format!(
r#"{}if (el.nodeType !== 1) return JSON.stringify({{__bao_err: "node is not an element (box model requires an Element node)"}});
var r = el.getBoundingClientRect();
var cs = window.getComputedStyle(el);
function px(name) {{ var v = parseFloat(cs.getPropertyValue(name)); return isNaN(v) ? 0 : v; }}
var bl = px("border-left-width"), bt = px("border-top-width");
var br = px("border-right-width"), bb = px("border-bottom-width");
var pl = px("padding-left"), pt = px("padding-top");
var pr = px("padding-right"), pb = px("padding-bottom");
var ml = px("margin-left"), mt = px("margin-top");
var mr = px("margin-right"), mb = px("margin-bottom");
function quad(x, y, w, h) {{ return [x, y, x + w, y, x + w, y + h, x, y + h]; }}
return JSON.stringify({{ model: {{
width: r.width,
height: r.height,
content: quad(r.left + bl + pl, r.top + bt + pt, r.width - bl - br - pl - pr, r.height - bt - bb - pt - pb),
padding: quad(r.left + bl, r.top + bt, r.width - bl - br, r.height - bt - bb),
border: quad(r.left, r.top, r.width, r.height),
margin: quad(r.left - ml, r.top - mt, r.width + ml + mr, r.height + mt + mb)
}} }});"#,
node_ref_prelude(r#ref),
)
}
pub(crate) fn resolve_node_body(r#ref: &NodeRef, object_group: Option<&str>) -> String {
let group_json = serde_json::to_string(object_group.unwrap_or("")).unwrap_or_default();
format!(
r##"{}var oid = window.__bao_cdp.alloc(el, {group_json});
var cn = "Object";
try {{ if (el.constructor && el.constructor.name) cn = el.constructor.name; }} catch (e) {{}}
var desc = String(el.nodeName);
try {{ if (el.nodeType === 1) desc = el.nodeName.toLowerCase() + (el.id ? "#" + el.id : ""); }} catch (e) {{}}
return JSON.stringify({{ object: {{ type: "object", subtype: "node", className: cn, description: desc, objectId: oid }} }});"##,
node_ref_prelude(r#ref),
group_json = group_json,
)
}
pub(crate) fn push_nodes_body(backend_node_ids: &[i64]) -> String {
let ids_json = serde_json::to_string(backend_node_ids).unwrap_or_default();
format!(
r#"var requested = {ids_json};
var out = [];
for (var i = 0; i < requested.length; i++) {{
if (__baoResolve(requested[i])) out.push(requested[i]);
}}
return JSON.stringify({{ nodeIds: out }});"#,
ids_json = ids_json,
)
}
pub(crate) fn require_edits(params: &Option<Value>) -> Result<Vec<Value>, CdpError> {
params
.as_ref()
.and_then(|p| p.get("edits"))
.and_then(|v| v.as_array())
.cloned()
.ok_or_else(|| CdpError {
code: ERR_INVALID_PARAMS,
message: "missing required parameter: edits".into(),
})
}
pub(crate) fn set_style_texts_body(edits: &[Value]) -> String {
let edits_json = serde_json::to_string(edits).unwrap_or_default();
format!(
r#"var edits = {edits_json};
var outStyles = [];
for (var i = 0; i < edits.length; i++) {{
var e = edits[i];
var sheetIdx = parseInt(e.styleSheetId, 10);
var sheet = (isNaN(sheetIdx)) ? null : document.styleSheets[sheetIdx];
if (!sheet) return JSON.stringify({{__bao_err: "unknown styleSheetId (this face addresses document.styleSheets ordinals; discover ids via getMatchedStylesForNode)"}});
var ruleIdx = (e.range && typeof e.range.startLine === "number") ? e.range.startLine : -1;
var rules = null;
try {{ rules = sheet.cssRules; }} catch (x) {{}}
if (!rules || ruleIdx < 0 || ruleIdx >= rules.length) return JSON.stringify({{__bao_err: "edit range must carry the rule index in range.startLine (0-based, within the sheet's cssRules)"}});
var rule = rules[ruleIdx];
if (!rule || !rule.style) return JSON.stringify({{__bao_err: "rule at range.startLine is not a style rule (no declaration block to edit)"}});
rule.style.cssText = e.text;
var edited = __baoDecls(rule.style);
edited.styleSheetId = e.styleSheetId;
outStyles.push(edited);
}}
return JSON.stringify({{ styles: outStyles }});"#,
edits_json = edits_json,
)
}