feat: render Markdown, mathematics and Mermaid diagrams server-side
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
// 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 < b & c</p>\n"},
|
||||
{"paragraph quotes escape", "say \"hi\"", "<p>say "hi"</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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user