use crate::engine::{CompiledPolicy, Hit};
use crate::input::Prepared;
use crate::views::NormView;
use crate::Config;
use super::sentence::{blocks, find_word, param_words, push_norm_hit, sentences};
pub const HANDLED: &[&str] = &["SLOP-O007", "SLOP-O008"];
const CALENDAR: &[&str] = &[
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
];
fn capitalized_items(slice: &str) -> usize {
let mut items = 0usize;
let mut in_item = false;
for token in slice.split_whitespace() {
let word = token.trim_matches(|c: char| !c.is_alphanumeric());
let capitalized = word.chars().next().is_some_and(char::is_uppercase)
&& word.len() > 1
&& !CALENDAR.contains(&word.to_ascii_lowercase().as_str());
if capitalized {
if !in_item {
items += 1;
in_item = true;
}
} else {
in_item = false;
}
if token.ends_with([',', ';', '.', ':']) {
in_item = false;
}
}
items
}
fn is_signoff(block: &str, valedictions: &[String]) -> bool {
let trimmed = block.trim();
if trimmed.is_empty() {
return true;
}
let lower = trimmed.to_ascii_lowercase();
if valedictions.iter().any(|v| lower.starts_with(v.as_str())) {
return true;
}
let words = trimmed.split_whitespace().count();
words <= 4 && !trimmed.ends_with(['.', '!', '?'])
}
pub fn evaluate(
cp: &CompiledPolicy,
prepared: &Prepared,
norm: &NormView,
config: &Config,
hits: &mut Vec<Hit>,
) {
let text = norm.text.as_str();
let src = prepared.text.as_str();
let lower = text.to_ascii_lowercase();
let blocks = blocks(norm);
if let Some(idx) = super::active(cp, config, "SLOP-O007") {
let rule = &cp.pkg.rules[idx];
let triggers: Vec<String> = rule
.params
.as_table()
.and_then(|t| t.get("triggers"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_ascii_lowercase()))
.collect()
})
.unwrap_or_default();
let after_triggers = param_words(rule, "triggers_after");
let min_items = super::param_i64(rule, "min_capitalized_items").unwrap_or(3) as usize;
for block in &blocks {
for sentence in sentences(text, block) {
let body = &text[sentence.clone()];
if body.contains(['"', '\u{201C}', '\u{201D}']) {
continue;
}
let sent_lower = &lower[sentence.clone()];
let after = triggers
.iter()
.filter_map(|t| find_word(sent_lower, t, 0).map(|p| p + t.len()))
.min()
.map(|at| capitalized_items(&body[at..]))
.unwrap_or(0);
let before = after_triggers
.iter()
.filter_map(|t| find_word(sent_lower, t, 0))
.min()
.map(|at| capitalized_items(&body[..at]))
.unwrap_or(0);
if after.max(before) >= min_items {
push_norm_hit(idx, norm, src, sentence.clone(), hits);
}
}
}
}
if let Some(idx) = super::active(cp, config, "SLOP-O008") {
let terms: Vec<String> = cp.pkg.rules[idx]
.terms
.iter()
.map(|t| t.to_ascii_lowercase())
.collect();
let valedictions: Vec<String> = cp
.pkg
.rule_by_id("SLOP-S004")
.map(|r| r.terms.iter().map(|t| t.to_ascii_lowercase()).collect())
.unwrap_or_default();
let last = blocks
.iter()
.rev()
.find(|b| !is_signoff(&text[(*b).clone()], &valedictions));
if let Some(last) = last {
let block_lower = &lower[last.clone()];
for term in &terms {
if let Some(pos) = find_word(block_lower, term, 0) {
let start = last.start + pos;
push_norm_hit(idx, norm, src, start..start + term.len(), hits);
break;
}
}
}
}
}