use std::ops::Range;
use super::{
Capture, CheckOutput, DocView, Gate, Host, Repair, anchors, frontmatter, keys, limits, markers,
sections,
};
use crate::{
Finding, Locator,
grammar::{RuleCommon, Schema, Section, States, Text},
markdown::BlockKind,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct CheckOptions {
pub scaffold: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Occurrence {
pub section: usize,
pub line: usize,
pub level: u8,
pub text: String,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct At {
pub region: Option<String>,
pub blocks: Vec<usize>,
pub rule_id: Option<String>,
pub named: Option<String>,
pub line: Option<usize>,
}
impl At {
pub(crate) fn section(section: &Section) -> At {
At {
region: Some(format!("section \"{}\"", section.label())),
rule_id: section.rule_id.clone(),
line: Some(section.common.line),
..At::default()
}
}
pub(crate) fn capture(cap: &Capture, sections: &[&Section]) -> At {
let section = cap.section.and_then(|j| sections.get(j));
At {
region: section.map(|s| format!("section \"{}\"", s.label())),
rule_id: cap.rule_id.clone(),
line: section.map(|s| s.common.line),
..At::default()
}
}
pub(crate) fn locate(&self, locator: &Locator) -> Locator {
match self.render() {
Some(at) => locator.at(at),
None => locator.clone(),
}
}
pub(crate) fn render(&self) -> Option<String> {
if let Some(id) = &self.rule_id {
return Some(format!("rule `{id}`"));
}
if let Some(n) = &self.named {
return Some(n.clone());
}
let mut parts: Vec<String> = self.region.clone().into_iter().collect();
if !self.blocks.is_empty() {
let at: Vec<String> = self.blocks.iter().map(|b| b.to_string()).collect();
parts.push(format!("block {}", at.join(".")));
}
(!parts.is_empty()).then(|| parts.join(" "))
}
}
pub(crate) struct Checker<'a> {
pub doc: DocView<'a>,
pub schema: &'a Schema,
pub sections: Vec<&'a Section>,
pub parent_of: Vec<Option<usize>>,
pub states: &'a States,
pub host: &'a dyn Host,
pub options: CheckOptions,
pub locator: Locator,
pub out: CheckOutput,
pub group_rule: Option<String>,
pub at: At,
pub occurrences: Vec<Occurrence>,
pub repeats: Vec<(Range<usize>, Range<usize>)>,
pub later: Vec<BlockKind>,
pub reserve: usize,
pub lines_block: Option<usize>,
}
impl Checker<'_> {
pub fn span(&self, o: &Occurrence) -> Range<usize> {
self.span_from(o.line, o.level)
}
pub fn span_from(&self, line: usize, level: u8) -> Range<usize> {
let end = self
.doc
.body
.headings
.iter()
.find(|h| h.line > line && h.level <= level)
.map_or(usize::MAX, |h| h.line);
line..end
}
pub fn front_value(&self, key: &str) -> Option<String> {
self.doc
.frontmatter
.and_then(|f| f.get(key))
.and_then(|v| v.scalar_text())
}
pub fn text_of(&self, text: &Text) -> Option<String> {
match text {
Text::Literal(t) => Some(t.clone()),
Text::From(key) => self.front_value(key),
}
}
fn locator(&self) -> Locator {
self.at.locate(&self.locator)
}
pub fn located<R>(&mut self, at: At, f: impl FnOnce(&mut Self) -> R) -> R {
let held = std::mem::replace(&mut self.at, at);
let out = f(self);
self.at = held;
out
}
pub fn emit(
&mut self,
rule: &RuleCommon,
line: Option<usize>,
criterion: &str,
message: impl Into<String>,
) {
let locator = self.locator();
self.out.findings.push(
Finding::new(
self.doc.path.clone(),
line,
rule.severity,
criterion,
message,
)
.of_rule(rule.line)
.at(&locator),
);
}
pub fn unbound_is_own_fault(&self, key: &str) -> bool {
self.doc.frontmatter.is_some_and(|f| f.get(key).is_none())
&& !self
.schema
.core
.frontmatter
.iter()
.any(|(k, rule)| k == key && !rule.optional && rule.common.only.admits(self.states))
}
pub fn gate(&mut self, rule: &RuleCommon, line: usize) {
if rule.only.is_gated() {
self.out.gates.push(Gate {
line,
only: rule.only.clone(),
rule: rule.line,
});
}
}
pub fn repair_heading(&mut self, repair: Repair) {
self.out.repairs.retain(|r| r.line != repair.line);
self.out.repairs.push(repair);
}
pub fn error(&mut self, line: Option<usize>, criterion: &str, message: impl Into<String>) {
let locator = self.locator();
let finding = Finding::error(self.doc.path.clone(), line, criterion, message);
self.out
.findings
.push(finding.of_rule(self.at.line.unwrap_or(0)).at(&locator));
}
}
pub fn check_against(
doc: DocView<'_>,
schema: &Schema,
states: &States,
host: &dyn Host,
options: CheckOptions,
locator: Locator,
) -> CheckOutput {
let sections: Vec<&Section> = schema
.sections
.iter()
.filter(|s| s.common.only.admits(states))
.collect();
let parent_of = parents(§ions);
let mut c = Checker {
doc,
schema,
sections,
parent_of,
states,
host,
options,
locator,
out: CheckOutput::default(),
group_rule: None,
at: At::default(),
occurrences: Vec::new(),
repeats: Vec::new(),
later: Vec::new(),
reserve: 0,
lines_block: None,
};
frontmatter::check(&mut c);
sections::check(&mut c);
c.out.captures.sort_by_key(|cap| cap.line);
keys::check(&mut c);
anchors::check(&mut c);
limits::check(&mut c);
markers::check(&mut c);
c.host
.check(&c.doc, c.states, c.options, &c.locator, &mut c.out);
c.out
}
fn parents(sections: &[&Section]) -> Vec<Option<usize>> {
sections
.iter()
.map(|s| {
let p = s.parent.as_deref()?;
sections
.iter()
.position(|o| o.rule_id.as_deref() == Some(p))
})
.collect()
}
pub(crate) fn short(text: &str) -> String {
const MAX: usize = 60;
if text.chars().count() <= MAX {
return text.to_string();
}
let cut: String = text.chars().take(MAX - 1).collect();
format!("{cut}…")
}