1use quarb::{AstAdapter, NodeId, Value};
23use scraper::{ElementRef, Html};
24use std::collections::HashMap;
25
26struct Node {
27 tag: Option<String>,
29 attrs: Vec<(String, String)>,
30 text: String,
32 parent: Option<NodeId>,
33 children: Vec<NodeId>,
34}
35
36impl Node {
37 fn attr(&self, name: &str) -> Option<&str> {
38 self.attrs
39 .iter()
40 .find(|(k, _)| k == name)
41 .map(|(_, v)| v.as_str())
42 }
43}
44
45pub struct HtmlAdapter {
47 nodes: Vec<Node>,
48 ids: HashMap<String, NodeId>,
50 root: NodeId,
51}
52
53impl HtmlAdapter {
54 pub fn parse(html: &str) -> Self {
56 let document = Html::parse_document(html);
57 let mut nodes = vec![Node {
58 tag: None,
59 attrs: Vec::new(),
60 text: String::new(),
61 parent: None,
62 children: Vec::new(),
63 }];
64 let mut ids = HashMap::new();
65 let root = NodeId(0);
66
67 let html_node = build(document.root_element(), Some(root), &mut nodes, &mut ids);
68 nodes[0].text = nodes[html_node.0 as usize].text.clone();
69 nodes[0].children = vec![html_node];
70
71 HtmlAdapter { nodes, ids, root }
72 }
73
74 pub fn locator(&self, node: NodeId) -> String {
78 let mut segments = Vec::new();
79 let mut cur = Some(node);
80 while let Some(id) = cur {
81 let n = &self.nodes[id.0 as usize];
82 if let Some(tag) = &n.tag {
83 segments.push(self.segment(id, tag));
84 }
85 cur = n.parent;
86 }
87 segments.reverse();
88 format!("/{}", segments.join("/"))
89 }
90
91 fn segment(&self, node: NodeId, tag: &str) -> String {
92 let Some(parent) = self.nodes[node.0 as usize].parent else {
93 return tag.to_string();
94 };
95 let siblings = &self.nodes[parent.0 as usize].children;
96 let same_tag: Vec<NodeId> = siblings
97 .iter()
98 .copied()
99 .filter(|&s| self.nodes[s.0 as usize].tag.as_deref() == Some(tag))
100 .collect();
101 if same_tag.len() > 1 {
102 let n = same_tag.iter().position(|&s| s == node).unwrap() + 1;
103 format!("{tag}[{n}]")
104 } else {
105 tag.to_string()
106 }
107 }
108}
109
110fn build(
121 el: ElementRef,
122 parent: Option<NodeId>,
123 nodes: &mut Vec<Node>,
124 ids: &mut HashMap<String, NodeId>,
125) -> NodeId {
126 let root_this = NodeId(nodes.len() as u64);
127 let mut stack = vec![(el, parent)];
129
130 while let Some((el, parent)) = stack.pop() {
131 let this = NodeId(nodes.len() as u64);
132
133 let tag = el.value().name().to_string();
134 let attrs: Vec<(String, String)> = el
135 .value()
136 .attrs()
137 .map(|(k, v)| (k.to_string(), v.to_string()))
138 .collect();
139 let text: String = el.text().collect();
140
141 if let Some(id) = el.value().id() {
142 ids.entry(id.to_string()).or_insert(this);
143 }
144
145 nodes.push(Node {
146 tag: Some(tag),
147 attrs,
148 text,
149 parent,
150 children: Vec::new(),
151 });
152
153 if let Some(p) = parent {
157 nodes[p.0 as usize].children.push(this);
158 }
159
160 let children: Vec<ElementRef> = el.children().filter_map(ElementRef::wrap).collect();
163 for child in children.into_iter().rev() {
164 stack.push((child, Some(this)));
165 }
166 }
167
168 root_this
169}
170
171const BLOCK: &[&str] = &[
173 "address",
174 "article",
175 "aside",
176 "blockquote",
177 "body",
178 "details",
179 "dialog",
180 "dd",
181 "div",
182 "dl",
183 "dt",
184 "fieldset",
185 "figcaption",
186 "figure",
187 "footer",
188 "form",
189 "h1",
190 "h2",
191 "h3",
192 "h4",
193 "h5",
194 "h6",
195 "header",
196 "hgroup",
197 "hr",
198 "html",
199 "li",
200 "main",
201 "nav",
202 "ol",
203 "p",
204 "pre",
205 "section",
206 "table",
207 "ul",
208];
209
210fn is_heading(tag: &str) -> bool {
211 matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
212}
213
214impl AstAdapter for HtmlAdapter {
215 fn root(&self) -> NodeId {
216 self.root
217 }
218
219 fn children(&self, node: NodeId) -> Vec<NodeId> {
220 self.nodes[node.0 as usize].children.clone()
221 }
222
223 fn name(&self, node: NodeId) -> Option<String> {
224 self.nodes[node.0 as usize].tag.clone()
225 }
226
227 fn parent(&self, node: NodeId) -> Option<NodeId> {
228 self.nodes[node.0 as usize].parent
229 }
230
231 fn traits(&self, node: NodeId) -> Vec<String> {
234 let n = &self.nodes[node.0 as usize];
235 let Some(tag) = &n.tag else {
236 return Vec::new();
237 };
238 let mut out = Vec::new();
239 if BLOCK.contains(&tag.as_str()) {
240 out.push("block".to_string());
241 } else {
242 out.push("inline".to_string());
243 }
244 if is_heading(tag) {
245 out.push("heading".to_string());
246 }
247 if tag == "a" && n.attr("href").is_some() {
248 out.push("link".to_string());
249 }
250 out
251 }
252
253 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
258 self.nodes[node.0 as usize]
259 .attr(name)
260 .map(|v| Value::Str(v.to_string()))
261 }
262
263 fn default_value(&self, node: NodeId) -> Option<Value> {
265 Some(Value::Str(self.nodes[node.0 as usize].text.clone()))
266 }
267
268 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
273 let n = &self.nodes[node.0 as usize];
274 match key {
275 "tag" => n.tag.clone().map(Value::Str),
276 "classes" => n.attr("class").map(|c| {
277 Value::List(
278 c.split_whitespace()
279 .map(|s| Value::Str(s.to_string()))
280 .collect(),
281 )
282 }),
283 "n-attrs" => Some(Value::Int(n.attrs.len() as i64)),
284 _ => None,
285 }
286 }
287
288 fn resolve(&self, node: NodeId, property: &str, _hint: Option<&str>) -> Option<NodeId> {
291 let value = self.nodes[node.0 as usize].attr(property)?;
292 let fragment = value.strip_prefix('#')?;
293 self.ids.get(fragment).copied()
294 }
295}