1use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum VNode {
8 Element(VElement),
10 Text(VText),
12 Component(VComponent),
14 Empty,
16}
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct VElement {
21 pub tag: String,
22 pub attrs: HashMap<String, String>,
23 pub children: Vec<VNode>,
24}
25
26impl VElement {
27 pub fn new(tag: impl Into<String>) -> Self {
29 Self {
30 tag: tag.into(),
31 attrs: HashMap::new(),
32 children: Vec::new(),
33 }
34 }
35
36 pub fn attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
38 self.attrs.insert(key.into(), value.into());
39 self
40 }
41
42 pub fn children(mut self, children: Vec<VNode>) -> Self {
44 self.children = children;
45 self
46 }
47
48 pub fn child(mut self, child: VNode) -> Self {
50 self.children.push(child);
51 self
52 }
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct VText {
58 pub content: String,
59}
60
61impl VText {
62 pub fn new(content: impl Into<String>) -> Self {
64 Self {
65 content: content.into(),
66 }
67 }
68}
69
70impl From<VElement> for VNode {
71 fn from(element: VElement) -> Self {
72 VNode::Element(element)
73 }
74}
75
76impl From<VText> for VNode {
77 fn from(text: VText) -> Self {
78 VNode::Text(text)
79 }
80}
81
82impl From<String> for VNode {
83 fn from(s: String) -> Self {
84 VNode::Text(VText::new(s))
85 }
86}
87
88impl From<&str> for VNode {
89 fn from(s: &str) -> Self {
90 VNode::Text(VText::new(s))
91 }
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub struct VComponent {
97 pub name: String,
98 pub props: HashMap<String, String>,
99}
100
101pub fn diff(old: &VNode, new: &VNode) -> Vec<Patch> {
103 let mut patches = Vec::new();
104 diff_recursive(old, new, &mut patches, vec![0]);
105 patches
106}
107
108fn diff_recursive(old: &VNode, new: &VNode, patches: &mut Vec<Patch>, path: Vec<usize>) {
109 match (old, new) {
110 (VNode::Text(old_text), VNode::Text(new_text)) => {
111 if old_text.content != new_text.content {
112 patches.push(Patch::UpdateText {
113 path: path.clone(),
114 content: new_text.content.clone(),
115 });
116 }
117 }
118 (VNode::Element(old_el), VNode::Element(new_el)) => {
119 if old_el.tag != new_el.tag {
120 patches.push(Patch::Replace {
121 path: path.clone(),
122 node: VNode::Element(new_el.clone()),
123 });
124 return;
125 }
126
127 for (key, new_value) in &new_el.attrs {
129 if old_el.attrs.get(key) != Some(new_value) {
130 patches.push(Patch::SetAttribute {
131 path: path.clone(),
132 key: key.clone(),
133 value: new_value.clone(),
134 });
135 }
136 }
137
138 let max_len = old_el.children.len().max(new_el.children.len());
140 for i in 0..max_len {
141 let mut child_path = path.clone();
142 child_path.push(i);
143
144 match (old_el.children.get(i), new_el.children.get(i)) {
145 (Some(old_child), Some(new_child)) => {
146 diff_recursive(old_child, new_child, patches, child_path);
147 }
148 (None, Some(new_child)) => {
149 patches.push(Patch::Append {
150 path: path.clone(),
151 node: new_child.clone(),
152 });
153 }
154 (Some(_), None) => {
155 patches.push(Patch::Remove { path: child_path });
156 }
157 (None, None) => unreachable!(),
158 }
159 }
160 }
161 _ => {
162 patches.push(Patch::Replace {
164 path,
165 node: new.clone(),
166 });
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq)]
173pub enum Patch {
174 Replace { path: Vec<usize>, node: VNode },
176 UpdateText { path: Vec<usize>, content: String },
178 SetAttribute {
180 path: Vec<usize>,
181 key: String,
182 value: String,
183 },
184 Append { path: Vec<usize>, node: VNode },
186 Remove { path: Vec<usize> },
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_velement_creation() {
196 let el = VElement::new("div")
197 .attr("class", "container")
198 .child(VNode::Text(VText::new("Hello")));
199
200 assert_eq!(el.tag, "div");
201 assert_eq!(el.attrs.get("class"), Some(&"container".to_string()));
202 assert_eq!(el.children.len(), 1);
203 }
204
205 #[test]
206 fn test_vtext_creation() {
207 let text = VText::new("Hello, World!");
208 assert_eq!(text.content, "Hello, World!");
209 }
210
211 #[test]
212 fn test_diff_text_update() {
213 let old = VNode::Text(VText::new("old"));
214 let new = VNode::Text(VText::new("new"));
215
216 let patches = diff(&old, &new);
217 assert_eq!(patches.len(), 1);
218 assert!(matches!(patches[0], Patch::UpdateText { .. }));
219 }
220
221 #[test]
222 fn test_diff_no_change() {
223 let node = VNode::Text(VText::new("same"));
224 let patches = diff(&node, &node);
225 assert_eq!(patches.len(), 0);
226 }
227}