123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 | package markdown
import (
"net/url"
"strings"
"testing"
"code.bruce-hill.com/repo-renderer/internal/highlight"
)
// escapePath percent-encodes each segment of a slash-separated path, as
// the renderer's URLs do.
func escapePath(p string) string {
segs := strings.Split(p, "/")
for i, s := range segs {
segs[i] = url.PathEscape(s)
}
return strings.Join(segs, "/")
}
func pageURL(p string) string {
if p == "" {
return "/code/r"
}
return "/code/r/" + escapePath(p)
}
func TestResolve(t *testing.T) {
ctx := Context{Dir: "docs/sub", PageURL: pageURL,
ImageURL: func(p string) string { return "/raw/r/" + escapePath(p) }}
cases := []struct{ in, want string }{
{"guide.md", "/code/r/docs/sub/guide.md"},
{"./guide.md#top", "/code/r/docs/sub/guide.md#top"},
{"../intro.md", "/code/r/docs/intro.md"},
{"../../README.md", "/code/r/README.md"},
{"../../", "/code/r"},
{"../../../escape.md", "../../../escape.md"},
{"a%20b.md", "/code/r/docs/sub/a%20b.md"},
{"https://example.com/x", "https://example.com/x"},
{"//cdn.example.com/x", "//cdn.example.com/x"},
{"mailto:a@b.c", "mailto:a@b.c"},
{"#frag", "#frag"},
{"/top.md", "/code/r/top.md"},
{"/src/", "/code/r/src"},
{"/", "/code/r"},
{"/../escape.md", "/../escape.md"},
{"", ""},
}
for _, c := range cases {
if got := Resolve(c.in, ctx, false); got != c.want {
t.Errorf("Resolve(%q) = %q, want %q", c.in, got, c.want)
}
}
if got := Resolve("pic.png", ctx, true); got != "/raw/r/docs/sub/pic.png" {
t.Errorf("image: %q", got)
}
if got := Resolve("/d.png", ctx, true); got != "/raw/r/d.png" {
t.Errorf("root-relative image: %q", got)
}
if got := Resolve("../d.svg", ctx, false); got != "/code/r/docs/d.svg" {
t.Errorf("link to an image: %q", got)
}
}
func TestRender(t *testing.T) {
hl, err := highlight.New(nil, nil, "github")
if err != nil {
t.Fatal(err)
}
r := New(hl)
src := "# Title\n\n- [x] done\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n~~gone~~ <em>raw</em>\n\n<div>block</div>\n\n```go\nvar x int\n```\n\n indented\n\nfoot[^1]\n\n[^1]: note\n"
out, err := r.Render([]byte(src), Context{PageURL: pageURL, ImageURL: func(p string) string { return "/raw/r/" + p }})
if err != nil {
t.Fatal(err)
}
s := string(out)
for _, want := range []string{
`<h1 id="title">Title</h1>`, `type="checkbox"`, `<table>`, `<del>gone</del>`,
`<em>raw</em>`, `<pre class="raw-html"><div>block</div>`,
`<pre class="chroma" data-lang="go"><code class="chroma"><span class="kd">var</span>`,
`<pre class="chroma"><code class="chroma">indented`, `class="footnotes"`,
} {
if !strings.Contains(s, want) {
t.Errorf("missing %q in:\n%s", want, s)
}
}
if strings.Contains(s, "<em>raw</em>") || strings.Contains(s, "<div>block</div>") {
t.Error("raw HTML passed through")
}
}
func TestStripPandocMeta(t *testing.T) {
cases := []struct{ in, want string }{
{"% Title\n% Me\n% 2026\n\n# Body\n", "\n# Body\n"},
{"% A very long\n title\n% Me; You\n# Body\n", "# Body\n"},
{"% Only title\nText\n", "Text\n"},
{"# No meta\n% not at start\n", "# No meta\n% not at start\n"},
{"%\n", ""},
{"% no newline", ""},
{"", ""},
}
for _, c := range cases {
if got := string(StripPandocMeta([]byte(c.in))); got != c.want {
t.Errorf("StripPandocMeta(%q) = %q, want %q", c.in, got, c.want)
}
}
hl, err := highlight.New(nil, nil, "github")
if err != nil {
t.Fatal(err)
}
out, err := New(hl).Render([]byte("% My Title\n% Author\n\nHello *world*\n"), Context{})
if err != nil {
t.Fatal(err)
}
if s := string(out); strings.Contains(s, "My Title") || strings.Contains(s, "Author") || !strings.Contains(s, "<em>world</em>") {
t.Errorf("rendered: %s", s)
}
}
// Script-capable URLs must never reach an href or src, however they are
// written (inline, reference, autolink, entity-encoded, image),
// and attribute-breaking text in a fence info string must stay inside the
// attribute.
func TestHostileMarkup(t *testing.T) {
hl, err := highlight.New(nil, nil, "github")
if err != nil {
t.Fatal(err)
}
r := New(hl)
src := "[a](javascript:alert(1)) [b](JaVaScRiPt:alert(2)) [c](javascript:alert(3))\n\n" +
"[d][ref] <javascript:alert(4)> ) [f](vbscript:x) [g](data:text/html;base64,AAAA)\n\n" +
"[ref]: javascript:alert(6)\n\n" +
"```x\" onmouseover=\"alert(7)\nbody\n```\n"
out, err := r.Render([]byte(src), Context{PageURL: pageURL, ImageURL: func(p string) string { return "/raw/r/" + p }})
if err != nil {
t.Fatal(err)
}
s := strings.ToLower(string(out))
for _, bad := range []string{`href="javascript`, `src="javascript`, `href="vbscript`, `href="data:`, `href="j`, `" onmouseover=`} {
if strings.Contains(s, bad) {
t.Errorf("output contains %q:\n%s", bad, out)
}
}
}
|