package syntax import ( "strings" "testing" ) // spansOf renders one line's spans as "text:class" strings, which reads like // the screen does and makes a failure legible. func spansOf(t *testing.T, language Language, src string, line int) []string { t.Helper() all := Highlight(language, src) if line >= len(all) { t.Fatalf("line %d is past the end of a %d-line document", line, len(all)) } text := []rune(strings.Split(src, "\n")[line]) out := make([]string, 0, len(all[line])) for _, s := range all[line] { out = append(out, string(text[s.Start:s.End])+":"+s.Class.String()) } return out } // wantLine compares one line's spans against what they should be. func wantLine(t *testing.T, language Language, src string, line int, want ...string) { t.Helper() got := spansOf(t, language, src, line) if len(got) != len(want) { t.Fatalf("line %d of %q gave\n %v\nwant\n %v", line, src, got, want) } for i := range want { if got[i] != want[i] { t.Errorf("line %d span %d = %q, want %q (whole line: %v)", line, i, got[i], want[i], got) } } } // wantMarkdown is wantLine for Markdown, which most of this file needs. func wantMarkdown(t *testing.T, src string, line int, want ...string) { t.Helper() wantLine(t, LanguageMarkdown, src, line, want...) } func TestMarkdownColoursAHeadingWholeLine(t *testing.T) { wantMarkdown(t, "# A title", 0, "# A title:heading") wantMarkdown(t, "### Deeper", 0, "### Deeper:heading") } func TestMarkdownColoursBoldAndItalic(t *testing.T) { wantMarkdown(t, "some *italic* here", 0, "*italic*:emphasis") wantMarkdown(t, "some **bold** here", 0, "**bold**:emphasis") wantMarkdown(t, "some _italic_ here", 0, "_italic_:emphasis") wantMarkdown(t, "some __bold__ here", 0, "__bold__:emphasis") } func TestBoldIsOneSpanNotTwoItalics(t *testing.T) { // The run of markers at the start has to be matched by the same run at the // end, or ** opens an italic that closes immediately. got := spansOf(t, LanguageMarkdown, "**bold**", 0) if len(got) != 1 { t.Errorf("**bold** gave %v, want one span", got) } } func TestMarkdownColoursACodeSpan(t *testing.T) { wantMarkdown(t, "call `go build` first", 0, "`go build`:string") } func TestMarkdownColoursALinkAndAnImage(t *testing.T) { wantMarkdown(t, "see [the docs](docs/README.md) now", 0, "[the docs](docs/README.md):link") wantMarkdown(t, "![a picture](x.png)", 0, "![a picture](x.png):link") } func TestAnUnclosedLinkIsLeftAlone(t *testing.T) { // Half a link is typed most of the time a link is being typed. if got := spansOf(t, LanguageMarkdown, "see [the docs", 0); len(got) != 0 { t.Errorf("an unfinished link gave %v, want nothing coloured", got) } if got := spansOf(t, LanguageMarkdown, "see [text] but no target", 0); len(got) != 0 { t.Errorf("a bracket with no target gave %v", got) } } func TestMarkdownColoursListMarkers(t *testing.T) { wantMarkdown(t, "- an item", 0, "-:punctuation") wantMarkdown(t, "* an item", 0, "*:punctuation") wantMarkdown(t, "+ an item", 0, "+:punctuation") wantMarkdown(t, "1. an item", 0, "1.:punctuation") wantMarkdown(t, "12) an item", 0, "12):punctuation") } func TestAListMarkerNeedsASpaceAfterIt(t *testing.T) { // "*emphasis*" starts the same way a bullet does. wantMarkdown(t, "*italic* text", 0, "*italic*:emphasis") } func TestMarkdownColoursABlockQuote(t *testing.T) { wantMarkdown(t, "> quoted **text**", 0, ">:punctuation", "**text**:emphasis") } func TestMarkdownColoursAHorizontalRule(t *testing.T) { for _, rule := range []string{"---", "***", "___", "- - -"} { t.Run(rule, func(t *testing.T) { wantMarkdown(t, rule, 0, rule+":punctuation") }) } } func TestAHorizontalRuleIsNotAListItem(t *testing.T) { // "---" and "- item" both start with a dash; the rule is checked first. got := spansOf(t, LanguageMarkdown, "---", 0) if len(got) != 1 || !strings.HasSuffix(got[0], ":punctuation") { t.Errorf("--- gave %v", got) } } func TestMarkdownColoursAFencedBlockAsOneThing(t *testing.T) { src := "before\n```go\nfunc main() {}\n```\nafter" wantMarkdown(t, src, 1, "```go:string") wantMarkdown(t, src, 2, "func main() {}:string") wantMarkdown(t, src, 3, "```:string") // The line after the fence is prose again. if got := spansOf(t, LanguageMarkdown, src, 4); len(got) != 0 { t.Errorf("the line after a fence gave %v, want plain prose", got) } } func TestATildeFenceWorksToo(t *testing.T) { src := "~~~\nx = 1\n~~~\n# after" wantMarkdown(t, src, 1, "x = 1:string") wantMarkdown(t, src, 3, "# after:heading") } func TestABacktickDoesNotCloseATildeFence(t *testing.T) { src := "~~~\n```\nstill inside\n~~~" wantMarkdown(t, src, 2, "still inside:string") } func TestMarkupInsideAFenceIsNotColoured(t *testing.T) { src := "```\n# not a heading\n**not bold**\n```" wantMarkdown(t, src, 1, "# not a heading:string") wantMarkdown(t, src, 2, "**not bold**:string") } func TestAnUnclosedFenceColoursToTheEndOfTheFile(t *testing.T) { src := "```\nstill going\nand going" wantMarkdown(t, src, 1, "still going:string") wantMarkdown(t, src, 2, "and going:string") } func TestALineOfProseWithBackticksDoesNotOpenAFence(t *testing.T) { src := "use `a` and `b`\n# a heading" wantMarkdown(t, src, 1, "# a heading:heading") } func TestMarkdownGivesOneEntryPerLine(t *testing.T) { if got := len(Highlight(LanguageMarkdown, "a\nb\n\nc\n")); got != 5 { t.Errorf("Highlight() returned %d lines for a 5-line document", got) } } func TestMarkdownSpansStayInsideTheirLine(t *testing.T) { src := "# Title\n\nSome **bold** and `code` and [a link](x).\n\n```go\nfunc f() {}\n```\n" assertSpansFit(t, LanguageMarkdown, src) } // assertSpansFit checks that every span sits inside its own line and is not // empty, which is what the editor indexes without a bounds check. func assertSpansFit(t *testing.T, language Language, src string) { t.Helper() lines := strings.Split(src, "\n") for i, spans := range Highlight(language, src) { width := len([]rune(lines[i])) for _, s := range spans { if s.Start < 0 || s.End > width || s.End <= s.Start { t.Errorf("line %d (%d runes) has span %d-%d %v", i, width, s.Start, s.End, s.Class) } } } } func TestMarkdownHandlesAccentsInRuneColumns(t *testing.T) { // A byte offset here would put the spans of this line out of step with // what is drawn. wantMarkdown(t, "évidemment **gras** ici", 0, "**gras**:emphasis") }