use std::borrow::Cow;
use super::{
At, Checker,
blocks::check_plain,
divide::{extent, extents},
short,
};
use crate::{
Finding, Severity,
grammar::{Block as Rule, States},
markdown::{Block, BlockKind},
};
pub(super) type Docs<'a> = Vec<Cow<'a, Block>>;
#[derive(Debug, Clone, Copy)]
pub(super) struct Savepoint {
findings: usize,
captures: usize,
repairs: usize,
gates: usize,
}
impl Checker<'_> {
pub(super) fn save(&self) -> Savepoint {
Savepoint {
findings: self.out.findings.len(),
captures: self.out.captures.len(),
repairs: self.out.repairs.len(),
gates: self.out.gates.len(),
}
}
pub(super) fn failed(&self, at: &Savepoint) -> bool {
self.out.findings[at.findings..]
.iter()
.any(|f| f.severity == Severity::Error)
}
pub(super) fn rollback(&mut self, at: &Savepoint) -> Vec<Finding> {
self.out.captures.truncate(at.captures);
self.out.repairs.truncate(at.repairs);
self.out.gates.truncate(at.gates);
self.out.findings.split_off(at.findings)
}
}
fn admitted<'r>(rule: &'r Rule, states: &States) -> impl Iterator<Item = &'r Rule> {
rule.blocks
.as_deref()
.unwrap_or(&[])
.iter()
.filter(move |b| b.common.only.admits(states))
}
fn matches_rule(block: &Block, rule: &Rule, states: &States) -> bool {
opens(rule, states) == Some(block.kind)
}
fn opens(rule: &Rule, states: &States) -> Option<BlockKind> {
if rule.kind != BlockKind::Group {
return Some(rule.kind);
}
admitted(rule, states)
.next()
.and_then(|first| opens(first, states))
}
fn opened(rules: &[&Rule], states: &States) -> Vec<BlockKind> {
rules.iter().filter_map(|r| opens(r, states)).collect()
}
fn joins(block: &Block, rule: &Rule, states: &States) -> bool {
rule.kind == BlockKind::Group
&& admitted(rule, states)
.skip(1)
.any(|b| matches_rule(block, b, states))
}
fn reserve_for(rules: &[&Rule], ri: usize, docs: &Docs<'_>, di: usize, states: &States) -> usize {
let kind = rules[ri].kind;
let Some(next) = docs.get(di + 1) else {
return 0;
};
if !is_run(kind) || docs[di].kind != kind {
return 0;
}
let after = &rules[ri + 1..];
if after.iter().any(|r| matches_rule(next, r, states)) {
return 0;
}
after
.iter()
.find(|r| opens(r, states) == Some(kind) && joins(next, r, states))
.and_then(|g| admitted(g, states).next())
.map_or(0, |opening| opening.min.max(1))
}
fn firm(rule: &Rule) -> bool {
!rule.optional && !rule.repeat && rule.kind != BlockKind::Group
}
pub(super) fn is_run(kind: BlockKind) -> bool {
matches!(
kind,
BlockKind::Prose | BlockKind::BulletList | BlockKind::NumberedList
)
}
pub(super) fn check_blocks(
c: &mut Checker<'_>,
rules: Option<&[Rule]>,
docs: &[&Block],
ordered: bool,
section: Option<usize>,
at: usize,
) {
let prose_only;
let rules: Vec<&Rule> = match rules {
Some(r) => r
.iter()
.filter(|b| b.common.only.admits(c.states))
.collect(),
None => {
prose_only = default_prose();
vec![&prose_only]
}
};
let mut docs: Docs<'_> = docs.iter().map(|b| Cow::Borrowed(*b)).collect();
if ordered {
let consumed = match_sequence(c, &rules, &mut docs, 0, section, at, None);
for b in &docs[consumed.min(docs.len())..] {
unclaimed(c, b, matches!(b, Cow::Owned(_)));
}
} else {
match_free(c, &rules, &mut docs, section, at);
}
}
fn match_free(
c: &mut Checker<'_>,
rules: &[&Rule],
docs: &mut Docs<'_>,
section: Option<usize>,
at: usize,
) {
let mut used = vec![0usize; rules.len()];
let mut di = 0;
while di < docs.len() {
let free: Vec<usize> = (0..rules.len())
.filter(|&ri| {
matches_rule(&docs[di], rules[ri], c.states) && (used[ri] == 0 || rules[ri].repeat)
})
.collect();
let mut taken = None;
let mut refusal = None;
c.later = opened(rules, c.states);
for ri in &free {
match claim(c, docs, di, rules[*ri], section, false, false) {
Ok(0) => {}
Ok(n) => {
taken = Some((*ri, n));
break;
}
Err(dropped) => refusal = refusal.or(Some(dropped)),
}
}
let taken = taken.or_else(|| {
let dropped = refusal.take()?;
let ri = *free.first()?;
c.out.findings.extend(dropped);
Some((ri, 1))
});
match taken {
Some((ri, n)) => {
used[ri] += 1;
di += n;
}
None => {
unclaimed(c, &docs[di], matches!(docs[di], Cow::Owned(_)));
di += 1;
}
}
}
for (ri, rule) in rules.iter().enumerate() {
if used[ri] == 0 && !rule.optional {
let at_rule = at_block(&c.at, rule);
c.located(at_rule, |c| {
c.emit(
&rule.common,
Some(at),
"TLMD_152",
format!(
"{} block is missing",
rule.kind.spoken_in(&c.host.dialect())
),
);
});
}
}
}
fn unclaimed(c: &mut Checker<'_>, b: &Block, left_over: bool) {
let kind = b.kind.name_in(&c.host.dialect());
match b.units.first().filter(|_| left_over) {
Some(first) => c.error(
Some(first.line),
"TLMD_50",
format!(
"`{}` remains after the last {kind} position that takes the run",
short(&first.text)
),
),
_ => c.error(Some(b.line), "TLMD_39", format!("unexpected {kind} block")),
}
}
fn default_prose() -> Rule {
Rule {
common: Default::default(),
kind: BlockKind::Prose,
optional: false,
repeat: false,
min: 1,
max: None,
each: None,
sequence: None,
rules: None,
units_open: false,
lang: None,
fence: 3,
columns: None,
columns_open: false,
source_pattern: None,
inline_html: false,
blocks: None,
rule_id: None,
examples: Vec::new(),
example_list: false,
guidance: None,
pos: 1,
}
}
#[allow(clippy::too_many_arguments)]
fn match_sequence(
c: &mut Checker<'_>,
rules: &[&Rule],
docs: &mut Docs<'_>,
start: usize,
section: Option<usize>,
at: usize,
wraps: Option<BlockKind>,
) -> usize {
let group_opens: Vec<BlockKind> = rules
.iter()
.filter(|r| r.kind == BlockKind::Group)
.filter_map(|r| opens(r, c.states))
.collect();
let divides = |kind: BlockKind| {
let declared = rules.iter().filter(|r| r.kind == kind).count();
declared >= 2 || wraps == Some(kind) || group_opens.contains(&kind)
};
let mut di = start;
for (ri, rule) in rules.iter().enumerate() {
let mut count = 0;
let mut refusal: Option<Vec<Finding>> = None;
let mut answered = false;
let mut stray = false;
let later = opened(&rules[ri + 1..], c.states);
while di < docs.len() {
let fits = matches_rule(&docs[di], rule, c.states)
|| (joins(&docs[di], rule, c.states) && !later.contains(&docs[di].kind));
if !fits {
if later.contains(&docs[di].kind) {
if count == 0
&& !rule.optional
&& rule.kind != BlockKind::Group
&& let Some(j) = (di + 1..docs.len()).find(|&j| {
matches_rule(&docs[j], rule, c.states) && !later.contains(&docs[j].kind)
})
{
let block = docs.remove(j);
c.located(at_block(&c.at, rule), |c| {
c.emit(
&rule.common,
Some(block.line),
"TLMD_169",
format!(
"the {} block is out of place; the blocks are in the order the schema lists them",
block.kind.name_in(&c.host.dialect())
),
);
});
check_plain(c, &block, rule, section);
count += 1;
}
break;
}
if count == 0
&& docs[di + 1..]
.iter()
.any(|b| matches_rule(b, rule, c.states))
{
unclaimed(c, &docs[di], matches!(docs[di], Cow::Owned(_)));
di += 1;
continue;
}
stray = count == 0;
break;
}
c.later = later.clone();
c.reserve = reserve_for(rules, ri, docs, di, c.states);
let claimed = claim(c, docs, di, rule, section, firm(rule), divides(rule.kind));
c.reserve = 0;
match claimed {
Ok(0) => {
count += 1;
break;
}
Ok(n) => {
di += n;
count += 1;
}
Err(dropped) => {
if later.contains(&docs[di].kind) {
refusal = Some(dropped);
break;
}
c.out.findings.extend(dropped);
answered = true;
di += 1;
continue;
}
}
if !rule.repeat || rule.kind == BlockKind::Group {
break;
}
}
if count == 0 && !rule.optional && !answered {
match refusal {
Some(dropped) => c.out.findings.extend(dropped),
None => {
let (line, found) = match docs.get(di) {
Some(b) => (
Some(b.line),
format!("; found {} block", b.kind.spoken_in(&c.host.dialect())),
),
None => (Some(at), String::new()),
};
let at_rule = at_block(&c.at, rule);
c.located(at_rule, |c| {
c.emit(
&rule.common,
line,
if found.is_empty() {
"TLMD_152"
} else {
"TLMD_69"
},
format!(
"expected {} block{found}",
rule.kind.spoken_in(&c.host.dialect())
),
);
});
if stray {
di += 1;
}
}
}
}
}
di - start
}
fn at_block(at: &At, rule: &Rule) -> At {
let mut blocks = at.blocks.clone();
blocks.push(rule.pos);
At {
region: at.region.clone(),
blocks,
rule_id: rule.rule_id.clone(),
named: None,
line: Some(rule.common.line),
}
}
fn claim(
c: &mut Checker<'_>,
docs: &mut Docs<'_>,
di: usize,
rule: &Rule,
section: Option<usize>,
firm: bool,
divides: bool,
) -> Result<usize, Vec<Finding>> {
c.located(at_block(&c.at, rule), |c| {
claim_located(c, docs, di, rule, section, firm, divides)
})
}
fn claim_located(
c: &mut Checker<'_>,
docs: &mut Docs<'_>,
di: usize,
rule: &Rule,
section: Option<usize>,
firm: bool,
divides: bool,
) -> Result<usize, Vec<Finding>> {
if let Some(bounds) = extents(&docs[di], rule, divides, c.reserve) {
return extent(c, docs, di, rule, section, firm, bounds);
}
let at = c.save();
let blocks = (rule.kind == BlockKind::Group).then(|| docs.clone());
let n = check_block(c, docs, di, rule, section);
let owns = rule.kind == BlockKind::Group && n > 0;
if firm || owns || !c.failed(&at) {
return Ok(n);
}
let dropped = c.rollback(&at);
if let Some(blocks) = blocks {
*docs = blocks;
}
Err(dropped)
}
fn check_block(
c: &mut Checker<'_>,
docs: &mut Docs<'_>,
di: usize,
rule: &Rule,
section: Option<usize>,
) -> usize {
if rule.kind != BlockKind::Group {
check_plain(c, &docs[di], rule, section);
return 1;
}
let line = docs[di].line;
c.gate(&rule.common, line);
let inner: Vec<&Rule> = admitted(rule, c.states).collect();
let outer = c.group_rule.take();
c.group_rule = rule.rule_id.clone().or(outer.clone());
let later = std::mem::take(&mut c.later);
let mut reps = 0;
let mut pos = di;
while pos < docs.len() {
let opening = inner
.first()
.is_some_and(|first| matches_rule(&docs[pos], first, c.states));
let claimed = later.contains(&docs[pos].kind);
if !opening && (claimed || !joins(&docs[pos], rule, c.states)) {
break;
}
let at = c.save();
let blocks = docs.clone();
let start = docs[pos].line;
let first = start..=docs[pos].end_line;
let wraps = (rule.max.is_none_or(|max| reps + 1 < max)
&& inner.first().map(|r| r.kind) == inner.last().map(|r| r.kind))
.then(|| inner.first().map(|r| r.kind))
.flatten();
let n = match_sequence(c, &inner, docs, pos, section, start, wraps);
if n > 0 && !c.failed(&at) {
pos += n;
reps += 1;
continue;
}
let owned = opening
&& !c.out.findings[at.findings..].iter().any(|f| {
f.severity == Severity::Error && f.line.is_some_and(|l| first.contains(&l))
});
if n > 0 && (reps < rule.min || !claimed || owned) {
pos += n;
reps += 1;
continue;
}
c.rollback(&at);
*docs = blocks;
break;
}
c.later = later;
c.group_rule = outer;
super::blocks::check_count(c, rule, reps, line);
pos - di
}