123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 | package highlight
import (
"bytes"
"html"
"os"
"path/filepath"
"strings"
"testing"
)
const toyLexer = `<lexer>
<config>
<name>toy</name>
<alias>toy</alias>
<filename>*.toy</filename>
</config>
<rules>
<state name="root">
<rule pattern="\bzap\b"><token type="Keyword"/></rule>
<rule pattern="\d+"><token type="LiteralNumber"/></rule>
<rule pattern="\s+"><token type="Text"/></rule>
<rule pattern="."><token type="Text"/></rule>
</state>
</rules>
</lexer>`
func TestCustomLexerAndParity(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "toy.xml"), []byte(toyLexer), 0o644); err != nil {
t.Fatal(err)
}
r, err := New([]string{filepath.Join(dir, "missing"), dir}, []Custom{{Name: "toy", Globs: []string{"*.tm"}, Aliases: []string{"tm"}}}, "tokyonight-night")
if err != nil {
t.Fatal(err)
}
src := []byte("zap 42\n")
byPath := r.HTML(src, r.ByPath("dir/x.tm"))
byAlias := r.HTML(src, r.ByName("TM"))
byName := r.HTML(src, r.ByName("toy"))
if byPath != byAlias || byPath != byName {
t.Errorf("lexer resolution differs:\n%s\n%s\n%s", byPath, byAlias, byName)
}
want := `<code class="chroma"><span class="k">zap</span> <span class="m">42</span>
</code>`
if string(byPath) != want {
t.Errorf("got %q\nwant %q", byPath, want)
}
if Name(r.ByPath("a.c")) != "C" || Name(r.ByName("python")) != "Python" {
t.Error("builtin lexers not found")
}
if !r.IsPlain(r.ByPath("no.such.ext")) || !r.IsPlain(r.ByName("nonsense")) {
t.Error("unknown should be plain")
}
if _, err := New([]string{dir}, []Custom{{Name: "nothere"}}, "tokyonight-night"); err == nil {
t.Error("missing lexer accepted")
}
if _, err := New([]string{dir}, nil, "no-such-style"); err == nil {
t.Error("bad style accepted")
}
}
func TestEscapingAndLines(t *testing.T) {
r, err := New(nil, nil, "github")
if err != nil {
t.Fatal(err)
}
out := string(r.HTML([]byte("<a>&\"'\n"), r.ByName("")))
if strings.Contains(out, "<a>") || !strings.Contains(out, "<a>&") {
t.Errorf("not escaped: %s", out)
}
for src, want := range map[string]int{"": 0, "a": 1, "a\n": 1, "a\nb": 2, "a\nb\n": 2, "\n\n": 2} {
if got := LineCount([]byte(src)); got != want {
t.Errorf("LineCount(%q) = %d, want %d", src, got, want)
}
}
if !IsBinary([]byte("ab\x00c")) || IsBinary([]byte("plain text")) {
t.Error("IsBinary")
}
var css strings.Builder
if err := r.WriteCSS(&css); err != nil || !strings.Contains(css.String(), ".chroma .k ") {
t.Errorf("css: %v %s", err, css.String()[:80])
}
}
// TestThemedCSSIsExclusive: with a light style, dark and light rules must
// never both apply to a token, or properties one style does not set (token
// backgrounds, bold) leak into the other.
func TestThemedCSSIsExclusive(t *testing.T) {
hl, err := New(nil, nil, "tokyonight-night")
if err != nil {
t.Fatal(err)
}
var plain bytes.Buffer
if err := hl.WriteCSS(&plain); err != nil {
t.Fatal(err)
}
if strings.Contains(plain.String(), "data-theme") {
t.Error("without a light style the CSS should be unscoped")
}
if err := hl.SetLight("github"); err != nil {
t.Fatal(err)
}
var out bytes.Buffer
if err := hl.WriteCSS(&out); err != nil {
t.Fatal(err)
}
for line := range strings.SplitSeq(out.String(), "\n") {
trimmed := strings.TrimSpace(line)
if strings.Contains(trimmed, "{") && strings.Contains(trimmed, ".chroma") && !strings.HasPrefix(trimmed, ":root") {
t.Fatalf("unscoped rule would apply to both themes: %s", line)
}
}
for _, want := range []string{`:root[data-theme="dark"] .chroma .k {`, `:root[data-theme="light"] .chroma .k {`,
"@media (prefers-color-scheme: dark)", "@media (prefers-color-scheme: light)", `:root:not([data-theme]) .chroma .k {`} {
if !strings.Contains(out.String(), want) {
t.Errorf("missing %q", want)
}
}
if err := hl.SetLight("no-such-style"); err == nil {
t.Error("unknown light style accepted")
}
}
func TestByFileShebang(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "toy.xml"), []byte(toyLexer), 0o644); err != nil {
t.Fatal(err)
}
r, err := New([]string{dir}, []Custom{{Name: "toy", Aliases: []string{"tm"}}}, "")
if err != nil {
t.Fatal(err)
}
cases := []struct{ path, src, want string }{
{"run", "#!/bin/sh\necho hi\n", "Bash"},
{"run", "#!/usr/bin/env bash\n", "Bash"},
{"run", "#!/usr/bin/env -S python3.12 -u\n", "Python"},
{"run", "#! /usr/bin/python3\n", "Python"},
{"run", "#!/usr/bin/env LANG=C tm\n", "toy"},
{"run", "#!/usr/bin/nonesuch\n", "plaintext"},
{"LICENSE", "Permission is hereby granted\n", "plaintext"},
{"x.c", "#!/bin/sh\n", "C"}, // the name wins when it knows the language
}
for _, c := range cases {
if got := Name(r.ByFile(c.path, []byte(c.src))); got != c.want {
t.Errorf("ByFile(%q, %q) = %s, want %s", c.path, c.src, got, c.want)
}
}
}
// TestBrokenLexers: chroma's Jungle and JSONata lexers loop forever on
// these inputs and its YANG lexer panics; HTML must give up and show them
// plain.
func TestBrokenLexers(t *testing.T) {
r, err := New(nil, nil, "github")
if err != nil {
t.Fatal(err)
}
for name, src := range map[string]string{"x.jungle": "a \"b<\n", "x.jsonata": "\x01<", "x.yang": "/*/*0<"} {
lx := r.ByPath(name)
if r.IsPlain(lx) {
t.Fatalf("%s: no lexer", name)
}
want := `<code class="chroma">` + html.EscapeString(src) + `</code>`
if got := string(r.HTML([]byte(src), lx)); got != want {
t.Errorf("%s: got %q, want %q", name, got, want)
}
}
}
|