use crate::models::models::{ContentStats, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashSet;
static WORD_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b\w+\b").expect("Valid regex pattern"));
static SENTENCE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[.!?]+\s+").expect("Valid regex pattern"));
static VOWEL_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[aeiouy]+").expect("Valid regex pattern"));
static SILENT_E: Lazy<Regex> = Lazy::new(|| Regex::new(r"e$").expect("Valid regex pattern"));
static CSS_CLASS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\.\w+[-\w]*\{[^}]*\}").expect("Valid regex pattern"));
static CSS_SELECTOR_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\.[a-zA-Z][\w-]*").expect("Valid regex pattern"));
static EXCESSIVE_WHITESPACE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\s{3,}").expect("Valid regex pattern"));
static TABS_AND_NEWLINES: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[\t\r\n]+").expect("Valid regex pattern"));
static NAVIGATION_WORDS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\b(?:move to sidebar|hide|toggle|menu|navigation|breadcrumb|search|login|logout|home|back|next|previous|skip to|jump to|main content|header|footer|sidebar)\b").expect("Valid regex pattern")
});
static REPEATED_PUNCTUATION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[.,;:!?]{2,}").expect("Valid regex pattern"));
pub fn clean_text(text: &str) -> String {
let mut cleaned = text.to_string();
cleaned = CSS_CLASS_REGEX.replace_all(&cleaned, "").to_string();
cleaned = CSS_SELECTOR_REGEX.replace_all(&cleaned, "").to_string();
cleaned = NAVIGATION_WORDS.replace_all(&cleaned, "").to_string();
cleaned = TABS_AND_NEWLINES.replace_all(&cleaned, " ").to_string();
cleaned = EXCESSIVE_WHITESPACE.replace_all(&cleaned, " ").to_string();
cleaned = REPEATED_PUNCTUATION.replace_all(&cleaned, ".").to_string();
cleaned = cleaned.replace(" ", " ");
cleaned = cleaned.replace("&", "&");
cleaned = cleaned.replace("<", "<");
cleaned = cleaned.replace(">", ">");
cleaned = cleaned.replace(""", "\"");
cleaned = cleaned.replace("–", "-");
cleaned = cleaned.replace("—", "--");
cleaned = cleaned.replace(" ", " ");
cleaned.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub fn calculate_noise_ratio(text: &str) -> f32 {
if text.is_empty() {
return 1.0;
}
let total_chars = text.len() as f32;
let noise_chars = text
.chars()
.filter(|c| c.is_whitespace() || c.is_ascii_punctuation() || c.is_control())
.count() as f32;
noise_chars / total_chars
}
pub fn is_navigation_content(text: &str) -> bool {
let text_lower = text.to_lowercase();
let nav_keywords = [
"move to sidebar",
"hide",
"toggle",
"menu",
"navigation",
"breadcrumb",
"search",
"login",
"logout",
"home",
"back",
"next",
"previous",
"skip to",
"jump to",
"main content",
"header",
"footer",
"sidebar",
"upload file",
"printable version",
"download",
"share",
"tools",
"actions",
"general",
"appearance",
];
let nav_word_count = nav_keywords
.iter()
.map(|keyword| text_lower.matches(keyword).count())
.sum::<usize>();
let total_words = text.split_whitespace().count();
if total_words == 0 {
return false;
}
(nav_word_count as f32 / total_words as f32) > 0.3
}
pub fn filter_content_chunks(
chunks: Vec<crate::models::models::ContentChunk>,
) -> Vec<crate::models::models::ContentChunk> {
chunks
.into_iter()
.filter(|chunk| is_quality_chunk(chunk))
.map(|mut chunk| {
chunk.text = clean_text(&chunk.text);
chunk
})
.filter(|chunk| !chunk.text.is_empty()) .collect()
}
fn is_quality_chunk(chunk: &crate::models::models::ContentChunk) -> bool {
use crate::models::models::ContentChunkType;
let min_length = match chunk.chunk_type {
ContentChunkType::Title => 5, ContentChunkType::Paragraph => 20, ContentChunkType::ListItem => 3, ContentChunkType::Quote => 10, ContentChunkType::Code => 3, ContentChunkType::Table => 2, ContentChunkType::Navigation => 0, ContentChunkType::Aside => 10, ContentChunkType::Unknown => 15, };
let text_len = chunk.text.len();
if text_len < min_length {
return false;
}
if chunk.confidence < 0.3 {
return false;
}
if calculate_noise_ratio(&chunk.text) > 0.7 {
return false;
}
if chunk.chunk_type == ContentChunkType::Navigation || is_navigation_content(&chunk.text) {
return false;
}
if is_repetitive_text(&chunk.text) {
return false;
}
if is_technical_artifact(&chunk.text) {
return false;
}
true
}
fn is_repetitive_text(text: &str) -> bool {
if text.len() < 10 {
return false;
}
let chars: Vec<char> = text.chars().collect();
let unique_chars: std::collections::HashSet<char> = chars.iter().cloned().collect();
if unique_chars.len() == 1 {
return true;
}
let words: Vec<&str> = text.split_whitespace().collect();
if words.len() > 3 {
let unique_words: std::collections::HashSet<&str> = words.iter().cloned().collect();
if (unique_words.len() as f32 / words.len() as f32) < 0.3 {
return true;
}
}
false
}
fn is_technical_artifact(text: &str) -> bool {
let text_lower = text.to_lowercase();
let technical_patterns = [
"mw-parser-output",
"hlist",
"display:inline",
"margin:0",
"padding:0",
"font-weight",
"content:",
"rgba(",
"px",
"em",
"rem",
"#",
"rgb(",
"class=",
"id=",
"data-",
"onclick",
"href=",
"src=",
"alt=",
];
let matches = technical_patterns
.iter()
.map(|pattern| text_lower.matches(pattern).count())
.sum::<usize>();
let word_count = text.split_whitespace().count();
if word_count == 0 {
return false;
}
(matches as f32 / word_count as f32) > 0.5
}
pub trait ContentProcessor {
fn process(&self, text: &str) -> Result<ContentStats>;
}
#[derive(Debug)]
pub struct DefaultContentProcessor {
stopwords: HashSet<String>,
min_word_length: usize,
}
impl Default for DefaultContentProcessor {
fn default() -> Self {
let stopwords = Self::create_stopwords_set();
Self {
stopwords,
min_word_length: 2,
}
}
}
impl DefaultContentProcessor {
pub fn new() -> Self {
Self::default()
}
fn create_stopwords_set() -> HashSet<String> {
let stopwords_list = [
"a",
"an",
"and",
"are",
"as",
"at",
"be",
"been",
"by",
"for",
"from",
"has",
"he",
"in",
"is",
"it",
"its",
"of",
"on",
"that",
"the",
"to",
"was",
"will",
"with",
"the",
"this",
"but",
"they",
"have",
"had",
"what",
"said",
"each",
"which",
"she",
"do",
"how",
"their",
"if",
"up",
"out",
"many",
"then",
"them",
"these",
"so",
"some",
"her",
"would",
"make",
"like",
"into",
"him",
"time",
"two",
"more",
"go",
"no",
"way",
"could",
"my",
"than",
"first",
"been",
"call",
"who",
"oil",
"sit",
"now",
"find",
"down",
"day",
"did",
"get",
"come",
"made",
"may",
"part",
"over",
"new",
"sound",
"take",
"only",
"little",
"work",
"know",
"place",
"year",
"live",
"me",
"back",
"give",
"most",
"very",
"after",
"thing",
"our",
"just",
"name",
"good",
"sentence",
"man",
"think",
"say",
"great",
"where",
"help",
"through",
"much",
"before",
"line",
"right",
"too",
"mean",
"old",
"any",
"same",
"tell",
"boy",
"follow",
"came",
"want",
"show",
"also",
"around",
"form",
"three",
"small",
"set",
"put",
"end",
"why",
"again",
"turn",
"here",
"off",
"went",
"old",
"number",
"great",
"tell",
"men",
"say",
"small",
"every",
"found",
"still",
"between",
"mane",
"should",
"home",
"big",
"give",
"air",
"line",
"set",
"own",
"under",
"read",
"last",
"never",
"us",
"left",
"end",
"along",
"while",
"might",
"next",
"sound",
"below",
"saw",
"something",
"thought",
"both",
"few",
"those",
"always",
"looked",
"show",
"large",
"often",
"together",
"asked",
"house",
"don",
"world",
"going",
"want",
"school",
"important",
"until",
"form",
"food",
"keep",
"children",
"feet",
"land",
"side",
"without",
"boy",
"once",
"animal",
"life",
"enough",
"took",
"sometimes",
"four",
"head",
"above",
"kind",
"began",
"almost",
"live",
"page",
"got",
"earth",
"need",
"far",
"hand",
"high",
"year",
"mother",
"light",
"country",
"father",
"let",
"night",
"picture",
"being",
"study",
"second",
"soon",
"story",
"since",
"white",
"ever",
"paper",
"hard",
"near",
"sentence",
"better",
"best",
"across",
"during",
"today",
"however",
"sure",
"knew",
"it's",
"try",
"told",
"young",
"sun",
"thing",
"whole",
"hear",
"example",
"heard",
"several",
"change",
"answer",
"room",
"sea",
"against",
"top",
"turned",
"learn",
"point",
"city",
"play",
"toward",
"five",
"himself",
"usually",
"money",
"seen",
"didn't",
"car",
"morning",
"i'm",
"body",
"upon",
"family",
"later",
"turn",
"move",
"face",
"door",
"cut",
"done",
"group",
"true",
"leave",
"color",
"red",
"friend",
"pretty",
"eat",
"front",
"feel",
"fact",
"hand",
"week",
"eye",
"been",
"word",
"final",
"gave",
"green",
"oh",
"quick",
"develop",
"talk",
"sleep",
"warm",
"free",
"minute",
"strong",
"special",
"mind",
"behind",
"clear",
"tail",
"produce",
"state",
"fact",
"street",
"inch",
"lot",
"nothing",
"course",
"stay",
"wheel",
"full",
"force",
"blue",
"object",
"decide",
"surface",
"moon",
"island",
"foot",
"yet",
"busy",
"test",
"record",
"boat",
"common",
"gold",
"possible",
"plane",
"age",
"dry",
"wonder",
"laugh",
"thousands",
"ago",
"ran",
"check",
"game",
"shape",
"yes",
"hot",
"miss",
"brought",
"heat",
"snow",
"bed",
"bring",
"sit",
"perhaps",
"fill",
"east",
"weight",
"language",
"among",
];
stopwords_list.iter().map(|s| s.to_string()).collect()
}
fn tokenize_words(&self, text: &str) -> Vec<String> {
#[cfg(feature = "nlp")]
{
use unicode_segmentation::UnicodeSegmentation;
text.split_word_bounds()
.filter(|word| {
word.chars().any(char::is_alphabetic) && word.len() >= self.min_word_length
})
.map(|word| word.to_lowercase())
.collect()
}
#[cfg(not(feature = "nlp"))]
{
WORD_REGEX
.find_iter(text)
.map(|m| m.as_str().to_lowercase())
.filter(|word| word.len() >= self.min_word_length)
.collect()
}
}
fn split_sentences(&self, text: &str) -> Vec<String> {
if text.contains('.') || text.contains('!') || text.contains('?') {
let sentences: Vec<String> = SENTENCE_REGEX
.split(text)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s.len() > 2) .collect();
if !sentences.is_empty() {
return sentences;
}
}
if text.contains('\n') {
let newline_sentences: Vec<String> = text
.split('\n')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s.len() > 3) .collect();
if newline_sentences.len() > 1 {
return newline_sentences;
}
}
Vec::new()
}
fn count_paragraphs(&self, text: &str) -> usize {
let trimmed_text = text.trim();
if trimmed_text.is_empty() || trimmed_text.len() < 10 {
return 0;
}
let normalized_text = text.replace("\r\n", "\n");
let double_newline_count = normalized_text
.split("\n\n")
.filter(|p| p.trim().len() > 10) .count();
if double_newline_count > 0 {
return double_newline_count;
}
let single_newline_count = normalized_text
.split('\n')
.filter(|p| p.trim().len() > 20) .count();
if single_newline_count > 0 {
return single_newline_count;
}
let sentences = self.split_sentences(text);
if sentences.is_empty() {
return 0; }
let substantial_sentences: Vec<&String> = sentences
.iter()
.filter(|s| s.trim().len() > 15) .collect();
if substantial_sentences.len() >= 2 {
return substantial_sentences.len();
}
let estimated_from_sentences = (sentences.len() as f32 / 2.5).ceil() as usize;
estimated_from_sentences.max(1)
}
#[cfg(test)]
pub fn debug_paragraph_counting(&self, text: &str) -> (usize, String) {
let result = self.count_paragraphs(text);
let debug_info = format!(
"Input length: {}, trimmed length: {}, sentences: {}, result: {}",
text.len(),
text.trim().len(),
self.split_sentences(text).len(),
result
);
(result, debug_info)
}
fn calculate_flesch_kincaid(&self, words: &[String], sentences: &[String]) -> Option<f32> {
if sentences.is_empty() || words.is_empty() {
return None;
}
let total_words = words.len() as f32;
let total_sentences = sentences.len() as f32;
let total_syllables = words.iter().map(|w| self.count_syllables(w)).sum::<usize>() as f32;
let score =
0.39 * (total_words / total_sentences) + 11.8 * (total_syllables / total_words) - 15.59;
Some(score.max(0.0).min(20.0)) }
fn count_syllables(&self, word: &str) -> usize {
if word.trim().is_empty() {
return 0; }
let word_lower = word.to_lowercase();
let vowel_groups = VOWEL_REGEX.find_iter(&word_lower).count();
let mut syllables = vowel_groups;
if SILENT_E.is_match(&word_lower) && syllables > 1 {
syllables -= 1;
}
syllables.max(1) }
fn calculate_content_density(&self, text: &str, words: &[String]) -> f32 {
if text.is_empty() {
return 0.0;
}
let total_chars = text.chars().count() as f32;
let word_chars: usize = words.iter().map(|w| w.chars().count()).sum();
word_chars as f32 / total_chars
}
fn estimate_reading_time(&self, word_count: usize) -> f32 {
word_count as f32 / 225.0
}
fn count_unique_words(&self, words: &[String]) -> usize {
let unique_words: HashSet<_> = words.iter().collect();
unique_words.len()
}
fn count_stopwords(&self, words: &[String]) -> usize {
words
.iter()
.filter(|word| self.stopwords.contains(*word))
.count()
}
fn calculate_avg_word_length(&self, words: &[String]) -> f32 {
if words.is_empty() {
return 0.0;
}
let total_length: usize = words.iter().map(|w| w.len()).sum();
total_length as f32 / words.len() as f32
}
fn calculate_avg_sentence_length(&self, words: &[String], sentences: &[String]) -> f32 {
if sentences.is_empty() {
return 0.0;
}
words.len() as f32 / sentences.len() as f32
}
}
impl ContentProcessor for DefaultContentProcessor {
fn process(&self, text: &str) -> Result<ContentStats> {
if text.trim().is_empty() {
return Ok(ContentStats::default());
}
let words = self.tokenize_words(text);
let sentences = self.split_sentences(text);
let paragraph_count = self.count_paragraphs(text);
let word_count = words.len();
let sentence_count = sentences.len();
let unique_words = self.count_unique_words(&words);
let stopword_count = self.count_stopwords(&words);
let avg_sentence_length = self.calculate_avg_sentence_length(&words, &sentences);
let avg_word_length = self.calculate_avg_word_length(&words);
let reading_time_minutes = self.estimate_reading_time(word_count);
let readability_fk_score = self.calculate_flesch_kincaid(&words, &sentences);
let content_density = self.calculate_content_density(text, &words);
let lexical_diversity = if word_count > 0 {
unique_words as f32 / word_count as f32
} else {
0.0
};
let total_syllables = words.iter().map(|w| self.count_syllables(w)).sum();
let complex_words = words
.iter()
.filter(|w| self.count_syllables(w) >= 3)
.count();
Ok(ContentStats {
word_count,
sentence_count,
paragraph_count,
unique_words,
stopword_count,
avg_sentence_length,
avg_word_length,
reading_time_minutes,
readability_fk_score,
readability_gunning_fog: None, lexical_diversity,
content_density,
language_confidence: None, syllable_count: total_syllables,
complex_word_count: complex_words,
})
}
}
#[cfg(feature = "nlp")]
pub struct NlpContentProcessor {
base_processor: DefaultContentProcessor,
}
#[cfg(feature = "nlp")]
impl NlpContentProcessor {
pub fn new() -> Self {
Self {
base_processor: DefaultContentProcessor::new(),
}
}
fn detect_language(&self, text: &str) -> Option<(String, f32)> {
use whatlang::{detect, Lang};
if let Some(info) = detect(text) {
let lang_code = match info.lang() {
Lang::Eng => "en",
Lang::Spa => "es",
Lang::Fra => "fr",
Lang::Deu => "de",
Lang::Ita => "it",
Lang::Por => "pt",
Lang::Rus => "ru",
Lang::Jpn => "ja",
Lang::Kor => "ko",
Lang::Cmn => "zh",
_ => "unknown",
};
Some((lang_code.to_string(), info.confidence() as f32))
} else {
None
}
}
fn calculate_sentiment_score(&self, text: &str) -> f32 {
let positive_words = [
"good",
"great",
"excellent",
"amazing",
"wonderful",
"fantastic",
"best",
"love",
"perfect",
"beautiful",
"awesome",
"brilliant",
];
let negative_words = [
"bad",
"terrible",
"awful",
"horrible",
"worst",
"hate",
"disgusting",
"disappointing",
"poor",
"useless",
"broken",
];
let text_lower = text.to_lowercase();
let words: Vec<&str> = text_lower.split_whitespace().collect();
let total_words = words.len() as f32;
if total_words == 0.0 {
return 0.0;
}
let positive_count = words
.iter()
.filter(|word| positive_words.contains(word))
.count() as f32;
let negative_count = words
.iter()
.filter(|word| negative_words.contains(word))
.count() as f32;
(positive_count - negative_count) / total_words.max(1.0)
}
fn calculate_nlp_quality_score(&self, stats: &ContentStats) -> f32 {
let mut quality_score: f32 = 50.0;
if let Some(fk_score) = stats.readability_fk_score {
if fk_score >= 6.0 && fk_score <= 10.0 {
quality_score += 10.0; } else if fk_score > 15.0 || fk_score < 3.0 {
quality_score -= 15.0; }
}
if stats.lexical_diversity > 0.5 {
quality_score += 15.0;
} else if stats.lexical_diversity < 0.3 {
quality_score -= 10.0;
}
if stats.content_density > 0.6 {
quality_score += 10.0;
} else if stats.content_density < 0.4 {
quality_score -= 5.0;
}
if stats.word_count >= 300 && stats.word_count <= 2000 {
quality_score += 5.0;
} else if stats.word_count < 100 {
quality_score -= 20.0;
}
quality_score.max(0.0).min(100.0)
}
}
#[cfg(feature = "nlp")]
impl ContentProcessor for NlpContentProcessor {
fn process(&self, text: &str) -> Result<ContentStats> {
let mut stats = self.base_processor.process(text)?;
if let Some((language, confidence)) = self.detect_language(text) {
stats.language_confidence = Some(confidence);
}
let sentiment_score = self.calculate_sentiment_score(text);
if let Some(fk_score) = stats.readability_fk_score {
stats.readability_fk_score = Some(fk_score + sentiment_score);
}
Ok(stats)
}
}
pub fn create_content_processor() -> Box<dyn ContentProcessor> {
#[cfg(feature = "nlp")]
{
Box::new(NlpContentProcessor::new())
}
#[cfg(not(feature = "nlp"))]
{
Box::new(DefaultContentProcessor::new())
}
}
pub fn create_basic_content_processor() -> DefaultContentProcessor {
DefaultContentProcessor::new()
}
#[cfg(feature = "nlp")]
pub fn create_nlp_content_processor() -> NlpContentProcessor {
NlpContentProcessor::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_content_processor_creation() {
let _processor = DefaultContentProcessor::new();
assert!(true);
}
#[test]
fn test_content_processor_empty_text() {
let processor = DefaultContentProcessor::new();
let result = processor.process("");
assert!(result.is_ok());
let stats = result.unwrap();
assert_eq!(stats.word_count, 0);
assert_eq!(stats.sentence_count, 0);
assert_eq!(stats.paragraph_count, 0);
assert_eq!(stats.content_density, 0.0);
assert!(stats.readability_fk_score.is_none());
}
#[test]
fn test_content_processor_simple_text() {
let processor = DefaultContentProcessor::new();
let text = "This is a simple test sentence. It has two sentences total.";
let result = processor.process(text);
assert!(result.is_ok());
let stats = result.unwrap();
assert_eq!(stats.word_count, 10); assert_eq!(stats.sentence_count, 2);
assert!(stats.paragraph_count >= 1);
assert!(stats.content_density > 0.0);
assert!(stats.readability_fk_score.is_some());
}
#[test]
fn test_content_processor_multiple_paragraphs() {
let processor = DefaultContentProcessor::new();
let text = "First paragraph with some content.\n\nSecond paragraph with more content.\n\nThird paragraph.";
let result = processor.process(text);
assert!(result.is_ok());
let stats = result.unwrap();
assert!(stats.word_count > 10);
assert!(stats.sentence_count >= 3);
assert!(stats.paragraph_count >= 2); }
#[test]
fn test_word_split() {
let processor = DefaultContentProcessor::new();
let words = processor.tokenize_words("Hello world, this is a test!");
assert_eq!(words.len(), 5); assert_eq!(words[0], "hello");
assert_eq!(words[1], "world");
let words = processor.tokenize_words("Don't you think it's working?");
assert!(words.len() >= 5);
let words = processor.tokenize_words("");
assert_eq!(words.len(), 0);
}
#[test]
fn test_sentence_split() {
let processor = DefaultContentProcessor::new();
let sentences =
processor.split_sentences("First sentence. Second sentence! Third sentence?");
assert_eq!(sentences.len(), 3);
let sentences = processor.split_sentences("Dr. Smith went to the U.S.A. He was happy.");
assert!(sentences.len() >= 1);
let sentences = processor.split_sentences("");
assert_eq!(sentences.len(), 0);
}
#[test]
fn test_paragraph_counting() {
let processor = DefaultContentProcessor::new();
let count = processor.count_paragraphs("This is a single paragraph without line breaks.");
assert_eq!(count, 1);
let count =
processor.count_paragraphs("First paragraph.\n\nSecond paragraph.\n\nThird paragraph.");
assert!(count >= 2);
let count = processor.count_paragraphs("");
assert_eq!(count, 0);
let count = processor.count_paragraphs(" \n \n ");
assert_eq!(count, 0);
}
#[test]
fn test_syllable_counting() {
let processor = DefaultContentProcessor::new();
assert_eq!(processor.count_syllables("cat"), 1);
assert_eq!(processor.count_syllables("dog"), 1);
assert!(processor.count_syllables("computer") >= 2);
assert!(processor.count_syllables("university") >= 3);
assert_eq!(processor.count_syllables(""), 0);
assert_eq!(processor.count_syllables("a"), 1);
assert_eq!(processor.count_syllables("I"), 1);
}
#[test]
fn test_flesch_kincaid_calculation() {
let processor = DefaultContentProcessor::new();
let words = vec![
"This".to_string(),
"is".to_string(),
"simple".to_string(),
"text".to_string(),
];
let sentences = vec!["This is simple text.".to_string()];
let score = processor.calculate_flesch_kincaid(&words, &sentences);
assert!(score.is_some());
let score = score.unwrap();
assert!(score >= 0.0 && score <= 20.0);
let score = processor.calculate_flesch_kincaid(&[], &[]);
assert!(score.is_none());
}
#[test]
fn test_content_density_calculation() {
let processor = DefaultContentProcessor::new();
let words = vec![
"meaningful".to_string(),
"content".to_string(),
"here".to_string(),
];
let density = processor.calculate_content_density("meaningful content here", &words);
assert!(density > 0.0);
assert!(density <= 1.0);
let density = processor.calculate_content_density("", &[]);
assert_eq!(density, 0.0);
}
#[test]
fn test_stopword_ratio_calculation() {
let processor = DefaultContentProcessor::new();
let words = vec![
"the".to_string(),
"quick".to_string(),
"brown".to_string(),
"fox".to_string(),
"jumps".to_string(),
"over".to_string(),
"the".to_string(),
"lazy".to_string(),
"dog".to_string(),
];
let stopword_count = processor.count_stopwords(&words);
let ratio = if words.is_empty() {
0.0
} else {
stopword_count as f32 / words.len() as f32
};
assert!(ratio >= 0.0 && ratio <= 1.0);
let _stopword_count = processor.count_stopwords(&[]);
let ratio = 0.0; assert_eq!(ratio, 0.0);
}
#[test]
fn test_unique_word_ratio_calculation() {
let processor = DefaultContentProcessor::new();
let words = vec![
"test".to_string(),
"word".to_string(),
"test".to_string(),
"another".to_string(),
"word".to_string(),
"unique".to_string(),
];
let unique_count = processor.count_unique_words(&words);
let ratio = if words.is_empty() {
0.0
} else {
unique_count as f32 / words.len() as f32
};
assert!(ratio >= 0.0 && ratio <= 1.0);
assert!(ratio < 1.0);
let words = vec!["one".to_string(), "two".to_string(), "three".to_string()];
let unique_count = processor.count_unique_words(&words);
let ratio = if words.is_empty() {
0.0
} else {
unique_count as f32 / words.len() as f32
};
assert_eq!(ratio, 1.0);
let _unique_count = processor.count_unique_words(&[]);
let ratio = 0.0; assert_eq!(ratio, 0.0);
}
#[test]
fn test_reading_time_estimation() {
let processor = DefaultContentProcessor::new();
let word_count = 250;
let time = processor.estimate_reading_time(word_count);
assert!(time >= 0.9 && time <= 1.2);
let time = processor.estimate_reading_time(0);
assert_eq!(time, 0.0);
let word_count = 2;
let time = processor.estimate_reading_time(word_count);
assert!(time > 0.0 && time < 1.0);
}
#[test]
fn test_content_processor_comprehensive() {
let processor = DefaultContentProcessor::new();
let text = r#"
This is a comprehensive test of the content processor functionality.
It includes multiple sentences with varying complexity levels.
The second paragraph contains different types of words, including
technical terms like "processor" and "functionality". We want to
test the readability scoring and various content metrics.
Finally, this third paragraph ensures we have enough content to
properly test paragraph counting, word counting, and sentence analysis.
"#;
let result = processor.process(text);
assert!(result.is_ok());
let stats = result.unwrap();
assert!(stats.word_count > 50);
assert!(stats.sentence_count >= 5); assert!(stats.paragraph_count >= 1); assert!(stats.content_density > 0.0);
assert!(stats.readability_fk_score.is_some());
let stopword_ratio = stats.stopword_count as f32 / stats.word_count as f32;
let unique_word_ratio = stats.unique_words as f32 / stats.word_count as f32;
assert!(stopword_ratio > 0.0 && stopword_ratio < 1.0);
assert!(unique_word_ratio > 0.0 && unique_word_ratio <= 1.0);
assert!(stats.reading_time_minutes > 0.0);
}
#[test]
fn test_debug_paragraph_counting() {
let processor = DefaultContentProcessor::new();
let text = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph.";
let (count, debug_info) = processor.debug_paragraph_counting(text);
assert!(count >= 2);
assert!(debug_info.contains("Input length:"));
assert!(debug_info.contains("result:"));
}
#[test]
fn test_create_content_processor() {
let processor = create_content_processor();
let result = processor.process("Test content");
assert!(result.is_ok());
}
#[cfg(feature = "nlp")]
#[test]
fn test_create_nlp_content_processor() {
let processor = create_nlp_content_processor();
let result = processor.process("Test content");
assert!(result.is_ok());
}
}