use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
const SAMPLE: &str = "# Title Here\n\
\n\
## Abstract\n\
\n\
Abs line one.\n\
Abs line two.\n\
\n\
\n\
## 1. First\n\
\n\
Para with math $x$ and cite [`k`] inline.\n\
\n\
```mermaid {span=column pos=both}\n\
graph TD\n\
%% caption: Cap one.\n\
a[x]-->b[y]\n\
```\n\
\n\
%% table {pos=barrier}\n\
\n\
| A | B |\n\
|---|---|\n\
| 1 | 2 |\n\
| 3 | 4 |\n\
\n\
### 1.1 Sub\n\
\n\
```mermaid {inline}\n\
graph TD\n\
c[x]-->d[y]\n\
```\n\
\n\
## Appendix\n\
\n\
Tail.\n";
fn hand_blocks(md: &str) -> Vec<String> {
let lines: Vec<&str> = md.lines().collect();
let mut out = Vec::new();
let mut i = 0;
let mut para_open = false;
let flush = |out: &mut Vec<String>, para_open: &mut bool| {
if *para_open {
out.push("Para".to_string());
*para_open = false;
}
};
while i < lines.len() {
let s = lines[i].trim();
if s.starts_with("```") {
flush(&mut out, &mut para_open);
let info = s.trim_start_matches('`');
let mut sp = info.splitn(2, char::is_whitespace);
let lang = sp.next().unwrap_or("");
let attr = sp.next().unwrap_or("").trim();
out.push(format!("Fence({lang}|{attr})"));
i += 1;
while i < lines.len() && !lines[i].trim().starts_with("```") {
i += 1;
}
i += 1;
continue;
}
if s.starts_with("%% table") {
flush(&mut out, &mut para_open);
out.push(format!("Directive({})", s.trim_start_matches("%% table").trim()));
i += 1;
continue;
}
if s.starts_with('|') {
flush(&mut out, &mut para_open);
let mut rows = 0;
let mut cols = 0;
while i < lines.len() && lines[i].trim().starts_with('|') {
let cells: Vec<&str> = lines[i]
.trim()
.trim_matches('|')
.split('|')
.map(str::trim)
.collect();
cols = cols.max(cells.len());
rows += 1;
i += 1;
}
out.push(format!("Table(rows={rows},cols={cols})"));
continue;
}
if let Some(h) = s.strip_prefix("### ") {
flush(&mut out, &mut para_open);
out.push(format!("H3({h})"));
} else if let Some(h) = s.strip_prefix("## ") {
flush(&mut out, &mut para_open);
out.push(format!("H2({h})"));
} else if let Some(h) = s.strip_prefix("# ") {
flush(&mut out, &mut para_open);
out.push(format!("H1({h})"));
} else if s.is_empty() {
flush(&mut out, &mut para_open);
} else {
para_open = true;
}
i += 1;
}
flush(&mut out, &mut para_open);
out
}
fn pdc_blocks(md: &str) -> Vec<String> {
let mut out = Vec::new();
let mut heading: Option<(String, String)> = None;
let mut row_cells = 0;
let mut in_row = false;
let mut table_rows: Vec<usize> = Vec::new();
for ev in Parser::new_ext(md, Options::ENABLE_TABLES) {
match ev {
Event::Start(Tag::Heading { level, .. }) => {
heading = Some((format!("{level:?}").to_uppercase(), String::new()));
}
Event::Text(t) => {
if let Some((_, ref mut text)) = heading {
text.push_str(&t);
} else if in_row {
row_cells += 1;
}
}
Event::End(tag) => match tag {
pulldown_cmark::TagEnd::Heading(_) => {
let (lvl, text) = heading.take().unwrap();
let n: &str = match lvl.as_str() {
"H1" => "1",
"H2" => "2",
_ => "3",
};
out.push(format!("H{n}({text})"));
}
pulldown_cmark::TagEnd::TableHead => {
table_rows.push(row_cells);
row_cells = 0;
in_row = false;
}
pulldown_cmark::TagEnd::TableRow => {
table_rows.push(row_cells);
row_cells = 0;
in_row = false;
}
pulldown_cmark::TagEnd::Table => {
out.push(format!("Table(head+rows={table_rows:?})"));
table_rows.clear();
}
pulldown_cmark::TagEnd::Paragraph => out.push("Para".to_string()),
_ => {}
},
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
let info = info.trim();
let mut sp = info.splitn(2, char::is_whitespace);
out.push(format!(
"Fence({}|{})",
sp.next().unwrap_or(""),
sp.next().unwrap_or("").trim()
));
}
Event::Start(Tag::Table(_)) => {}
Event::Start(Tag::TableHead | Tag::TableRow) => {
in_row = true;
row_cells = 0;
}
_ => {}
}
}
out
}
#[test]
fn spike_event_diff() {
let hand = hand_blocks(SAMPLE);
let pdc = pdc_blocks(SAMPLE);
eprintln!("HAND: {hand:#?}\nPDC: {pdc:#?}");
let pinfos: Vec<String> = pdc
.iter()
.filter_map(|b| b.strip_prefix("Fence("))
.map(|s| s.strip_suffix(')').unwrap().to_string())
.collect();
assert_eq!(pinfos, ["mermaid|{span=column pos=both}", "mermaid|{inline}"]);
let hheads: Vec<&str> = hand
.iter()
.filter(|b| b.starts_with('H'))
.map(String::as_str)
.collect();
let pheads: Vec<&str> = pdc
.iter()
.filter(|b| b.starts_with('H'))
.map(String::as_str)
.collect();
assert_eq!(hheads, pheads);
assert!(hand.contains(&"Table(rows=4,cols=2)".to_string()));
assert!(pdc.contains(&"Table(head+rows=[2, 2, 2])".to_string()));
let hparas = hand.iter().filter(|b| *b == "Para").count();
let pparas = pdc.iter().filter(|b| *b == "Para").count();
assert_eq!((hparas, pparas), (3, 4));
assert!(hand.contains(&"Directive({pos=barrier})".to_string()));
}