Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/modules/highlight.go

d658fcc4ea9f3bb562fd00dce8f863934560c56c · 2.5 KB · 79 lines raw

1 package modules
2
3 import (
4 "html/template"
5 "strings"
6
7 "github.com/alecthomas/chroma/v2"
8 "github.com/alecthomas/chroma/v2/lexers"
9 )
10
11 // maxHighlightSize bounds how much source chroma will tokenise. Past this,
12 // a file falls back to plain (still viewable, just uncoloured) text rather
13 // than risk a slow lex on something pathological.
14 const maxHighlightSize = 512 * 1024
15
16 // highlightLines lexes src by the language implied by path and returns one
17 // escaped, span-wrapped HTML fragment per line. ok is false when path has no
18 // recognised language or src is too large to bother lexing; callers should
19 // fall back to plain escaped lines in that case.
20 func highlightLines(path string, src []byte) (lines []template.HTML, ok bool) {
21 if len(src) > maxHighlightSize {
22 return nil, false
23 }
24 lexer := lexers.Match(path)
25 if lexer == nil {
26 return nil, false
27 }
28 it, err := chroma.Coalesce(lexer).Tokenise(nil, string(src))
29 if err != nil {
30 return nil, false
31 }
32 for _, toks := range chroma.SplitTokensIntoLines(it.Tokens()) {
33 var b strings.Builder
34 for i, tok := range toks {
35 // SplitTokensIntoLines keeps each line's newline attached to
36 // its last token; drop it so it doesn't render as a blank
37 // row inside the (white-space: pre) table cell.
38 if i == len(toks)-1 {
39 tok.Value = strings.TrimSuffix(tok.Value, "\n")
40 }
41 esc := template.HTMLEscapeString(tok.Value)
42 if cls := tokenClass(tok.Type); cls != "" {
43 b.WriteString(`<span class="`)
44 b.WriteString(cls)
45 b.WriteString(`">`)
46 b.WriteString(esc)
47 b.WriteString(`</span>`)
48 } else {
49 b.WriteString(esc)
50 }
51 }
52 lines = append(lines, template.HTML(b.String()))
53 }
54 return lines, true
55 }
56
57 // tokenClass buckets chroma's ~70 token types down to the handful of colours
58 // the theme actually defines (see --syn-* in base.css). Punctuation,
59 // operators and plain names are left uncoloured on purpose -- a source line
60 // with every token tinted reads as noise, not signal.
61 func tokenClass(t chroma.TokenType) string {
62 switch {
63 case t.InCategory(chroma.Comment):
64 return "tok-c"
65 case t.InCategory(chroma.Keyword):
66 return "tok-k"
67 case t.InSubCategory(chroma.LiteralString):
68 return "tok-s"
69 case t.InSubCategory(chroma.LiteralNumber):
70 return "tok-n"
71 case t == chroma.NameFunction, t == chroma.NameFunctionMagic,
72 t == chroma.NameClass, t == chroma.NameNamespace,
73 t == chroma.NameBuiltin, t == chroma.NameBuiltinPseudo,
74 t == chroma.NameTag, t == chroma.NameDecorator:
75 return "tok-fn"
76 default:
77 return ""
78 }
79 }