use crate::parser::utils::{self as parser_utils};
use crate::utils::dom_cache::{DomCache, TextMetricsCache};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tl::{Node, VDom};
pub struct ElementProcessor<'a> {
dom: &'a VDom<'a>,
dom_cache: Option<&'a DomCache<'a>>,
text_cache: TextMetricsCache,
results_cache: Arc<RwLock<HashMap<String, usize>>>,
}
pub type SharedResultsCache = Arc<RwLock<HashMap<String, usize>>>;
impl<'a> ElementProcessor<'a> {
pub fn new(dom: &'a VDom<'a>) -> Self {
Self {
dom,
dom_cache: None,
text_cache: TextMetricsCache::new(),
results_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn with_cache(dom: &'a VDom<'a>, dom_cache: &'a DomCache<'a>) -> Self {
Self {
dom,
dom_cache: Some(dom_cache),
text_cache: TextMetricsCache::new(),
results_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn with_shared_cache(
dom: &'a VDom<'a>,
dom_cache: Option<&'a DomCache<'a>>,
shared_cache: SharedResultsCache,
) -> Self {
Self {
dom,
dom_cache,
text_cache: TextMetricsCache::new(),
results_cache: shared_cache,
}
}
pub fn count_elements(&mut self, selector: &str) -> usize {
if let Ok(cache) = self.results_cache.read() {
if let Some(&cached_result) = cache.get(selector) {
return cached_result;
}
}
let count = if let Some(cache) = &self.dom_cache {
match selector {
"img" => cache.get_images().len(),
"a[href]" | "a" => cache.get_links().len(),
"h1, h2, h3, h4, h5, h6" => cache.get_headings().len(),
"form" => cache.get_forms().len(),
"input, textarea, select" => cache.get_inputs().len(),
"script" => cache.get_scripts().len(),
"style" => cache.get_styles().len(),
"meta" => cache.get_meta_elements().len(),
"img, video, audio, picture, svg" => cache.get_media_elements().len(),
"button, input, select, textarea, a[href]" => {
cache.get_interactive_elements().len()
}
_ => parser_utils::select_all(self.dom, selector).len(),
}
} else {
parser_utils::select_all(self.dom, selector).len()
};
if let Ok(mut cache) = self.results_cache.write() {
cache.insert(selector.to_string(), count);
}
count
}
pub fn has_elements(&mut self, selector: &str) -> bool {
if let Some(cache) = &self.dom_cache {
match selector {
"img" => !cache.get_images().is_empty(),
"a[href]" | "a" => !cache.get_links().is_empty(),
"h1, h2, h3, h4, h5, h6" => !cache.get_headings().is_empty(),
"form" => !cache.get_forms().is_empty(),
"input, textarea, select" => !cache.get_inputs().is_empty(),
"script" => !cache.get_scripts().is_empty(),
"style" => !cache.get_styles().is_empty(),
"meta" => !cache.get_meta_elements().is_empty(),
"img, video, audio, picture, svg" => !cache.get_media_elements().is_empty(),
"button, input, select, textarea, a[href]" => {
!cache.get_interactive_elements().is_empty()
}
_ => parser_utils::select_first(self.dom, selector).is_some(),
}
} else {
parser_utils::select_first(self.dom, selector).is_some()
}
}
pub fn count_elements_batch(&mut self, selectors: &[&str]) -> Vec<usize> {
let mut results = Vec::with_capacity(selectors.len());
for &selector in selectors {
results.push(self.count_elements(selector));
}
results
}
pub fn count_elements_batch_optimized(&mut self, selectors: &[&str]) -> Vec<usize> {
let mut results = Vec::with_capacity(selectors.len());
let mut uncached_selectors = Vec::new();
for (idx, &selector) in selectors.iter().enumerate() {
if let Ok(cache) = self.results_cache.read() {
if let Some(&cached_result) = cache.get(selector) {
results.push((idx, cached_result));
continue;
}
}
uncached_selectors.push((idx, selector));
}
for (idx, selector) in uncached_selectors {
let count = self.count_elements(selector);
results.push((idx, count));
}
results.sort_by_key(|(idx, _)| *idx);
results.into_iter().map(|(_, count)| count).collect()
}
pub fn count_elements_sum(&mut self, selectors: &[&str]) -> usize {
selectors
.iter()
.map(|selector| self.count_elements(selector))
.sum()
}
pub fn get_elements(&self, selector: &str) -> Vec<&Node<'_>> {
if let Some(cache) = &self.dom_cache {
match selector {
"img" => cache.get_images().to_vec(),
"a[href]" | "a" => cache.get_links().to_vec(),
"h1, h2, h3, h4, h5, h6" => cache.get_headings().to_vec(),
"form" => cache.get_forms().to_vec(),
"input, textarea, select" => cache.get_inputs().to_vec(),
"script" => cache.get_scripts().to_vec(),
"style" => cache.get_styles().to_vec(),
"meta" => cache.get_meta_elements().to_vec(),
_ => parser_utils::select_all(self.dom, selector),
}
} else {
parser_utils::select_all(self.dom, selector)
}
}
pub fn calculate_text_length(&mut self, selector: &str) -> usize {
let cache_key = format!("text_len:{}", selector);
if let Ok(cache) = self.results_cache.read() {
if let Some(&cached_result) = cache.get(&cache_key) {
return cached_result;
}
}
let elements = if let Some(cache) = &self.dom_cache {
match selector {
"img" => cache.get_images().to_vec(),
"a[href]" | "a" => cache.get_links().to_vec(),
"h1, h2, h3, h4, h5, h6" => cache.get_headings().to_vec(),
"form" => cache.get_forms().to_vec(),
"input, textarea, select" => cache.get_inputs().to_vec(),
"script" => cache.get_scripts().to_vec(),
"style" => cache.get_styles().to_vec(),
"meta" => cache.get_meta_elements().to_vec(),
_ => parser_utils::select_all(self.dom, selector),
}
} else {
parser_utils::select_all(self.dom, selector)
};
let mut total = 0;
for node in elements {
total += self.text_cache.get_text_length(node, self.dom);
}
if let Ok(mut cache) = self.results_cache.write() {
cache.insert(cache_key, total);
}
total
}
pub fn has_text_content(&mut self, selector: &str) -> bool {
let elements = if let Some(cache) = &self.dom_cache {
match selector {
"img" => cache.get_images().to_vec(),
"a[href]" | "a" => cache.get_links().to_vec(),
"h1, h2, h3, h4, h5, h6" => cache.get_headings().to_vec(),
"form" => cache.get_forms().to_vec(),
"input, textarea, select" => cache.get_inputs().to_vec(),
"script" => cache.get_scripts().to_vec(),
"style" => cache.get_styles().to_vec(),
"meta" => cache.get_meta_elements().to_vec(),
_ => parser_utils::select_all(self.dom, selector),
}
} else {
parser_utils::select_all(self.dom, selector)
};
for node in elements {
if self.text_cache.has_text_content(node, self.dom) {
return true;
}
}
false
}
pub fn extract_attributes(&self, selector: &str, attribute: &str) -> Vec<String> {
self.get_elements(selector)
.into_iter()
.filter_map(|element| parser_utils::get_attribute(element, attribute))
.collect()
}
pub fn count_with_attribute(&mut self, selector: &str, attribute: &str) -> usize {
let cache_key = format!("attr:{}:{}", selector, attribute);
if let Ok(cache) = self.results_cache.read() {
if let Some(&cached_result) = cache.get(&cache_key) {
return cached_result;
}
}
let count = self
.get_elements(selector)
.into_iter()
.filter(|element| parser_utils::has_attribute(element, attribute))
.count();
if let Ok(mut cache) = self.results_cache.write() {
cache.insert(cache_key, count);
}
count
}
pub fn count_with_attribute_value(
&mut self,
selector: &str,
attribute: &str,
value: &str,
) -> usize {
let cache_key = format!("attr_val:{}:{}:{}", selector, attribute, value);
if let Ok(cache) = self.results_cache.read() {
if let Some(&cached_result) = cache.get(&cache_key) {
return cached_result;
}
}
let count = self
.get_elements(selector)
.into_iter()
.filter(|element| {
if let Some(attr_val) = parser_utils::get_attribute(element, attribute) {
attr_val == value
} else {
false
}
})
.count();
if let Ok(mut cache) = self.results_cache.write() {
cache.insert(cache_key, count);
}
count
}
pub fn get_attribute_counts(&self, selector: &str, attribute: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for element in self.get_elements(selector) {
if let Some(attr_value) = parser_utils::get_attribute(element, attribute) {
*counts.entry(attr_value).or_insert(0) += 1;
}
}
counts
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_html;
use crate::utils::dom_cache::DomCache;
const TEST_HTML: &str = 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">
<img src="test2.jpg">
<form>
<input type="text" name="test" required>
<button type="submit">Submit</button>
</form>
<script src="test.js"></script>
<script>console.log('inline');</script>
</body>
</html>
"#;
#[test]
fn test_element_processor_basic() {
let dom = parse_html(TEST_HTML).unwrap();
let mut processor = ElementProcessor::new(&dom);
assert_eq!(processor.count_elements("img"), 2);
assert_eq!(processor.count_elements("h1"), 1);
assert_eq!(processor.count_elements("h2"), 1);
assert!(processor.has_elements("form"));
assert!(!processor.has_elements("video"));
}
#[test]
fn test_element_processor_with_cache() {
let dom = parse_html(TEST_HTML).unwrap();
let dom_cache = DomCache::new(&dom);
let mut processor = ElementProcessor::with_cache(&dom, &dom_cache);
assert_eq!(processor.count_elements("img"), 2);
assert_eq!(processor.count_elements("script"), 2);
assert!(processor.has_elements("input"));
}
#[test]
fn test_element_counting() {
let dom = parse_html(TEST_HTML).unwrap();
let mut processor = ElementProcessor::new(&dom);
let headings = &["h1", "h2", "h3", "h4", "h5", "h6"];
let media = &["img", "video", "audio", "picture", "svg"];
let form_elements = &["input", "textarea", "select", "button"];
let interactive = &["a[href]", "button", "input", "textarea", "select"];
assert_eq!(processor.count_elements_sum(headings), 2);
assert_eq!(processor.count_elements_sum(media), 2); assert_eq!(processor.count_elements_sum(form_elements), 2); assert!(processor.count_elements_sum(interactive) > 0);
}
#[test]
fn test_batch_counting() {
let dom = parse_html(TEST_HTML).unwrap();
let mut processor = ElementProcessor::new(&dom);
let selectors = &["h1", "h2", "img", "form"];
let counts = processor.count_elements_batch(selectors);
assert_eq!(counts, vec![1, 1, 2, 1]);
let total = processor.count_elements_sum(selectors);
assert_eq!(total, 5);
}
#[test]
fn test_attribute_processing() {
let dom = parse_html(TEST_HTML).unwrap();
let mut processor = ElementProcessor::new(&dom);
assert_eq!(processor.count_with_attribute("img", "alt"), 1);
assert_eq!(processor.count_with_attribute("input", "required"), 1);
let hrefs = processor.extract_attributes("a", "href");
assert_eq!(hrefs.len(), 1);
assert!(hrefs[0].contains("example.com"));
}
#[test]
fn test_text_processing() {
let dom = parse_html(TEST_HTML).unwrap();
let mut processor = ElementProcessor::new(&dom);
assert!(processor.has_text_content("h1"));
assert!(processor.calculate_text_length("p") > 0);
assert!(!processor.has_text_content("img")); }
}