| 🛟 Updated. 28d5985 k33g 17h ago | 1 | package syntax |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // spansOf renders one line's spans as "text:class" strings, which reads like |
| 9 | // the screen does and makes a failure legible. |
| 10 | func spansOf(t *testing.T, language Language, src string, line int) []string { |
| 11 | t.Helper() |
| 12 | |
| 13 | all := Highlight(language, src) |
| 14 | if line >= len(all) { |
| 15 | t.Fatalf("line %d is past the end of a %d-line document", line, len(all)) |
| 16 | } |
| 17 | |
| 18 | text := []rune(strings.Split(src, "\n")[line]) |
| 19 | out := make([]string, 0, len(all[line])) |
| 20 | for _, s := range all[line] { |
| 21 | out = append(out, string(text[s.Start:s.End])+":"+s.Class.String()) |
| 22 | } |
| 23 | return out |
| 24 | } |
| 25 | |
| 26 | // wantLine compares one line's spans against what they should be. |
| 27 | func wantLine(t *testing.T, language Language, src string, line int, want ...string) { |
| 28 | t.Helper() |
| 29 | |
| 30 | got := spansOf(t, language, src, line) |
| 31 | if len(got) != len(want) { |
| 32 | t.Fatalf("line %d of %q gave\n %v\nwant\n %v", line, src, got, want) |
| 33 | } |
| 34 | for i := range want { |
| 35 | if got[i] != want[i] { |
| 36 | t.Errorf("line %d span %d = %q, want %q (whole line: %v)", line, i, got[i], want[i], got) |
| 37 | } |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // wantMarkdown is wantLine for Markdown, which most of this file needs. |
| 42 | func wantMarkdown(t *testing.T, src string, line int, want ...string) { |
| 43 | t.Helper() |
| 44 | wantLine(t, LanguageMarkdown, src, line, want...) |
| 45 | } |
| 46 | |
| 47 | func TestMarkdownColoursAHeadingWholeLine(t *testing.T) { |
| 48 | wantMarkdown(t, "# A title", 0, "# A title:heading") |
| 49 | wantMarkdown(t, "### Deeper", 0, "### Deeper:heading") |
| 50 | } |
| 51 | |
| 52 | func TestMarkdownColoursBoldAndItalic(t *testing.T) { |
| 53 | wantMarkdown(t, "some *italic* here", 0, "*italic*:emphasis") |
| 54 | wantMarkdown(t, "some **bold** here", 0, "**bold**:emphasis") |
| 55 | wantMarkdown(t, "some _italic_ here", 0, "_italic_:emphasis") |
| 56 | wantMarkdown(t, "some __bold__ here", 0, "__bold__:emphasis") |
| 57 | } |
| 58 | |
| 59 | func TestBoldIsOneSpanNotTwoItalics(t *testing.T) { |
| 60 | // The run of markers at the start has to be matched by the same run at the |
| 61 | // end, or ** opens an italic that closes immediately. |
| 62 | got := spansOf(t, LanguageMarkdown, "**bold**", 0) |
| 63 | if len(got) != 1 { |
| 64 | t.Errorf("**bold** gave %v, want one span", got) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | func TestMarkdownColoursACodeSpan(t *testing.T) { |
| 69 | wantMarkdown(t, "call `go build` first", 0, "`go build`:string") |
| 70 | } |
| 71 | |
| 72 | func TestMarkdownColoursALinkAndAnImage(t *testing.T) { |
| 73 | wantMarkdown(t, "see [the docs](docs/README.md) now", 0, "[the docs](docs/README.md):link") |
| 74 | wantMarkdown(t, "", 0, ":link") |
| 75 | } |
| 76 | |
| 77 | func TestAnUnclosedLinkIsLeftAlone(t *testing.T) { |
| 78 | // Half a link is typed most of the time a link is being typed. |
| 79 | if got := spansOf(t, LanguageMarkdown, "see [the docs", 0); len(got) != 0 { |
| 80 | t.Errorf("an unfinished link gave %v, want nothing coloured", got) |
| 81 | } |
| 82 | if got := spansOf(t, LanguageMarkdown, "see [text] but no target", 0); len(got) != 0 { |
| 83 | t.Errorf("a bracket with no target gave %v", got) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | func TestMarkdownColoursListMarkers(t *testing.T) { |
| 88 | wantMarkdown(t, "- an item", 0, "-:punctuation") |
| 89 | wantMarkdown(t, "* an item", 0, "*:punctuation") |
| 90 | wantMarkdown(t, "+ an item", 0, "+:punctuation") |
| 91 | wantMarkdown(t, "1. an item", 0, "1.:punctuation") |
| 92 | wantMarkdown(t, "12) an item", 0, "12):punctuation") |
| 93 | } |
| 94 | |
| 95 | func TestAListMarkerNeedsASpaceAfterIt(t *testing.T) { |
| 96 | // "*emphasis*" starts the same way a bullet does. |
| 97 | wantMarkdown(t, "*italic* text", 0, "*italic*:emphasis") |
| 98 | } |
| 99 | |
| 100 | func TestMarkdownColoursABlockQuote(t *testing.T) { |
| 101 | wantMarkdown(t, "> quoted **text**", 0, ">:punctuation", "**text**:emphasis") |
| 102 | } |
| 103 | |
| 104 | func TestMarkdownColoursAHorizontalRule(t *testing.T) { |
| 105 | for _, rule := range []string{"---", "***", "___", "- - -"} { |
| 106 | t.Run(rule, func(t *testing.T) { |
| 107 | wantMarkdown(t, rule, 0, rule+":punctuation") |
| 108 | }) |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | func TestAHorizontalRuleIsNotAListItem(t *testing.T) { |
| 113 | // "---" and "- item" both start with a dash; the rule is checked first. |
| 114 | got := spansOf(t, LanguageMarkdown, "---", 0) |
| 115 | if len(got) != 1 || !strings.HasSuffix(got[0], ":punctuation") { |
| 116 | t.Errorf("--- gave %v", got) |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | func TestMarkdownColoursAFencedBlockAsOneThing(t *testing.T) { |
| 121 | src := "before\n```go\nfunc main() {}\n```\nafter" |
| 122 | |
| 123 | wantMarkdown(t, src, 1, "```go:string") |
| 124 | wantMarkdown(t, src, 2, "func main() {}:string") |
| 125 | wantMarkdown(t, src, 3, "```:string") |
| 126 | // The line after the fence is prose again. |
| 127 | if got := spansOf(t, LanguageMarkdown, src, 4); len(got) != 0 { |
| 128 | t.Errorf("the line after a fence gave %v, want plain prose", got) |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | func TestATildeFenceWorksToo(t *testing.T) { |
| 133 | src := "~~~\nx = 1\n~~~\n# after" |
| 134 | |
| 135 | wantMarkdown(t, src, 1, "x = 1:string") |
| 136 | wantMarkdown(t, src, 3, "# after:heading") |
| 137 | } |
| 138 | |
| 139 | func TestABacktickDoesNotCloseATildeFence(t *testing.T) { |
| 140 | src := "~~~\n```\nstill inside\n~~~" |
| 141 | |
| 142 | wantMarkdown(t, src, 2, "still inside:string") |
| 143 | } |
| 144 | |
| 145 | func TestMarkupInsideAFenceIsNotColoured(t *testing.T) { |
| 146 | src := "```\n# not a heading\n**not bold**\n```" |
| 147 | |
| 148 | wantMarkdown(t, src, 1, "# not a heading:string") |
| 149 | wantMarkdown(t, src, 2, "**not bold**:string") |
| 150 | } |
| 151 | |
| 152 | func TestAnUnclosedFenceColoursToTheEndOfTheFile(t *testing.T) { |
| 153 | src := "```\nstill going\nand going" |
| 154 | |
| 155 | wantMarkdown(t, src, 1, "still going:string") |
| 156 | wantMarkdown(t, src, 2, "and going:string") |
| 157 | } |
| 158 | |
| 159 | func TestALineOfProseWithBackticksDoesNotOpenAFence(t *testing.T) { |
| 160 | src := "use `a` and `b`\n# a heading" |
| 161 | |
| 162 | wantMarkdown(t, src, 1, "# a heading:heading") |
| 163 | } |
| 164 | |
| 165 | func TestMarkdownGivesOneEntryPerLine(t *testing.T) { |
| 166 | if got := len(Highlight(LanguageMarkdown, "a\nb\n\nc\n")); got != 5 { |
| 167 | t.Errorf("Highlight() returned %d lines for a 5-line document", got) |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | func TestMarkdownSpansStayInsideTheirLine(t *testing.T) { |
| 172 | src := "# Title\n\nSome **bold** and `code` and [a link](x).\n\n```go\nfunc f() {}\n```\n" |
| 173 | assertSpansFit(t, LanguageMarkdown, src) |
| 174 | } |
| 175 | |
| 176 | // assertSpansFit checks that every span sits inside its own line and is not |
| 177 | // empty, which is what the editor indexes without a bounds check. |
| 178 | func assertSpansFit(t *testing.T, language Language, src string) { |
| 179 | t.Helper() |
| 180 | |
| 181 | lines := strings.Split(src, "\n") |
| 182 | for i, spans := range Highlight(language, src) { |
| 183 | width := len([]rune(lines[i])) |
| 184 | for _, s := range spans { |
| 185 | if s.Start < 0 || s.End > width || s.End <= s.Start { |
| 186 | t.Errorf("line %d (%d runes) has span %d-%d %v", i, width, s.Start, s.End, s.Class) |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | func TestMarkdownHandlesAccentsInRuneColumns(t *testing.T) { |
| 193 | // A byte offset here would put the spans of this line out of step with |
| 194 | // what is drawn. |
| 195 | wantMarkdown(t, "évidemment **gras** ici", 0, "**gras**:emphasis") |
| 196 | } |