css.go
| 1 | package highlight |
| 2 | |
| 3 | import ( |
| 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. |
| 23 | var 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. |
| 39 | func 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. |
| 48 | var 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. |
| 58 | func 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 |