//! A primitive diffing algorithm //! //! //! //! //! use std::{collections::HashMap, mem}; use crate::innerlude::*; use crate::patch::Patch; use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; use generational_arena::Index; pub struct DiffMachine { immediate_queue: Vec, diffed: FxHashSet, need_to_diff: FxHashSet, marked_for_removal: Vec, } impl DiffMachine { pub fn new() -> Self { Self { immediate_queue: vec![], diffed: FxHashSet::default(), need_to_diff: FxHashSet::default(), marked_for_removal: vec![], } } /// Given two VirtualNode's generate Patch's that would turn the old virtual node's /// real DOM node equivalent into the new VirtualNode's real DOM node equivalent. pub fn diff<'a>(&mut self, old: &'a VNode, new: &'a VNode) -> Vec> { self.diff_recursive(&old, &new, &mut 0) } pub fn diff_recursive<'a, 'b>( &mut self, old: &'a VNode, new: &'a VNode, cur_node_idx: &'b mut usize, ) -> Vec> { let mut patches = vec![]; let mut replace = false; // Different enum variants, replace! if mem::discriminant(old) != mem::discriminant(new) { replace = true; } if let (VNode::Element(old_element), VNode::Element(new_element)) = (old, new) { // Replace if there are different element tags if old_element.tag_name != new_element.tag_name { // if old_element.tag != new_element.tag { replace = true; } // Replace if two elements have different keys // TODO: More robust key support. This is just an early stopgap to allow you to force replace // an element... say if it's event changed. Just change the key name for now. // In the future we want keys to be used to create a Patch::ReOrder to re-order siblings // todo! // if old_element.attributes.get("key").is_some() // && old_element.attrs.get("key") != new_element.attrs.get("key") // { // replace = true; // } } // Handle replacing of a node if replace { patches.push(Patch::Replace(*cur_node_idx, &new)); if let VNode::Element(old_element_node) = old { for child in old_element_node.children.iter() { increment_node_idx_for_children(child, cur_node_idx); } } return patches; } // The following comparison can only contain identical variants, other // cases have already been handled above by comparing variant // discriminants. match (old, new) { // We're comparing two text nodes (VNode::Text(old_text), VNode::Text(new_text)) => { if old_text != new_text { patches.push(Patch::ChangeText(*cur_node_idx, &new_text)); } } // We're comparing two element nodes (VNode::Element(old_element), VNode::Element(new_element)) => { // let b: HashMap<&str, &str, FxBuildHasher> = HashMap::new() let old_attrs = old_element .attributes .iter() .map(|f| (f.name, f.value)) .collect::>(); let new_attrs = old_element .attributes .iter() .map(|f| (f.name, f.value)) .collect::>(); let mut add_attributes = FxHashMap::<&'static str, &str>::default(); // [("blah", "blah")] // .into_iter() // .map(|f| (f.0, f.1)) // .collect::>(); // let mut add_attribute = HashMap::<&str, &str, FxBuildHasher>::new(); let mut remove_attributes: Vec<&str> = vec![]; // TODO: -> split out into func for (new_attr_name, new_attr_val) in new_attrs.iter() { // for (new_attr_name, new_attr_val) in new_element.attrs.iter() { match old_attrs.get(new_attr_name) { // match old_element.attrs.get(new_attr_name) { Some(ref old_attr_val) => { if old_attr_val != &new_attr_val { add_attributes.insert(new_attr_name, new_attr_val); } } None => { add_attributes.insert(new_attr_name, new_attr_val); } }; } // TODO: -> split out into func for (old_attr_name, old_attr_val) in old_attrs.iter() { // for (old_attr_name, old_attr_val) in old_element.attrs.iter() { if add_attributes.get(&old_attr_name[..]).is_some() { continue; }; match new_attrs.get(old_attr_name) { // match new_element.attrs.get(old_attr_name) { Some(ref new_attr_val) => { if new_attr_val != &old_attr_val { remove_attributes.push(old_attr_name); } } None => { remove_attributes.push(old_attr_name); } }; } if add_attributes.len() > 0 { patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes)); } if remove_attributes.len() > 0 { patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes)); } let old_child_count = old_element.children.len(); let new_child_count = new_element.children.len(); if new_child_count > old_child_count { let append_patch: Vec<&'a VNode> = new_element.children[old_child_count..].iter().collect(); patches.push(Patch::AppendChildren(*cur_node_idx, append_patch)) } if new_child_count < old_child_count { patches.push(Patch::TruncateChildren(*cur_node_idx, new_child_count)) } let min_count = std::cmp::min(old_child_count, new_child_count); for index in 0..min_count { *cur_node_idx = *cur_node_idx + 1; let old_child = &old_element.children[index]; let new_child = &new_element.children[index]; patches.append(&mut self.diff_recursive(&old_child, &new_child, cur_node_idx)) } if new_child_count < old_child_count { for child in old_element.children[min_count..].iter() { increment_node_idx_for_children(child, cur_node_idx); } } } (VNode::Suspended, _) | (_, VNode::Suspended) | (VNode::Component(_), _) | (_, VNode::Component(_)) => { todo!("cant yet handle these two") } (VNode::Text(_), VNode::Element(_)) | (VirtualNode::Element(_), VirtualNode::Text(_)) => { unreachable!("Unequal variant discriminants should already have been handled"); } }; // new_root.create_element() patches } } fn increment_node_idx_for_children<'a, 'b>(old: &'a VirtualNode, cur_node_idx: &'b mut usize) { *cur_node_idx += 1; if let VirtualNode::Element(element_node) = old { for child in element_node.children.iter() { increment_node_idx_for_children(&child, cur_node_idx); } } } // #[cfg(test)] mod tests { use bumpalo::Bump; use super::*; fn test_diff( tree1: impl Fn(&Bump) -> VNode<'_>, tree2: impl Fn(&Bump) -> VNode<'_>, expected_patches: Vec, description: &'static str, ) { let bump = Bump::new(); let nodes1 = tree1(&bump); let nodes2 = tree1(&bump); let mut machine = DiffMachine::new(); let patches = machine.diff(&nodes1, &nodes2); patches .iter() .zip(expected_patches.iter()) .for_each(|f| assert_eq!(compare_patch(f.0, f.1), true, "{}", description)); } // todo: make this actually perform real comparisons // by default, nothing is derived for vnodes or patches fn compare_patch(patch1: &Patch, patch2: &Patch) -> bool { match (patch1, patch2) { (Patch::AppendChildren(_, _), Patch::AppendChildren(_, _)) => true, (Patch::AppendChildren(_, _), _) => false, (Patch::TruncateChildren(_, _), Patch::TruncateChildren(_, _)) => true, (Patch::TruncateChildren(_, _), _) => false, (Patch::Replace(_, _), Patch::Replace(_, _)) => true, (Patch::Replace(_, _), _) => false, (Patch::AddAttributes(_, _), Patch::AddAttributes(_, _)) => true, (Patch::AddAttributes(_, _), _) => false, (Patch::RemoveAttributes(_, _), Patch::RemoveAttributes(_, _)) => true, (Patch::RemoveAttributes(_, _), _) => false, (Patch::ChangeText(_, _), Patch::ChangeText(_, _)) => true, (Patch::ChangeText(_, _), _) => false, } } fn printdiff( tree1: impl for<'a> Fn(&'a Bump) -> VNode<'a>, tree2: impl for<'a> Fn(&'a Bump) -> VNode<'a>, desc: &'static str, ) { let bump = Bump::new(); let nodes1 = tree1(&bump); let nodes2 = tree2(&bump); let mut machine = DiffMachine::new(); let patches = machine.diff(&nodes1, &nodes2); patches.iter().for_each(|f| match f { Patch::AppendChildren(idx, a) => { println!("AppendChildren"); } Patch::TruncateChildren(idx, a) => { println!("TruncateChildren"); } Patch::Replace(idx, a) => { println!("Replace"); } Patch::AddAttributes(idx, a) => { println!("AddAttributes"); } Patch::RemoveAttributes(idx, a) => { println!("RemoveAttributes"); } Patch::ChangeText(idx, a) => { println!("ChangeText"); } }); } #[test] fn example_diff() { printdiff( html! {
}, html! {
"Hello world!"
}, "demo the difference between two simple dom tree", ); printdiff( html! {
"Hello world!"
}, html! {
"Hello world!" "Hello world!" "Hello world!" "Hello world!" "Hello world!"
}, "demo the difference between two simple dom tree", ); } }