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> {
256 let n = &self.nodes[node.0 as usize];
257 if name == "text" {
258 Some(Value::Str(n.text.clone()))
259 } else {
260 n.attr(name).map(|v| Value::Str(v.to_string()))
261 }
262 }
263
264 fn default_value(&self, node: NodeId) -> Option<Value> {
266 Some(Value::Str(self.nodes[node.0 as usize].text.clone()))
267 }
268
269 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
272 let n = &self.nodes[node.0 as usize];
273 match key {
274 "tag" => n.tag.clone().map(Value::Str),
275 "text" => Some(Value::Str(n.text.clone())),
276 "id" => n.attr("id").map(|v| Value::Str(v.to_string())),
277 "classes" => n.attr("class").map(|c| {
278 Value::List(
279 c.split_whitespace()
280 .map(|s| Value::Str(s.to_string()))
281 .collect(),
282 )
283 }),
284 "n-attrs" => Some(Value::Int(n.attrs.len() as i64)),
285 other => n.attr(other).map(|v| Value::Str(v.to_string())),
286 }
287 }
288
289 fn resolve(&self, node: NodeId, property: &str, _hint: Option<&str>) -> Option<NodeId> {
292 let value = self.nodes[node.0 as usize].attr(property)?;
293 let fragment = value.strip_prefix('#')?;
294 self.ids.get(fragment).copied()
295 }
296}