use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use crate::document::fences::find_metadata_blocks;
fn pulldown_fence_spans(md: &str) -> Vec<(usize, usize)> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
let mut spans = Vec::new();
let mut open: Vec<usize> = Vec::new();
for (ev, range) in Parser::new_ext(md, opts).into_offset_iter() {
match ev {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(_)))
| Event::Start(Tag::MetadataBlock(_)) => open.push(range.start),
Event::End(TagEnd::CodeBlock) | Event::End(TagEnd::MetadataBlock(_)) => {
if let Some(start) = open.pop() {
spans.push((start, range.end));
}
}
_ => {}
}
}
spans
}
fn trim_fence(s: &str) -> &str {
s.trim_end_matches(['\n', '\r'])
}
fn nonconformance(md: &str) -> Option<String> {
let Ok((blocks, _warnings)) = find_metadata_blocks(md) else {
return None;
};
let pd = pulldown_fence_spans(md);
for b in &blocks {
let ours = trim_fence(&md[b.start..b.end]);
let matched = pd
.iter()
.any(|&(ps, pe)| ps == b.start && trim_fence(&md[ps..pe.min(md.len())]) == ours);
if !matched {
return Some(format!(
"card-yaml block at [{}..{}] = {:?} has no matching pulldown fence \
(pulldown spans: {:?})",
b.start,
b.end,
ours,
pd.iter()
.map(|&(s, e)| trim_fence(&md[s..e.min(md.len())]))
.collect::<Vec<_>>()
));
}
}
None
}
fn assert_conformant(label: &str, md: &str) {
if let Some(why) = nonconformance(md) {
panic!("fence non-conformance in {label}:\n{why}\n--- source ---\n{md}");
}
}
#[test]
fn scanner_agrees_with_commonmark_on_synthetic_inputs() {
let cases: &[(&str, &str)] = &[
("bare root", "~~~\n$quill: q\n$kind: main\n~~~\n\nBody.\n"),
(
"legacy info string",
"~~~card-yaml\n$quill: q\n$kind: main\n~~~\n\nBody.\n",
),
(
"root + composable card",
"~~~\n$quill: q\n$kind: main\n~~~\n\nB\n\n~~~\n$kind: note\nx: 1\n~~~\n\nAfter.\n",
),
(
"three blocks",
"~~~\n$quill: q\n$kind: main\n~~~\n\n~~~\n$kind: a\n~~~\n\nfirst\n\n~~~\n$kind: b\n~~~\n\nsecond\n",
),
(
"--- root alias",
"---\n$quill: q\n$kind: main\n---\n\nBody.\n",
),
(
"--- root then bare card",
"---\n$quill: q\n$kind: main\n---\n\nBody.\n\n~~~\n$kind: note\nx: 1\n~~~\n",
),
(
"shielded fences inside a backtick block",
"~~~\n$quill: q\n$kind: main\n~~~\n\n```text\n~~~\n$kind: nope\n~~~\n```\n\nBody.\n",
),
(
"longer-tilde card (shorter inner ~~~ stays payload)",
"~~~\n$quill: q\n$kind: main\n~~~\n\n~~~~\n$kind: c\nx: 1\n~~~~\n",
),
(
"five-tilde root",
"~~~~~\n$quill: q\n$kind: main\n~~~~~\n\nBody.\n",
),
(
"backtick code block shielding tildes",
"~~~\n$quill: q\n$kind: main\n~~~\n\n```\n~~~\nx\n~~~\n```\n\nBody.\n",
),
(
"unclosed `~~~` in body is CommonMark code to EOF, not a card",
"~~~\n$quill: q\n$kind: main\n~~~\n\nIntro.\n\n~~~\nstray\n",
),
(
"tilde fence with language info in body",
"~~~\n$quill: q\n$kind: main\n~~~\n\n~~~rust\nlet x = 1;\n~~~\n",
),
(
"card after a paragraph",
"~~~\n$quill: q\n$kind: main\n~~~\n\nParagraph one.\n\n~~~\n$kind: c\nk: v\n~~~\n",
),
(
"card with nested yaml payload",
"~~~\n$quill: q\n$kind: main\nmeta:\n a: 1\n b:\n - x\n - y\n~~~\n\nBody.\n",
),
(
"empty payload block",
"~~~\n$quill: q\n$kind: main\n~~~\n\n~~~\n$kind: marker\n~~~\n",
),
(
"CRLF line endings",
"~~~\r\n$quill: q\r\n$kind: main\r\n~~~\r\n\r\nBody.\r\n",
),
(
"trailing newline absent on closer",
"~~~\n$quill: q\n$kind: main\n~~~",
),
(
"indented opener is not a card (stays a CommonMark code block)",
"~~~\n$quill: q\n$kind: main\n~~~\n\nB\n\n ~~~\n$kind: c\nx: 1\n ~~~\n",
),
(
"opener with trailing spaces",
"~~~ \n$quill: q\n$kind: main\n~~~\n\nBody.\n",
),
(
"indented closer on root",
"~~~\n$quill: q\n$kind: main\n ~~~\n\nBody.\n",
),
(
"closer longer than opener",
"~~~\n$quill: q\n$kind: main\n~~~~~\n\nBody.\n",
),
];
for (label, md) in cases {
assert_conformant(label, md);
}
}
fn collect_md(root: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_md(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("md") {
out.push(path);
}
}
}
#[test]
fn scanner_agrees_with_commonmark_on_fixtures() {
let fixtures_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.join("crates/fixtures/resources");
let mut files = Vec::new();
collect_md(&fixtures_root, &mut files);
let mut checked = 0;
let mut failures = Vec::new();
for path in &files {
let Ok(src) = std::fs::read_to_string(path) else {
continue;
};
let label = path
.strip_prefix(&fixtures_root)
.unwrap_or(path)
.display()
.to_string();
if let Some(why) = nonconformance(&src) {
failures.push(format!("{label}: {why}"));
} else {
checked += 1;
}
}
assert!(
failures.is_empty(),
"fence non-conformance in {} fixture(s):\n{}",
failures.len(),
failures.join("\n\n")
);
assert!(checked > 0, "no fixtures exercised the cross-check");
}