use crate::finding::{Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{DocumentCheck, DocumentView};
use crate::shape::Shape;
use headwater_doc::body::Body;
pub const RULE: &str = "section.required.missing";
pub struct Sections {
owed: Vec<(String, Vec<String>)>,
}
impl Sections {
pub fn over(shape: &Shape) -> Self {
Sections {
owed: shape
.kinds
.iter()
.map(|kind| (kind.name.clone(), shape.required_sections(&kind.name)))
.filter(|(_, sections)| !sections.is_empty())
.collect(),
}
}
fn owed_by(&self, kind: &str) -> &[String] {
self.owed
.iter()
.find(|(known, _)| known == kind)
.map(|(_, sections)| sections.as_slice())
.unwrap_or_default()
}
}
impl DocumentCheck for Sections {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 1;
const NEEDS_BODY: bool = true;
fn instantiates(&self, kind: &str) -> bool {
!self.owed_by(kind).is_empty()
}
fn evaluate(&self, view: &DocumentView<'_>) -> Outcome {
let Some(body) = view.body() else {
return Outcome::Passed;
};
let kind = view.kind();
let owed = self.owed_by(kind).to_vec();
let findings = absent_from_body(body, &owed)
.into_iter()
.map(|section| Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: view.path().to_string(),
line: 0,
column: 0,
message: format!(
"`{kind}` requires the section `{section}`, and no heading of this document says so"
),
remediation: format!(
"add a `{section}` heading to {}, with the content the kind is for",
view.path()
),
patch: None,
})
.collect();
Outcome::failed(findings)
}
}
pub fn absent_from_body(body: &Body, required: &[String]) -> Vec<String> {
let headings: Vec<String> = body
.headings()
.filter(|heading| heading.quote_depth == 0)
.map(|heading| normalized(&heading.text()))
.collect();
required
.iter()
.filter(|section| !headings.contains(&normalized(section)))
.cloned()
.collect()
}
pub fn absent_from_source(source: &str, required: &[String]) -> Vec<String> {
let body = match headwater_doc::split::split(source) {
Ok(split) => headwater_doc::body::scan(source, split.body, split.body_offset),
Err(_) => headwater_doc::body::scan(source, source, 0),
};
absent_from_body(&body, required)
}
fn normalized(text: &str) -> String {
text.trim().to_lowercase()
}