use std::collections::BTreeMap;
use crate::Element;
pub(crate) const SNAP_DEPTH: usize = 2;
pub(crate) struct SnapNode {
pub role: String,
pub name: String,
pub text: Option<String>,
pub children: Vec<SnapNode>,
}
impl SnapNode {
pub fn format_tree(&self, indent: usize) -> String {
let label = elem_label(&self.role, &self.name);
let prefix = " ".repeat(indent);
let line = match &self.text {
Some(t) if !t.is_empty() && t != &self.name => {
format!("{prefix}{label}: {t:?}")
}
_ => format!("{prefix}{label}"),
};
let mut lines = vec![line];
for child in &self.children {
lines.push(child.format_tree(indent + 1));
}
lines.join("\n")
}
pub fn capture<E: Element>(el: &E, depth: usize) -> Self {
let name = el.name().unwrap_or_default();
let role = el.role();
let text = if role == "text" { el.text().ok() } else { None };
let children = if depth > 0 {
el.children()
.unwrap_or_default()
.into_iter()
.map(|c| SnapNode::capture(&c, depth - 1))
.collect()
} else {
vec![]
};
SnapNode {
role,
name,
text,
children,
}
}
pub fn diff_into(&self, new: &SnapNode, path: &str, out: &mut Vec<String>) {
if self.name != new.name {
out.push(format!(
"dom: {path}: name {:?} → {:?}",
self.name, new.name
));
}
if self.text != new.text {
let was_mirror = self.text.as_deref() == Some(self.name.as_str());
let is_mirror = new.text.as_deref() == Some(new.name.as_str());
if !(was_mirror && is_mirror) {
out.push(format!(
"dom: {path}: text {:?} → {:?}",
self.text, new.text
));
}
}
diff_children(path, &self.children, &new.children, out);
}
}
fn diff_children(parent_path: &str, old: &[SnapNode], new: &[SnapNode], out: &mut Vec<String>) {
let mut old_groups: BTreeMap<(String, String), Vec<&SnapNode>> = BTreeMap::new();
for n in old {
old_groups
.entry((n.role.clone(), n.name.clone()))
.or_default()
.push(n);
}
let mut new_groups: BTreeMap<(String, String), Vec<&SnapNode>> = BTreeMap::new();
for n in new {
new_groups
.entry((n.role.clone(), n.name.clone()))
.or_default()
.push(n);
}
let all_keys: BTreeMap<(String, String), ()> = old_groups
.keys()
.chain(new_groups.keys())
.map(|k| (k.clone(), ()))
.collect();
let empty: Vec<&SnapNode> = vec![];
let mut orphan_removes: BTreeMap<String, Vec<&SnapNode>> = BTreeMap::new();
let mut orphan_adds: BTreeMap<String, Vec<&SnapNode>> = BTreeMap::new();
for (key, _) in &all_keys {
let old_vec = old_groups.get(key).unwrap_or(&empty);
let new_vec = new_groups.get(key).unwrap_or(&empty);
let label = elem_label(&key.0, &key.1);
for node in new_vec.iter().skip(old_vec.len()) {
orphan_adds.entry(key.0.clone()).or_default().push(node);
}
for node in old_vec.iter().skip(new_vec.len()) {
orphan_removes.entry(key.0.clone()).or_default().push(node);
}
let match_count = old_vec.len().min(new_vec.len());
for i in 0..match_count {
let child_path = if match_count > 1 {
format!("{parent_path} > {label}[{i}]")
} else {
format!("{parent_path} > {label}")
};
old_vec[i].diff_into(new_vec[i], &child_path, out);
}
}
let all_orphan_roles: BTreeMap<String, ()> = orphan_removes
.keys()
.chain(orphan_adds.keys())
.map(|r| (r.clone(), ()))
.collect();
for (role, _) in &all_orphan_roles {
let removes = orphan_removes.get(role).map(Vec::as_slice).unwrap_or(&[]);
let adds = orphan_adds.get(role).map(Vec::as_slice).unwrap_or(&[]);
let change_count = removes.len().min(adds.len());
for i in 0..change_count {
let old_label = elem_label(&removes[i].role, &removes[i].name);
let new_label = elem_label(&adds[i].role, &adds[i].name);
out.push(format!(
"dom: {parent_path}: CHANGED {old_label} → {new_label}"
));
let child_path = if change_count > 1 {
format!("{parent_path} > [{role}][{i}]")
} else {
format!("{parent_path} > [{role}]")
};
diff_children(&child_path, &removes[i].children, &adds[i].children, out);
}
for node in removes.iter().skip(change_count) {
let label = elem_label(&node.role, &node.name);
out.push(format!("dom: {parent_path}: REMOVED {label}"));
}
for node in adds.iter().skip(change_count) {
out.push(format!(
"dom: {parent_path}: ADDED\n{}",
node.format_tree(0)
));
}
}
}
fn elem_label(role: &str, name: &str) -> String {
if name.is_empty() {
format!("[{role}]")
} else {
format!("[{role} {name:?}]")
}
}