use brief::convert::{Hole, convert};
fn run(md: &str) -> (String, Vec<Hole>) {
let r = convert(md, "test.md");
let holes: Vec<Hole> = r.diagnostics.iter().map(|d| d.hole).collect();
(r.brief_source, holes)
}
#[test]
fn plain_paragraph() {
let (out, holes) = run("hello world\n");
assert_eq!(out, "hello world\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn two_paragraphs() {
let (out, holes) = run("first\n\nsecond\n");
assert_eq!(out, "first\n\nsecond\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn atx_heading_h1() {
let (out, holes) = run("# Title\n");
assert_eq!(out, "# Title\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn atx_heading_h6() {
let (out, holes) = run("###### deep\n");
assert_eq!(out, "###### deep\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn setext_h1_rewritten_to_atx() {
let (out, holes) = run("Title\n=====\n");
assert_eq!(
out,
"# Title\n// TODO[B-hole:setext-heading]: rewritten to `# ...`\n"
);
assert_eq!(holes, vec![Hole::SetextHeading]);
}
#[test]
fn setext_h2_rewritten_to_atx() {
let (out, holes) = run("Title\n-----\n");
assert_eq!(
out,
"## Title\n// TODO[B-hole:setext-heading]: rewritten to `## ...`\n"
);
assert_eq!(holes, vec![Hole::SetextHeading]);
}
#[test]
fn underscore_italic_clean() {
let (out, holes) = run("a _italic_ b\n");
assert_eq!(out, "a _italic_ b\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn asterisk_italic_rewritten() {
let (out, holes) = run("a *italic* b\n");
assert_eq!(
out,
"a _italic_ b\n\n// TODO[B-hole:asterisk-emphasis]: Markdown `*italic*` rewritten to Brief `_italic_`\n"
);
assert_eq!(holes, vec![Hole::AsteriskEmphasis]);
}
#[test]
fn double_asterisk_strong() {
let (out, holes) = run("a **bold** b\n");
assert_eq!(
out,
"a *bold* b\n\n// TODO[B-hole:double-emphasis]: doubled emphasis marker rewritten to single `*`\n"
);
assert_eq!(holes, vec![Hole::DoubleEmphasis]);
}
#[test]
fn double_underscore_strong() {
let (out, holes) = run("a __bold__ b\n");
assert_eq!(
out,
"a *bold* b\n\n// TODO[B-hole:double-emphasis]: doubled emphasis marker rewritten to single `*`\n"
);
assert_eq!(holes, vec![Hole::DoubleEmphasis]);
}
#[test]
fn strikethrough() {
let (out, holes) = run("a ~~strike~~ b\n");
assert_eq!(
out,
"a ~strike~ b\n\n// TODO[B-hole:double-emphasis]: doubled strikethrough rewritten to single `~`\n"
);
assert_eq!(holes, vec![Hole::DoubleEmphasis]);
}
#[test]
fn inline_code_simple() {
let (out, holes) = run("use `printf` here\n");
assert_eq!(out, "use `printf` here\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn inline_code_with_backtick_uses_double() {
let (out, holes) = run("run ``echo `whoami``` always\n");
assert_eq!(out, "run ``echo `whoami``` always\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn fenced_code_with_lang() {
let (out, holes) = run("```rust\nfn x() {}\n```\n");
assert_eq!(out, "```rust\nfn x() {}\n```\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn fenced_code_no_lang() {
let (out, holes) = run("```\nplain\n```\n");
assert_eq!(out, "```\nplain\n```\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn tilde_fence_rewritten() {
let (out, holes) = run("~~~rust\nfn x() {}\n~~~\n");
assert_eq!(
out,
"```rust\nfn x() {}\n```\n// TODO[B-hole:tilde-fence]: `~~~` fence rewritten to ```` ``` ```` fence\n"
);
assert_eq!(holes, vec![Hole::TildeFence]);
}
#[test]
fn indented_code_block_rewritten() {
let (out, holes) = run(" let x = 1;\n let y = 2;\n");
assert_eq!(
out,
"```\nlet x = 1;\nlet y = 2;\n```\n// TODO[B-hole:indented-code-block]: indented code block rewritten to fenced block\n"
);
assert_eq!(holes, vec![Hole::IndentedCodeBlock]);
}
#[test]
fn soft_break_inside_paragraph() {
let (out, holes) = run("first line\nsecond line\n");
assert_eq!(out, "first line\nsecond line\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn hard_break_with_backslash() {
let (out, holes) = run("line one\\\nline two\n");
assert_eq!(out, "line one\\\nline two\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn dash_bullet_clean() {
let (out, holes) = run("- a\n- b\n- c\n");
assert_eq!(out, "- a\n- b\n- c\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn star_bullet_rewritten() {
let (out, holes) = run("* a\n* b\n");
assert_eq!(
out,
"- a\n- b\n// TODO[B-hole:alt-bullet]: `*` bullet rewritten to `-`\n// TODO[B-hole:alt-bullet]: `*` bullet rewritten to `-`\n"
);
assert_eq!(holes, vec![Hole::AltBullet, Hole::AltBullet]);
}
#[test]
fn plus_bullet_rewritten() {
let (out, holes) = run("+ a\n+ b\n");
assert_eq!(
out,
"- a\n- b\n// TODO[B-hole:alt-bullet]: `+` bullet rewritten to `-`\n// TODO[B-hole:alt-bullet]: `+` bullet rewritten to `-`\n"
);
assert_eq!(holes, vec![Hole::AltBullet, Hole::AltBullet]);
}
#[test]
fn ordered_list_clean() {
let (out, holes) = run("1. one\n2. two\n3. three\n");
assert_eq!(out, "1. one\n2. two\n3. three\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn ordered_list_starting_at_5() {
let (out, holes) = run("5. one\n6. two\n");
assert_eq!(
out,
"1. one\n2. two\n// TODO[B-hole:ordered-renumber]: ordered list started at 5; renumbered from 1\n"
);
assert_eq!(holes, vec![Hole::OrderedRenumber]);
}
#[test]
fn two_space_nesting_clean() {
let (out, holes) = run("- top\n - nested\n- back\n");
assert_eq!(out, "- top\n - nested\n- back\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
#[ignore = "pulldown-cmark normalizes nesting indent in its source ranges \
before our walker sees it (a 4-space-indented nested item arrives \
with range starting at the +2 column), so we cannot detect this \
from the event stream alone. The output is still normalized to \
2-space (which the assert_eq!(out, ...) line would verify when \
un-ignored), but the diagnostic is silent in v0.2."]
fn four_space_nesting_normalized() {
let (out, holes) = run("- top\n - nested\n- back\n");
assert_eq!(out, "- top\n - nested\n- back\n");
assert_eq!(holes, vec![Hole::NestIndentNormalize]);
}
#[test]
fn task_list_checked() {
let (out, holes) = run("- [x] done thing\n");
assert_eq!(out, "- [x] done thing\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn task_list_unchecked() {
let (out, holes) = run("- [ ] todo thing\n");
assert_eq!(out, "- [ ] todo thing\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn plain_blockquote() {
let (out, holes) = run("> a quote\n> still quoted\n");
assert_eq!(out, "> a quote\n> still quoted\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn nested_blockquote() {
let (out, holes) = run("> outer\n>\n> > nested\n");
assert_eq!(
out,
"> outer\n\n> > nested\n// TODO[B-hole:blockquote-paragraph-split]: in-quote paragraph break rewritten to adjacent blockquotes\n"
);
assert_eq!(holes, vec![Hole::BlockquoteParagraphSplit]);
}
#[test]
fn gfm_alert_note() {
let (out, holes) = run("> [!NOTE]\n> body\n");
assert_eq!(
out,
"@callout(kind: note)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: note)`\nbody\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn gfm_alert_warning() {
let (out, holes) = run("> [!WARNING]\n> careful\n");
assert_eq!(
out,
"@callout(kind: warning)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: warning)`\ncareful\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn hr_clean() {
let (out, holes) = run("---\n");
assert_eq!(out, "---\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn hr_asterisks_rewritten() {
let (out, holes) = run("***\n");
assert_eq!(
out,
"---\n// TODO[B-hole:alt-horizontal-rule]: `***` rewritten to `---`\n"
);
assert_eq!(holes, vec![Hole::AltHorizontalRule]);
}
#[test]
fn hr_underscores_rewritten() {
let (out, holes) = run("___\n");
assert_eq!(
out,
"---\n// TODO[B-hole:alt-horizontal-rule]: `___` rewritten to `---`\n"
);
assert_eq!(holes, vec![Hole::AltHorizontalRule]);
}
#[test]
fn hr_spaced_dashes_rewritten() {
let (out, holes) = run("- - -\n");
assert_eq!(
out,
"---\n// TODO[B-hole:alt-horizontal-rule]: `- - -` rewritten to `---`\n"
);
assert_eq!(holes, vec![Hole::AltHorizontalRule]);
}
#[test]
fn simple_table() {
let md = "| Name | Age |\n| --- | --- |\n| Ada | 30 |\n| Bob | 25 |\n";
let (out, holes) = run(md);
assert_eq!(out, "@t\n| Name | Age\n| Ada | 30\n| Bob | 25\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn table_with_alignment() {
let md = "| L | C | R |\n| :--- | :---: | ---: |\n| a | b | c |\n";
let (out, holes) = run(md);
assert_eq!(
out,
"@t(align: [left, center, right])\n| L | C | R\n| a | b | c\n"
);
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn link_inline_clean() {
let (out, holes) = run("see [the spec](https://example.com)\n");
assert_eq!(out, "see @link[the spec](https://example.com)\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn link_with_title_preserved() {
let (out, holes) = run("[t](https://x \"some title\")\n");
assert_eq!(out, "@link(title: \"some title\")[t](https://x)\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn link_autolink_rewrap() {
let (out, holes) = run("see <https://example.com>\n");
assert_eq!(out, "see @link[https://example.com](https://example.com)\n");
assert_eq!(holes, vec![Hole::AutolinkRewrap]);
}
#[test]
fn link_reference_inlined() {
let md = "see [docs][ref]\n\n[ref]: https://example.com\n";
let (out, holes) = run(md);
assert_eq!(out, "see @link[docs](https://example.com)\n");
assert_eq!(holes, vec![Hole::RefLinkInlined]);
}
#[test]
fn image_basic() {
let (out, holes) = run("\n");
assert_eq!(out, "@image(src: \"arch.png\", alt: \"diagram\")[]\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn image_with_title_drops_title() {
let (out, holes) = run("\n");
assert_eq!(out, "@image(src: \"src.png\", alt: \"alt\")[]\n");
assert_eq!(holes, vec![Hole::LinkTitleDropped]);
}
#[test]
fn footnote_inlined() {
let md = "claim[^1] here\n\n[^1]: source: Smith 2025\n";
let (out, holes) = run(md);
assert_eq!(out, "claim@footnote[source: Smith 2025] here\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn footnote_body_preserves_emphasis() {
let md = "claim[^1].\n\n[^1]: see _ibid._, p. 5\n";
let (out, holes) = run(md);
assert_eq!(out, "claim@footnote[see _ibid._, p. 5].\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn footnote_body_preserves_link() {
let md = "claim[^1].\n\n[^1]: see [docs](https://example.com)\n";
let (out, holes) = run(md);
assert_eq!(
out,
"claim@footnote[see @link[docs](https://example.com)].\n"
);
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn footnote_body_preserves_code_span() {
let md = "claim[^1].\n\n[^1]: run `cargo build`\n";
let (out, holes) = run(md);
assert_eq!(out, "claim@footnote[run `cargo build`].\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn footnote_body_emphasis_diagnostics_propagate() {
let md = "claim[^1].\n\n[^1]: see **bold** text\n";
let (out, holes) = run(md);
assert_eq!(out, "claim@footnote[see *bold* text].\n");
assert_eq!(holes, vec![Hole::DoubleEmphasis]);
}
#[test]
fn inline_sub_rewritten_to_shortcode() {
let md = "before <sub>2</sub> after\n";
let (out, holes) = run(md);
assert_eq!(out, "before @sub[2] after\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn inline_sup_rewritten_to_shortcode() {
let md = "x<sup>2</sup>\n";
let (out, holes) = run(md);
assert_eq!(out, "x@sup[2]\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn inline_kbd_rewritten_to_shortcode() {
let md = "press <kbd>Ctrl+C</kbd>\n";
let (out, holes) = run(md);
assert_eq!(out, "press @kbd[Ctrl+C]\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn inline_br_becomes_brief_hard_break() {
let md = "line one<br>line two\n";
let (out, holes) = run(md);
assert_eq!(out, "line one\\\nline two\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn unknown_inline_html_still_emits_todo() {
let md = "see <abbr title=\"x\">A</abbr> here\n";
let (out, holes) = run(md);
assert!(out.contains("// TODO[B-hole:inline-html]"), "out={:?}", out);
assert_eq!(
holes,
vec![Hole::InlineHtml, Hole::InlineHtml],
"{:?}",
holes
);
}
#[test]
fn html_block_in_block_comment() {
let md = "<div class=\"x\">\n<p>raw</p>\n</div>\n";
let (out, holes) = run(md);
assert!(out.contains("// TODO[B-hole:html-block]"), "out={:?}", out);
assert!(out.contains("/*"));
assert!(out.contains("</div>"));
assert!(out.contains("*/"));
assert_eq!(holes, vec![Hole::HtmlBlock]);
}
#[test]
fn details_block_self_contained_rewritten() {
let md = "<details><summary>Why</summary>Because</details>\n";
let (out, holes) = run(md);
assert!(out.contains("@details(summary: \"Why\")"), "out={:?}", out);
assert!(out.contains("Because"));
assert!(out.contains("@end"));
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn details_block_with_markdown_body() {
let md = "<details>\n<summary>Click</summary>\n\n*Important* note.\n\n</details>\n";
let (out, holes) = run(md);
assert!(
out.contains("@details(summary: \"Click\")"),
"out={:?}",
out
);
assert!(out.contains("_Important_ note."), "out={:?}", out);
assert!(out.contains("@end"));
assert_eq!(holes, vec![Hole::AsteriskEmphasis], "{:?}", holes);
}
#[test]
fn heading_anchor_passthrough() {
let md = "## Hello world {#hello-world}\n";
let (out, holes) = run(md);
assert_eq!(out, "## Hello world {#hello-world}\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn converted_heading_anchor_round_trips_through_compiler() {
use brief::lexer::lex;
use brief::parser::parse;
use brief::span::SourceMap;
let md = "# Top {#top}\n\n## Sub {#some-sub}\n\nbody\n";
let r = convert(md, "in.md");
let src = SourceMap::new("in.brf", r.brief_source.clone());
let toks = lex(&src).expect("lex");
let (doc, diags) = parse(toks, &src);
assert!(diags.is_empty(), "{:?}\n---\n{}", diags, r.brief_source);
let mut anchors: Vec<String> = Vec::new();
for b in &doc.blocks {
if let brief::ast::Block::Heading { anchor, .. } = b {
if let Some(a) = anchor {
anchors.push(a.clone());
}
}
}
assert_eq!(anchors, vec!["top".to_string(), "some-sub".to_string()]);
}
#[test]
fn converted_inline_shortcodes_round_trip_through_compiler() {
use brief::lexer::lex;
use brief::parser::parse;
use brief::resolve::resolve;
use brief::shortcode::Registry;
use brief::span::SourceMap;
use brief::validate::{ValidateOpts, validate};
let md = "press <kbd>Ctrl+C</kbd> to abort. x<sup>2</sup> + y<sub>0</sub>\n";
let r = convert(md, "in.md");
let src = SourceMap::new("in.brf", &r.brief_source);
let toks = lex(&src).expect("lex");
let (mut doc, mut diags) = parse(toks, &src);
let reg = Registry::with_builtins();
diags.extend(resolve(&mut doc, ®));
diags.extend(validate(&doc, &ValidateOpts::default(), &src));
assert!(
diags.is_empty(),
"diags:\n{:#?}\n---\n{}",
diags,
r.brief_source
);
}
#[test]
fn converted_details_round_trips_through_compiler() {
use brief::lexer::lex;
use brief::parser::parse;
use brief::resolve::resolve;
use brief::shortcode::Registry;
use brief::span::SourceMap;
use brief::validate::{ValidateOpts, validate};
let md = "<details><summary>Why</summary>Because reasons.</details>\n";
let r = convert(md, "in.md");
let src = SourceMap::new("in.brf", &r.brief_source);
let toks = lex(&src).expect("lex");
let (mut doc, mut diags) = parse(toks, &src);
let reg = Registry::with_builtins();
diags.extend(resolve(&mut doc, ®));
diags.extend(validate(&doc, &ValidateOpts::default(), &src));
assert!(
diags.is_empty(),
"diags:\n{:#?}\n---\n{}",
diags,
r.brief_source
);
}
#[test]
fn heading_anchor_slugified_when_invalid() {
let md = "## Title {#Some_ID}\n";
let (out, holes) = run(md);
assert_eq!(
out,
"## Title {#some-id}\n// TODO[B-hole:heading-anchor-slugged]: anchor `Some_ID` rewritten to `some-id`\n"
);
assert_eq!(holes, vec![Hole::HeadingAnchorSlugged]);
}
#[test]
fn yaml_frontmatter_replaced_with_todo() {
let md = "---\ntitle: Hello\nauthor: Ada\n---\n\nbody\n";
let (out, holes) = run(md);
assert!(
out.starts_with("// TODO[B-hole:frontmatter]:"),
"out={:?}",
out
);
assert!(out.contains("body"));
assert_eq!(holes, vec![Hole::Frontmatter]);
}
#[test]
fn html_entities_decoded_silently_in_v02() {
let md = "rock & roll\n";
let (out, _holes) = run(md);
assert_eq!(out, "rock & roll\n");
}
#[test]
fn inline_math_passthrough() {
let (out, holes) = run("the energy is $E = mc^2$\n");
assert_eq!(out, "the energy is @math[E = mc^2]\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn display_math_block_single_line() {
let (out, holes) = run("$$x + y$$\n");
assert_eq!(out, "@math\nx + y\n@end\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn display_math_block_multiline() {
let (out, holes) = run("$$\n\\int_0^1 x \\, dx\n$$\n");
assert_eq!(out, "@math\n\\int_0^1 x \\, dx\n@end\n");
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn dollar_without_pair_is_text() {
let (out, holes) = run("the price is $5 today\n");
assert_eq!(out, "the price is $5 today\n");
assert!(holes.is_empty(), "{:?}", holes);
}
use brief::lexer::lex;
use brief::parser::parse;
use brief::resolve::resolve;
use brief::shortcode::Registry;
use brief::span::SourceMap;
use brief::validate::{ValidateOpts, validate};
#[test]
fn convert_simple_dl() {
let md = "Term\n: Definition.\n";
let (out, holes) = run(md);
assert!(out.contains("@dl\n"), "got: {}", out);
assert!(out.contains("Term\n: Definition.\n"), "got: {}", out);
assert!(out.contains("@end\n"), "got: {}", out);
assert!(holes.is_empty(), "unexpected holes: {:?}", holes);
}
#[test]
fn convert_dl_multiple_defs_emits_hole() {
let md = "Term\n: First.\n: Second.\n";
let (out, holes) = run(md);
let term_count = out.matches("Term\n").count();
assert_eq!(term_count, 2, "term should be duplicated, got: {}", out);
assert!(out.contains(": First."), "got: {}", out);
assert!(out.contains(": Second."), "got: {}", out);
let dl_holes: Vec<_> = holes
.iter()
.filter(|h| **h == Hole::DefinitionListMultipleDefs)
.collect();
assert_eq!(dl_holes.len(), 1, "got: {:?}", holes);
}
#[test]
fn roundtrip_sample_compiles_clean() {
let md = include_str!("fixtures/sample.md");
let result = convert(md, "sample.md");
let src = SourceMap::new("sample.brf", &result.brief_source);
let tokens = lex(&src).expect("lex must succeed");
let (mut doc, mut diags) = parse(tokens, &src);
let reg = Registry::with_builtins();
diags.extend(resolve(&mut doc, ®));
diags.extend(validate(&doc, &ValidateOpts::default(), &src));
assert!(
diags.is_empty(),
"converter produced invalid Brief:\n--- BRIEF ---\n{}\n--- DIAGS ---\n{:#?}",
result.brief_source,
diags
);
}
#[test]
fn link_with_title_preserved_in_convert() {
let (out, holes) = run("[t](https://x \"the title\")\n");
assert_eq!(out, "@link(title: \"the title\")[t](https://x)\n");
assert!(
!holes.contains(&Hole::LinkTitleDropped),
"LinkTitleDropped must not be emitted: {:?}",
holes
);
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn link_without_title_no_kwarg() {
let (out, holes) = run("[text](https://example.com)\n");
assert_eq!(
out,
"see @link[text](https://example.com)\n".replace("see ", "")
);
assert!(holes.is_empty(), "{:?}", holes);
}
#[test]
fn link_with_title_roundtrips_through_compiler() {
let md = "[doc](https://example.com \"the title\")\n";
let result = convert(md, "test.md");
assert!(
result.brief_source.contains("@link(title: \"the title\")"),
"brief: {}",
result.brief_source
);
let src = SourceMap::new("t.brf", &result.brief_source);
let tokens = lex(&src).expect("lex must succeed");
let (mut doc, mut diags) = parse(tokens, &src);
let reg = Registry::with_builtins();
diags.extend(resolve(&mut doc, ®));
diags.extend(validate(&doc, &ValidateOpts::default(), &src));
assert!(
diags.is_empty(),
"converted Brief with title failed to compile:\n---\n{}\n---\n{:#?}",
result.brief_source,
diags
);
}
#[test]
fn gfm_alert_tip() {
let (out, holes) = run("> [!TIP]\n> body\n");
assert_eq!(
out,
"@callout(kind: tip)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: tip)`\nbody\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn gfm_alert_important() {
let (out, holes) = run("> [!IMPORTANT]\n> body\n");
assert_eq!(
out,
"@callout(kind: important)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: important)`\nbody\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn gfm_alert_caution() {
let (out, holes) = run("> [!CAUTION]\n> body\n");
assert_eq!(
out,
"@callout(kind: caution)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: caution)`\nbody\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn gfm_alert_warning_maps_to_warning() {
let (out, holes) = run("> [!WARNING]\n> careful\n");
assert_eq!(
out,
"@callout(kind: warning)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: warning)`\ncareful\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn gfm_alert_note_maps_to_note() {
let (out, holes) = run("> [!NOTE]\n> content\n");
assert_eq!(
out,
"@callout(kind: note)\n// TODO[B-hole:gfm-alert]: GFM alert mapped to `@callout(kind: note)`\ncontent\n@end\n"
);
assert_eq!(holes, vec![Hole::GfmAlert]);
}
#[test]
fn gfm_all_five_alert_kinds_roundtrip() {
for (kind, _) in [
("> [!NOTE]\n> body\n", "note"),
("> [!TIP]\n> body\n", "tip"),
("> [!IMPORTANT]\n> body\n", "important"),
("> [!WARNING]\n> body\n", "warning"),
("> [!CAUTION]\n> body\n", "caution"),
] {
let result = convert(kind, "test.md");
let src = SourceMap::new("t.brf", &result.brief_source);
let tokens = lex(&src).expect("lex must succeed");
let (mut doc, mut diags) = parse(tokens, &src);
let reg = Registry::with_builtins();
diags.extend(resolve(&mut doc, ®));
diags.extend(validate(&doc, &ValidateOpts::default(), &src));
assert!(
diags.is_empty(),
"GFM alert roundtrip failed:\n---\n{}\n---\n{:#?}",
result.brief_source,
diags
);
}
}