use regex::Regex;
use std::sync::OnceLock;
static JS_PATTERN: OnceLock<Regex> = OnceLock::new();
static SEPARATOR_PATTERN: OnceLock<Regex> = OnceLock::new();
fn js_regex() -> &'static Regex {
JS_PATTERN.get_or_init(|| {
Regex::new(
r"(?i)(function\s*\(|eval\(|document\.|window\.|\.innerHTML|<script|javascript:)",
)
.expect("scorer JS regex literal compiles — change requires updating this expect")
})
}
fn separator_regex() -> &'static Regex {
SEPARATOR_PATTERN.get_or_init(|| {
Regex::new(r"(?m)^[-=_]{5,}$").expect(
"scorer separator regex literal compiles — change requires updating this expect",
)
})
}
pub fn score_text_impl(text: &str) -> f64 {
if text.is_empty() || text.len() < 10 {
return 0.0;
}
let length = text.len() as f64;
let total_chars = text.chars().count().max(1) as f64;
let mut penalties: f64 = 0.0;
let garbage = text
.chars()
.filter(|c| {
matches!(*c, '\u{FFFD}')
|| ('\u{E000}'..='\u{F8FF}').contains(c) || ('\u{F0000}'..='\u{FFFFD}').contains(c) || ('\u{100000}'..='\u{10FFFD}').contains(c) })
.count() as f64;
let garbage_ratio = garbage / total_chars;
if garbage_ratio > 0.30 {
penalties += 0.5;
} else if garbage_ratio > 0.10 {
penalties += garbage_ratio * 1.5;
}
let control_count = text
.chars()
.filter(|c| (*c as u32) < 32 && !matches!(*c, '\n' | '\r' | '\t'))
.count() as f64;
let control_ratio = control_count / total_chars;
if control_ratio > 0.05 {
penalties += 0.4;
} else if control_ratio > 0.01 {
penalties += control_ratio * 8.0;
}
let js_matches = js_regex().find_iter(text).count() as f64;
let js_ratio = js_matches / (length / 100.0).max(1.0);
if js_ratio > 0.10 {
penalties += 0.5;
} else if js_ratio > 0.03 {
penalties += js_ratio * 4.0;
}
let lines: Vec<&str> = text.split('\n').filter(|l| !l.trim().is_empty()).collect();
if !lines.is_empty() {
let avg_line_len = lines.iter().map(|l| l.len()).sum::<usize>() as f64 / lines.len() as f64;
if avg_line_len < 5.0 {
penalties += 0.4;
} else if avg_line_len < 15.0 {
penalties += 0.15;
}
let separators = separator_regex().find_iter(text).count() as f64;
let sep_ratio = separators / lines.len() as f64;
if sep_ratio > 0.3 {
penalties += 0.2;
}
} else {
penalties += 0.3;
}
let word_count = text.split_whitespace().count() as f64;
if word_count < 5.0 {
penalties += 0.3;
} else if word_count / length < 0.05 {
penalties += 0.1;
}
(1.0 - penalties).clamp(0.0, 1.0)
}