package main
import (
"os/exec"
"strings"
"testing"
)
// A fence in a known language gets pandoc's markup without line numbers;
// any other fence is plain.
func TestFences(t *testing.T) {
h, err := renderMarkdown([]byte("```go\nx := 1\n```\n\n```nosuch\n<b>\n```\n"), nil, nil)
if err != nil {
t.Fatal(err)
}
body := string(h)
if !strings.Contains(body, `<div class="sourceCode"><pre class="sourceCode go"><code class="sourceCode go"><span>x <span class="op">:=</span> <span class="dv">1</span></span></code></pre></div>`) {
t.Errorf("go fence not highlighted as pandoc does: %s", body)
}
if !strings.Contains(body, "<pre><code><b>\n</code></pre>") {
t.Errorf("unknown fence not plain: %s", body)
}
}
// Every line of a code page is numbered, a blank last line included, and
// file content cannot forge a line id.
func TestCodeLines(t *testing.T) {
var b strings.Builder
highlight(&b, codeLexer("x.txt"), "a\n\"<span id=\"L9\">\n\n", true, false)
out := b.String()
if n := strings.Count(out, `<span id="L`); n != 3 {
t.Errorf("%d numbered lines, want 3: %s", n, out)
}
if !strings.Contains(out, `<span id="L2"><a href="#L2"></a>"<span id="L9"></span>`) {
t.Errorf("content not escaped: %s", out)
}
}
func TestCodeLexerByName(t *testing.T) {
for name, want := range map[string]string{"main.go": "go", "Makefile": "make", "Dockerfile": "docker", "notes.unknownext": ""} {
if got := langName(codeLexer(name)); got != want {
t.Errorf("%s: language %q, want %q", name, got, want)
}
}
}
// Task list boxes are not disabled, so they keep the browser's colour.
func TestTaskBoxes(t *testing.T) {
h, err := renderMarkdown([]byte("- [x] done\n- [ ] open\n"), nil, nil)
if err != nil {
t.Fatal(err)
}
if body := string(h); !strings.Contains(body, `<li><input type="checkbox" checked=""> done</li>`) || !strings.Contains(body, `<li><input type="checkbox"> open</li>`) || strings.Contains(body, "disabled") {
t.Errorf("task boxes not rendered as pandoc's: %s", body)
}
}
// A quote that opens with [!KIND] alone on its line is an alert: the marker
// becomes a label. Any other quote stays a quote.
func TestAlerts(t *testing.T) {
for src, want := range map[string]string{
"> [!WARNING]\n> Be `careful`.\n": "<blockquote class=\"alert warning\"><p class=\"own alert\">warning</p>\n<p>Be <code>careful</code>.</p>\n</blockquote>\n",
"> [!tip]\n>\n> ```sh\n> make\n> ```\n": "<blockquote class=\"alert tip\"><p class=\"own alert\">tip</p>\n<div class=\"sourceCode\">",
"> [!NOTE] Same line.\n": "<blockquote>\n<p>[!NOTE] Same line.</p>\n</blockquote>\n",
"> [!TODO]\n> Not a kind.\n": "<blockquote>\n<p>[!TODO]\nNot a kind.</p>\n</blockquote>\n",
} {
h, err := renderMarkdown([]byte(src), nil, nil)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(h), want) {
t.Errorf("%q rendered\n%s\nwant prefix\n%s", src, h, want)
}
}
}
// Go is lexed by the standard library's scanner: every byte comes back,
// and tokens get the classes chroma's Go lexer gives them.
func TestGoTokens(t *testing.T) {
src := "package a\r\n\n//go:embed x\n/* block\n */\nfunc F(s string) int { return len(s) + 0x1F + 0o7 }\nvar r = `raw\nstring` // c\n"
var text strings.Builder
for _, tok := range goTokens(src) {
text.WriteString(tok.Value)
}
if text.String() != src {
t.Fatalf("text not preserved:\n%q\n%q", text.String(), src)
}
var b strings.Builder
highlight(&b, codeLexer("a.go"), src, false, false)
for _, want := range []string{`<span class="pp">//go:embed x</span>`, `<span class="kw">func</span> <span class="fu">F</span>`,
`<span class="dt">string</span>`, `<span class="bu">len</span>`, `<span class="bn">0x1F</span>`, `<span class="bn">0o7</span>`,
`<span class="st">` + "`raw</span>", `<span class="co">// c</span>`} {
if !strings.Contains(b.String(), want) {
t.Errorf("no %s in %s", want, b.String())
}
}
}
// A relative link to a missing path is marked; others are left alone.
func TestBrokenLinks(t *testing.T) {
missing := func(dest string) bool { return dest == "gone.md" || dest == "a b.md" }
h, err := renderMarkdown([]byte("[x](gone.md#top) [y](here.md) [z](https://example.com/gone.md) [w](a%20b.md) [v](#gone.md) [u](/gone.md)\n"), missing, nil)
if err != nil {
t.Fatal(err)
}
s := string(h)
if n := strings.Count(s, `class="broken"`); n != 2 {
t.Errorf("%d broken links, want 2: %s", n, s)
}
if !strings.Contains(s, `<a href="gone.md#top" class="broken" title="missing: gone.md">`) {
t.Errorf("gone.md not marked: %s", s)
}
}
// A dot fence becomes inline SVG when dot is installed, and code with the
// reason otherwise.
func TestGraphFence(t *testing.T) {
h, err := renderMarkdown([]byte("```dot\ndigraph { a -> b }\n```\n"), nil, nil)
if err != nil {
t.Fatal(err)
}
s := string(h)
if _, err := exec.LookPath("dot"); err != nil {
if !strings.Contains(s, "dot is not installed") {
t.Errorf("no reason given: %s", s)
}
return
}
if !strings.Contains(s, `<div class="graph"><svg`) || strings.Contains(s, "<?xml") {
t.Errorf("not inline SVG: %.200s", s)
}
h, _ = renderMarkdown([]byte("```dot\ndigraph { a -> }\n```\n"), nil, nil)
if s := string(h); !strings.Contains(s, `class="own warn"`) || !strings.Contains(s, "<pre") {
t.Errorf("bad graph not shown as code with a reason: %s", s)
}
}
// A wiki link resolves as a relative link does, takes a label and a
// heading, is marked broken when its target is missing, and leaves
// CommonMark's own links alone.
func TestWikiLinks(t *testing.T) {
missing := func(dest string) bool { return dest == "gone.md" }
for src, want := range map[string]string{
"[[name]]": `<a href="name.md">name</a>`,
"[[file.md]]": `<a href="file.md">file.md</a>`,
"[[sub/a b|the label]]": `<a href="sub/a%20b.md">the label</a>`,
"[[name#Two Words]]": `<a href="name.md#two-words">name#Two Words</a>`,
"[[#Here]]": `<a href="#here">#Here</a>`,
"[[a:b]]": `<a href="./a:b.md">a:b</a>`,
"[[gone]]": `<a href="gone.md" class="broken" title="missing: gone.md">gone</a>`,
"[[x]](y.md)": `<a href="y.md">[x]</a>`,
"[[]] and [[a]b]]": `<p>[[]] and [[a]b]]</p>`,
"`[[code]]`": `<code>[[code]]</code>`,
"see [[a]] and [[b|B]] ok": `see <a href="a.md">a</a> and <a href="b.md">B</a> ok`,
} {
h, err := renderMarkdown([]byte(src), missing, nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(h), want) {
t.Errorf("%s: want %s in %s", src, want, h)
}
}
}
// A link to a forge's repo, bare or not, is followed by a mark that reads
// it on the web address; other links are not.
func TestWebMarks(t *testing.T) {
base := "http://127.31.1.2:8080"
src := "[fzf](https://github.com/junegunn/fzf/blob/master/README.md#usage), https://github.com/junegunn/fzf and [x](https://example.com/a/b)\n"
h, err := renderMarkdown([]byte(src), nil, &base)
if err != nil {
t.Fatal(err)
}
s := string(h)
if n := strings.Count(s, `class="web"`); n != 2 {
t.Errorf("%d marks, want 2: %s", n, s)
}
for _, want := range []string{
`fzf</a><a class="web" href="http://127.31.1.2:8080/github.com/junegunn/fzf/README.md#usage" title="read github.com/junegunn/fzf in ` + progName + `">` + progName + `</a>`,
`https://github.com/junegunn/fzf</a><a class="web" href="http://127.31.1.2:8080/github.com/junegunn/fzf/"`,
} {
if !strings.Contains(s, want) {
t.Errorf("no %s in %s", want, s)
}
}
if h, _ := renderMarkdown([]byte(src), nil, nil); strings.Contains(string(h), `class="web"`) {
t.Errorf("marks without a web address: %s", h)
}
}
// On the web address a forge link reads its repo there, and its mark
// leads to the forge; a bare URL keeps its text.
func TestWebLinksTurnAround(t *testing.T) {
base := ""
src := "[fzf](https://github.com/junegunn/fzf/blob/master/README.md#usage \"t\"), https://github.com/junegunn/fzf, [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt) and [x](https://example.com/a/b)\n"
h, err := renderMarkdown([]byte(src), nil, &base)
if err != nil {
t.Fatal(err)
}
s := string(h)
for _, want := range []string{
`<a href="/github.com/junegunn/fzf/README.md#usage" title="t">fzf</a><a class="web" href="https://github.com/junegunn/fzf/blob/master/README.md#usage" title="open on github.com">github</a>`,
`<a href="/github.com/junegunn/fzf/">https://github.com/junegunn/fzf</a><a class="web" href="https://github.com/junegunn/fzf" title="open on github.com">github</a>`,
`<a href="/github.com/rivo/uniseg/LICENSE.txt">MIT</a><a class="web" href="https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt" title="open on raw.githubusercontent.com">github</a>`,
`<a href="https://example.com/a/b">x</a></p>`,
} {
if !strings.Contains(s, want) {
t.Errorf("no %s in %s", want, s)
}
}
if n := strings.Count(s, `class="web"`); n != 3 {
t.Errorf("%d marks, want 3: %s", n, s)
}
}
// Lines of a doc join into one paragraph, as standard Markdown has them,
// and a dash after a line starts a list.
func TestLinesJoin(t *testing.T) {
s, _, _ := serveTree(t)
body := get(s.handler(), "/index.md").Body.String()
if !strings.Contains(body, "<p>line one\nline two</p>") {
t.Errorf("lines not joined into one paragraph: %s", body)
}
if !strings.Contains(body, "<ul>\n<li>item</li>") {
t.Errorf("a dash after a line does not start a list: %s", body)
}
}
func TestBareURLIsLink(t *testing.T) {
s, _, _ := serveTree(t)
body := get(s.handler(), "/index.md").Body.String()
if !strings.Contains(body, `<a href="https://example.com/x"`) {
t.Errorf("bare URL not linked: %s", body)
}
}