const CODE_INDENT: usize = 4;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Previous {
Start,
Blank,
Code,
Fence,
Text,
}
impl Previous {
fn permits_code(self) -> bool {
self != Previous::Text
}
}
pub fn mask<S: AsRef<str>>(lines: &[S], fenced: &[bool]) -> Vec<bool> {
let mut out = Vec::with_capacity(lines.len());
let mut previous = Previous::Start;
let mut in_list = false;
for (line, fenced) in lines.iter().zip(fenced) {
let line = line.as_ref();
if line.trim().is_empty() {
out.push(*fenced);
previous = Previous::Blank;
continue;
}
let indent = indent_columns(line);
if indent < CODE_INDENT {
if starts_list_item(line) {
in_list = true;
} else if indent == 0 && previous == Previous::Blank {
in_list = false;
}
}
if *fenced {
out.push(true);
previous = Previous::Fence;
} else if indent >= CODE_INDENT && previous.permits_code() && !in_list {
out.push(true);
previous = Previous::Code;
} else {
out.push(false);
previous = Previous::Text;
}
}
mark_blanks_between_code(lines, fenced, &mut out);
out
}
fn mark_blanks_between_code<S: AsRef<str>>(lines: &[S], fenced: &[bool], out: &mut [bool]) {
let mut start = 0;
while start < lines.len() {
if out[start] || !lines[start].as_ref().trim().is_empty() {
start += 1;
continue;
}
let end = (start..lines.len())
.find(|i| !lines[*i].as_ref().trim().is_empty())
.unwrap_or(lines.len());
let indented = |i: usize| out[i] && !fenced[i];
if start > 0 && end < lines.len() && indented(start - 1) && indented(end) {
out[start..end].fill(true);
}
start = end;
}
}
pub(super) fn indent_columns(line: &str) -> usize {
let mut columns = 0;
for byte in line.bytes() {
match byte {
b' ' => columns += 1,
b'\t' => columns += CODE_INDENT - columns % CODE_INDENT,
_ => break,
}
}
columns
}
fn starts_list_item(line: &str) -> bool {
let rest = line.trim_start();
let after = if let Some(after) = rest.strip_prefix(['-', '*', '+']) {
after
} else {
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
if !(1..=9).contains(&digits) {
return false;
}
match rest[digits..].strip_prefix(['.', ')']) {
Some(after) => after,
None => return false,
}
};
after.is_empty() || after.starts_with([' ', '\t'])
}