// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package markdown import "testing" // blockCorpus is the project's own hand-written corpus. Every case states // an input and the exact HTML the engine produces for it. var blockCorpus = []struct { name string input string want string }{ // ATX headings. {"atx level 1", "# foo", "

foo

\n"}, {"atx level 6", "###### foo", "
foo
\n"}, {"atx seven hashes", "####### foo", "

####### foo

\n"}, {"atx needs a space", "#foo", "

#foo

\n"}, {"atx closing sequence", "### foo ###", "

foo

\n"}, {"atx closing sequence mid content", "### foo ### ###", "

foo ###

\n"}, {"atx hash without space stays", "# foo#", "

foo#

\n"}, {"atx indented three", " # foo", "

foo

\n"}, {"atx indented four is code", " # foo", "
# foo\n
\n"}, {"atx bare hash", "#", "

\n"}, {"atx empty content", "###", "

\n"}, // Setext headings. {"setext level 1", "Foo\n===", "

Foo

\n"}, {"setext level 2", "Foo\n---", "

Foo

\n"}, {"setext multi line", "Foo\nBar\n---", "

Foo\nBar

\n"}, {"setext takes whole paragraph", "Foo\nbar\n---", "

Foo\nbar

\n"}, {"setext indented underline", "Foo\n ===", "

Foo

\n"}, {"setext dashes with spaces stay thematic", "Foo\n- - -", "

Foo

\n
\n"}, {"setext cannot be lazy", "> Foo\n===", "
\n

Foo\n===

\n
\n"}, {"setext inside quote", "> foo\n> ---", "
\n

foo

\n
\n"}, // Thematic breaks. {"thematic stars", "***", "
\n"}, {"thematic underscores", "___", "
\n"}, {"thematic spaced dashes", "- - -", "
\n"}, {"thematic interrupts paragraph", "foo\n***", "

foo

\n
\n"}, {"thematic long", "---------------------------------------", "
\n"}, {"thematic trailing spaces", "*** ", "
\n"}, {"thematic indented three", " ***", "
\n"}, // Indented code blocks. {"indented code", " code", "
code\n
\n"}, {"indented code two lines", " code\n more", "
code\nmore\n
\n"}, {"indented code keeps internal blank", " a\n\n b", "
a\n\nb\n
\n"}, {"indented code strips trailing blanks", " a\n\n\n", "
a\n
\n"}, {"tab indents code", "\tcode", "
code\n
\n"}, {"indented code cannot interrupt paragraph", "foo\n bar", "

foo\nbar

\n"}, // Fenced code blocks. {"fenced backticks", "```\ncode\n```", "
code\n
\n"}, {"fenced tildes", "~~~\ncode\n~~~", "
code\n
\n"}, {"fenced info class", "```ruby\nx = 1\n```", "
x = 1\n
\n"}, {"fenced longer close", "````\n```\n````", "
```\n
\n"}, {"fenced short close is content", "````\ncode\n```", "
code\n```\n
\n"}, {"fenced indent stripping", " ```\n foo\nbar\n```", "
foo\nbar\n
\n"}, {"fenced empty", "```\n```", "
\n"}, {"fenced unclosed", "```\ncode", "
code\n
\n"}, {"tilde info may hold backticks", "~~~ js `x`\ncode\n~~~", "
code\n
\n"}, {"fenced blank lines kept", "```\na\n\nb\n```", "
a\n\nb\n
\n"}, // HTML blocks. {"html type six", "
\nfoo\n
", "
\nfoo\n
\n"}, {"html comment", "", "\n"}, {"html processing instruction", "", "\n"}, {"html declaration ends at bracket", "\nfoo", "\n

foo

\n"}, {"html cdata", "", "\n"}, {"html script block", "", "\n"}, {"html blank ends six", "
\nfoo\n\nbar", "
\nfoo\n

bar

\n"}, {"html six interrupts paragraph", "Foo\n
", "

Foo

\n
\n"}, {"html textarea", "", "\n"}, {"html seven complete tag", "\nfoo", "\nfoo\n"}, // Block quotes. {"quote basic", "> foo", "
\n

foo

\n
\n"}, {"quote two lines", "> foo\n> bar", "
\n

foo\nbar

\n
\n"}, {"quote without space", ">foo", "
\n

foo

\n
\n"}, {"quote lazy", "> foo\nbar", "
\n

foo\nbar

\n
\n"}, {"quote blank closes paragraph", "> foo\n\nbar", "
\n

foo

\n
\n

bar

\n"}, {"quote empty", ">", "
\n
\n"}, {"quote nested", "> > foo", "
\n
\n

foo

\n
\n
\n"}, {"quote heading", "> # foo", "
\n

foo

\n
\n"}, {"quote list", "> - foo", "
\n
    \n
  • foo
  • \n
\n
\n"}, {"quote thematic interrupt", "> foo\n---", "
\n

foo

\n
\n
\n"}, {"quote blank line inside", "> foo\n>\n> bar", "
\n

foo

\n

bar

\n
\n"}, {"quote blank line between", "> a\n\n> b", "
\n

a

\n
\n
\n

b

\n
\n"}, {"quote tab content", ">\tfoo", "
\n

foo

\n
\n"}, // Lists. {"list bullet", "- foo", "
    \n
  • foo
  • \n
\n"}, {"list star", "* foo", "
    \n
  • foo
  • \n
\n"}, {"list plus", "+ foo", "
    \n
  • foo
  • \n
\n"}, {"list tight items", "- foo\n- bar", "
    \n
  • foo
  • \n
  • bar
  • \n
\n"}, {"list loose items", "- foo\n\n- bar", "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
\n"}, {"list blank splits items loose", "- foo\n- bar\n\n- baz", "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
  • \n

    baz

    \n
  • \n
\n"}, {"list bullet change splits", "* foo\n+ bar", "
    \n
  • foo
  • \n
\n
    \n
  • bar
  • \n
\n"}, {"ordered list", "1. foo\n2. bar", "
    \n
  1. foo
  2. \n
  3. bar
  4. \n
\n"}, {"ordered start", "3. foo", "
    \n
  1. foo
  2. \n
\n"}, {"ordered paren delimiter", "1) foo", "
    \n
  1. foo
  2. \n
\n"}, {"ordered delimiter change splits", "1. foo\n1) bar", "
    \n
  1. foo
  2. \n
\n
    \n
  1. bar
  2. \n
\n"}, {"list item two paragraphs loose", "- foo\n\n bar", "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n"}, {"list nested", "- foo\n - bar", "
    \n
  • foo\n
      \n
    • bar
    • \n
    \n
  • \n
\n"}, {"list item continuation", "- foo\n bar", "
    \n
  • foo\nbar
  • \n
\n"}, {"list item lazy", "- foo\nbar", "
    \n
  • foo\nbar
  • \n
\n"}, {"list lazy carries on", "- foo\n bar\ncar", "
    \n
  • foo\nbar\ncar
  • \n
\n"}, {"ordered nine digits", "123456789. foo", "
    \n
  1. foo
  2. \n
\n"}, {"ordered ten digits not a list", "1234567890. foo", "

1234567890. foo

\n"}, {"empty items", "- foo\n-\n- bar", "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n"}, {"list interrupts paragraph", "foo\n- bar", "

foo

\n
    \n
  • bar
  • \n
\n"}, {"ordered two cannot interrupt", "foo\n2. bar", "

foo\n2. bar

\n"}, {"single dash setext under paragraph", "foo\n-", "

foo

\n"}, {"bullet blank cannot interrupt", "foo\n+", "

foo\n+

\n"}, {"list marker five spaces makes code", "- indented code", "
    \n
  • \n
    indented code\n
    \n
  • \n
\n"}, {"item fence on marker line", "- ```\n foo\n ```", "
    \n
  • \n
    foo\n
    \n
  • \n
\n"}, {"item fence after paragraph", "- foo\n ```\n bar\n ```", "
    \n
  • foo\n
    bar\n
    \n
  • \n
\n"}, {"item paragraph indented content", "1. foo\n bar", "
    \n
  1. foo\nbar
  2. \n
\n"}, {"ordered paragraph two lines", "1. A paragraph\n with two lines.", "
    \n
  1. A paragraph\nwith two lines.
  2. \n
\n"}, {"second item less indented", "- a\n - b", "
    \n
  • a
  • \n
  • b
  • \n
\n"}, {"lazy into quoted item", "> - foo\nbar", "
\n
    \n
  • foo\nbar
  • \n
\n
\n"}, {"loose ordered with blank", "1. a\n\n 2. b", "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
\n"}, {"list item second paragraph after blank", "- a\n- b\n\n c", "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n

    c

    \n
  • \n
\n"}, // Paragraphs. {"paragraph single", "foo", "

foo

\n"}, {"paragraph lines", "foo\nbar", "

foo\nbar

\n"}, {"paragraph leading spaces", " foo", "

foo

\n"}, {"paragraph escaping", "a < b & c", "

a < b & c

\n"}, {"paragraph quotes escape", "say \"hi\"", "

say "hi"

\n"}, {"blank lines separate", "foo\n\nbar", "

foo

\n

bar

\n"}, {"leading blanks ignored", "\n\nfoo", "

foo

\n"}, {"crlf normalised", "foo\r\nbar\r\n", "

foo\nbar

\n"}, // Link reference definitions. {"reference definition alone", "[foo]: /url", ""}, {"reference definition with title", "[foo]: /url \"title\"", ""}, {"reference definition then text", "[foo]: /url\nused", "

used

\n"}, {"reference definition on two lines", "[foo]:\n/url", ""}, {"reference definition on three lines", "[foo]:\n/url\n\"title\"", ""}, {"junk after destination", "[foo]: /url junk", "

[foo]: /url junk

\n"}, {"two definitions", "[foo]: /a\n[bar]: /b", ""}, {"definition indented", " [foo]: /url", ""}, // Documents. {"empty input", "", ""}, {"only blanks", "\n\n\n", ""}, {"composite document", "# Title\n\nIntro text here.\n\n- one\n- two\n\n```go\nfmt.Println(1)\n```\n\n> quoted\n", "

Title

\n

Intro text here.

\n
    \n
  • one
  • \n
  • two
  • \n
\n" + "
fmt.Println(1)\n
\n
\n

quoted

\n
\n"}, } func TestBlockCorpus(t *testing.T) { for _, tc := range blockCorpus { t.Run(tc.name, func(t *testing.T) { got := string(RenderHTML([]byte(tc.input))) if got != tc.want { t.Errorf("input %q\ngot: %q\nwant: %q", tc.input, got, tc.want) } }) } }