use super::{
Checker,
blocks::{check_unit, noun, reads, unit_noun},
short,
};
use crate::{
grammar::{Block as Rule, RuleCommon, Shape, Unit as UnitRule, squash},
markdown::{Block, BlockDetail, Unit},
text,
};
fn content_lines(block: &Block) -> Vec<(usize, &str)> {
let first = match block.detail {
BlockDetail::Code { fence, .. } if fence > 0 => block.line + 1,
_ => block.line,
};
match block.units.first() {
Some(u) => u
.text
.lines()
.enumerate()
.map(|(i, text)| (first + i, text.trim_start()))
.filter(|(_, text)| !text.trim().is_empty())
.collect(),
None => Vec::new(),
}
}
pub(super) fn check_lines(
c: &mut Checker<'_>,
block: &Block,
unit_rule: &UnitRule,
rule: &Rule,
section: Option<usize>,
) {
let Some(rules) = &unit_rule.lines else {
return;
};
let units: Vec<Unit> = content_lines(block)
.into_iter()
.map(|(line, text)| Unit::leaf(text.to_string(), line, line))
.collect();
let held = c.lines_block.replace(block.line);
let taken = apply_rules(c, &units, rules, rule, section, block.line, "lines");
c.lines_block = held;
if !unit_rule.lines_open {
untaken(
c,
&units,
&taken,
rules,
&unit_rule.common,
("the fence", "line"),
"lines-open",
);
}
}
fn apply_rules(
c: &mut Checker<'_>,
units: &[Unit],
rules: &[UnitRule],
rule: &Rule,
section: Option<usize>,
at: usize,
noun: &str,
) -> Vec<bool> {
let production: Vec<Option<usize>> = units
.iter()
.map(|u| {
rules
.iter()
.position(|r| r.is_production() && satisfies_shape(c, u, r))
})
.collect();
let mut taken = vec![false; units.len()];
for (ri, guarded) in rules.iter().enumerate() {
let selected: Vec<usize> = (0..units.len())
.filter(|&i| match &guarded.select {
Some(s) => s.is_match(&squash(&units[i].text)),
None if guarded.has_shape() => production[i] == Some(ri),
None => true,
})
.collect();
let n = selected.len();
let what = match (&guarded.select, &guarded.shape, &guarded.format) {
(Some(s), _, _) | (None, Some(Shape::Pattern(s)), _) => {
format!("the {noun} matching `{}`", s.as_str())
}
(None, Some(Shape::Template(t)), _) => {
format!("the {noun} of the template `{}`", t.source)
}
(None, Some(shape @ Shape::Text(_)), _) => {
format!("the {noun} reading `{}`", shape.label())
}
(None, None, Some(name)) => format!("the {noun} of the format `{name}`"),
(None, None, None) => format!("the block's {noun}"),
};
if n < guarded.min {
c.emit(
&guarded.common,
Some(at),
"TLMD_93",
format!(
"{what}: at least {} {} needed; there {} {n}",
guarded.min,
text::is_are(guarded.min),
text::is_are(n)
),
);
}
if let Some(max) = guarded.max
&& n > max
{
let line = selected.get(max).map_or(at, |&i| units[i].line);
c.emit(
&guarded.common,
Some(line),
"TLMD_159",
format!(
"{what}: at most {max} {} allowed; there {} {n}",
text::is_are(max),
text::is_are(n)
),
);
}
if guarded.is_counter() {
continue;
}
for i in selected {
taken[i] = true;
check_unit(c, &units[i], guarded, rule, section);
}
}
taken
}
fn satisfies_shape(c: &Checker<'_>, unit: &Unit, rule: &UnitRule) -> bool {
rule.fits(&c.schema.core.formats, &squash(&unit.text), |k| {
c.front_value(k)
})
}
fn untaken(
c: &mut Checker<'_>,
units: &[Unit],
taken: &[bool],
rules: &[UnitRule],
common: &RuleCommon,
(whose, unit_is): (&str, &str),
open: &str,
) {
for (unit, seen) in units.iter().zip(taken) {
if *seen {
continue;
}
let text = squash(&unit.text);
let best = rules
.iter()
.filter(|r| r.is_production())
.fold(None::<(&UnitRule, usize)>, |best, r| {
let n = reach(c, r, &text);
match best {
Some((_, m)) if m >= n => best,
_ => Some((r, n)),
}
})
.filter(|(_, n)| *n > 0)
.map(|(r, _)| r);
match best {
Some(r) => {
let reason = shape_failure(c, r, &text, unit_is);
c.emit(
&r.common,
Some(unit.line),
"TLMD_57",
format!(
"`{}` matches no rule of {whose}; the nearest {reason}",
short(&text)
),
);
}
None => c.emit(
common,
Some(unit.line),
if open == "lines-open" {
"TLMD_87"
} else {
"TLMD_53"
},
format!(
"`{}` matches no rule of {whose}; declare it or set `{open}`",
short(&text)
),
),
}
}
}
pub(super) fn reach(c: &Checker<'_>, rule: &UnitRule, text: &str) -> usize {
rule.reach(&c.schema.core.formats, text)
}
fn shape_failure(c: &Checker<'_>, rule: &UnitRule, text: &str, unit_is: &str) -> String {
match (&rule.shape, &rule.format) {
(Some(shape), _) => format!("{}{}", shape.failure(text), reads(unit_is, rule.example())),
(None, Some(name)) => match rule
.format_in(&c.schema.core.formats)
.map(|f| f.matching_as(text, unit_is))
{
Some(Err(reason)) => reason,
_ => format!("format `{name}`"),
},
(None, None) => String::new(),
}
}
pub(super) fn check_rules(
c: &mut Checker<'_>,
block: &Block,
rules: &[UnitRule],
rule: &Rule,
section: Option<usize>,
) {
let taken = apply_rules(
c,
&block.units,
rules,
rule,
section,
block.line,
noun(rule.kind),
);
if !rule.units_open {
untaken(
c,
&block.units,
&taken,
rules,
&rule.common,
("the block", unit_noun(c, rule.kind)),
"units-open",
);
}
}