Files

166 lines
9.9 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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", "<h1>foo</h1>\n"},
{"atx level 6", "###### foo", "<h6>foo</h6>\n"},
{"atx seven hashes", "####### foo", "<p>####### foo</p>\n"},
{"atx needs a space", "#foo", "<p>#foo</p>\n"},
{"atx closing sequence", "### foo ###", "<h3>foo</h3>\n"},
{"atx closing sequence mid content", "### foo ### ###", "<h3>foo ###</h3>\n"},
{"atx hash without space stays", "# foo#", "<h1>foo#</h1>\n"},
{"atx indented three", " # foo", "<h1>foo</h1>\n"},
{"atx indented four is code", " # foo", "<pre><code># foo\n</code></pre>\n"},
{"atx bare hash", "#", "<h1></h1>\n"},
{"atx empty content", "###", "<h3></h3>\n"},
// Setext headings.
{"setext level 1", "Foo\n===", "<h1>Foo</h1>\n"},
{"setext level 2", "Foo\n---", "<h2>Foo</h2>\n"},
{"setext multi line", "Foo\nBar\n---", "<h2>Foo\nBar</h2>\n"},
{"setext takes whole paragraph", "Foo\nbar\n---", "<h2>Foo\nbar</h2>\n"},
{"setext indented underline", "Foo\n ===", "<h1>Foo</h1>\n"},
{"setext dashes with spaces stay thematic", "Foo\n- - -", "<p>Foo</p>\n<hr />\n"},
{"setext cannot be lazy", "> Foo\n===", "<blockquote>\n<p>Foo\n===</p>\n</blockquote>\n"},
{"setext inside quote", "> foo\n> ---", "<blockquote>\n<h2>foo</h2>\n</blockquote>\n"},
// Thematic breaks.
{"thematic stars", "***", "<hr />\n"},
{"thematic underscores", "___", "<hr />\n"},
{"thematic spaced dashes", "- - -", "<hr />\n"},
{"thematic interrupts paragraph", "foo\n***", "<p>foo</p>\n<hr />\n"},
{"thematic long", "---------------------------------------", "<hr />\n"},
{"thematic trailing spaces", "*** ", "<hr />\n"},
{"thematic indented three", " ***", "<hr />\n"},
// Indented code blocks.
{"indented code", " code", "<pre><code>code\n</code></pre>\n"},
{"indented code two lines", " code\n more", "<pre><code>code\nmore\n</code></pre>\n"},
{"indented code keeps internal blank", " a\n\n b", "<pre><code>a\n\nb\n</code></pre>\n"},
{"indented code strips trailing blanks", " a\n\n\n", "<pre><code>a\n</code></pre>\n"},
{"tab indents code", "\tcode", "<pre><code>code\n</code></pre>\n"},
{"indented code cannot interrupt paragraph", "foo\n bar", "<p>foo\nbar</p>\n"},
// Fenced code blocks.
{"fenced backticks", "```\ncode\n```", "<pre><code>code\n</code></pre>\n"},
{"fenced tildes", "~~~\ncode\n~~~", "<pre><code>code\n</code></pre>\n"},
{"fenced info class", "```ruby\nx = 1\n```", "<pre><code class=\"language-ruby\">x = 1\n</code></pre>\n"},
{"fenced longer close", "````\n```\n````", "<pre><code>```\n</code></pre>\n"},
{"fenced short close is content", "````\ncode\n```", "<pre><code>code\n```\n</code></pre>\n"},
{"fenced indent stripping", " ```\n foo\nbar\n```", "<pre><code>foo\nbar\n</code></pre>\n"},
{"fenced empty", "```\n```", "<pre><code></code></pre>\n"},
{"fenced unclosed", "```\ncode", "<pre><code>code\n</code></pre>\n"},
{"tilde info may hold backticks", "~~~ js `x`\ncode\n~~~", "<pre><code class=\"language-js\">code\n</code></pre>\n"},
{"fenced blank lines kept", "```\na\n\nb\n```", "<pre><code>a\n\nb\n</code></pre>\n"},
// HTML blocks.
{"html type six", "<div>\nfoo\n</div>", "<div>\nfoo\n</div>\n"},
{"html comment", "<!-- comment\n-->", "<!-- comment\n-->\n"},
{"html processing instruction", "<?php\necho 1;\n?>", "<?php\necho 1;\n?>\n"},
{"html declaration ends at bracket", "<!DOCTYPE html>\nfoo", "<!DOCTYPE html>\n<p>foo</p>\n"},
{"html cdata", "<![CDATA[\nfoo\n]]>", "<![CDATA[\nfoo\n]]>\n"},
{"html script block", "<script>\nvar x = 1;\n</script>", "<script>\nvar x = 1;\n</script>\n"},
{"html blank ends six", "<div>\nfoo\n\nbar", "<div>\nfoo\n<p>bar</p>\n"},
{"html six interrupts paragraph", "Foo\n<div>", "<p>Foo</p>\n<div>\n"},
{"html textarea", "<textarea>\nfoo\n</textarea>", "<textarea>\nfoo\n</textarea>\n"},
{"html seven complete tag", "<a href=\"x\">\nfoo", "<a href=\"x\">\nfoo\n"},
// Block quotes.
{"quote basic", "> foo", "<blockquote>\n<p>foo</p>\n</blockquote>\n"},
{"quote two lines", "> foo\n> bar", "<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n"},
{"quote without space", ">foo", "<blockquote>\n<p>foo</p>\n</blockquote>\n"},
{"quote lazy", "> foo\nbar", "<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n"},
{"quote blank closes paragraph", "> foo\n\nbar", "<blockquote>\n<p>foo</p>\n</blockquote>\n<p>bar</p>\n"},
{"quote empty", ">", "<blockquote>\n</blockquote>\n"},
{"quote nested", "> > foo", "<blockquote>\n<blockquote>\n<p>foo</p>\n</blockquote>\n</blockquote>\n"},
{"quote heading", "> # foo", "<blockquote>\n<h1>foo</h1>\n</blockquote>\n"},
{"quote list", "> - foo", "<blockquote>\n<ul>\n<li>foo</li>\n</ul>\n</blockquote>\n"},
{"quote thematic interrupt", "> foo\n---", "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n"},
{"quote blank line inside", "> foo\n>\n> bar", "<blockquote>\n<p>foo</p>\n<p>bar</p>\n</blockquote>\n"},
{"quote blank line between", "> a\n\n> b", "<blockquote>\n<p>a</p>\n</blockquote>\n<blockquote>\n<p>b</p>\n</blockquote>\n"},
{"quote tab content", ">\tfoo", "<blockquote>\n<p>foo</p>\n</blockquote>\n"},
// Lists.
{"list bullet", "- foo", "<ul>\n<li>foo</li>\n</ul>\n"},
{"list star", "* foo", "<ul>\n<li>foo</li>\n</ul>\n"},
{"list plus", "+ foo", "<ul>\n<li>foo</li>\n</ul>\n"},
{"list tight items", "- foo\n- bar", "<ul>\n<li>foo</li>\n<li>bar</li>\n</ul>\n"},
{"list loose items", "- foo\n\n- bar", "<ul>\n<li>\n<p>foo</p>\n</li>\n<li>\n<p>bar</p>\n</li>\n</ul>\n"},
{"list blank splits items loose", "- foo\n- bar\n\n- baz", "<ul>\n<li>\n<p>foo</p>\n</li>\n<li>\n<p>bar</p>\n</li>\n<li>\n<p>baz</p>\n</li>\n</ul>\n"},
{"list bullet change splits", "* foo\n+ bar", "<ul>\n<li>foo</li>\n</ul>\n<ul>\n<li>bar</li>\n</ul>\n"},
{"ordered list", "1. foo\n2. bar", "<ol>\n<li>foo</li>\n<li>bar</li>\n</ol>\n"},
{"ordered start", "3. foo", "<ol start=\"3\">\n<li>foo</li>\n</ol>\n"},
{"ordered paren delimiter", "1) foo", "<ol>\n<li>foo</li>\n</ol>\n"},
{"ordered delimiter change splits", "1. foo\n1) bar", "<ol>\n<li>foo</li>\n</ol>\n<ol>\n<li>bar</li>\n</ol>\n"},
{"list item two paragraphs loose", "- foo\n\n bar", "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n"},
{"list nested", "- foo\n - bar", "<ul>\n<li>foo\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ul>\n"},
{"list item continuation", "- foo\n bar", "<ul>\n<li>foo\nbar</li>\n</ul>\n"},
{"list item lazy", "- foo\nbar", "<ul>\n<li>foo\nbar</li>\n</ul>\n"},
{"list lazy carries on", "- foo\n bar\ncar", "<ul>\n<li>foo\nbar\ncar</li>\n</ul>\n"},
{"ordered nine digits", "123456789. foo", "<ol start=\"123456789\">\n<li>foo</li>\n</ol>\n"},
{"ordered ten digits not a list", "1234567890. foo", "<p>1234567890. foo</p>\n"},
{"empty items", "- foo\n-\n- bar", "<ul>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ul>\n"},
{"list interrupts paragraph", "foo\n- bar", "<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n"},
{"ordered two cannot interrupt", "foo\n2. bar", "<p>foo\n2. bar</p>\n"},
{"single dash setext under paragraph", "foo\n-", "<h2>foo</h2>\n"},
{"bullet blank cannot interrupt", "foo\n+", "<p>foo\n+</p>\n"},
{"list marker five spaces makes code", "- indented code", "<ul>\n<li>\n<pre><code>indented code\n</code></pre>\n</li>\n</ul>\n"},
{"item fence on marker line", "- ```\n foo\n ```", "<ul>\n<li>\n<pre><code>foo\n</code></pre>\n</li>\n</ul>\n"},
{"item fence after paragraph", "- foo\n ```\n bar\n ```", "<ul>\n<li>foo\n<pre><code>bar\n</code></pre>\n</li>\n</ul>\n"},
{"item paragraph indented content", "1. foo\n bar", "<ol>\n<li>foo\nbar</li>\n</ol>\n"},
{"ordered paragraph two lines", "1. A paragraph\n with two lines.", "<ol>\n<li>A paragraph\nwith two lines.</li>\n</ol>\n"},
{"second item less indented", "- a\n - b", "<ul>\n<li>a</li>\n<li>b</li>\n</ul>\n"},
{"lazy into quoted item", "> - foo\nbar", "<blockquote>\n<ul>\n<li>foo\nbar</li>\n</ul>\n</blockquote>\n"},
{"loose ordered with blank", "1. a\n\n 2. b", "<ol>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n</ol>\n"},
{"list item second paragraph after blank", "- a\n- b\n\n c", "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n<p>c</p>\n</li>\n</ul>\n"},
// Paragraphs.
{"paragraph single", "foo", "<p>foo</p>\n"},
{"paragraph lines", "foo\nbar", "<p>foo\nbar</p>\n"},
{"paragraph leading spaces", " foo", "<p>foo</p>\n"},
{"paragraph escaping", "a < b & c", "<p>a &lt; b &amp; c</p>\n"},
{"paragraph quotes escape", "say \"hi\"", "<p>say &quot;hi&quot;</p>\n"},
{"blank lines separate", "foo\n\nbar", "<p>foo</p>\n<p>bar</p>\n"},
{"leading blanks ignored", "\n\nfoo", "<p>foo</p>\n"},
{"crlf normalised", "foo\r\nbar\r\n", "<p>foo\nbar</p>\n"},
// Link reference definitions.
{"reference definition alone", "[foo]: /url", ""},
{"reference definition with title", "[foo]: /url \"title\"", ""},
{"reference definition then text", "[foo]: /url\nused", "<p>used</p>\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", "<p>[foo]: /url junk</p>\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",
"<h1>Title</h1>\n<p>Intro text here.</p>\n<ul>\n<li>one</li>\n<li>two</li>\n</ul>\n" +
"<pre><code class=\"language-go\">fmt.Println(1)\n</code></pre>\n<blockquote>\n<p>quoted</p>\n</blockquote>\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)
}
})
}
}