package markdown import ( "strings" "testing" ) func TestResolveHrefNotRewritten(t *testing.T) { cases := []struct{ dir, href string }{ {"", "http://example.com/img.png"}, {"docs", "http://example.com/img.png"}, {"", "https://example.com/img.png"}, {"", "ftp://files.example.com/"}, {"", "mailto:foo@bar.com"}, {"", "data:image/png;base64,abc123"}, {"", "ssh://git@example.com/repo.git"}, {"", "myapp://open/something"}, {"", "#section-heading"}, {"docs/guide", "#toc"}, {"", "#"}, } for _, c := range cases { if got, ok := ResolveHref(c.dir, c.href); ok { t.Errorf("ResolveHref(%q, %q) = %q, want no rewrite", c.dir, c.href, got) } } } func TestResolveHref(t *testing.T) { cases := []struct{ dir, href, want string }{ // Root-relative: dir is ignored. {"", "/img.png", "img.png"}, {"docs", "/subdir/img.png", "subdir/img.png"}, {"x/y/z", "/a/b/c.png", "a/b/c.png"}, {"docs", "/assets/", "assets/"}, // Path-relative from the repo root. {"", "img.png", "img.png"}, {"", "./img.png", "img.png"}, {"", "subdir/img.png", "subdir/img.png"}, {"", "./subdir/img.png", "subdir/img.png"}, {"", "../img.png", "img.png"}, // Path-relative from one level down. {"docs", "img.png", "docs/img.png"}, {"docs", "./img.png", "docs/img.png"}, {"docs", "../img.png", "img.png"}, {"docs", "subdir/img.png", "docs/subdir/img.png"}, {"docs", "./subdir/img.png", "docs/subdir/img.png"}, // Path-relative from a nested directory. {"docs/guide", "img.png", "docs/guide/img.png"}, {"docs/guide", "../img.png", "docs/img.png"}, {"docs/guide", "../../img.png", "img.png"}, {"docs/guide", "../assets/img.png", "docs/assets/img.png"}, {"docs/guide", "./img.png", "docs/guide/img.png"}, {"docs/guide", "sub/img.png", "docs/guide/sub/img.png"}, } for _, c := range cases { got, ok := ResolveHref(c.dir, c.href) if !ok || got != c.want { t.Errorf("ResolveHref(%q, %q) = %q,%v want %q,true", c.dir, c.href, got, ok, c.want) } } } func TestRenderGFM(t *testing.T) { r := New() cases := []struct { name string in string want []string }{ { "table alignment", "| a | b |\n|---|:-:|\n| 1 | 2 |", []string{"", "", ``, ``}, }, {"strikethrough", "~~gone~~", []string{"gone"}}, { "task list", "- [x] done\n- [ ] todo", []string{``}}, { "details survive sanitization", "
sbody
", []string{"
", "s", "body", "
"}, }, } for _, c := range cases { got := r.Render(c.in, "", nil) for _, want := range c.want { if !strings.Contains(got, want) { t.Errorf("%s: %q is missing %q", c.name, got, want) } } } } func TestRenderFencedCodeIsHighlighted(t *testing.T) { r := New() got := r.Render("```js\nlet x=1;\n```", "", nil) if !strings.Contains(got, `class="language-js"`) { t.Errorf("missing language class: %q", got) } if !strings.Contains(got, `alert(1)", "", nil) if strings.Contains(got, "script") || strings.Contains(got, "onerror") { t.Errorf("unsafe html survived: %q", got) } if got := r.Render("[x](javascript:alert(1))", "", nil); strings.Contains(got, "javascript:") { t.Errorf("javascript link survived: %q", got) } } func TestRenderRepoContext(t *testing.T) { r := New() ctx := &Context{Repo: "myrepo", Ref: "main", Dir: "docs"} got := r.Render("[r](./a.md) [abs](/b.md) [ext](https://e.com) [an](#s) ![i](./x.png)", "", ctx) for _, want := range []string{ `r`, `abs`, `ext`, `an`, `src="/myrepo/raw/main/docs/x.png"`, } { if !strings.Contains(got, want) { t.Errorf("%q is missing %q", got, want) } } if got := r.Render("[r](./a.md)", "", nil); !strings.Contains(got, `href="./a.md"`) { t.Errorf("without context the href must stay: %q", got) } } func TestRenderCache(t *testing.T) { r := New() first := r.Render("# hello", "key", nil) // A cache hit must ignore the new source. if second := r.Render("# different", "key", nil); second != first { t.Errorf("cache miss: %q != %q", second, first) } for i := range maxMDCache + 5 { r.Render("x", string(rune('a'+i%26))+string(rune(i)), nil) } if r.cache.Len() > maxMDCache { t.Errorf("cache grew to %d, want at most %d", r.cache.Len(), maxMDCache) } } func TestToPlaintext(t *testing.T) { cases := []struct{ name, in, want string }{ {"unordered list", "- a\n- b", "- a\n- b"}, {"ordered list keeps its start", "3. a\n4. b", "3. a\n4. b"}, {"task list markers", "- [x] done\n- [ ] todo\n- plain", "- [x] done\n- [ ] todo\n- plain"}, {"table cells are pipe-joined", "| a | b |\n|---|---|\n| 1 | 2 |", "a | b\n1 | 2"}, {"blockquote keeps its marker", "> one\n> two", "> one\n> two"}, { "inline markup is unwrapped", "# Title\n\nSome **bold** [link](https://e.com) and `code`.\n\n---\n\n![alt](x.png)", "Title\n\nSome bold link and `code`.\n\n---\n\n[Image: alt]", }, {"raw html is stripped", "a b c\n\n
d
", "a b c"}, {"empty input", "", ""}, } for _, c := range cases { if got := ToPlaintext(c.in); got != c.want { t.Errorf("%s: ToPlaintext(%q) = %q, want %q", c.name, c.in, got, c.want) } } } // The stylesheet selects on both classes, so the markup must stay exactly this. func TestTaskListMarkup(t *testing.T) { want := ` ` if got := New().Render("- [x] done\n- [ ] todo\n", "", nil); got != want { t.Errorf("task list markup:\n got %q\nwant %q", got, want) } } // A README at one commit is reachable under every ref that points at it. The // rewritten links carry the ref, so the ref has to be part of the cache key. func TestRenderCacheKeyIncludesRef(t *testing.T) { r := New() key := "readme:repo:README.md:" main := r.Render("[doc](doc.md)", key, &Context{Repo: "repo", Ref: "main"}) tag := r.Render("[doc](doc.md)", key, &Context{Repo: "repo", Ref: "v1.0"}) if !strings.Contains(main, "/blob/main/") { t.Fatalf("main render lost its ref: %s", main) } if !strings.Contains(tag, "/blob/v1.0/") { t.Errorf("tag render served the cached main entry: %s", tag) } }
ab2