webpage_quality_analyzer 1.0.2

High-performance webpage quality analyzer with 115 comprehensive metrics - Rust library with WASM, C++, and Python bindings
Documentation
//! DOM Caching System for Performance Optimization
//!
//! This module provides a caching layer for DOM selections to eliminate redundant
//! DOM traversals during metrics calculation. By pre-computing common selections
//! once and reusing them, we can achieve significant performance improvements.

use crate::parser::utils;
use std::collections::HashMap;
use tl::{Node, VDom};

/// Cached DOM selections for common elements to avoid repeated traversals
pub struct DomCache<'a> {
    /// All image elements in the DOM
    pub images: Vec<&'a Node<'a>>,
    /// All link elements (a[href]) in the DOM
    pub links: Vec<&'a Node<'a>>,
    /// All heading elements (h1-h6) in the DOM
    pub headings: Vec<&'a Node<'a>>,
    /// All form elements in the DOM
    pub forms: Vec<&'a Node<'a>>,
    /// All input elements (input, textarea, select) in the DOM
    pub inputs: Vec<&'a Node<'a>>,
    /// All script elements in the DOM
    pub scripts: Vec<&'a Node<'a>>,
    /// All style elements in the DOM
    pub styles: Vec<&'a Node<'a>>,
    /// All meta elements in the DOM
    pub meta_elements: Vec<&'a Node<'a>>,
    /// All media elements (img, video, audio, picture, svg) in the DOM
    pub media_elements: Vec<&'a Node<'a>>,
    /// All interactive elements (button, input, select, textarea, a) in the DOM
    pub interactive_elements: Vec<&'a Node<'a>>,
    /// Cached selector results for arbitrary selectors
    selector_cache: HashMap<String, Vec<&'a Node<'a>>>,
}

impl<'a> DomCache<'a> {
    /// Create a new DOM cache by pre-computing common selections
    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(),
        }
    }

    /// Get cached elements or perform selection and cache result
    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")
    }

    /// Get count of elements by selector (cached)
    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(),
        }
    }

    /// Check if any elements match a selector (cached, more efficient than counting)
    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(),
        }
    }

    /// Get specific cached element collections
    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
    }
}

/// Compute text-related metrics efficiently using cached elements
pub struct TextMetricsCache {
    /// Cached text lengths for elements
    text_lengths: HashMap<usize, usize>,
    /// Cached presence flags
    has_text_cache: HashMap<usize, bool>,
}

impl TextMetricsCache {
    pub fn new() -> Self {
        Self {
            text_lengths: HashMap::new(),
            has_text_cache: HashMap::new(),
        }
    }

    /// Get text content length for a node, caching the result
    pub fn get_text_length(&mut self, node: &Node, dom: &VDom) -> usize {
        let node_id = node as *const _ as usize; // Use pointer as unique ID

        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
    }

    /// Check if node has text content, caching the result
    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); // h1, h2
        assert_eq!(cache.links.len(), 1); // one a[href]
        assert_eq!(cache.images.len(), 1); // one img
        assert_eq!(cache.forms.len(), 1); // one form
        assert_eq!(cache.inputs.len(), 1); // one input
        assert_eq!(cache.meta_elements.len(), 1); // one meta
    }

    #[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);

        // Multiple calls should use cached results
        assert_eq!(cache.count_elements(&dom, "img"), 3);
        assert_eq!(cache.count_elements(&dom, "img"), 3); // Second call uses cache
        assert!(cache.has_elements(&dom, "img"));
    }
}