use low_expectations::ExpectationSuite;
use prosesmasher_domain_types::{CheckConfig, Document, Locale};
use serde_json::json;
use crate::check::Check;
#[derive(Debug)]
pub struct SentenceCaseCheck;
impl Check for SentenceCaseCheck {
fn id(&self) -> &'static str {
"sentence-case"
}
fn label(&self) -> &'static str {
"Sentence Case"
}
fn supported_locales(&self) -> Option<&'static [Locale]> {
Some(&[Locale::En, Locale::Es, Locale::Pt, Locale::Fr, Locale::Id])
}
fn run(&self, doc: &Document, config: &CheckConfig, suite: &mut ExpectationSuite) {
if !config.quality.heuristics.sentence_case.enabled {
return;
}
for (section_index, section) in doc.sections.iter().enumerate() {
let Some(heading) = §ion.heading else {
continue;
};
let words: Vec<&str> = heading.text.split_whitespace().collect();
let capitalized_non_first: usize = words
.iter()
.skip(1)
.filter(|w| is_title_cased(w))
.count();
let is_title_case = capitalized_non_first >= 3;
let col = format!("sentence-case-{}", heading.text);
let evidence = if is_title_case {
vec![json!({
"section_index": section_index,
"heading_level": heading.level,
"heading_text": heading.text,
"capitalized_non_first_words": capitalized_non_first,
"sentence_case_expected": true,
})]
} else {
Vec::new()
};
let _result = suite
.record_custom_values(
&col,
!is_title_case,
json!({
"rule": "sentence case",
"max_capitalized_non_first_words": 2,
}),
json!(heading.text),
&evidence,
)
.label("Sentence Case")
.checking(&format!("heading: \"{}\"", heading.text));
}
}
}
fn is_title_cased(word: &str) -> bool {
let mut chars = word.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_uppercase() {
return false;
}
chars.any(char::is_lowercase)
}
#[cfg(test)]
#[path = "sentence_case_tests.rs"]
mod tests;