const OPEN: &str = "<!-- spec:count";
const CLOSE: &str = "<!-- /spec:count -->";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Marker {
pub line: usize,
pub raw: String,
pub source: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Malformed {
pub line: usize,
pub message: String,
}
fn mask_code(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut inside = false;
for c in line.chars() {
if c == '`' {
inside = !inside;
out.push(c);
} else {
if inside {
out.extend(std::iter::repeat_n(' ', c.len_utf8()));
} else {
out.push(c);
}
}
}
out
}
fn scan_line(n: usize, line: &str, ok: &mut Vec<Marker>, bad: &mut Vec<Malformed>) {
let masked = mask_code(line);
let mut rest = 0;
while rest < masked.len() {
let hay = &masked[rest..];
let (Some(o), c) = (hay.find(OPEN), hay.find(CLOSE)) else {
if hay.contains(CLOSE) {
bad.push(Malformed {
line: n,
message: "closing <!-- /spec:count --> without an opening marker".into(),
});
}
return;
};
if c.is_some_and(|c| c < o) {
bad.push(Malformed { line: n, message: "closing <!-- /spec:count --> without an opening marker".into() });
}
let start = rest + o;
let Some(head_end) = masked[start..].find("-->").map(|i| start + i) else {
bad.push(Malformed { line: n, message: "spec:count marker is not closed with -->".into() });
return;
};
let source = masked[start + OPEN.len()..head_end].trim().to_string();
let body_start = head_end + 3;
let Some(end) = masked[body_start..].find(CLOSE).map(|i| body_start + i) else {
bad.push(Malformed { line: n, message: "spec:count has no <!-- /spec:count --> on the same line".into() });
return;
};
let value = &masked[body_start..end];
if value.contains(OPEN) {
bad.push(Malformed { line: n, message: "spec:count markers cannot be nested".into() });
return;
}
let stop = end + CLOSE.len();
ok.push(Marker { line: n, raw: line[start..stop].to_string(), source, value: value.trim().to_string() });
rest = stop;
}
}
pub fn scan(text: &str) -> (Vec<Marker>, Vec<Malformed>) {
let (mut ok, mut bad) = (vec![], vec![]);
let mut fence: Option<String> = None;
for (i, line) in text.lines().enumerate() {
let t = line.trim_start();
let ch = t.chars().next().unwrap_or(' ');
if ch == '`' || ch == '~' {
let run: String = t.chars().take_while(|c| *c == ch).collect();
if run.len() >= 3 {
match &fence {
Some(f) if run.starts_with(f.as_str()) && t[run.len()..].trim().is_empty() => fence = None,
None => fence = Some(run),
_ => {}
}
continue;
}
}
if fence.is_none() {
scan_line(i + 1, line, &mut ok, &mut bad);
}
}
(ok, bad)
}
pub fn rewrite(m: &Marker, n: usize) -> String {
let head_end = m.raw.find("-->").map(|i| i + 3).unwrap_or(0);
format!("{}{n}{CLOSE}", &m.raw[..head_end])
}
pub fn table_rows(text: &str, section: &str) -> Option<usize> {
let (lo, hi) = super::sections::span(text, section)?;
let lines: Vec<&str> = text.lines().collect();
let mut rows = 0;
let mut in_table = false;
for l in &lines[lo..hi] {
let Some(cells) = super::tables::cells(l) else {
in_table = false;
continue;
};
let sep = cells.iter().all(|c| !c.is_empty() && c.chars().all(|ch| matches!(ch, '-' | ':' | ' ')));
if !in_table {
in_table = true;
} else if !sep {
rows += 1;
}
}
Some(rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_markers_and_skips_code() {
let t = "a <!-- spec:count find type=adr -->3<!-- /spec:count --> b\n```\n<!-- spec:count x -->1<!-- /spec:count -->\n```\n`<!-- spec:count x -->1<!-- /spec:count -->`\n";
let (ok, bad) = scan(t);
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].source, "find type=adr");
assert_eq!(ok[0].value, "3");
assert_eq!(rewrite(&ok[0], 5), "<!-- spec:count find type=adr -->5<!-- /spec:count -->");
assert!(bad.is_empty());
}
#[test]
fn reports_malformed() {
for t in [
"<!-- spec:count find -->3",
"3<!-- /spec:count -->",
"<!-- spec:count find --><!-- spec:count find -->1<!-- /spec:count -->",
] {
assert_eq!(scan(t).1.len(), 1, "{t}");
}
}
#[test]
fn counts_table_rows() {
let t = "# A\n## Items\n| k | v |\n|---|---|\n| a | 1 |\n| b | 2 |\n## B\n| x |\n|---|\n| y |\n";
assert_eq!(table_rows(t, "Items"), Some(2));
assert_eq!(table_rows(t, "Nope"), None);
}
}