wip: broken, but solved
This commit is contained in:
1 parent
879e107634
commit
cb74d70f83
21 files changed
+2398
-2341
No files matched your search
@@ -0,0 +1,329 @@
|
||||
//! 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<Index>,
|
||||
diffed: FxHashSet<Index>,
|
||||
need_to_diff: FxHashSet<Index>,
|
||||
marked_for_removal: Vec<Index>,
|
||||
}
|
||||
|
||||
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<Patch<'a>> {
|
||||
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<Patch<'a>> {
|
||||
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::<HashMap<&'static str, &str, FxBuildHasher>>();
|
||||
|
||||
let new_attrs = old_element
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|f| (f.name, f.value))
|
||||
.collect::<HashMap<&'static str, &str, FxBuildHasher>>();
|
||||
|
||||
let mut add_attributes = FxHashMap::<&'static str, &str>::default();
|
||||
// [("blah", "blah")]
|
||||
// .into_iter()
|
||||
// .map(|f| (f.0, f.1))
|
||||
// .collect::<HashMap<&'static str, &str, FxBuildHasher>>();
|
||||
|
||||
// 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<Patch>,
|
||||
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! { <div> </div> },
|
||||
html! { <div>"Hello world!" </div> },
|
||||
"demo the difference between two simple dom tree",
|
||||
);
|
||||
|
||||
printdiff(
|
||||
html! {
|
||||
<div>
|
||||
"Hello world!"
|
||||
</div>
|
||||
},
|
||||
html! {
|
||||
<div>
|
||||
<div>
|
||||
"Hello world!"
|
||||
"Hello world!"
|
||||
"Hello world!"
|
||||
"Hello world!"
|
||||
"Hello world!"
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
"demo the difference between two simple dom tree",
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user