package docsite
import (
"html"
"regexp"
"strings"
"testing"
)
// spanText matches one tok-* span.
var spanText = regexp.MustCompile(`([^<]*)`)
// tokens returns the class and unescaped text of every tok-* span.
func tokens(out string) map[string][]string {
got := map[string][]string{}
for _, m := range spanText.FindAllStringSubmatch(out, -1) {
got[m[1]] = append(got[m[1]], html.UnescapeString(m[2]))
}
return got
}
func assertToken(t *testing.T, lang, out, class, text string) {
t.Helper()
for _, v := range tokens(out)[class] {
if strings.TrimSpace(v) == text {
return
}
}
t.Errorf("%s: no %s span for %q in\n%s", lang, class, text, out)
}
// assertNoTags fails when output has markup other than tok-* spans.
func assertNoTags(t *testing.T, lang, out string) {
t.Helper()
rest := spanText.ReplaceAllString(out, "")
if strings.ContainsAny(rest, "<>") {
t.Errorf("%s: unexpected markup in %q", lang, rest)
}
if strings.Contains(out, "style=") {
t.Errorf("%s: inline style in %q", lang, out)
}
}
func TestHighlightGo(t *testing.T) {
src := "package main\n\n// Say says.\nfunc Say() (int, string) {\n\treturn 42, \"a\"\nport: 5432\nenabled: true\nplain: text value\n"
for _, lang := range []string{"yaml", "yml"} {
out := highlight(lang, src)
assertToken(t, lang, out, "tok-com", "# comment")
assertToken(t, lang, out, "tok-key", "name")
assertToken(t, lang, out, "tok-str", `"quoted "`)
assertToken(t, lang, out, "tok-num", "5432")
assertToken(t, lang, out, "tok-num", "true")
assertToken(t, lang, out, "tok-str", "text value")
assertNoTags(t, lang, out)
}
out := highlight("json", `{"a": null, "b": 1}`)
assertToken(t, "json", out, "tok-num", "null")
assertToken(t, "json", out, "tok-num", "1")
assertNoTags(t, "json", out)
}
func TestHighlightShell(t *testing.T) {
src := "$ summer docs:build --out \"site\" # build\n# a comment line\n ./bin/acme migrate \\\n --force\n\nsummer\n"
for _, lang := range []string{"sh", "shell", "bash", "console"} {
out := highlight(lang, src)
assertToken(t, lang, out, "tok-prompt", "$")
assertToken(t, lang, out, "tok-kw", "summer")
assertToken(t, lang, out, "tok-kw", "./bin/acme")
assertToken(t, lang, out, "tok-com", "# a comment line")
assertToken(t, lang, out, "tok-com", "# build")
assertToken(t, lang, out, "tok-str", `"site"`)
assertNoTags(t, lang, out)
// The continuation line has no command word.
for _, kw := range tokens(out)["tok-kw"] {
if strings.Contains(kw, "--force") {
t.Errorf("%s: continuation line highlighted as a command: %q", lang, kw)
}
}
if !strings.HasPrefix(out, `$ summer `) {
t.Errorf("%s: output starts %q", lang, out[:min(len(out), 80)])
}
if !strings.Contains(out, "\n ./bin/acme") {
t.Errorf("%s: indentation before a command was not kept:\n%s", lang, out)
}
if html.UnescapeString(spanText.ReplaceAllString(out, "$2")) != src {
t.Errorf("%s: output does not round-trip to the source:\n%s", lang, out)
}
}
// A line fragment without a trailing newline stays one line.
if out := highlight("sh", "summer serve"); strings.Contains(out, "\n") {
t.Errorf("sh fragment gained a newline: %q", out)
}
}
func TestHighlightFallback(t *testing.T) {
src := "a < b && c > \"d\" 'e'"
want := html.EscapeString(src)
for _, lang := range []string{"", "text", "txt", "plain", "plaintext", "no-such-language"} {
if got := highlight(lang, src); got != want {
t.Errorf("highlight(%q) = %q, want escaped plain text %q", lang, got, want)
}
}
if lexerFor("go") == nil || lexerFor("text") != nil || lexerFor("no-such-language") != nil {
t.Error("lexerFor")
}
var b strings.Builder
writeSpan(&b, "tok-kw", "")
writeSpan(&b, "", "")
if b.String() != "<x>" {
t.Errorf("writeSpan = %q", b.String())
}
}
func TestHighlightFenceMarkup(t *testing.T) {
s, pages := renderFixture(t, map[string]string{
"docs/setup/start.md": page("Start", "setup", 10, "```\nno \n```\n\n```go title=x\nx := 1\n```\n"),
})
out := string(renderHTML(t, s, pages["setup/start"]).html)
for _, want := range []string{
`",
`
x := 1`,
} {
if !strings.Contains(out, want) {
t.Errorf("missing %s in\n%s", want, out)
}
}
if strings.Contains(out, "") {
t.Error("a fence without src= has a caption")
}
}