use quarb::{AstAdapter, NodeId, Value};
use scraper::{ElementRef, Html};
use std::collections::HashMap;
struct Node {
tag: Option<String>,
attrs: Vec<(String, String)>,
text: String,
parent: Option<NodeId>,
children: Vec<NodeId>,
}
impl Node {
fn attr(&self, name: &str) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
}
}
pub struct HtmlAdapter {
nodes: Vec<Node>,
ids: HashMap<String, NodeId>,
root: NodeId,
}
impl HtmlAdapter {
pub fn parse(html: &str) -> Self {
let document = Html::parse_document(html);
let mut nodes = vec![Node {
tag: None,
attrs: Vec::new(),
text: String::new(),
parent: None,
children: Vec::new(),
}];
let mut ids = HashMap::new();
let root = NodeId(0);
let html_node = build(document.root_element(), Some(root), &mut nodes, &mut ids);
nodes[0].text = nodes[html_node.0 as usize].text.clone();
nodes[0].children = vec![html_node];
HtmlAdapter { nodes, ids, root }
}
pub fn locator(&self, node: NodeId) -> String {
let mut segments = Vec::new();
let mut cur = Some(node);
while let Some(id) = cur {
let n = &self.nodes[id.0 as usize];
if let Some(tag) = &n.tag {
segments.push(self.segment(id, tag));
}
cur = n.parent;
}
segments.reverse();
format!("/{}", segments.join("/"))
}
fn segment(&self, node: NodeId, tag: &str) -> String {
let Some(parent) = self.nodes[node.0 as usize].parent else {
return tag.to_string();
};
let siblings = &self.nodes[parent.0 as usize].children;
let same_tag: Vec<NodeId> = siblings
.iter()
.copied()
.filter(|&s| self.nodes[s.0 as usize].tag.as_deref() == Some(tag))
.collect();
if same_tag.len() > 1 {
let n = same_tag.iter().position(|&s| s == node).unwrap() + 1;
format!("{tag}[{n}]")
} else {
tag.to_string()
}
}
}
fn build(
el: ElementRef,
parent: Option<NodeId>,
nodes: &mut Vec<Node>,
ids: &mut HashMap<String, NodeId>,
) -> NodeId {
let root_this = NodeId(nodes.len() as u64);
let mut stack = vec![(el, parent)];
while let Some((el, parent)) = stack.pop() {
let this = NodeId(nodes.len() as u64);
let tag = el.value().name().to_string();
let attrs: Vec<(String, String)> = el
.value()
.attrs()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let text: String = el.text().collect();
if let Some(id) = el.value().id() {
ids.entry(id.to_string()).or_insert(this);
}
nodes.push(Node {
tag: Some(tag),
attrs,
text,
parent,
children: Vec::new(),
});
if let Some(p) = parent {
nodes[p.0 as usize].children.push(this);
}
let children: Vec<ElementRef> = el.children().filter_map(ElementRef::wrap).collect();
for child in children.into_iter().rev() {
stack.push((child, Some(this)));
}
}
root_this
}
const BLOCK: &[&str] = &[
"address",
"article",
"aside",
"blockquote",
"body",
"details",
"dialog",
"dd",
"div",
"dl",
"dt",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hgroup",
"hr",
"html",
"li",
"main",
"nav",
"ol",
"p",
"pre",
"section",
"table",
"ul",
];
fn is_heading(tag: &str) -> bool {
matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
}
impl AstAdapter for HtmlAdapter {
fn root(&self) -> NodeId {
self.root
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.nodes[node.0 as usize].children.clone()
}
fn name(&self, node: NodeId) -> Option<String> {
self.nodes[node.0 as usize].tag.clone()
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.nodes[node.0 as usize].parent
}
fn traits(&self, node: NodeId) -> Vec<String> {
let n = &self.nodes[node.0 as usize];
let Some(tag) = &n.tag else {
return Vec::new();
};
let mut out = Vec::new();
if BLOCK.contains(&tag.as_str()) {
out.push("block".to_string());
} else {
out.push("inline".to_string());
}
if is_heading(tag) {
out.push("heading".to_string());
}
if tag == "a" && n.attr("href").is_some() {
out.push("link".to_string());
}
out
}
fn property(&self, node: NodeId, name: &str) -> Option<Value> {
self.nodes[node.0 as usize]
.attr(name)
.map(|v| Value::Str(v.to_string()))
}
fn default_value(&self, node: NodeId) -> Option<Value> {
Some(Value::Str(self.nodes[node.0 as usize].text.clone()))
}
fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
let n = &self.nodes[node.0 as usize];
match key {
"tag" => n.tag.clone().map(Value::Str),
"classes" => n.attr("class").map(|c| {
Value::List(
c.split_whitespace()
.map(|s| Value::Str(s.to_string()))
.collect(),
)
}),
"n-attrs" => Some(Value::Int(n.attrs.len() as i64)),
_ => None,
}
}
fn resolve(&self, node: NodeId, property: &str, _hint: Option<&str>) -> Option<NodeId> {
let value = self.nodes[node.0 as usize].attr(property)?;
let fragment = value.strip_prefix('#')?;
self.ids.get(fragment).copied()
}
}