orinium_browser/engine/layouter/
dom_snapshot.rs1use std::cell::RefCell;
10use std::collections::HashMap;
11use std::rc::{Rc, Weak};
12
13use crate::engine::html::HtmlNodeType;
14use crate::engine::html::parser::DomTree;
15use crate::engine::tree::{NodeRef, TreeNode};
16
17pub type NodeId = u32;
19
20#[derive(Debug, Clone)]
22pub struct SnapNode {
23 pub kind: HtmlNodeType,
24 pub children: Vec<NodeId>,
25 pub dom_id: u64,
29 pub checked: bool,
33 pub value: Option<String>,
37}
38
39#[derive(Debug, Default)]
44pub struct DomSnapshot {
45 nodes: Vec<SnapNode>,
46 roots: Vec<NodeId>,
47}
48
49impl DomSnapshot {
50 pub fn from_tree(
56 root: &NodeRef<HtmlNodeType>,
57 ) -> (Self, Vec<Weak<RefCell<TreeNode<HtmlNodeType>>>>) {
58 let mut snapshot = DomSnapshot::default();
59 let mut dom_refs: Vec<Weak<RefCell<TreeNode<HtmlNodeType>>>> = Vec::new();
60 let id = snapshot.walk(root, &mut dom_refs);
61 snapshot.roots.push(id);
62 (snapshot, dom_refs)
63 }
64
65 pub fn from_mirror(root: &NodeRef<HtmlNodeType>, dom_ids: &HashMap<usize, u64>) -> Self {
72 fn walk(
73 snapshot: &mut DomSnapshot,
74 node: &NodeRef<HtmlNodeType>,
75 dom_ids: &HashMap<usize, u64>,
76 ) -> NodeId {
77 let id = snapshot.nodes.len() as NodeId;
78 let node_borrow = node.borrow();
79 let value = node_borrow.value.get_attr("value").map(|s| s.to_string());
80 snapshot.nodes.push(SnapNode {
81 kind: node_borrow.value.clone(),
82 children: Vec::new(),
83 dom_id: dom_ids
84 .get(&(Rc::as_ptr(node) as usize))
85 .copied()
86 .unwrap_or(0),
87 checked: crate::engine::html::parser::checkedness(node),
88 value,
89 });
90 let children: Vec<NodeId> = node
91 .borrow()
92 .children()
93 .iter()
94 .map(|child| walk(snapshot, child, dom_ids))
95 .collect();
96 snapshot.nodes[id as usize].children = children;
97 id
98 }
99 let mut snapshot = DomSnapshot::default();
100 let id = walk(&mut snapshot, root, dom_ids);
101 snapshot.roots.push(id);
102 snapshot
103 }
104
105 pub fn into_tree(&self) -> (DomTree, HashMap<usize, u64>) {
111 fn build(id: NodeId, snapshot: &DomSnapshot) -> NodeRef<HtmlNodeType> {
112 let node = TreeNode::new(snapshot.nodes[id as usize].kind.clone());
113 for &child in &snapshot.nodes[id as usize].children {
114 let child_node = build(child, snapshot);
115 TreeNode::add_child(&node, child_node);
116 }
117 node
118 }
119
120 let root = build(self.roots[0], self);
121 let tree = DomTree::from_root(root);
122 let mut dom_ids = HashMap::new();
123 let mut index = 0usize;
124 tree.traverse(|node| {
125 let dom_id = self.nodes[index].dom_id;
126 if dom_id != 0 {
127 dom_ids.insert(Rc::as_ptr(node) as usize, dom_id);
128 }
129 index += 1;
130 });
131 (tree, dom_ids)
132 }
133
134 fn walk(
135 &mut self,
136 node: &NodeRef<HtmlNodeType>,
137 dom_refs: &mut Vec<Weak<RefCell<TreeNode<HtmlNodeType>>>>,
138 ) -> NodeId {
139 let id = self.nodes.len() as NodeId;
140 let node_borrow = node.borrow();
141 let value = node_borrow.value.get_attr("value").map(|s| s.to_string());
142 self.nodes.push(SnapNode {
143 kind: node_borrow.value.clone(),
144 children: Vec::new(),
145 dom_id: 0,
146 checked: crate::engine::html::parser::checkedness(node),
147 value,
148 });
149 dom_refs.push(Rc::downgrade(node));
150 let children: Vec<NodeId> = node
151 .borrow()
152 .children()
153 .iter()
154 .map(|child| self.walk(child, dom_refs))
155 .collect();
156 self.nodes[id as usize].children = children;
157 id
158 }
159
160 pub fn roots(&self) -> &[NodeId] {
162 &self.roots
163 }
164
165 pub fn nodes(&self) -> &[SnapNode] {
167 &self.nodes
168 }
169
170 pub fn node(&self, id: NodeId) -> &SnapNode {
172 &self.nodes[id as usize]
173 }
174
175 pub fn children(&self, id: NodeId) -> &[NodeId] {
177 &self.nodes[id as usize].children
178 }
179
180 pub fn inner_text(&self, id: NodeId) -> String {
182 let node = &self.nodes[id as usize];
183 match &node.kind {
184 HtmlNodeType::Text(content) => content.clone(),
185 HtmlNodeType::Element { .. } | HtmlNodeType::Document => node
186 .children
187 .iter()
188 .map(|&child| self.inner_text(child))
189 .collect(),
190 _ => String::new(),
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::engine::html::parser::DomTree;
199 use crate::engine::html::parser::Parser as HtmlParser;
200
201 fn tree(html: &str) -> DomTree {
202 HtmlParser::new(html).parse()
203 }
204
205 #[test]
206 fn snapshot_is_built_in_preorder_and_is_send() {
207 let dom = tree("<html><body><div><p>hi</p></div><button>ok</button></body></html>");
208 let (snapshot, _dom_refs) = DomSnapshot::from_tree(&dom.root);
209 let root = snapshot.roots()[0];
210
211 let doc = snapshot.node(root);
213 assert_eq!(doc.kind.tag_name(), None);
214
215 let html = doc.children[0];
217 let html_node = snapshot.node(html);
218 assert_eq!(html_node.kind.tag_name(), Some("html"));
219 assert_eq!(html_node.children.len(), 2);
220
221 assert_eq!(
223 snapshot.node(html_node.children[0]).kind.tag_name(),
224 Some("head")
225 );
226 let body = html_node.children[1];
227 assert_eq!(snapshot.node(body).kind.tag_name(), Some("body"));
228
229 std::thread::spawn(move || {
231 let _ = snapshot.inner_text(root);
232 })
233 .join()
234 .unwrap();
235 }
236
237 #[test]
238 fn inner_text_concatenates_descendants() {
239 let dom = tree("<div><p>hello</p><p>world</p></div>");
240 let (snapshot, _) = DomSnapshot::from_tree(&dom.root);
241 let root = snapshot.roots()[0];
242 assert_eq!(snapshot.inner_text(root), "helloworld");
243 }
244
245 #[test]
246 fn dom_refs_map_node_id_to_live_node() {
247 let dom = tree("<input value='a'>");
248 let (snapshot, dom_refs) = DomSnapshot::from_tree(&dom.root);
249 let root = snapshot.roots()[0];
250
251 fn find(snapshot: &DomSnapshot, id: NodeId, tag: &str) -> Option<NodeId> {
253 if snapshot.node(id).kind.tag_name() == Some(tag) {
254 return Some(id);
255 }
256 snapshot
257 .children(id)
258 .iter()
259 .find_map(|&c| find(snapshot, c, tag))
260 }
261 let input_id = find(&snapshot, root, "input").unwrap();
262
263 let live = dom_refs[input_id as usize].upgrade().unwrap();
264 assert_eq!(live.borrow().value.tag_name(), Some("input"));
265 }
266
267 #[test]
268 fn from_tree_roundtrip_preserves_structure() {
269 let dom = tree("<html><body><div><p>hi</p></div><button>ok</button></body></html>");
270 let (snapshot, _) = DomSnapshot::from_tree(&dom.root);
271 let (rebuilt, dom_ids) = snapshot.into_tree();
272
273 assert_eq!(rebuilt.root.borrow().value.tag_name(), None);
274 assert!(dom_ids.is_empty());
275 assert_eq!(
276 rebuilt.root.borrow().children()[0]
277 .borrow()
278 .value
279 .tag_name(),
280 Some("html")
281 );
282 assert_eq!(
283 rebuilt.root.borrow().children()[0].borrow().children()[0]
284 .borrow()
285 .value
286 .tag_name(),
287 Some("head")
288 );
289 assert_eq!(
290 rebuilt.root.borrow().children()[0].borrow().children()[1]
291 .borrow()
292 .value
293 .tag_name(),
294 Some("body")
295 );
296 assert_eq!(rebuilt.version(), 0);
297 }
298
299 #[test]
300 fn from_mirror_and_into_tree_preserve_dom_ids() {
301 let dom = tree("<div><p>hi</p><p>yo</p></div>");
302
303 let mut dom_ids = HashMap::new();
305 let mut next = 1u64;
306 let mut counter = 0usize;
307 dom.traverse(|node| {
308 if counter.is_multiple_of(2) {
309 dom_ids.insert(Rc::as_ptr(node) as usize, next);
310 next += 1;
311 }
312 counter += 1;
313 });
314
315 let snapshot = DomSnapshot::from_mirror(&dom.root, &dom_ids);
316 let (rebuilt, rebuilt_ids) = snapshot.into_tree();
317
318 assert_eq!(rebuilt_ids.len(), dom_ids.len());
319 let mut rebuilt_preorder: Vec<u64> = Vec::new();
321 rebuilt.traverse(|node| {
322 rebuilt_preorder.push(
323 rebuilt_ids
324 .get(&(Rc::as_ptr(node) as usize))
325 .copied()
326 .unwrap_or(0),
327 );
328 });
329 let mut expected: Vec<u64> = Vec::new();
330 dom.traverse(|node| {
331 expected.push(
332 dom_ids
333 .get(&(Rc::as_ptr(node) as usize))
334 .copied()
335 .unwrap_or(0),
336 );
337 });
338 assert_eq!(rebuilt_preorder, expected);
339 }
340}