1use crate::dom::Dom;
12use crate::node::NodeData;
13use crate::node_id::NodeId;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum InvariantViolation {
18 ParentNotContainingChild { parent: NodeId, child: NodeId },
21 SiblingChainBroken { a: NodeId, b: NodeId },
24 FirstChildNotLeader { parent: NodeId },
26 LastChildNotTail { parent: NodeId },
28 WrongParentPointer { child: NodeId, expected: NodeId },
30 DanglingPointer { from: NodeId, to: NodeId },
32 IndexOrphan {
35 kind: &'static str,
36 key: String,
37 id: NodeId,
38 },
39 IndexMissing {
42 kind: &'static str,
43 key: String,
44 id: NodeId,
45 },
46}
47
48impl<Ext> Dom<Ext> {
49 pub fn validate(&self) -> Vec<InvariantViolation> {
53 let mut out = Vec::new();
54
55 for (idx, slot) in self.nodes.iter().enumerate() {
56 let Some(node) = &slot.node else { continue };
57 let id = NodeId::from_parts(idx, slot.generation);
58
59 for (label, target) in [
61 ("parent", node.parent),
62 ("first_child", node.first_child),
63 ("last_child", node.last_child),
64 ("prev_sibling", node.prev_sibling),
65 ("next_sibling", node.next_sibling),
66 ] {
67 if let Some(t) = target
68 && self.get_node(t).is_none()
69 {
70 let _ = label; out.push(InvariantViolation::DanglingPointer { from: id, to: t });
72 }
73 }
74
75 if let Some(next) = node.next_sibling
77 && let Some(next_node) = self.get_node(next)
78 && next_node.prev_sibling != Some(id)
79 {
80 out.push(InvariantViolation::SiblingChainBroken { a: id, b: next });
81 }
82 if let Some(prev) = node.prev_sibling
83 && let Some(prev_node) = self.get_node(prev)
84 && prev_node.next_sibling != Some(id)
85 {
86 out.push(InvariantViolation::SiblingChainBroken { a: prev, b: id });
87 }
88
89 if let Some(parent) = node.parent {
91 let mut found = false;
92 let mut cur = self.get_node(parent).and_then(|n| n.first_child);
93 while let Some(c) = cur {
94 if c == id {
95 found = true;
96 break;
97 }
98 cur = self.get_node(c).and_then(|n| n.next_sibling);
99 }
100 if !found {
101 out.push(InvariantViolation::ParentNotContainingChild { parent, child: id });
102 }
103 }
104
105 if let Some(first) = node.first_child
107 && let Some(fn_) = self.get_node(first)
108 {
109 if fn_.prev_sibling.is_some() {
110 out.push(InvariantViolation::FirstChildNotLeader { parent: id });
111 }
112 if fn_.parent != Some(id) {
113 out.push(InvariantViolation::WrongParentPointer {
114 child: first,
115 expected: id,
116 });
117 }
118 }
119 if let Some(last) = node.last_child
120 && let Some(ln) = self.get_node(last)
121 {
122 if ln.next_sibling.is_some() {
123 out.push(InvariantViolation::LastChildNotTail { parent: id });
124 }
125 if ln.parent != Some(id) {
126 out.push(InvariantViolation::WrongParentPointer {
127 child: last,
128 expected: id,
129 });
130 }
131 }
132
133 if let NodeData::Element {
136 tag,
137 attrs,
138 classes,
139 ..
140 } = &node.data
141 {
142 if !self
143 .indexes
144 .by_tag
145 .get(tag)
146 .is_some_and(|v| v.contains(&id))
147 {
148 out.push(InvariantViolation::IndexMissing {
149 kind: "tag",
150 key: tag.clone(),
151 id,
152 });
153 }
154 if let Some(id_attr) = attrs.get("id")
155 && !id_attr.is_empty()
156 && !self
157 .indexes
158 .by_id
159 .get(id_attr)
160 .is_some_and(|v| v.contains(&id))
161 {
162 out.push(InvariantViolation::IndexMissing {
163 kind: "id",
164 key: id_attr.clone(),
165 id,
166 });
167 }
168 for c in classes {
169 if !self
170 .indexes
171 .by_class
172 .get(c)
173 .is_some_and(|v| v.contains(&id))
174 {
175 out.push(InvariantViolation::IndexMissing {
176 kind: "class",
177 key: c.clone(),
178 id,
179 });
180 }
181 }
182 }
183 }
184
185 for (tag, ids) in &self.indexes.by_tag {
188 for &iid in ids {
189 match self.get_node(iid).map(|n| &n.data) {
190 Some(NodeData::Element { tag: t, .. }) if t == tag => {}
191 _ => out.push(InvariantViolation::IndexOrphan {
192 kind: "tag",
193 key: tag.clone(),
194 id: iid,
195 }),
196 }
197 }
198 }
199 for (key, ids) in &self.indexes.by_id {
200 for &iid in ids {
201 let has_id = matches!(
202 self.get_node(iid).map(|n| &n.data),
203 Some(NodeData::Element { attrs, .. }) if attrs.get("id") == Some(key)
204 );
205 if !has_id {
206 out.push(InvariantViolation::IndexOrphan {
207 kind: "id",
208 key: key.clone(),
209 id: iid,
210 });
211 }
212 }
213 }
214 for (cls, ids) in &self.indexes.by_class {
215 for &iid in ids {
216 let has_cls = matches!(
217 self.get_node(iid).map(|n| &n.data),
218 Some(NodeData::Element { classes, .. }) if classes.contains(cls)
219 );
220 if !has_cls {
221 out.push(InvariantViolation::IndexOrphan {
222 kind: "class",
223 key: cls.clone(),
224 id: iid,
225 });
226 }
227 }
228 }
229
230 out
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use crate::Dom;
237
238 #[test]
239 fn empty_dom_validates() {
240 let dom: Dom = Dom::new();
241 assert!(dom.validate().is_empty());
242 }
243
244 #[test]
245 fn simple_tree_validates() {
246 let mut dom: Dom = Dom::new();
247 let root = dom.root();
248 let a = dom.create_element("a");
249 let b = dom.create_element("b");
250 let c = dom.create_element("c");
251 dom.append_child(root, a).unwrap();
252 dom.append_child(root, b).unwrap();
253 dom.append_child(a, c).unwrap();
254 assert!(dom.validate().is_empty());
255 }
256
257 #[test]
258 fn deep_tree_validates() {
259 let mut dom: Dom = Dom::new();
260 let mut cur = dom.root();
261 for _ in 0..100 {
262 let el = dom.create_element("div");
263 dom.append_child(cur, el).unwrap();
264 cur = el;
265 }
266 assert!(dom.validate().is_empty());
267 }
268
269 #[test]
270 fn mutations_preserve_invariants() {
271 let mut dom: Dom = Dom::new();
272 let root = dom.root();
273 let a = dom.create_element("a");
274 let b = dom.create_element("b");
275 let c = dom.create_element("c");
276 dom.append_child(root, a).unwrap();
277 dom.append_child(root, b).unwrap();
278 dom.append_child(root, c).unwrap();
279
280 dom.remove_child(root, b).unwrap();
281 assert!(dom.validate().is_empty());
282
283 let d = dom.create_element("d");
284 dom.insert_before(root, d, Some(c)).unwrap();
285 assert!(dom.validate().is_empty());
286
287 dom.replace_child(root, a, b).unwrap();
288 assert!(dom.validate().is_empty());
289 }
290}