1use quarb::names::Names;
23use quarb::{AstAdapter, NodeId, Value};
24mod sink;
25use std::collections::HashMap;
26
27struct Node {
28 tag: Option<u32>,
31 attrs: Vec<(u32, String)>,
33 pieces: Vec<Piece>,
39 text: std::cell::OnceCell<String>,
40 parent: Option<NodeId>,
41 children: Vec<NodeId>,
42}
43
44enum Piece {
46 Text(String),
47 Child(usize),
48}
49
50impl Node {
51 fn attr(&self, names: &Names, name: &str) -> Option<&str> {
52 let k = names.key(name)?;
53 self.attrs
54 .iter()
55 .find(|(a, _)| *a == k)
56 .map(|(_, v)| v.as_str())
57 }
58}
59
60pub fn declared_encoding(bytes: &[u8]) -> Option<String> {
70 if let Some(label) = xml_prologue_encoding(bytes) {
71 return Some(label);
72 }
73 let head = &bytes[..bytes.len().min(1024)];
74 let lower: Vec<u8> = head.iter().map(|b| b.to_ascii_lowercase()).collect();
75 let mut at = 0;
76 while let Some(i) = find(&lower[at..], b"<meta") {
77 let start = at + i + 5;
78 let end = start + find(&lower[start..], b">").unwrap_or(lower.len() - start);
79 let attrs = meta_attributes(&lower[start..end]);
80 let get = |name: &str| {
81 attrs
82 .iter()
83 .find(|(k, _)| k == name)
84 .map(|(_, v)| v.as_str())
85 };
86 if let Some(label) = get("charset").filter(|l| !l.is_empty()) {
87 return Some(label.to_string());
88 }
89 if get("http-equiv") == Some("content-type")
90 && let Some(content) = get("content")
91 && let Some(p) = content.find("charset=")
92 {
93 let label = content[p + 8..]
94 .trim_matches(|c: char| c == '"' || c == '\'' || c.is_ascii_whitespace())
95 .split([';', ' ', '"', '\''])
96 .next()
97 .unwrap_or("");
98 if !label.is_empty() {
99 return Some(label.to_string());
100 }
101 }
102 at = end;
103 }
104 None
105}
106
107fn xml_prologue_encoding(bytes: &[u8]) -> Option<String> {
112 let head = bytes.strip_prefix(b"<?xml")?;
113 if !head.first().is_some_and(|b| b.is_ascii_whitespace()) {
114 return None;
115 }
116 let end = find(head, b"?>")?;
117 let prologue = std::str::from_utf8(&head[..end]).ok()?;
118 let rest = prologue
119 .split_once("encoding")?
120 .1
121 .trim_start()
122 .strip_prefix('=')?
123 .trim_start();
124 let quote = rest.chars().next()?;
125 (quote == '"' || quote == '\'')
126 .then(|| {
127 rest[1..]
128 .split(quote)
129 .next()
130 .unwrap_or("")
131 .trim()
132 .to_string()
133 })
134 .filter(|l| !l.is_empty())
135}
136
137fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
138 hay.windows(needle.len()).position(|w| w == needle)
139}
140
141fn meta_attributes(tag: &[u8]) -> Vec<(String, String)> {
146 let mut out = Vec::new();
147 let mut i = 0;
148 let n = tag.len();
149 while i < n {
150 while i < n && (tag[i].is_ascii_whitespace() || tag[i] == b'/') {
151 i += 1;
152 }
153 let name_start = i;
154 while i < n && !tag[i].is_ascii_whitespace() && tag[i] != b'=' && tag[i] != b'/' {
155 i += 1;
156 }
157 if i == name_start {
158 break;
159 }
160 let name = String::from_utf8_lossy(&tag[name_start..i]).into_owned();
161 while i < n && tag[i].is_ascii_whitespace() {
162 i += 1;
163 }
164 let mut value = String::new();
165 if i < n && tag[i] == b'=' {
166 i += 1;
167 while i < n && tag[i].is_ascii_whitespace() {
168 i += 1;
169 }
170 if i < n && (tag[i] == b'"' || tag[i] == b'\'') {
171 let q = tag[i];
172 i += 1;
173 let v = i;
174 while i < n && tag[i] != q {
175 i += 1;
176 }
177 value = String::from_utf8_lossy(&tag[v..i]).trim().to_string();
178 i = (i + 1).min(n);
179 } else {
180 let v = i;
181 while i < n && !tag[i].is_ascii_whitespace() {
182 i += 1;
183 }
184 value = String::from_utf8_lossy(&tag[v..i]).to_string();
185 }
186 }
187 out.push((name, value));
188 }
189 out
190}
191
192pub struct HtmlAdapter {
194 nodes: Vec<Node>,
195 names: Names,
197 ids: HashMap<String, NodeId>,
199 root: NodeId,
200 document_url: Option<url::Url>,
205}
206
207impl HtmlAdapter {
208 fn rel_ok(&self, node: NodeId, hint: Option<&str>) -> bool {
215 match hint {
216 None | Some("*") => true,
217 Some(h) => self.nodes[node.0 as usize]
218 .attr(&self.names, "rel")
219 .is_some_and(|rel| rel.split_whitespace().any(|w| w.eq_ignore_ascii_case(h))),
220 }
221 }
222
223 pub fn set_document_url(&mut self, url: &str) {
226 self.document_url = url::Url::parse(url).ok();
227 }
228
229 fn text_of(&self, node: NodeId) -> &str {
234 self.nodes[node.0 as usize].text.get_or_init(|| {
235 let mut out = String::new();
236 let mut stack: Vec<(usize, usize, usize)> = vec![(node.0 as usize, 0, 0)];
241 while let Some(top) = stack.last_mut() {
242 let (cur, at, start) = *top;
243 let n = &self.nodes[cur];
244 if at >= n.pieces.len() {
245 if cur != node.0 as usize {
246 let _ = n.text.set(out[start..].to_string());
247 }
248 stack.pop();
249 continue;
250 }
251 top.1 += 1;
252 match &n.pieces[at] {
253 Piece::Text(t) => out.push_str(t),
254 Piece::Child(k) => {
255 let c = n.children[*k].0 as usize;
256 match self.nodes[c].text.get() {
257 Some(t) => out.push_str(t),
258 None => stack.push((c, 0, out.len())),
259 }
260 }
261 }
262 }
263 out
264 })
265 }
266
267 pub fn parse(html: &str) -> Self {
269 use html5ever::tendril::TendrilSink;
270 let sink =
271 html5ever::driver::parse_document(sink::Sink::new(), Default::default()).one(html);
272 let mut nodes = vec![Node {
273 tag: None,
274 attrs: Vec::new(),
275 pieces: vec![Piece::Child(0)],
276 text: std::cell::OnceCell::new(),
277 parent: None,
278 children: Vec::new(),
279 }];
280 let mut ids = HashMap::new();
281 let root = NodeId(0);
282 let sink::Sink {
283 nodes: mut sunk,
284 mut names,
285 } = sink;
286 let html_el = sunk[0].kids.iter().find_map(|k| match k {
289 sink::Kid::Node(n) if !sunk[*n].skip => Some(*n),
290 _ => None,
291 });
292 if let Some(html_el) = html_el {
293 let html_node = build(
294 html_el,
295 Some(root),
296 &mut sunk,
297 &mut nodes,
298 &mut names,
299 &mut ids,
300 );
301 nodes[0].children = vec![html_node];
302 }
303
304 HtmlAdapter {
305 nodes,
306 names,
307 ids,
308 root,
309 document_url: None,
310 }
311 }
312
313 pub fn locator(&self, node: NodeId) -> String {
317 let mut segments = Vec::new();
318 let mut cur = Some(node);
319 while let Some(id) = cur {
320 let n = &self.nodes[id.0 as usize];
321 if let Some(tag) = n.tag {
322 segments.push(self.segment(id, tag));
323 }
324 cur = n.parent;
325 }
326 segments.reverse();
327 format!("/{}", segments.join("/"))
328 }
329
330 fn segment(&self, node: NodeId, tag: u32) -> String {
331 let name = self.names.get(tag);
332 let Some(parent) = self.nodes[node.0 as usize].parent else {
333 return name.to_string();
334 };
335 let siblings = &self.nodes[parent.0 as usize].children;
336 let same_tag: Vec<NodeId> = siblings
337 .iter()
338 .copied()
339 .filter(|&s| self.nodes[s.0 as usize].tag == Some(tag))
340 .collect();
341 if same_tag.len() > 1 {
342 let n = same_tag.iter().position(|&s| s == node).unwrap() + 1;
343 format!("{name}[{n}]")
344 } else {
345 name.to_string()
346 }
347 }
348}
349
350fn build(
357 el: usize,
358 parent: Option<NodeId>,
359 sunk: &mut [sink::SinkNode],
360 nodes: &mut Vec<Node>,
361 names: &mut Names,
362 ids: &mut HashMap<String, NodeId>,
363) -> NodeId {
364 let root_this = NodeId(nodes.len() as u64);
365 let mut stack = vec![(el, parent)];
366 while let Some((el, parent)) = stack.pop() {
367 let this = NodeId(nodes.len() as u64);
368 let s = &mut sunk[el];
369 let tag = names.intern(&s.name.as_ref().expect("an element").local);
370 let attrs = std::mem::take(&mut s.attrs);
371 let kids = std::mem::take(&mut s.kids);
372 let mut pieces = Vec::new();
373 let mut children: Vec<usize> = Vec::new();
374 let mut ordinal = 0;
375 for k in kids {
376 match k {
377 sink::Kid::Text(t) => pieces.push(Piece::Text(t)),
378 sink::Kid::Node(n) => {
379 if sunk[n].skip {
380 continue;
381 }
382 pieces.push(Piece::Child(ordinal));
383 ordinal += 1;
384 children.push(n);
385 }
386 }
387 }
388 if let Some(k) = names.key("id")
389 && let Some((_, id)) = attrs.iter().find(|(a, _)| *a == k)
390 {
391 ids.entry(id.clone()).or_insert(this);
392 }
393 nodes.push(Node {
394 tag: Some(tag),
395 attrs,
396 pieces,
397 text: std::cell::OnceCell::new(),
398 parent,
399 children: Vec::new(),
400 });
401 if let Some(p) = parent {
402 nodes[p.0 as usize].children.push(this);
403 }
404 for child in children.into_iter().rev() {
405 stack.push((child, Some(this)));
406 }
407 }
408 root_this
409}
410
411const BLOCK: &[&str] = &[
413 "address",
414 "article",
415 "aside",
416 "blockquote",
417 "body",
418 "details",
419 "dialog",
420 "dd",
421 "div",
422 "dl",
423 "dt",
424 "fieldset",
425 "figcaption",
426 "figure",
427 "footer",
428 "form",
429 "h1",
430 "h2",
431 "h3",
432 "h4",
433 "h5",
434 "h6",
435 "header",
436 "hgroup",
437 "hr",
438 "html",
439 "li",
440 "main",
441 "nav",
442 "ol",
443 "p",
444 "pre",
445 "section",
446 "table",
447 "ul",
448];
449
450fn is_heading(tag: &str) -> bool {
451 matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
452}
453
454impl AstAdapter for HtmlAdapter {
455 fn estimate(&self, scope: &[&str], plan: &quarb::StepPlan<'_>) -> Option<quarb::Estimate> {
457 quarb::adapter::arena_estimate(self, scope, plan, quarb::Latency::Memory, None)
458 }
459
460 fn root(&self) -> NodeId {
461 self.root
462 }
463
464 fn walks_for_references(&self) -> bool {
467 true
468 }
469
470 fn children(&self, node: NodeId) -> Vec<NodeId> {
471 self.nodes[node.0 as usize].children.clone()
472 }
473
474 fn name(&self, node: NodeId) -> Option<String> {
475 self.nodes[node.0 as usize]
476 .tag
477 .map(|t| self.names.get(t).to_string())
478 }
479
480 fn parent(&self, node: NodeId) -> Option<NodeId> {
481 self.nodes[node.0 as usize].parent
482 }
483
484 fn traits(&self, node: NodeId) -> Vec<String> {
489 let n = &self.nodes[node.0 as usize];
490 let Some(tag) = n.tag.map(|t| self.names.get(t)) else {
491 return vec!["document".to_string()];
492 };
493 let mut out = vec![tag.to_string()];
494 if BLOCK.contains(&tag) {
495 out.push("block".to_string());
496 } else {
497 out.push("inline".to_string());
498 }
499 if is_heading(tag) {
500 out.push("heading".to_string());
501 }
502 if tag == "a" && n.attr(&self.names, "href").is_some() {
503 out.push("link".to_string());
504 }
505 if tag == "table" {
508 out.push("tabula".to_string());
509 }
510 out
511 }
512
513 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
516 self.nodes[node.0 as usize]
517 .attr(&self.names, name)
518 .map(|v| Value::Str(v.to_string()))
519 }
520
521 fn default_value(&self, node: NodeId) -> Option<Value> {
523 Some(Value::Str(self.text_of(node).to_string()))
524 }
525
526 fn metadata_catalogue(&self) -> &'static [&'static str] {
536 &["tag", "classes", "attrs", "n-attrs"]
537 }
538
539 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
540 let n = &self.nodes[node.0 as usize];
541 match key {
542 "tag" => n.tag.map(|t| Value::Str(self.names.get(t).to_string())),
543 "classes" => n.attr(&self.names, "class").map(|c| {
544 Value::list(
545 c.split_whitespace()
546 .map(|s| Value::Str(s.to_string()))
547 .collect(),
548 )
549 }),
550 "attrs" => {
551 let mut names: Vec<&str> =
552 n.attrs.iter().map(|(k, _)| self.names.get(*k)).collect();
553 names.sort_unstable();
554 Some(Value::list(
555 names
556 .into_iter()
557 .map(|k| Value::Str(k.to_string()))
558 .collect(),
559 ))
560 }
561 "n-attrs" => Some(Value::Int(n.attrs.len() as i64)),
562 _ => None,
563 }
564 }
565
566 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
569 if property == "href" && !self.rel_ok(node, hint) {
570 return None;
571 }
572 let value = self.nodes[node.0 as usize].attr(&self.names, property)?;
573 let fragment = value.strip_prefix('#')?;
574 self.ids.get(fragment).copied()
575 }
576
577 fn external_ref(
587 &self,
588 node: NodeId,
589 property: &str,
590 hint: Option<&str>,
591 ) -> Option<quarb::ExternalRef> {
592 if property == "href" && !self.rel_ok(node, hint) {
593 return None;
594 }
595 let value = self.nodes[node.0 as usize].attr(&self.names, property)?;
596 let t = value.trim();
597 if t.is_empty() || t.starts_with('#') {
598 return None;
599 }
600 let u = match url::Url::parse(t) {
601 Ok(u) => u,
602 Err(url::ParseError::RelativeUrlWithoutBase) => match &self.document_url {
607 Some(base) => base.join(t).ok()?,
608 None => return Some(quarb::ExternalRef::uri(t)),
609 },
610 Err(_) => return None,
611 };
612 if u.scheme() != "http" && u.scheme() != "https" {
613 return None;
614 }
615 Some(quarb::ExternalRef::uri(u.as_str()))
616 }
617
618 fn resolve_fragment(&self, _node: NodeId, fragment: &str) -> Option<NodeId> {
621 self.ids.get(fragment).copied()
622 }
623
624 fn property_names(&self, node: NodeId) -> Vec<String> {
628 self.nodes[node.0 as usize]
629 .attrs
630 .iter()
631 .map(|(k, _)| self.names.get(*k).to_string())
632 .collect()
633 }
634
635 fn can_refer_to(&self, _source: NodeId, _target: NodeId) -> Option<bool> {
638 Some(true)
639 }
640
641 fn ref_property(&self, node: NodeId) -> Option<String> {
642 let n = &self.nodes[node.0 as usize];
643 let (prop, tags): (&str, &[&str]) = match n.tag.map(|t| self.names.get(t)) {
644 Some(t) if ["a", "link", "area"].contains(&t) => ("href", &["a", "link", "area"]),
645 Some(t) if ["iframe", "frame"].contains(&t) => ("src", &["iframe", "frame"]),
646 _ => return None,
647 };
648 let _ = tags;
649 n.attr(&self.names, prop).map(|_| prop.to_string())
650 }
651
652 fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
657 if property != "href" {
658 return None;
659 }
660 let rel = self.nodes[node.0 as usize].attr(&self.names, "rel")?;
661 let rel = rel.trim();
662 (!rel.is_empty()).then(|| rel.to_string())
663 }
664}
665
666#[cfg(test)]
667mod declared_tests {
668 use super::declared_encoding;
669
670 #[test]
671 fn meta_charset_forms() {
672 assert_eq!(
673 declared_encoding(
674 b"<!DOCTYPE html><html><head><meta charset=\"windows-1251\"><title>x</title>"
675 ),
676 Some("windows-1251".into())
677 );
678 assert_eq!(
679 declared_encoding(b"<html><head><META http-equiv=\"Content-Type\" content=\"text/html; charset=koi8-r\">"),
680 Some("koi8-r".into())
681 );
682 assert_eq!(
683 declared_encoding(b"<html><head><meta charset=utf-8></head>"),
684 Some("utf-8".into())
685 );
686 assert_eq!(
687 declared_encoding(b"<html><head><meta name=\"charset-note\" content=\"x\"></head>"),
688 None
689 );
690 assert_eq!(
691 declared_encoding(b"<?xml version=\"1.0\" encoding=\"ISO-8859-5\"?><html/>"),
692 Some("ISO-8859-5".into())
693 );
694 assert_eq!(
696 declared_encoding(
697 b"<meta name=\"description\" content=\"why charset=koi8-r was used\"><title>x</title>"
698 ),
699 None
700 );
701 assert_eq!(
703 declared_encoding(b"<?xml-stylesheet href=\"a.xsl\" encoding=\"koi8-r\"?><html/>"),
704 None
705 );
706 let mut late = vec![b' '; 1100];
707 late.extend_from_slice(b"<meta charset=\"koi8-r\">");
708 assert_eq!(declared_encoding(&late), None);
709 }
710}