glory_hot_reload/
diff.rs

1use crate::node::{LAttributeValue, LNode};
2use indexmap::IndexMap;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Default)]
6struct OldChildren(IndexMap<LNode, Vec<usize>>);
7
8impl LNode {
9    pub fn diff(&self, other: &LNode) -> Vec<Patch> {
10        let mut old_children = OldChildren::default();
11        self.add_old_children(vec![], &mut old_children);
12        self.diff_at(other, &[], &old_children)
13    }
14
15    fn to_replacement_node(&self, old_children: &OldChildren) -> ReplacementNode {
16        match old_children.0.get(self) {
17            // if the child already exists in the DOM, we can pluck it out
18            // and move it around
19            Some(path) => ReplacementNode::Path(path.to_owned()),
20            // otherwise, we should generate some HTML
21            // but we need to do this recursively in case we're replacing an element
22            // with children who need to be plucked out
23            None => match self {
24                LNode::Fragment(fragment) => ReplacementNode::Fragment(fragment.iter().map(|node| node.to_replacement_node(old_children)).collect()),
25                LNode::Element { name, attrs, children } => ReplacementNode::Element {
26                    name: name.to_owned(),
27                    attrs: attrs
28                        .iter()
29                        .filter_map(|(name, value)| match value {
30                            LAttributeValue::Boolean => Some((name.to_owned(), name.to_owned())),
31                            LAttributeValue::Static(value) => Some((name.to_owned(), value.to_owned())),
32                            _ => None,
33                        })
34                        .collect(),
35                    children: children.iter().map(|node| node.to_replacement_node(old_children)).collect(),
36                },
37                LNode::Text(_) | LNode::Component { .. } | LNode::DynChild(_) => ReplacementNode::Html(self.to_html()),
38            },
39        }
40    }
41
42    fn add_old_children(&self, path: Vec<usize>, positions: &mut OldChildren) {
43        match self {
44            LNode::Fragment(frag) => {
45                for (idx, child) in frag.iter().enumerate() {
46                    let mut new_path = path.clone();
47                    new_path.push(idx);
48                    child.add_old_children(new_path, positions);
49                }
50            }
51            LNode::Element { children, .. } => {
52                for (idx, child) in children.iter().enumerate() {
53                    let mut new_path = path.clone();
54                    new_path.push(idx);
55                    child.add_old_children(new_path, positions);
56                }
57            }
58            // need to insert dynamic content and children, as these might change
59            LNode::DynChild(_) => {
60                positions.0.insert(self.clone(), path);
61            }
62            LNode::Component { children, .. } => {
63                positions.0.insert(self.clone(), path.clone());
64
65                for (idx, child) in children.iter().enumerate() {
66                    let mut new_path = path.clone();
67                    new_path.push(idx);
68                    child.add_old_children(new_path, positions);
69                }
70            }
71            // can just create text nodes, whatever
72            LNode::Text(_) => {}
73        }
74    }
75
76    fn diff_at(&self, other: &LNode, path: &[usize], orig_children: &OldChildren) -> Vec<Patch> {
77        if std::mem::discriminant(self) != std::mem::discriminant(other) {
78            return vec![Patch {
79                path: path.to_owned(),
80                action: PatchAction::ReplaceWith(other.to_replacement_node(orig_children)),
81            }];
82        }
83        match (self, other) {
84            // fragment: diff children
85            (LNode::Fragment(old), LNode::Fragment(new)) => LNode::diff_children(path, old, new, orig_children),
86            // text node: replace text
87            (LNode::Text(_), LNode::Text(new)) => vec![Patch {
88                path: path.to_owned(),
89                action: PatchAction::SetText(new.to_owned()),
90            }],
91            // elements
92            (
93                LNode::Element {
94                    name: old_name,
95                    attrs: old_attrs,
96                    children: old_children,
97                },
98                LNode::Element {
99                    name: new_name,
100                    attrs: new_attrs,
101                    children: new_children,
102                },
103            ) => {
104                let tag_patch = (old_name != new_name).then(|| Patch {
105                    path: path.to_owned(),
106                    action: PatchAction::ChangeTagName(new_name.to_owned()),
107                });
108
109                let attrs_patch = LNode::diff_attrs(path, old_attrs, new_attrs);
110
111                let children_patch = LNode::diff_children(path, old_children, new_children, orig_children);
112
113                attrs_patch
114                    .into_iter()
115                    // tag patch comes second so we remove old attrs before copying them over
116                    .chain(tag_patch)
117                    .chain(children_patch)
118                    .collect()
119            }
120            // components + dynamic context: no patches
121            (
122                LNode::Component {
123                    name: old_name,
124                    children: old_children,
125                    ..
126                },
127                LNode::Component {
128                    name: new_name,
129                    children: new_children,
130                    ..
131                },
132            ) if old_name == new_name => {
133                let mut path = path.to_vec();
134                path.push(0);
135                path.push(0);
136                LNode::diff_children(&path, old_children, new_children, orig_children)
137            }
138            _ => vec![],
139        }
140    }
141
142    fn diff_attrs<'a>(
143        path: &'a [usize],
144        old: &'a [(String, LAttributeValue)],
145        new: &'a [(String, LAttributeValue)],
146    ) -> impl Iterator<Item = Patch> + 'a {
147        let additions = new
148            .iter()
149            .filter_map(|(name, new_value)| {
150                let old_attr = old.iter().find(|(o_name, _)| o_name == name);
151                let replace = match old_attr {
152                    None => true,
153                    Some((_, old_value)) if old_value != new_value => true,
154                    _ => false,
155                };
156                if replace {
157                    match &new_value {
158                        LAttributeValue::Boolean => Some((name.to_owned(), "".to_string())),
159                        LAttributeValue::Static(s) => Some((name.to_owned(), s.to_owned())),
160                        _ => None,
161                    }
162                } else {
163                    None
164                }
165            })
166            .map(|(name, value)| Patch {
167                path: path.to_owned(),
168                action: PatchAction::SetAttribute(name, value),
169            });
170
171        let removals = old.iter().filter_map(|(name, _)| {
172            if !new.iter().any(|(new_name, _)| new_name == name) {
173                Some(Patch {
174                    path: path.to_owned(),
175                    action: PatchAction::RemoveAttribute(name.to_owned()),
176                })
177            } else {
178                None
179            }
180        });
181
182        additions.chain(removals)
183    }
184
185    fn diff_children(path: &[usize], old: &[LNode], new: &[LNode], old_children: &OldChildren) -> Vec<Patch> {
186        if old.is_empty() && new.is_empty() {
187            vec![]
188        } else if old.is_empty() {
189            vec![Patch {
190                path: path.to_owned(),
191                action: PatchAction::AppendChildren(new.iter().map(LNode::to_html).map(ReplacementNode::Html).collect()),
192            }]
193        } else if new.is_empty() {
194            vec![Patch {
195                path: path.to_owned(),
196                action: PatchAction::ClearChildren,
197            }]
198        } else {
199            let mut a = 0;
200            let mut b = std::cmp::max(old.len(), new.len()) - 1; // min is 0, have checked both have items
201            let mut patches = vec![];
202            // common prefix
203            while a < b {
204                let old = old.get(a);
205                let new = new.get(a);
206
207                match (old, new) {
208                    (None, None) => {}
209                    (None, Some(new)) => patches.push(Patch {
210                        path: path.to_owned(),
211                        action: PatchAction::InsertChild {
212                            before: a,
213                            child: new.to_replacement_node(old_children),
214                        },
215                    }),
216                    (Some(_), None) => patches.push(Patch {
217                        path: path.to_owned(),
218                        action: PatchAction::RemoveChild { at: a },
219                    }),
220                    (Some(old), Some(new)) => {
221                        if old != new {
222                            break;
223                        }
224                    }
225                }
226
227                a += 1;
228            }
229
230            // common suffix
231            while b >= a {
232                let old = old.get(b);
233                let new = new.get(b);
234
235                match (old, new) {
236                    (None, None) => {}
237                    (None, Some(new)) => patches.push(Patch {
238                        path: path.to_owned(),
239                        action: PatchAction::InsertChildAfter {
240                            after: b - 1,
241                            child: new.to_replacement_node(old_children),
242                        },
243                    }),
244                    (Some(_), None) => patches.push(Patch {
245                        path: path.to_owned(),
246                        action: PatchAction::RemoveChild { at: b },
247                    }),
248                    (Some(old), Some(new)) => {
249                        if old != new {
250                            break;
251                        }
252                    }
253                }
254
255                if b == 0 {
256                    break;
257                } else {
258                    b -= 1;
259                }
260            }
261
262            // diffing in middle
263            if b >= a {
264                let old_slice_end = if b >= old.len() { old.len() - 1 } else { b };
265                let new_slice_end = if b >= new.len() { new.len() - 1 } else { b };
266                let old = &old[a..=old_slice_end];
267                let new = &new[a..=new_slice_end];
268
269                for (new_idx, new_node) in new.iter().enumerate() {
270                    match old.get(new_idx) {
271                        Some(old_node) => {
272                            let mut new_path = path.to_vec();
273                            new_path.push(new_idx + a);
274                            let diffs = old_node.diff_at(new_node, &new_path, old_children);
275                            patches.extend(&mut diffs.into_iter());
276                        }
277                        None => patches.push(Patch {
278                            path: path.to_owned(),
279                            action: PatchAction::InsertChild {
280                                before: new_idx,
281                                child: new_node.to_replacement_node(old_children),
282                            },
283                        }),
284                    }
285                }
286            }
287
288            patches
289        }
290    }
291}
292
293#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
294pub struct Patches(pub Vec<(String, Vec<Patch>)>);
295
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297pub struct Patch {
298    path: Vec<usize>,
299    action: PatchAction,
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
303pub enum PatchAction {
304    ReplaceWith(ReplacementNode),
305    ChangeTagName(String),
306    RemoveAttribute(String),
307    SetAttribute(String, String),
308    SetText(String),
309    ClearChildren,
310    AppendChildren(Vec<ReplacementNode>),
311    RemoveChild { at: usize },
312    InsertChild { before: usize, child: ReplacementNode },
313    InsertChildAfter { after: usize, child: ReplacementNode },
314}
315
316#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
317pub enum ReplacementNode {
318    Html(String),
319    Path(Vec<usize>),
320    Fragment(Vec<ReplacementNode>),
321    Element {
322        name: String,
323        attrs: Vec<(String, String)>,
324        children: Vec<ReplacementNode>,
325    },
326}
327
328#[cfg(test)]
329mod tests {
330    use crate::{
331        diff::{Patch, PatchAction, ReplacementNode},
332        node::LAttributeValue,
333        LNode,
334    };
335
336    #[test]
337    fn patches_text() {
338        let a = LNode::Text("foo".into());
339        let b = LNode::Text("bar".into());
340        let delta = a.diff(&b);
341        assert_eq!(
342            delta,
343            vec![Patch {
344                path: vec![],
345                action: PatchAction::SetText("bar".into())
346            }]
347        );
348    }
349
350    #[test]
351    fn patches_attrs() {
352        let a = LNode::Element {
353            name: "button".into(),
354            attrs: vec![
355                ("class".into(), LAttributeValue::Static("a".into())),
356                ("type".into(), LAttributeValue::Static("button".into())),
357            ],
358            children: vec![],
359        };
360        let b = LNode::Element {
361            name: "button".into(),
362            attrs: vec![
363                ("class".into(), LAttributeValue::Static("a b".into())),
364                ("id".into(), LAttributeValue::Static("button".into())),
365            ],
366            children: vec![],
367        };
368        let delta = a.diff(&b);
369        assert_eq!(
370            delta,
371            vec![
372                Patch {
373                    path: vec![],
374                    action: PatchAction::SetAttribute("class".into(), "a b".into())
375                },
376                Patch {
377                    path: vec![],
378                    action: PatchAction::SetAttribute("id".into(), "button".into())
379                },
380                Patch {
381                    path: vec![],
382                    action: PatchAction::RemoveAttribute("type".into())
383                },
384            ]
385        );
386    }
387
388    #[test]
389    fn patches_child_text() {
390        let a = LNode::Element {
391            name: "button".into(),
392            attrs: vec![],
393            children: vec![LNode::Text("foo".into()), LNode::Text("bar".into())],
394        };
395        let b = LNode::Element {
396            name: "button".into(),
397            attrs: vec![],
398            children: vec![LNode::Text("foo".into()), LNode::Text("baz".into())],
399        };
400        let delta = a.diff(&b);
401        assert_eq!(
402            delta,
403            vec![Patch {
404                path: vec![1],
405                action: PatchAction::SetText("baz".into())
406            },]
407        );
408    }
409
410    #[test]
411    fn inserts_child() {
412        let a = LNode::Element {
413            name: "div".into(),
414            attrs: vec![],
415            children: vec![LNode::Element {
416                name: "button".into(),
417                attrs: vec![],
418                children: vec![LNode::Text("bar".into())],
419            }],
420        };
421        let b = LNode::Element {
422            name: "div".into(),
423            attrs: vec![],
424            children: vec![
425                LNode::Element {
426                    name: "button".into(),
427                    attrs: vec![],
428                    children: vec![LNode::Text("foo".into())],
429                },
430                LNode::Element {
431                    name: "button".into(),
432                    attrs: vec![],
433                    children: vec![LNode::Text("bar".into())],
434                },
435            ],
436        };
437        let delta = a.diff(&b);
438        assert_eq!(
439            delta,
440            vec![
441                Patch {
442                    path: vec![],
443                    action: PatchAction::InsertChildAfter {
444                        after: 0,
445                        child: ReplacementNode::Element {
446                            name: "button".into(),
447                            attrs: vec![],
448                            children: vec![ReplacementNode::Html("bar".into())]
449                        }
450                    }
451                },
452                Patch {
453                    path: vec![0, 0],
454                    action: PatchAction::SetText("foo".into())
455                }
456            ]
457        );
458    }
459
460    #[test]
461    fn removes_child() {
462        let a = LNode::Element {
463            name: "div".into(),
464            attrs: vec![],
465            children: vec![
466                LNode::Element {
467                    name: "button".into(),
468                    attrs: vec![],
469                    children: vec![LNode::Text("foo".into())],
470                },
471                LNode::Element {
472                    name: "button".into(),
473                    attrs: vec![],
474                    children: vec![LNode::Text("bar".into())],
475                },
476            ],
477        };
478        let b = LNode::Element {
479            name: "div".into(),
480            attrs: vec![],
481            children: vec![LNode::Element {
482                name: "button".into(),
483                attrs: vec![],
484                children: vec![LNode::Text("foo".into())],
485            }],
486        };
487        let delta = a.diff(&b);
488        assert_eq!(
489            delta,
490            vec![Patch {
491                path: vec![],
492                action: PatchAction::RemoveChild { at: 1 }
493            },]
494        );
495    }
496}