use crate::parser::utils;
use std::collections::HashMap;
use tl::{Node, VDom};
pub struct DomCache<'a> {
pub images: Vec<&'a Node<'a>>,
pub links: Vec<&'a Node<'a>>,
pub headings: Vec<&'a Node<'a>>,
pub forms: Vec<&'a Node<'a>>,
pub inputs: Vec<&'a Node<'a>>,
pub scripts: Vec<&'a Node<'a>>,
pub styles: Vec<&'a Node<'a>>,
pub meta_elements: Vec<&'a Node<'a>>,
pub media_elements: Vec<&'a Node<'a>>,
pub interactive_elements: Vec<&'a Node<'a>>,
selector_cache: HashMap<String, Vec<&'a Node<'a>>>,
}
impl<'a> DomCache<'a> {
pub fn new(dom: &'a VDom) -> Self {
let images = utils::select_all(dom, "img");
let links = utils::select_all(dom, "a[href]");
let headings = utils::select_all(dom, "h1, h2, h3, h4, h5, h6");
let forms = utils::select_all(dom, "form");
let inputs = utils::select_all(dom, "input, textarea, select");
let scripts = utils::select_all(dom, "script");
let styles = utils::select_all(dom, "style");
let meta_elements = utils::select_all(dom, "meta");
let media_elements = utils::select_all(dom, "img, video, audio, picture, svg");
let interactive_elements =
utils::select_all(dom, "button, input, select, textarea, a[href]");
Self {
images,
links,
headings,
forms,
inputs,
scripts,
styles,
meta_elements,
media_elements,
interactive_elements,
selector_cache: HashMap::new(),
}
}
pub fn get_or_select(&mut self, dom: &'a VDom, selector: &str) -> &Vec<&'a Node<'a>> {
if !self.selector_cache.contains_key(selector) {
let elements = utils::select_all(dom, selector);
self.selector_cache.insert(selector.to_string(), elements);
}
self.selector_cache.get(selector).expect("Just inserted")
}
pub fn count_elements(&mut self, dom: &'a VDom, selector: &str) -> usize {
match selector {
"img" => self.images.len(),
"a[href]" | "a" => self.links.len(),
"h1, h2, h3, h4, h5, h6" => self.headings.len(),
"form" => self.forms.len(),
"input, textarea, select" => self.inputs.len(),
"script" => self.scripts.len(),
"style" => self.styles.len(),
"meta" => self.meta_elements.len(),
"img, video, audio, picture, svg" => self.media_elements.len(),
"button, input, select, textarea, a[href]" => self.interactive_elements.len(),
_ => self.get_or_select(dom, selector).len(),
}
}
pub fn has_elements(&mut self, dom: &'a VDom, selector: &str) -> bool {
match selector {
"img" => !self.images.is_empty(),
"a[href]" | "a" => !self.links.is_empty(),
"h1, h2, h3, h4, h5, h6" => !self.headings.is_empty(),
"form" => !self.forms.is_empty(),
"input, textarea, select" => !self.inputs.is_empty(),
"script" => !self.scripts.is_empty(),
"style" => !self.styles.is_empty(),
"meta" => !self.meta_elements.is_empty(),
"img, video, audio, picture, svg" => !self.media_elements.is_empty(),
"button, input, select, textarea, a[href]" => !self.interactive_elements.is_empty(),
_ => !self.get_or_select(dom, selector).is_empty(),
}
}
pub fn get_images(&self) -> &[&'a Node<'a>] {
&self.images
}
pub fn get_links(&self) -> &[&'a Node<'a>] {
&self.links
}
pub fn get_headings(&self) -> &[&'a Node<'a>] {
&self.headings
}
pub fn get_forms(&self) -> &[&'a Node<'a>] {
&self.forms
}
pub fn get_inputs(&self) -> &[&'a Node<'a>] {
&self.inputs
}
pub fn get_scripts(&self) -> &[&'a Node<'a>] {
&self.scripts
}
pub fn get_styles(&self) -> &[&'a Node<'a>] {
&self.styles
}
pub fn get_meta_elements(&self) -> &[&'a Node<'a>] {
&self.meta_elements
}
pub fn get_media_elements(&self) -> &[&'a Node<'a>] {
&self.media_elements
}
pub fn get_interactive_elements(&self) -> &[&'a Node<'a>] {
&self.interactive_elements
}
}
pub struct TextMetricsCache {
text_lengths: HashMap<usize, usize>,
has_text_cache: HashMap<usize, bool>,
}
impl TextMetricsCache {
pub fn new() -> Self {
Self {
text_lengths: HashMap::new(),
has_text_cache: HashMap::new(),
}
}
pub fn get_text_length(&mut self, node: &Node, dom: &VDom) -> usize {
let node_id = node as *const _ as usize;
if let Some(&length) = self.text_lengths.get(&node_id) {
return length;
}
let length = crate::parser::utils::get_text_content_length(node, dom);
self.text_lengths.insert(node_id, length);
length
}
pub fn has_text_content(&mut self, node: &Node, dom: &VDom) -> bool {
let node_id = node as *const _ as usize;
if let Some(&has_text) = self.has_text_cache.get(&node_id) {
return has_text;
}
let has_text = crate::parser::utils::has_text_content(node, dom);
self.has_text_cache.insert(node_id, has_text);
has_text
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_html;
#[test]
fn test_dom_cache_creation() {
let html = r#"
<html>
<head>
<title>Test</title>
<meta name="description" content="Test page">
</head>
<body>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<p>Some text with <a href="https://example.com">a link</a></p>
<img src="test.jpg" alt="Test image">
<form>
<input type="text" name="test">
</form>
</body>
</html>
"#;
let dom = parse_html(html).unwrap();
let cache = DomCache::new(&dom);
assert_eq!(cache.headings.len(), 2); assert_eq!(cache.links.len(), 1); assert_eq!(cache.images.len(), 1); assert_eq!(cache.forms.len(), 1); assert_eq!(cache.inputs.len(), 1); assert_eq!(cache.meta_elements.len(), 1); }
#[test]
fn test_cache_performance_benefits() {
let html = r#"
<html>
<body>
<div>
<img src="1.jpg" alt="1">
<img src="2.jpg" alt="2">
<img src="3.jpg" alt="3">
</div>
</body>
</html>
"#;
let dom = parse_html(html).unwrap();
let mut cache = DomCache::new(&dom);
assert_eq!(cache.count_elements(&dom, "img"), 3);
assert_eq!(cache.count_elements(&dom, "img"), 3); assert!(cache.has_elements(&dom, "img"));
}
}