webpage_quality_analyzer 1.0.2

High-performance webpage quality analyzer with 115 comprehensive metrics - Rust library with WASM, C++, and Python bindings
Documentation
/// Centralized scoring utilities to reduce redundancy and improve consistency
use crate::utils::math::clamp_score;

/// Apply weighted scoring with bounds checking
pub fn apply_weighted_score(
    score: f32,
    weight: f32,
    total_score: &mut f32,
    total_weight: &mut f32,
) {
    if weight > 0.0 && score.is_finite() {
        let clamped_score = clamp_score(score);
        *total_score += clamped_score * weight;
        *total_weight += weight;
    }
}

/// Calculate final weighted score with proper bounds checking
pub fn finalize_weighted_score(total_score: f32, total_weight: f32, default_score: f32) -> f32 {
    if total_weight > 0.0 {
        clamp_score(total_score / total_weight)
    } else {
        clamp_score(default_score)
    }
}

/// Score value by range with interpolation
pub fn score_by_range(
    value: usize,
    optimal_range: Option<(usize, usize)>,
    default_ranges: &[(usize, usize, f32)],
) -> f32 {
    if let Some((min_len, max_len)) = optimal_range {
        match value {
            0 => 0.0,
            v if v < min_len => (v as f32 / min_len as f32) * 60.0,
            v if v >= min_len && v <= max_len => 100.0,
            v if v <= max_len + (max_len - min_len) / 2 => 80.0,
            _ => 50.0,
        }
    } else {
        // Use default ranges
        for (min, max, score) in default_ranges {
            if value >= *min && value <= *max {
                return *score;
            }
        }
        // Fallback for values outside all ranges
        30.0
    }
}

/// Score by percentage with smooth transitions
pub fn score_by_percentage(
    percentage: f32,
    excellent_threshold: f32,
    good_threshold: f32,
    fair_threshold: f32,
) -> f32 {
    match percentage {
        p if p >= excellent_threshold => 100.0,
        p if p >= good_threshold => {
            let range = excellent_threshold - good_threshold;
            if range > 0.0 {
                80.0 + ((p - good_threshold) / range) * 20.0
            } else {
                80.0
            }
        }
        p if p >= fair_threshold => {
            let range = good_threshold - fair_threshold;
            if range > 0.0 {
                60.0 + ((p - fair_threshold) / range) * 20.0
            } else {
                60.0
            }
        }
        p => (p / fair_threshold) * 60.0,
    }
}

/// Calculate length penalty for scores that depend on appropriate length ranges
pub fn calculate_length_penalty(value: usize, optimal_min: usize, optimal_max: usize) -> f32 {
    match value {
        0 => 0.0,
        v if v < optimal_min => (v as f32 / optimal_min as f32) * 60.0,
        v if v >= optimal_min && v <= optimal_max => 100.0,
        v if v <= optimal_max + (optimal_max - optimal_min) / 2 => 80.0,
        _ => 50.0,
    }
}

/// Calculate readability penalty for content that's too difficult or too simple
pub fn calculate_readability_penalty(fk_score: Option<f32>) -> f32 {
    match fk_score {
        Some(score) if score > 18.0 => (score - 18.0) * 2.0, // Too difficult
        Some(score) if score < 2.0 => (2.0 - score) * 3.0,   // Too simple
        _ => 0.0,                                            // Good readability or not available
    }
}

/// Convert Flesch-Kincaid score to percentage score using explicit thresholds
pub fn readability_fk_to_score(fk_score: f32) -> f32 {
    // Optimal readability is between 6-12 on Flesch-Kincaid scale
    // Lower FK score = better readability
    let readability_percentage = if fk_score <= 6.0 {
        100.0 // Excellent readability
    } else if fk_score <= 9.0 {
        90.0 - ((fk_score - 6.0) / 3.0) * 10.0 // Good readability
    } else if fk_score <= 12.0 {
        80.0 - ((fk_score - 9.0) / 3.0) * 20.0 // Fair readability
    } else {
        (60.0 - ((fk_score - 12.0) * 2.0)).max(20.0) // Poor readability
    };

    // Map percentage to score using explicit excellent/good/fair thresholds
    score_by_percentage(readability_percentage, 100.0, 80.0, 60.0)
}

/// Common scoring patterns for different metric types
pub struct ScoringPatterns;

impl ScoringPatterns {
    /// Score word count with configurable thresholds
    pub fn word_count_score(word_count: usize, min_words: Option<usize>) -> f32 {
        if let Some(min_words) = min_words {
            // Calculate percentage of minimum requirement met
            let percentage_met = (word_count as f32 / min_words as f32) * 100.0;

            match word_count {
                0 => 0.0,
                // Severe deficiency: less than 25% of minimum (e.g., <200 words for 800 min)
                count if count < min_words / 4 => {
                    (percentage_met / 25.0) * 5.0 // Max 5 points for severe deficiency
                }
                // Major deficiency: 25-50% of minimum (e.g., 200-400 words for 800 min)
                count if count < min_words / 2 => {
                    5.0 + ((percentage_met - 25.0) / 25.0) * 10.0 // 5-15 points
                }
                // Moderate deficiency: 50-75% of minimum (e.g., 400-600 words for 800 min)
                count if count < (min_words * 3) / 4 => {
                    15.0 + ((percentage_met - 50.0) / 25.0) * 20.0 // 15-35 points
                }
                // Minor deficiency: 75-100% of minimum (e.g., 600-800 words for 800 min)
                count if count < min_words => {
                    35.0 + ((percentage_met - 75.0) / 25.0) * 25.0 // 35-60 points
                }
                // Meets minimum requirements
                count if count >= min_words && count <= min_words * 5 => 100.0,
                count if count <= min_words * 10 => 85.0,
                _ => 65.0,
            }
        } else {
            // Default word count scoring
            match word_count {
                0 => 0.0,
                1..=50 => 20.0,
                51..=150 => 50.0,
                151..=300 => 75.0,
                301..=2000 => 100.0,
                2001..=5000 => 85.0,
                _ => 65.0,
            }
        }
    }

    /// Score image count
    pub fn image_count_score(image_count: usize) -> f32 {
        match image_count {
            0 => 30.0,       // No images might be okay for text content
            1..=3 => 80.0,   // Few images
            4..=10 => 100.0, // Good number of images
            11..=20 => 90.0, // Many images
            _ => 70.0,       // Too many images
        }
    }

    /// Score heading count
    pub fn heading_count_score(heading_count: usize) -> f32 {
        match heading_count {
            0 => 10.0,       // No headings
            1 => 40.0,       // Single heading
            2..=8 => 100.0,  // Good number of headings
            9..=15 => 75.0,  // Many headings
            16..=30 => 50.0, // Too many headings
            _ => 20.0,       // Way too many headings
        }
    }

    /// Score paragraph count
    pub fn paragraph_count_score(paragraph_count: usize) -> f32 {
        match paragraph_count {
            0 => 0.0,        // No paragraphs
            1 => 30.0,       // Single paragraph
            2 => 70.0,       // Two paragraphs
            3..=20 => 100.0, // Good number of paragraphs
            _ => 85.0,       // Many paragraphs
        }
    }
}

/// Simple range-based scoring (used by tests)
pub fn score_by_simple_range(value: usize, min: usize, max: usize) -> f32 {
    if value <= min {
        0.0
    } else if value >= max {
        100.0
    } else {
        ((value - min) as f32 / (max - min) as f32) * 100.0
    }
}

/// Inverse range-based scoring - higher values get lower scores
pub fn score_by_inverse_range(value: usize, min: usize, max: usize) -> f32 {
    if value <= min {
        100.0
    } else if value >= max {
        0.0
    } else {
        100.0 - ((value - min) as f32 / (max - min) as f32) * 100.0
    }
}

/// Target-based scoring - closer to target gets higher score
pub fn score_by_target(value: usize, target: usize) -> f32 {
    if value == target {
        100.0
    } else {
        let diff = if value > target {
            value - target
        } else {
            target - value
        };
        let max_diff = target.max(100 - target); // Assume reasonable max difference
        let score = 100.0 - (diff as f32 / max_diff as f32 * 100.0);
        score.max(0.0)
    }
}

/// Calculate broken link penalty
pub fn calculate_broken_link_penalty(broken_ratio: f32, total_links: usize) -> f32 {
    if total_links == 0 || broken_ratio == 0.0 {
        0.0
    } else {
        // More severe penalty for higher ratios and more links
        let base_penalty = broken_ratio * 50.0; // Up to 50% penalty for all broken
        let link_factor = (total_links as f32).ln().max(1.0); // Logarithmic scaling
        (base_penalty * link_factor).min(50.0) // Cap at 50% penalty
    }
}

/// Target-based scoring with range tolerance
pub fn score_by_target_range(value: usize, lower: usize, upper: usize) -> f32 {
    if value >= lower && value <= upper {
        100.0 // Perfect score within range
    } else if value < lower {
        let diff = lower - value;
        let max_diff = lower; // Assume reasonable max difference
        let penalty = (diff as f32 / max_diff as f32) * 12.5; // Up to 12.5% penalty per unit difference
        (100.0 - penalty).max(0.0)
    } else {
        let diff = value - upper;
        let max_diff = upper; // Assume reasonable max difference
        let penalty = (diff as f32 / max_diff as f32) * 10.0; // Up to 10% penalty per unit difference
        (100.0 - penalty).max(0.0)
    }
}

/// Calculate image alt text penalty
pub fn calculate_image_alt_penalty(alt_coverage: f32, image_count: usize) -> f32 {
    if image_count == 0 {
        0.0
    } else {
        let missing_ratio = 1.0 - alt_coverage;
        missing_ratio * (image_count as f32 * 5.0) // 5 points penalty per missing alt text
    }
}