Artifact c7609bd31272593bb51f45c36846839b8c6c2bfe9da217b59638698d18e6e47a:
- File
internal/markup/fossilwiki_test.go
— part of check-in
[0ba22dea9e]
at
2026-08-23 22:31:40
on branch trunk
— markdown: mentions carry the identity mark, quotes are visible
An @mention now renders the same glyph as the person's byline, through the resolver seam so markup still knows nothing about accounts. The identicon moved to internal/idn — web and api each hashed the name separately and agreed by luck. Blockquotes had no rule at all. (user: dmestas, size: 6095) [annotate] [blame] [check-ins using]
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 "" }