| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "net/url" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | func TestMarkdownRendersCommonFormsAndEscapesTheRest(t *testing.T) { |
| 10 | got := string(renderMarkdown("# Title\n\nA `code` span with **bold** and *em* and [a link](https://example.com).\n\n```sh\ngo build <x>\n```\n\n- one\n- two\n\n1. first\n2. second\n\n<script>alert(1)</script> and [bad](javascript:alert(1))" )) |
| 11 | for _, want := range []string{"<h1>Title</h1>" , "<code>code</code>" , "<strong>bold</strong>" , "<em>em</em>" , `<a href="https://example.com">a link</a>` , `<pre><code data-lang="sh">go build <x></code></pre>` , "<ul><li>one</li><li>two</li></ul>" , "<ol><li>first</li><li>second</li></ol>" , "<script>alert(1)</script>" } { |
| 12 | if !strings.Contains(got, want) { |
| 13 | t.Errorf("missing %q in %s" , want, got) |
| 14 | } |
| 15 | } |
| 16 | if strings.Contains(got, "<script>" ) || strings.Contains(got, `href="javascript:` ) { |
| 17 | t.Fatalf("unsafe markup leaked: %s" , got) |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | func TestRepositoryMarkdownResolvesRelativeLinks(t *testing.T) { |
| 22 | query := url.Values{"owner" : {"alice" }, "repo" : {"notes" }, "ref" : {"refs/heads/main" }} |
| 23 | got := string(renderRepositoryMarkdown("[guide](docs/guide.md) [up](../secret.md) [site](https://example.com)" , query)) |
| 24 | if !strings.Contains(got, `href="/repo?owner=alice&path=docs%2Fguide.md&ref=refs%2Fheads%2Fmain&repo=notes&view=file"` ) { |
| 25 | t.Fatalf("relative link was not mapped to repository viewer: %s" , got) |
| 26 | } |
| 27 | if !strings.Contains(got, `href="https://example.com"` ) { |
| 28 | t.Fatalf("repository link handling changed unsafe or external links: %s" , got) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | func TestRepositoryMarkdownDecodesPathsAndPreservesFragments(t *testing.T) { |
| 33 | query := url.Values{"owner" : {"alice" }, "repo" : {"notes" }, "ref" : {"refs/heads/main" }} |
| 34 | got := string(renderRepositoryMarkdown("[guide](docs/caf%C3%A9%20guide.md?download=1#L2)" , query)) |
| 35 | want := `href="/repo?download=1&owner=alice&path=docs%2Fcaf%C3%A9+guide.md&ref=refs%2Fheads%2Fmain&repo=notes&view=file#L2"` |
| 36 | if !strings.Contains(got, want) { |
| 37 | t.Fatalf("encoded README link lost its resource or fragment: %s" , got) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func TestChatMarkdownBreaksLinesAndAutolinksOutsideLinksAndCode(t *testing.T) { |
| 42 | got := string(renderChatMarkdown("It works: **[the page](https://012.run/wiki/agents)**.\nsee https://example.com/x, and nostr:npub1ttrypewl3au52wqux86r22yt506c077k3maj02a0jste97wrvd5sjfutc2\n\n- `code https://not.a.link`\n- [x](javascript:alert(1))\n\n```\nhttps://in.code\n```" )) |
| 43 | for _, want := range []string{ |
| 44 | `<strong><a href="https://012.run/wiki/agents">the page</a></strong>.<br>see <a href="https://example.com/x" rel="noopener">https://example.com/x</a>, and <a href="/open?target=nostr%3Anpub1ttrypewl3au52wqux86r22yt506c077k3maj02a0jste97wrvd5sjfutc2">nostr:npub1ttrypewl3au52wqux86r22yt506c077k3maj02a0jste97wrvd5sjfutc2</a></p>` , |
| 45 | "<li><code>code https://not.a.link</code></li>" , |
| 46 | "<li>[x](javascript:alert(1))</li>" , |
| 47 | "<pre><code>https://in.code</code></pre>" , |
| 48 | } { |
| 49 | if !strings.Contains(got, want) { |
| 50 | t.Errorf("missing %q in %s" , want, got) |
| 51 | } |
| 52 | } |
| 53 | if strings.Count(got, "<a " ) != 3 { |
| 54 | t.Fatalf("expected three links, got %s" , got) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestMarkdownRendersTablesWithAlignmentAndInlineMarkdown(t *testing.T) { |
| 59 | source := "| Name | Count | Notes |\n| :--- | ---: | :---: |\n| **Ada** | 3 | [docs](https://example.com) |\n| Grace | | `a | b` |" |
| 60 | got := string(renderMarkdown(source)) |
| 61 | for _, want := range []string{ |
| 62 | `<div data-markdown-table="" role="region" aria-label="Table" tabindex="0"><table>` , |
| 63 | `<thead><tr><th scope="col" data-align="left">Name</th><th scope="col" data-align="right">Count</th><th scope="col" data-align="center">Notes</th></tr></thead>` , |
| 64 | `<tbody><tr><td data-align="left"><strong>Ada</strong></td><td data-align="right">3</td><td data-align="center"><a href="https://example.com">docs</a></td></tr>` , |
| 65 | `<tr><td data-align="left">Grace</td><td data-align="right"></td><td data-align="center"><code>a | b</code></td></tr></tbody></table></div>` , |
| 66 | } { |
| 67 | if !strings.Contains(got, want) { |
| 68 | t.Errorf("missing %q in %s" , want, got) |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func TestMarkdownTableEscapesPipesAndLeavesMalformedInputAsText(t *testing.T) { |
| 74 | got := string(renderMarkdown("a \\| b\n---\n\n| one | two |\n| --- |\n| three | four | five |" )) |
| 75 | if strings.Contains(got, `data-markdown-table` ) { |
| 76 | t.Fatalf("malformed table was rendered: %s" , got) |
| 77 | } |
| 78 | if !strings.Contains(got, `a \| b ---` ) || !strings.Contains(got, `| one | two |` ) { |
| 79 | t.Fatalf("malformed table was not preserved as text: %s" , got) |
| 80 | } |
| 81 | |
| 82 | got = string(renderMarkdown("a \\| b | c\n--- | ---\nvalue \\| kept | ok" )) |
| 83 | if !strings.Contains(got, `<th scope="col">a | b</th>` ) || !strings.Contains(got, `<td>value | kept</td>` ) { |
| 84 | t.Fatalf("escaped pipe was split or escaped incorrectly: %s" , got) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func TestChatMarkdownAutolinksTableText(t *testing.T) { |
| 89 | got := string(renderChatMarkdown("| Link |\n| --- |\n| https://example.com/path |" )) |
| 90 | if !strings.Contains(got, `<td><a href="https://example.com/path" rel="noopener">https://example.com/path</a></td>` ) { |
| 91 | t.Fatalf("table cell was not autolinked: %s" , got) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func TestMarkdownTableStopsBeforeHeadingsAndFences(t *testing.T) { |
| 96 | got := string(renderMarkdown("| Name | Value |\n| --- | --- |\n| one | two |\n# | Heading |\n\n```sh | example\n| code |\n```" )) |
| 97 | if strings.Contains(got, "<td># | Heading |</td>" ) || strings.Contains(got, "<td>```sh | example</td>" ) { |
| 98 | t.Fatalf("table consumed a following block: %s" , got) |
| 99 | } |
| 100 | if !strings.Contains(got, "<h1>| Heading |</h1>" ) || !strings.Contains(got, "<pre><code data-lang=\"sh\">| code |</code></pre>" ) { |
| 101 | t.Fatalf("following heading or fence was not rendered separately: %s" , got) |
| 102 | } |
| 103 | } |