// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package markdown import "testing" // inlineCorpus is the project's own hand-written corpus for the inline // layer, on top of a paragraph unless the case states otherwise. var inlineCorpus = []struct { name string input string want string }{ // Emphasis. {"em asterisk", "*foo*", "

foo

\n"}, {"strong asterisk", "**foo**", "

foo

\n"}, {"em and strong", "***foo***", "

foo

\n"}, {"strong with inner em", "**foo *bar* baz**", "

foo bar baz

\n"}, {"intraword asterisk", "foo*bar*baz", "

foobarbaz

\n"}, {"intraword underscore stays", "foo_bar_baz", "

foo_bar_baz

\n"}, {"em underscore with spaces", "_foo bar_", "

foo bar

\n"}, {"underscore cannot open after space", "_ foo_", "

_ foo_

\n"}, {"literal asterisk when spaced", "a * foo *", "

a * foo *

\n"}, {"escaped asterisk", "\\*not em\\*", "

*not em*

\n"}, {"escaped backslash then em", "\\\\*foo*", "

\\foo

\n"}, {"lone closer stays", "a *", "

a *

\n"}, {"em inside word boundaries", "a*b*c", "

abc

\n"}, {"em holding strong", "*foo**bar**baz*", "

foobarbaz

\n"}, {"strong inside em with text", "***foo** bar*", "

foo bar

\n"}, {"em inside strong at end", "**foo *bar***", "

foo bar

\n"}, {"unmatched inner run stays", "*foo**bar*", "

foo**bar

\n"}, {"intraword digits", "5*6*78", "

5678

\n"}, {"underscore opens before punctuation", "_(bar)_", "

(bar)

\n"}, {"intraword underscore before punctuation stays", "foo_(bar)_", "

foo_(bar)_

\n"}, // Code spans. {"code span", "`foo`", "

foo

\n"}, {"code span strips one space margin", "` foo `", "

foo

\n"}, {"code span keeps margin with doubles", "`` foo ``", "

foo

\n"}, {"code span double backticks", "``foo ` bar``", "

foo ` bar

\n"}, {"code span has no escapes", "`foo\\`bar`", "

foo\\bar`

\n"}, {"code span unmatched", "foo ` bar", "

foo ` bar

\n"}, {"code span escapes markup", "`*em*`", "

*em*

\n"}, // Links and images. {"inline link", "[foo](/uri)", "

foo

\n"}, {"inline link with title", "[foo](/uri \"title\")", "

foo

\n"}, {"inline link single quoted title", "[foo](/uri 'title')", "

foo

\n"}, {"inline link empty destination", "[foo]()", "

foo

\n"}, {"angle destination with space", "[foo]()", "

foo

\n"}, {"trailing paren stays text", "[foo](bar))", "

foo)

\n"}, {"em inside link", "[*foo*](/uri)", "

foo

\n"}, {"image with title", "![foo](/url \"title\")", "

\"foo\"

\n"}, {"image inside link", "[![alt](img)](page)", "

\"alt\"

\n"}, {"no nested links", "[a [b](x)](y)", "

[a b](y)

\n"}, {"undefined reference stays", "[foo]", "

[foo]

\n"}, {"link destination escaped ampersand", "[a](/url?a=1&b=2)", "

a

\n"}, // Autolinks. {"uri autolink", "", "

http://example.com

\n"}, {"email autolink", "", "

foo@bar.example.com

\n"}, {"not an autolink", "<3>", "

<3>

\n"}, // Extended autolinks (GFM). {"extended www", "Visit www.commonmark.org for more.", "

Visit www.commonmark.org for more.

\n"}, {"extended www with path", "go to www.commonmark.org/help today", "

go to www.commonmark.org/help today

\n"}, {"extended trailing punctuation", "see www.example.com.", "

see www.example.com.

\n"}, {"extended paren balance", "www.example.com/query?q=(a+b)))", "

www.example.com/query?q=(a+b)))

\n"}, {"extended entity suffix", "www.example.com?q=x&hl;", "

www.example.com?q=x&hl;

\n"}, {"extended https", "open https://example.com/page", "

open https://example.com/page

\n"}, {"extended ftp", "ftp://files.example.org/pub", "

ftp://files.example.org/pub

\n"}, {"extended email", "write to a.b-c_d@example.com soon", "

write to a.b-c_d@example.com soon

\n"}, {"extended email trailing dot", "mail me at user@example.net.", "

mail me at user@example.net.

\n"}, {"plus before at only", "hello@mail+xyz.example is not, but hello+xyz@mail.example is", "

hello@mail+xyz.example is not, but hello+xyz@mail.example is

\n"}, {"underscore banned in last segments", "www.un_der_score.org stays text", "

www.un_der_score.org stays text

\n"}, {"no autolink mid word", "awww.example.com stays text", "

awww.example.com stays text

\n"}, // Raw inline HTML and entities. {"raw inline html", "a c d", "

a c d

\n"}, {"html comment inline", "a b", "

a b

\n"}, {"entity ampersand", "AT&T", "

AT&T

\n"}, {"entity numeric", "#", "

#

\n"}, {"entity hex", """, "

"

\n"}, {"bare ampersand", "AT&T", "

AT&T

\n"}, {"not an entity", "&x;", "

&x;

\n"}, {"less than escaped", "a < b", "

a < b

\n"}, // Breaks. {"soft break", "foo\nbar", "

foo\nbar

\n"}, {"hard break spaces", "foo \nbar", "

foo
\nbar

\n"}, {"hard break backslash", "foo\\\nbar", "

foo
\nbar

\n"}, {"one trailing space is soft", "foo \nbar", "

foo\nbar

\n"}, {"trailing spaces at end dropped", "foo ", "

foo

\n"}, } func TestInlineCorpus(t *testing.T) { for _, tc := range inlineCorpus { 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 links need definitions from earlier blocks, so these cases // carry multi-block inputs. func TestReferenceLinks(t *testing.T) { cases := []struct { name string input string want string }{ {"explicit reference", "[foo][bar]\n\n[bar]: /url", "

foo

\n"}, {"collapsed reference", "[foo][]\n\n[foo]: /url", "

foo

\n"}, {"shortcut reference", "[foo]\n\n[foo]: /url", "

foo

\n"}, {"reference with title", "[foo]\n\n[foo]: /url \"the title\"", "

foo

\n"}, {"reference label case folded", "[Foo]\n\n[foo]: /url", "

Foo

\n"}, {"image reference", "![foo]\n\n[foo]: /url", "

\"foo\"

\n"}, {"shortcut takes whole text", "[foo *bar*]\n\n[foo *bar*]: /url", "

foo bar

\n"}, {"inline beats reference", "[foo](/inline)\n\n[foo]: /ref", "

foo

\n"}, {"link in heading", "# [foo](/uri)", "

foo

\n"}, {"code span in heading", "## a `b` c", "

a b c

\n"}, } for _, tc := range cases { 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) } }) } }