Skip to main content

foundry_runtime/
html.rs

1use crate::scene::{ElementKind, NodeId, SceneGraph};
2use html5ever::parse_document;
3use html5ever::tendril::TendrilSink;
4use markup5ever_rcdom::{Handle, NodeData, RcDom};
5
6pub fn parse_html(html: &str) -> SceneGraph {
7    let (scene, _, _, _, _) = parse_html_full(html);
8    scene
9}
10
11/// Single-pass parse: returns (scene, inline_styles, inline_scripts, ext_style_hrefs, ext_script_srcs)
12pub fn parse_html_full(
13    html: &str,
14) -> (
15    SceneGraph,
16    Vec<String>,
17    Vec<String>,
18    Vec<String>,
19    Vec<String>,
20) {
21    let dom = parse_document(RcDom::default(), Default::default())
22        .from_utf8()
23        .read_from(&mut html.as_bytes())
24        .expect("failed to parse HTML");
25
26    let mut scene = SceneGraph::new();
27    let mut styles = Vec::new();
28    let mut scripts = Vec::new();
29    let mut ext_styles = Vec::new();
30    let mut ext_scripts = Vec::new();
31
32    walk_node_full(
33        &dom.document,
34        &mut scene,
35        None,
36        &mut styles,
37        &mut scripts,
38        &mut ext_styles,
39        &mut ext_scripts,
40    );
41
42    (scene, styles, scripts, ext_styles, ext_scripts)
43}
44
45fn walk_node_full(
46    handle: &Handle,
47    scene: &mut SceneGraph,
48    parent: Option<NodeId>,
49    styles: &mut Vec<String>,
50    scripts: &mut Vec<String>,
51    ext_styles: &mut Vec<String>,
52    ext_scripts: &mut Vec<String>,
53) {
54    match &handle.data {
55        NodeData::Document => {
56            for child in handle.children.borrow().iter() {
57                walk_node_full(
58                    child,
59                    scene,
60                    parent,
61                    styles,
62                    scripts,
63                    ext_styles,
64                    ext_scripts,
65                );
66            }
67        }
68        NodeData::Element { name, attrs, .. } => {
69            let tag = name.local.to_string();
70
71            // Extract <style> contents
72            if tag == "style" {
73                for child in handle.children.borrow().iter() {
74                    if let NodeData::Text { contents } = &child.data {
75                        styles.push(contents.borrow().to_string());
76                    }
77                }
78                return;
79            }
80
81            // Extract <script> contents or src
82            if tag == "script" {
83                let attrs_ref = attrs.borrow();
84                let src = attrs_ref
85                    .iter()
86                    .find(|a| a.name.local.to_string() == "src")
87                    .map(|a| a.value.to_string());
88                if let Some(src_path) = src {
89                    ext_scripts.push(src_path);
90                } else {
91                    for child in handle.children.borrow().iter() {
92                        if let NodeData::Text { contents } = &child.data {
93                            scripts.push(contents.borrow().to_string());
94                        }
95                    }
96                }
97                return;
98            }
99
100            // Extract <link rel="stylesheet" href="...">
101            if tag == "link" {
102                let attrs_ref = attrs.borrow();
103                let is_stylesheet = attrs_ref.iter().any(|a| {
104                    a.name.local.to_string() == "rel" && a.value.to_string() == "stylesheet"
105                });
106                if is_stylesheet {
107                    if let Some(href) = attrs_ref
108                        .iter()
109                        .find(|a| a.name.local.to_string() == "href")
110                    {
111                        ext_styles.push(href.value.to_string());
112                    }
113                }
114                return;
115            }
116
117            // Skip non-renderable head elements
118            match tag.as_str() {
119                "head" | "meta" | "title" => {
120                    for child in handle.children.borrow().iter() {
121                        walk_node_full(
122                            child,
123                            scene,
124                            parent,
125                            styles,
126                            scripts,
127                            ext_styles,
128                            ext_scripts,
129                        );
130                    }
131                    return;
132                }
133                _ => {}
134            }
135
136            // Build scene node
137            let kind = ElementKind::from_tag(&tag);
138            let id = scene.add_node(kind, tag.clone());
139
140            let attrs_ref = attrs.borrow();
141            for attr in attrs_ref.iter() {
142                let attr_name = attr.name.local.to_string();
143                let attr_value = attr.value.to_string();
144
145                match attr_name.as_str() {
146                    "id" => {
147                        scene.get_mut(id).element_id = Some(attr_value.clone());
148                    }
149                    "class" => {
150                        scene.get_mut(id).classes = attr_value
151                            .split_whitespace()
152                            .map(|s| s.to_string())
153                            .collect();
154                    }
155                    _ => {}
156                }
157
158                if let Some(event) = attr_name.strip_prefix("on") {
159                    scene
160                        .get_mut(id)
161                        .event_handlers
162                        .insert(event.to_string(), attr_value.clone());
163                }
164
165                scene.get_mut(id).attributes.insert(attr_name, attr_value);
166            }
167
168            if let Some(parent_id) = parent {
169                scene.add_child(parent_id, id);
170            }
171
172            for child in handle.children.borrow().iter() {
173                walk_node_full(
174                    child,
175                    scene,
176                    Some(id),
177                    styles,
178                    scripts,
179                    ext_styles,
180                    ext_scripts,
181                );
182            }
183        }
184        NodeData::Text { contents } => {
185            let text = contents.borrow().to_string();
186            let trimmed = text.trim();
187            if trimmed.is_empty() {
188                return;
189            }
190
191            let id = scene.add_node(ElementKind::Text, "text".to_string());
192            scene.get_mut(id).text_content = Some(trimmed.to_string());
193
194            if let Some(parent_id) = parent {
195                scene.add_child(parent_id, id);
196            }
197        }
198        _ => {}
199    }
200}
201
202// Legacy single-purpose walker, kept for parse_html() convenience wrapper
203#[allow(dead_code)]
204fn walk_node(handle: &Handle, scene: &mut SceneGraph, parent: Option<NodeId>) {
205    let node = handle;
206
207    match &node.data {
208        NodeData::Document => {
209            for child in node.children.borrow().iter() {
210                walk_node(child, scene, parent);
211            }
212        }
213        NodeData::Element { name, attrs, .. } => {
214            let tag = name.local.to_string();
215            let kind = ElementKind::from_tag(&tag);
216
217            // Skip head, meta, title, script (handled separately), link, style (handled separately)
218            match tag.as_str() {
219                "head" | "meta" | "title" | "link" => {
220                    // Still walk children to find script/style tags
221                    for child in node.children.borrow().iter() {
222                        if let NodeData::Element { name, .. } = &child.data {
223                            let child_tag = name.local.to_string();
224                            if child_tag == "style" || child_tag == "script" {
225                                walk_node(child, scene, parent);
226                            }
227                        }
228                    }
229                    return;
230                }
231                _ => {}
232            }
233
234            let id = scene.add_node(kind, tag.clone());
235
236            // Process attributes
237            let attrs = attrs.borrow();
238            for attr in attrs.iter() {
239                let attr_name = attr.name.local.to_string();
240                let attr_value = attr.value.to_string();
241
242                match attr_name.as_str() {
243                    "id" => {
244                        scene.get_mut(id).element_id = Some(attr_value.clone());
245                    }
246                    "class" => {
247                        scene.get_mut(id).classes = attr_value
248                            .split_whitespace()
249                            .map(|s| s.to_string())
250                            .collect();
251                    }
252                    "style" => {
253                        // Inline styles are parsed later by the CSS module
254                    }
255                    _ => {}
256                }
257
258                // Store event handlers
259                if let Some(event) = attr_name.strip_prefix("on") {
260                    scene
261                        .get_mut(id)
262                        .event_handlers
263                        .insert(event.to_string(), attr_value.clone());
264                }
265
266                scene.get_mut(id).attributes.insert(attr_name, attr_value);
267            }
268
269            if let Some(parent_id) = parent {
270                scene.add_child(parent_id, id);
271            }
272
273            // Walk children
274            for child in node.children.borrow().iter() {
275                walk_node(child, scene, Some(id));
276            }
277        }
278        NodeData::Text { contents } => {
279            let text = contents.borrow().to_string();
280            let trimmed = text.trim();
281            if trimmed.is_empty() {
282                return;
283            }
284
285            let id = scene.add_node(ElementKind::Text, "text".to_string());
286            scene.get_mut(id).text_content = Some(trimmed.to_string());
287
288            if let Some(parent_id) = parent {
289                scene.add_child(parent_id, id);
290            }
291        }
292        _ => {
293            // Comments, processing instructions, etc. - skip
294        }
295    }
296}