use crate::diagnostic::Diagnostic;
use crate::rule::LintRule;
use mdwright_document::Document;
use mdwright_document::ListGroup;
pub struct ListTightnessFlipped;
impl LintRule for ListTightnessFlipped {
fn name(&self) -> &str {
"list-tightness-flipped"
}
fn description(&self) -> &str {
"list tightness from the tree disagrees with tightness from source bytes"
}
fn explain(&self) -> &str {
include_str!("explain/list_tightness_flipped.md")
}
fn is_default(&self) -> bool {
false
}
fn is_advisory(&self) -> bool {
true
}
fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
let source = doc.source();
for (group, tree_tight) in doc.list_tightness_view() {
let source_tight = source_view_tight(group, source);
if tree_tight == source_tight {
continue;
}
let message = if tree_tight {
"list reads as tight from parsed items but source has a blank line between items".to_owned()
} else {
"list reads as loose from parsed items but source has no blank line between items".to_owned()
};
let start = group.raw_range.start;
let local = 0..1;
if let Some(d) = Diagnostic::at(doc, start, local, message, None) {
out.push(d);
}
}
}
}
fn source_view_tight(group: &ListGroup, source: &str) -> bool {
let bytes = source.as_bytes();
for pair in group.items.windows(2) {
let [a, b] = pair else { continue };
let gap_start = a.raw_range.end.min(bytes.len());
let gap_end = b.raw_range.start.min(bytes.len());
let Some(gap) = bytes.get(gap_start..gap_end) else {
continue;
};
if has_blank_line(gap) {
return false;
}
}
true
}
fn has_blank_line(bytes: &[u8]) -> bool {
let mut i = 0;
while i < bytes.len() {
let line_start = i;
while bytes.get(i).is_some_and(|b| *b != b'\n') {
i = i.saturating_add(1);
}
let line = bytes.get(line_start..i).unwrap_or(&[]);
if line_start > 0 && line.iter().all(|b| matches!(*b, b' ' | b'\t' | b'\r')) {
return true;
}
i = i.saturating_add(1);
}
false
}