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> {
487 let n = &self.nodes[node.0 as usize];
488 let Some(tag) = n.tag.map(|t| self.names.get(t)) else {
489 return Vec::new();
490 };
491 let mut out = Vec::new();
492 if BLOCK.contains(&tag) {
493 out.push("block".to_string());
494 } else {
495 out.push("inline".to_string());
496 }
497 if is_heading(tag) {
498 out.push("heading".to_string());
499 }
500 if tag == "a" && n.attr(&self.names, "href").is_some() {
501 out.push("link".to_string());
502 }
503 if tag == "table" {
506 out.push("tabula".to_string());
507 }
508 out
509 }
510
511 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
514 self.nodes[node.0 as usize]
515 .attr(&self.names, name)
516 .map(|v| Value::Str(v.to_string()))
517 }
518
519 fn default_value(&self, node: NodeId) -> Option<Value> {
521 Some(Value::Str(self.text_of(node).to_string()))
522 }
523
524 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
532 let n = &self.nodes[node.0 as usize];
533 match key {
534 "tag" => n.tag.map(|t| Value::Str(self.names.get(t).to_string())),
535 "classes" => n.attr(&self.names, "class").map(|c| {
536 Value::list(
537 c.split_whitespace()
538 .map(|s| Value::Str(s.to_string()))
539 .collect(),
540 )
541 }),
542 "attrs" => {
543 let mut names: Vec<&str> =
544 n.attrs.iter().map(|(k, _)| self.names.get(*k)).collect();
545 names.sort_unstable();
546 Some(Value::list(
547 names
548 .into_iter()
549 .map(|k| Value::Str(k.to_string()))
550 .collect(),
551 ))
552 }
553 "n-attrs" => Some(Value::Int(n.attrs.len() as i64)),
554 _ => None,
555 }
556 }
557
558 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
561 if property == "href" && !self.rel_ok(node, hint) {
562 return None;
563 }
564 let value = self.nodes[node.0 as usize].attr(&self.names, property)?;
565 let fragment = value.strip_prefix('#')?;
566 self.ids.get(fragment).copied()
567 }
568
569 fn external_ref(
579 &self,
580 node: NodeId,
581 property: &str,
582 hint: Option<&str>,
583 ) -> Option<quarb::ExternalRef> {
584 if property == "href" && !self.rel_ok(node, hint) {
585 return None;
586 }
587 let value = self.nodes[node.0 as usize].attr(&self.names, property)?;
588 let t = value.trim();
589 if t.is_empty() || t.starts_with('#') {
590 return None;
591 }
592 let u = match url::Url::parse(t) {
593 Ok(u) => u,
594 Err(url::ParseError::RelativeUrlWithoutBase) => match &self.document_url {
599 Some(base) => base.join(t).ok()?,
600 None => return Some(quarb::ExternalRef::uri(t)),
601 },
602 Err(_) => return None,
603 };
604 if u.scheme() != "http" && u.scheme() != "https" {
605 return None;
606 }
607 Some(quarb::ExternalRef::uri(u.as_str()))
608 }
609
610 fn resolve_fragment(&self, _node: NodeId, fragment: &str) -> Option<NodeId> {
613 self.ids.get(fragment).copied()
614 }
615
616 fn property_names(&self, node: NodeId) -> Vec<String> {
620 self.nodes[node.0 as usize]
621 .attrs
622 .iter()
623 .map(|(k, _)| self.names.get(*k).to_string())
624 .collect()
625 }
626
627 fn can_refer_to(&self, _source: NodeId, _target: NodeId) -> Option<bool> {
630 Some(true)
631 }
632
633 fn ref_property(&self, node: NodeId) -> Option<String> {
634 let n = &self.nodes[node.0 as usize];
635 let (prop, tags): (&str, &[&str]) = match n.tag.map(|t| self.names.get(t)) {
636 Some(t) if ["a", "link", "area"].contains(&t) => ("href", &["a", "link", "area"]),
637 Some(t) if ["iframe", "frame"].contains(&t) => ("src", &["iframe", "frame"]),
638 _ => return None,
639 };
640 let _ = tags;
641 n.attr(&self.names, prop).map(|_| prop.to_string())
642 }
643
644 fn ref_label(&self, node: NodeId, property: &str) -> Option<String> {
649 if property != "href" {
650 return None;
651 }
652 let rel = self.nodes[node.0 as usize].attr(&self.names, "rel")?;
653 let rel = rel.trim();
654 (!rel.is_empty()).then(|| rel.to_string())
655 }
656}
657
658#[cfg(test)]
659mod declared_tests {
660 use super::declared_encoding;
661
662 #[test]
663 fn meta_charset_forms() {
664 assert_eq!(
665 declared_encoding(
666 b"<!DOCTYPE html><html><head><meta charset=\"windows-1251\"><title>x</title>"
667 ),
668 Some("windows-1251".into())
669 );
670 assert_eq!(
671 declared_encoding(b"<html><head><META http-equiv=\"Content-Type\" content=\"text/html; charset=koi8-r\">"),
672 Some("koi8-r".into())
673 );
674 assert_eq!(
675 declared_encoding(b"<html><head><meta charset=utf-8></head>"),
676 Some("utf-8".into())
677 );
678 assert_eq!(
679 declared_encoding(b"<html><head><meta name=\"charset-note\" content=\"x\"></head>"),
680 None
681 );
682 assert_eq!(
683 declared_encoding(b"<?xml version=\"1.0\" encoding=\"ISO-8859-5\"?><html/>"),
684 Some("ISO-8859-5".into())
685 );
686 assert_eq!(
688 declared_encoding(
689 b"<meta name=\"description\" content=\"why charset=koi8-r was used\"><title>x</title>"
690 ),
691 None
692 );
693 assert_eq!(
695 declared_encoding(b"<?xml-stylesheet href=\"a.xsl\" encoding=\"koi8-r\"?><html/>"),
696 None
697 );
698 let mut late = vec![b' '; 1100];
699 late.extend_from_slice(b"<meta charset=\"koi8-r\">");
700 assert_eq!(declared_encoding(&late), None);
701 }
702}