use ff_rdp_core::{DomWalkerActor, InspectorActor};
use serde_json::{Value, json};
use crate::cli::args::Cli;
use crate::error::AppError;
use crate::hints::{HintContext, HintSource};
use crate::output;
use crate::output_pipeline::OutputPipeline;
use super::connect_tab::connect_and_get_target;
pub fn run(cli: &Cli, selector: Option<&str>, depth: u32, max_chars: u32) -> Result<(), AppError> {
let mut ctx = connect_and_get_target(cli)?;
let inspector_actor = ctx
.target
.inspector_actor
.clone()
.ok_or_else(|| AppError::User("no inspector actor available".to_string()))?;
let walker =
InspectorActor::get_walker(ctx.transport_mut(), &inspector_actor).map_err(map_dom_error)?;
let root =
DomWalkerActor::document_element(ctx.transport_mut(), &walker).map_err(map_dom_error)?;
let target_node = if let Some(sel) = selector {
let node_actor = root.actor.as_deref().ok_or_else(|| {
AppError::User(
"document element has no actor ID — cannot run querySelector".to_string(),
)
})?;
let node_actor_id = node_actor.into();
DomWalkerActor::query_selector(ctx.transport_mut(), &walker, &node_actor_id, sel)
.map_err(map_dom_error)?
.ok_or_else(|| AppError::User(format!("no element matching selector '{sel}'")))?
} else {
root
};
let tree =
DomWalkerActor::walk_tree(ctx.transport_mut(), &walker, &target_node, depth, max_chars)
.map_err(map_dom_error)?;
let mut results = serde_json::to_value(&tree).map_err(|e| AppError::Internal(e.into()))?;
strip_actor_ids(&mut results);
let meta = if let Some(sel) = selector {
json!({
"host": cli.host,
"port": cli.port,
"depth": depth,
"max_chars": max_chars,
"selector": sel,
})
} else {
json!({
"host": cli.host,
"port": cli.port,
"depth": depth,
"max_chars": max_chars,
})
};
if cli.format == "text" && cli.jq.is_none() {
render_dom_tree_text(&results);
return Ok(());
}
let envelope = output::envelope(&results, 1, &meta);
let hint_ctx = HintContext::new(HintSource::DomTree);
OutputPipeline::from_cli(cli)?
.finalize_with_hints(&envelope, Some(&hint_ctx))
.map_err(AppError::from)
}
fn render_dom_tree_text(node: &Value) {
if node.is_null() {
println!("(empty tree)");
return;
}
render_dom_node(node, 0);
}
fn render_dom_node(node: &Value, depth: usize) {
use std::fmt::Write as _;
let indent = " ".repeat(depth);
let node_name = node.get("nodeName").and_then(Value::as_str).unwrap_or("?");
let node_type = node.get("nodeType").and_then(Value::as_u64).unwrap_or(0);
match node_type {
3 => {
if let Some(text) = node.get("nodeValue").and_then(Value::as_str) {
let text = text.trim();
if !text.is_empty() {
if text.len() > 80 {
let truncated: String = text.chars().take(77).collect();
println!("{indent}\"{truncated}...\"");
} else {
println!("{indent}\"{text}\"");
}
}
}
}
1 => {
let tag = node_name.to_lowercase();
let mut line = format!("{indent}<{tag}");
if let Some(attrs) = node.get("attrs").and_then(Value::as_array) {
for attr in attrs {
if let (Some(name), Some(value)) = (
attr.get("name").and_then(Value::as_str),
attr.get("value").and_then(Value::as_str),
) && matches!(
name,
"id" | "class" | "href" | "src" | "type" | "name" | "role" | "aria-label"
) {
let val = if value.chars().count() > 40 {
let truncated: String = value.chars().take(37).collect();
format!("{truncated}...")
} else {
value.to_string()
};
let _ = write!(line, " {name}={val:?}");
}
}
}
if let Some(truncated) = node.get("truncated").and_then(Value::as_str) {
let _ = write!(line, " ({truncated})");
}
println!("{line}>");
if let Some(children) = node.get("children").and_then(Value::as_array) {
for child in children {
render_dom_node(child, depth + 1);
}
}
}
_ => {
println!("{indent}{node_name}");
if let Some(children) = node.get("children").and_then(Value::as_array) {
for child in children {
render_dom_node(child, depth + 1);
}
}
}
}
}
fn map_dom_error(err: ff_rdp_core::ProtocolError) -> AppError {
match &err {
ff_rdp_core::ProtocolError::ActorError { error, .. }
if error == "noSuchActor" || error == "unknownActor" =>
{
AppError::User(
"DOM walker actor is no longer valid \
— the DOM walker actor may have expired after navigation. Re-run the command"
.to_string(),
)
}
_ => AppError::from(err),
}
}
fn strip_actor_ids(value: &mut Value) {
match value {
Value::Object(map) => {
map.remove("actor");
for v in map.values_mut() {
strip_actor_ids(v);
}
}
Value::Array(arr) => {
for v in arr {
strip_actor_ids(v);
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn render_dom_tree_text_does_not_panic_null() {
render_dom_tree_text(&Value::Null);
}
#[test]
fn render_dom_tree_text_element_with_children() {
let tree = json!({
"nodeType": 1,
"nodeName": "HTML",
"attrs": [{"name": "lang", "value": "en"}],
"children": [
{"nodeType": 1, "nodeName": "BODY", "children": [
{"nodeType": 3, "nodeName": "#text", "nodeValue": "Hello World"}
]}
]
});
render_dom_tree_text(&tree);
}
#[test]
fn render_dom_tree_text_with_truncation() {
let tree = json!({
"nodeType": 1,
"nodeName": "DIV",
"truncated": "5 children not shown"
});
render_dom_tree_text(&tree);
}
#[test]
fn strip_actor_ids_removes_actor_field() {
let mut val = json!({
"actor": "conn1/domwalker1",
"nodeType": 1,
"nodeName": "HTML",
"children": [
{"actor": "conn1/node2", "nodeType": 1, "nodeName": "BODY"}
]
});
strip_actor_ids(&mut val);
assert!(val.get("actor").is_none());
assert!(val["children"][0].get("actor").is_none());
assert_eq!(val["children"][0]["nodeName"], "BODY");
}
#[test]
fn strip_actor_ids_leaves_other_fields_intact() {
let mut val = json!({
"actor": "conn1/node1",
"nodeType": 3,
"nodeName": "#text",
"nodeValue": "Hello"
});
strip_actor_ids(&mut val);
assert!(val.get("actor").is_none());
assert_eq!(val["nodeType"], 3);
assert_eq!(val["nodeName"], "#text");
assert_eq!(val["nodeValue"], "Hello");
}
#[test]
fn strip_actor_ids_handles_no_actor_field() {
let mut val = json!({"nodeType": 1, "nodeName": "DIV"});
strip_actor_ids(&mut val);
assert!(val.get("actor").is_none());
assert_eq!(val["nodeName"], "DIV");
}
#[test]
fn strip_actor_ids_handles_nested_arrays() {
let mut val = json!({
"actor": "root",
"children": [
{"actor": "child1", "nodeName": "P"},
{"actor": "child2", "nodeName": "SPAN", "children": [
{"actor": "grandchild", "nodeName": "#text"}
]}
]
});
strip_actor_ids(&mut val);
assert!(val.get("actor").is_none());
assert!(val["children"][0].get("actor").is_none());
assert!(val["children"][1].get("actor").is_none());
assert!(val["children"][1]["children"][0].get("actor").is_none());
}
}