package highlight import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" "sync" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/styles" ) // CSS returns the stylesheet for both themes. Serve it as a static asset. // // Rules are unscoped (".ch-k { ... }") so the same classes work inside the // blob table, diff cells and markdown code blocks without a wrapper element. // Dark rules follow the app's existing switch: the data-theme attribute that // assets/theme.js sets from the theme cookie, with prefers-color-scheme as the // default when no cookie is set. var CSS = sync.OnceValue(func() string { var b strings.Builder light := withBackground(styles.Get("github"), "bg:#ffffff #24292e") dark := withBackground(styles.Get("github-dark"), "bg:#24292e #e1e4e8") lightClasses := writeTheme(&b, light, "", nil) b.WriteString(`@media (prefers-color-scheme: dark) {` + "\n") writeTheme(&b, dark, `:root:not([data-theme="light"]) `, lightClasses) b.WriteString("}\n") writeTheme(&b, dark, `:root[data-theme="dark"] `, lightClasses) return b.String() }) // withBackground swaps a style's Background entry. Chroma's github-light and // github-dark pick different page colours than the previous highlighter, so // pin the old colours to keep the look. func withBackground(base *chroma.Style, entry string) *chroma.Style { st, err := base.Builder().Add(chroma.Background, entry).Build() if err != nil { return base } return st } // CSSVersion is a short content hash for cache busting the stylesheet URL. var CSSVersion = sync.OnceValue(func() string { sum := sha256.Sum256([]byte(CSS())) return hex.EncodeToString(sum[:4]) }) // writeTheme writes the background variables and one rule per styled token. // scope prefixes every selector, e.g. `:root[data-theme="dark"] `. // Classes in reset that this style leaves unstyled get `color: inherit`, so // a scoped dark theme overrides every light rule instead of only its own. // It returns the set of classes it emitted. func writeTheme(b *strings.Builder, style *chroma.Style, scope string, reset map[string]bool) map[string]bool { emitted := map[string]bool{} bg := style.Get(chroma.Background) root := ":root" if scope != "" { root = strings.TrimSpace(scope) } fg := "inherit" if bg.Colour.IsSet() { fg = bg.Colour.String() } bgColour := "transparent" if bg.Background.IsSet() { bgColour = bg.Background.String() } fmt.Fprintf(b, "%s { --code-bg: %s; --code-fg: %s; }\n", root, bgColour, fg) types := make([]int, 0, len(chroma.StandardTypes)) for t := range chroma.StandardTypes { types = append(types, int(t)) } sort.Ints(types) for _, ti := range types { t := chroma.TokenType(ti) switch t { case chroma.Background, chroma.PreWrapper, chroma.Line, chroma.LineNumbers, chroma.LineNumbersTable, chroma.LineTable, chroma.LineTableTD, chroma.LineLink, chroma.LineHighlight, chroma.CodeLine: continue } cls := tokenClass(t) if cls == "" { continue } // Subtract the background entry so tokens only carry their own colour. entry := style.Get(t).Sub(bg) css := html.StyleEntryToCSS(entry) if css == "" { if reset[cls] { fmt.Fprintf(b, "%s.%s { color: inherit }\n", scope, cls) } continue } emitted[cls] = true fmt.Fprintf(b, "%s.%s { %s }\n", scope, cls, css) } return emitted }