css.go
⎇
Raw
1package highlight
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "sort"
8 "strings"
9 "sync"
10
11 "github.com/alecthomas/chroma/v2"
12 "github.com/alecthomas/chroma/v2/formatters/html"
13 "github.com/alecthomas/chroma/v2/styles"
14)
15
16// CSS returns the stylesheet for both themes. Serve it as a static asset.
17//
18// Rules are unscoped (".ch-k { ... }") so the same classes work inside the
19// blob table, diff cells and markdown code blocks without a wrapper element.
20// Dark rules follow the app's existing switch: the data-theme attribute that
21// assets/theme.js sets from the theme cookie, with prefers-color-scheme as the
22// default when no cookie is set.
23var CSS = sync.OnceValue(func() string {
24 var b strings.Builder
25 light := withBackground(styles.Get("github"), "bg:#ffffff #24292e")
26 dark := withBackground(styles.Get("github-dark"), "bg:#24292e #e1e4e8")
27
28 lightClasses := writeTheme(&b, light, "", nil)
29 b.WriteString(`@media (prefers-color-scheme: dark) {` + "\n")
30 writeTheme(&b, dark, `:root:not([data-theme="light"]) `, lightClasses)
31 b.WriteString("}\n")
32 writeTheme(&b, dark, `:root[data-theme="dark"] `, lightClasses)
33 return b.String()
34})
35
36// withBackground swaps a style's Background entry. Chroma's github-light and
37// github-dark pick different page colours than the previous highlighter, so
38// pin the old colours to keep the look.
39func withBackground(base *chroma.Style, entry string) *chroma.Style {
40 st, err := base.Builder().Add(chroma.Background, entry).Build()
41 if err != nil {
42 return base
43 }
44 return st
45}
46
47// CSSVersion is a short content hash for cache busting the stylesheet URL.
48var CSSVersion = sync.OnceValue(func() string {
49 sum := sha256.Sum256([]byte(CSS()))
50 return hex.EncodeToString(sum[:4])
51})
52
53// writeTheme writes the background variables and one rule per styled token.
54// scope prefixes every selector, e.g. `:root[data-theme="dark"] `.
55// Classes in reset that this style leaves unstyled get `color: inherit`, so
56// a scoped dark theme overrides every light rule instead of only its own.
57// It returns the set of classes it emitted.
58func writeTheme(b *strings.Builder, style *chroma.Style, scope string, reset map[string]bool) map[string]bool {
59 emitted := map[string]bool{}
60 bg := style.Get(chroma.Background)
61 root := ":root"
62 if scope != "" {
63 root = strings.TrimSpace(scope)
64 }
65 fg := "inherit"
66 if bg.Colour.IsSet() {
67 fg = bg.Colour.String()
68 }
69 bgColour := "transparent"
70 if bg.Background.IsSet() {
71 bgColour = bg.Background.String()
72 }
73 fmt.Fprintf(b, "%s { --code-bg: %s; --code-fg: %s; }\n", root, bgColour, fg)
74
75 types := make([]int, 0, len(chroma.StandardTypes))
76 for t := range chroma.StandardTypes {
77 types = append(types, int(t))
78 }
79 sort.Ints(types)
80 for _, ti := range types {
81 t := chroma.TokenType(ti)
82 switch t {
83 case chroma.Background, chroma.PreWrapper, chroma.Line, chroma.LineNumbers,
84 chroma.LineNumbersTable, chroma.LineTable, chroma.LineTableTD,
85 chroma.LineLink, chroma.LineHighlight, chroma.CodeLine:
86 continue
87 }
88 cls := tokenClass(t)
89 if cls == "" {
90 continue
91 }
92 // Subtract the background entry so tokens only carry their own colour.
93 entry := style.Get(t).Sub(bg)
94 css := html.StyleEntryToCSS(entry)
95 if css == "" {
96 if reset[cls] {
97 fmt.Fprintf(b, "%s.%s { color: inherit }\n", scope, cls)
98 }
99 continue
100 }
101 emitted[cls] = true
102 fmt.Fprintf(b, "%s.%s { %s }\n", scope, cls, css)
103 }
104 return emitted
105}
106