package markup
import (
"strings"
"testing"
)
type fakeRes struct{}
func (fakeRes) ResolveHash(p string) string { return "/r/x/checkin/" + p }
func (fakeRes) ResolveWiki(n string) string {
if n == "Missing" {
return ""
}
return "/r/x/wiki/" + n
}
func TestFossilWikiParagraphsAndLists(t *testing.T) {
src := `First paragraph line one.
line two of the same paragraph.
Second paragraph.
* bullet one
* bullet two
# first enumerated
# second enumerated
`
got := string(mustFW(t, src, nil))
if strings.Count(got, "<p>") != 2 {
t.Fatalf("want 2 paragraphs:\n%s", got)
}
if !strings.Contains(got, "<ul>") || strings.Count(got, "<li>") != 4 {
t.Fatalf("lists wrong:\n%s", got)
}
if !strings.Contains(got, "<ol>") {
t.Fatalf("enumeration list missing:\n%s", got)
}
}
// The construct that motivated all of this: [target|label] links, which
// markdown renders as literal text.
func TestFossilWikiLinks(t *testing.T) {
got := string(mustFW(t, "See [WikiPage|the page] and [https://example.com|a site].", fakeRes{}))
if !strings.Contains(got, `<a href="/r/x/wiki/WikiPage" rel="nofollow">the page</a>`) &&
!strings.Contains(got, `href="/r/x/wiki/WikiPage"`) {
t.Fatalf("wiki link not resolved:\n%s", got)
}
if !strings.Contains(got, `href="https://example.com"`) || !strings.Contains(got, ">a site<") {
t.Fatalf("external link not rendered:\n%s", got)
}
// Bare [target] uses the target as the label.
got = string(mustFW(t, "Go to [WikiPage].", fakeRes{}))
if !strings.Contains(got, ">WikiPage<") {
t.Fatalf("bare link label:\n%s", got)
}
// Unresolvable targets stay literal rather than becoming dead links.
got = string(mustFW(t, "See [Missing|nope].", fakeRes{}))
if strings.Contains(got, "<a ") {
t.Fatalf("unresolvable target linked:\n%s", got)
}
}
func TestFossilWikiVerbatimAndHTML(t *testing.T) {
got := string(mustFW(t, "<verbatim>[not a link] *not a bullet*</verbatim>", nil))
if !strings.Contains(got, "<pre>") || !strings.Contains(got, "[not a link]") {
t.Fatalf("verbatim not preserved:\n%s", got)
}
if strings.Contains(got, "<a ") {
t.Fatal("verbatim content was link-processed")
}
// Ordinary HTML passes through (rule 6) but still meets the sanitizer.
got = string(mustFW(t, "Text with <b>bold</b> and <script>alert(1)</script>.", nil))
if !strings.Contains(got, "<b>bold</b>") {
t.Fatalf("inline HTML dropped:\n%s", got)
}
if strings.Contains(got, "<script") || strings.Contains(got, "alert(1)") {
t.Fatalf("sanitizer gate leaked script:\n%s", got)
}
}
func TestFossilWikiIndentedParagraph(t *testing.T) {
got := string(mustFW(t, "Normal paragraph.\n\n indented paragraph here\n", nil))
if !strings.Contains(got, "<blockquote>") {
t.Fatalf("indented paragraph not rendered:\n%s", got)
}
}
func mustFW(t *testing.T, src string, r Resolver) []byte {
t.Helper()
out, err := RenderFossilWiki([]byte(src), r)
if err != nil {
t.Fatal(err)
}
return out
}
// Fossil site-relative routes: /doc maps onto quarry's file route, and
// a verb quarry has no page for drops the link rather than 404ing.
func (fakeRes) ResolvePath(p string) string {
if strings.HasPrefix(p, "/doc/") {
return "/r/x/file/" + strings.TrimPrefix(p, "/doc/")
}
return ""
}
func TestFossilWikiSiteRelativeLinks(t *testing.T) {
got := string(mustFW(t, "See [/doc/trunk/README.md|the readme] and [/finfo?name=x|file history].", fakeRes{}))
if !strings.Contains(got, `href="/r/x/file/trunk/README.md"`) {
t.Fatalf("/doc not mapped to the file route:\n%s", got)
}
if strings.Contains(got, "/finfo") || strings.Contains(got, "[/finfo") {
t.Fatalf("unmapped route left as a link or raw markup:\n%s", got)
}
if !strings.Contains(got, "file history") {
t.Fatalf("label lost when link was dropped:\n%s", got)
}
}
// Fossil's docs link each other relatively — [./quickstart.wiki|Quick
// Start] — and a wiki artifact, which sits at no path, has no sibling to
// resolve that against. Only a DocResolver gets those links; everyone
// else keeps the old literal, so a wiki page cannot emit an href
// relative to a URL it does not live at.
type docRes struct{ fakeRes }
func (docRes) DocRelative(t string) string { return t }
func TestRelativeLinksResolveOnlyForDocuments(t *testing.T) {
src := []byte(`[./quickstart.wiki | Quick Start] and [../COPYRIGHT-BSD2.txt|License] ` +
`and [./concepts.wiki#workflow | autosync]`)
doc, err := RenderFossilWiki(src, docRes{})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
`href="./quickstart.wiki"`,
`href="../COPYRIGHT-BSD2.txt"`,
`href="./concepts.wiki#workflow"`, // a fragment rides along
} {
if !strings.Contains(string(doc), want) {
t.Errorf("document render missing %s:\n%s", want, doc)
}
}
// A wiki page must not: it would emit a link relative to /wiki/Name.
page, err := RenderFossilWiki(src, fakeRes{})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(page), `href="./`) {
t.Errorf("a wiki artifact emitted a relative document link:\n%s", page)
}
}
// "a safe subset of HTML" includes character references, so an ampersand
// the author already spelled must not be spelled again.
func TestLinkLabelsKeepEntitiesTheAuthorWrote(t *testing.T) {
out, err := RenderFossilWiki([]byte(`[./hints.wiki | Tips & Hints]`), docRes{})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(out), "&amp;") {
t.Errorf("entity escaped twice:\n%s", out)
}
if !strings.Contains(string(out), "Tips & Hints") {
t.Errorf("label lost its ampersand:\n%s", out)
}
// A bare ampersand still has to be escaped exactly once.
bare, err := RenderFossilWiki([]byte(`[./a.wiki | Tom & Jerry]`), docRes{})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(bare), "Tom & Jerry") {
t.Errorf("bare ampersand not escaped:\n%s", bare)
}
}
// The test resolvers draw no identity mark: an empty answer is valid and
// renders a mention as a plain link, which keeps these assertions about
// the constructs they are actually testing.
func (fakeRes) UserMark(string) string { return "" }
// The base fake knows no attachments; attachRes in attach_test.go is the
// one that does.
func (fakeRes) ResolveAttachment(string, bool) string { return "" }