use crate::{Props, VNode};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq)]
pub enum Patch {
Replace(VNode),
SetAttr(String, String),
RemoveAttr(String),
UpdateChild(usize, Vec<Patch>),
InsertChild(usize, VNode),
RemoveChild(usize),
MoveChild(usize, usize),
}
impl VNode {
pub fn key(&self) -> Option<String> {
self.key_str().map(str::to_string)
}
pub fn key_str(&self) -> Option<&str> {
match self {
VNode::Element { props, .. } => props.attrs.get("key").map(String::as_str),
_ => None,
}
}
}
pub fn diff(old: &VNode, new: &VNode) -> Vec<Patch> {
match (old, new) {
(VNode::Text(a), VNode::Text(b)) => {
if a != b {
vec![Patch::Replace(new.clone())]
} else {
vec![]
}
}
(
VNode::Element {
tag: tag_a,
props: props_a,
children: children_a,
},
VNode::Element {
tag: tag_b,
props: props_b,
children: children_b,
},
) => {
if tag_a != tag_b {
return vec![Patch::Replace(new.clone())];
}
let mut patches = Vec::new();
patches.extend(diff_props(props_a, props_b));
patches.extend(diff_children(children_a, children_b));
patches
}
_ => vec![Patch::Replace(new.clone())],
}
}
fn diff_props(a: &Props, b: &Props) -> Vec<Patch> {
let mut patches = Vec::new();
for (k, v_new) in &b.attrs {
match a.attrs.get(k) {
Some(v_old) if v_old == v_new => {}
_ => patches.push(Patch::SetAttr(k.clone(), v_new.clone())),
}
}
for k in a.attrs.keys() {
if !b.attrs.contains_key(k) {
patches.push(Patch::RemoveAttr(k.clone()));
}
}
patches
}
fn diff_children_keyed(
old: &[VNode],
new: &[VNode],
get_key: impl Fn(&VNode) -> Option<&str>,
) -> Vec<Patch> {
enum Slot {
Old(usize),
New,
}
let mut sim: Vec<Slot> = (0..old.len()).map(Slot::Old).collect();
let mut used_old: HashSet<usize> = HashSet::new();
let mut patches: Vec<Patch> = Vec::new();
for (new_idx, new_node) in new.iter().enumerate() {
let key = get_key(new_node);
let mut reuse: Option<usize> = None; if let Some(key) = key {
for (j, slot) in sim.iter().enumerate() {
if let Slot::Old(i) = slot
&& !used_old.contains(i)
&& get_key(&old[*i]) == Some(key)
{
reuse = Some(j);
break;
}
}
}
match reuse {
Some(cur) => {
let old_idx = match &sim[cur] {
Slot::Old(i) => *i,
Slot::New => unreachable!("reuse index always points at an Old slot"),
};
if cur != new_idx {
let slot = sim.remove(cur);
sim.insert(new_idx, slot);
patches.push(Patch::MoveChild(cur, new_idx));
}
used_old.insert(old_idx);
let child_patches = diff(&old[old_idx], new_node);
if !child_patches.is_empty() {
patches.push(Patch::UpdateChild(new_idx, child_patches));
}
}
None => {
sim.insert(new_idx, Slot::New);
patches.push(Patch::InsertChild(new_idx, new_node.clone()));
}
}
}
let mut j = 0;
while j < sim.len() {
if let Slot::Old(i) = sim[j]
&& !used_old.contains(&i)
{
patches.push(Patch::RemoveChild(j));
sim.remove(j);
} else {
j += 1;
}
}
patches
}
fn diff_children(a: &[VNode], b: &[VNode]) -> Vec<Patch> {
let has_keys = b.iter().any(|n| n.key_str().is_some());
if has_keys {
return diff_children_keyed(a, b, |n| n.key_str());
}
let mut patches = Vec::new();
let common = a.len().min(b.len());
for i in 0..common {
let child_patches = diff(&a[i], &b[i]);
if !child_patches.is_empty() {
patches.push(Patch::UpdateChild(i, child_patches));
}
}
if b.len() > a.len() {
for (i, node) in b.iter().enumerate().skip(a.len()) {
patches.push(Patch::InsertChild(i, node.clone()));
}
}
if a.len() > b.len() {
for i in (b.len()..a.len()).rev() {
patches.push(Patch::RemoveChild(i));
}
}
patches
}