// Package highlight is quarry's thin chroma wrapper (0002 §2 lists the
// Forgejo one as [COPY]; theirs is coupled to their settings and caches,
// so this is a smaller fresh write). Output uses CSS classes, never
// inline styles, so the two-theme story stays in one stylesheet.
package highlight
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"html"
"regexp"
"strings"
"sync"
"golang.org/x/sync/singleflight"
"github.com/alecthomas/chroma/v2"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
)
var formatter = chromahtml.New(
chromahtml.WithClasses(true),
chromahtml.PreventSurroundingPre(false),
)
// MaxBytes is a SAFETY VALVE, not a routine limit: past it, content is
// served as plain text rather than tokenised.
//
// It was briefly set at 256KB, which turned ten of sqlite's src files —
// btree.c, select.c, vdbe.c, where.c, expr.c among them — permanently
// grey. That is trading the feature for the fix. The real cost is not
// file size but the DOM: chroma emits a span per token, so a 198KB
// build.c becomes 40,083 spans and 137ms of DOMContentLoaded, against
// 29ms for a plain 407KB btree.c. Highlighting AFTER load does not help
// either — the same spans cost the same parse, just later.
//
// The answer is to highlight only what is on screen (windowing, the way
// the diff pages already work), at which point no threshold is needed.
// Until that lands this stays high enough to catch only content no
// reader is really reading, and the per-blob cache below carries the
// cost of everything else.
const MaxBytes = 2 << 20
// TooBig reports whether Code will decline this content on size alone,
// so a caller can say why rather than silently dropping the colour.
func TooBig(code []byte) bool { return len(code) > MaxBytes }
// cache memoises highlighted output. A blob is immutable, so its
// rendering is too: the key is the content hash plus the lexer hints,
// and a hit skips tokenising entirely.
//
// Bounded by TOTAL BYTES rather than entry count, because entries are
// wildly uneven — one btree.c is worth thousands of small files, and a
// count-based bound would hold a gigabyte or nothing depending on what
// was browsed. On overflow the map is dropped wholesale: a generational
// sweep with no bookkeeping, which for a read-through cache of
// immutable content costs one recompute rather than correctness.
var cache = struct {
sync.Mutex
m map[string]string
bytes int
}{m: map[string]string{}}
// cacheBudget is how much highlighted HTML to retain. Roughly a dozen
// large source files, or thousands of ordinary ones.
//
// Deliberately modest: quarry also runs embedded beside a zit install,
// where a process helping itself to hundreds of megabytes to memoise
// syntax colouring is rude. The memo is disposable — losing it costs
// time, never correctness — so when in doubt it should hold less.
const cacheBudget = 64 << 20
// group collapses concurrent misses on the SAME key into one call.
//
// Without it the cache does nothing for the case that matters. A
// read-through memo only helps once a value is present, so N readers
// arriving together on a cold key all miss, all tokenise the identical
// immutable bytes, and the machine does N times the work at the moment
// it is least able to afford it. Measured before this existed: one cold
// request for sqlite's where.c took 0.42s, and ten concurrent ones took
// 0.95s EACH — ten full tokenisations of the same file.
//
// Safe precisely because the value is a pure function of its key:
// sharing one computation between callers cannot give any of them a
// different answer than computing it themselves.
var group singleflight.Group
func cacheKey(code []byte, language, filename string) string {
sum := sha256.Sum256(code)
return hex.EncodeToString(sum[:16]) + "\x00" + language + "\x00" + filename
}
func cacheGet(key string) (string, bool) {
cache.Lock()
defer cache.Unlock()
v, ok := cache.m[key]
return v, ok
}
func cachePut(key, html string) {
cache.Lock()
defer cache.Unlock()
if cache.bytes+len(html) > cacheBudget {
cache.m = map[string]string{}
cache.bytes = 0
}
cache.m[key] = html
cache.bytes += len(html)
}
// Code highlights source for the given language hint and/or filename;
// either may be empty. ok is false when no lexer matched, or when the
// content is past MaxBytes — callers fall back to an escaped <pre>.
func Code(code []byte, language, filename string) (string, bool) {
if TooBig(code) {
return "", false
}
key := cacheKey(code, language, filename)
if html, ok := cacheGet(key); ok {
return html, html != ""
}
v, _, _ := group.Do(key, func() (any, error) {
// Re-check inside the flight: the winner of a race may have
// filled the entry while this caller waited for the group.
if html, ok := cacheGet(key); ok {
return html, nil
}
html, _ := code_(code, language, filename)
// A miss is cached too, as the empty string: "no lexer matched"
// is as expensive to discover as a match and just as stable.
cachePut(key, html)
return html, nil
})
html, _ := v.(string)
return html, html != ""
}
func code_(code []byte, language, filename string) (string, bool) {
// Content analysis ONLY for a shebang. Chroma's Analyse is eager
// enough to read an English README as SQL (is/and/or/in/where are
// all keywords) and paint the prose red; a #! line is the one
// content signal that names a language on purpose.
lexer := lexerFor(code, language, filename)
if lexer == nil {
return "", false
}
it, err := lexer.Tokenise(nil, string(code))
if err != nil {
return "", false
}
var buf bytes.Buffer
if err := formatter.Format(&buf, quarryStyle, it); err != nil {
return "", false
}
return buf.String(), true
}
// lexerFor resolves the lexer once, so the window path and the whole-
// file path cannot disagree about what language a file is.
func lexerFor(code []byte, language, filename string) chroma.Lexer {
var lexer chroma.Lexer
if language != "" {
lexer = lexers.Get(language)
}
if lexer == nil && filename != "" {
lexer = lexers.Match(filename)
}
if lexer == nil && bytes.HasPrefix(code, []byte("#!")) {
lexer = lexers.Analyse(string(code))
}
if lexer == nil {
return nil
}
return chroma.Coalesce(lexer)
}
// Lines highlights lines [from, from+count) of code, counting from
// zero, one HTML string per line with no trailing newline, and reports
// how many lines the content holds in total when it reached the end.
// The caller draws the gutter: a line's number and anchor are page
// structure, so they are not left to a formatter's markup.
//
// The point is what it does NOT do. Chroma emits a span per token, so
// highlighting a whole large file builds a DOM nobody asked for: a
// 198KB build.c becomes 40,083 nodes and 137ms of parse, and a 407KB
// btree.c roughly twice that. Deferring the work does not help — the
// same spans cost the same parse whenever they arrive. Only sending
// fewer of them helps.
//
// Lexing still starts at byte zero, because it must: a window opening
// inside a block comment or a string cannot be coloured correctly from
// its own first byte. But chroma's iterator is pull-based, so this
// consumes tokens only as far as the window's last line and then
// stops — the tail of the file is never tokenised at all.
//
// A file no lexer claims is still drawn, uncoloured, through chroma's
// fallback lexer, because the numbers are the point; coloured says
// which happened.
func Lines(code []byte, language, filename string, from, count int) (lines []string, total int, coloured bool) {
lexer := lexerFor(code, language, filename)
coloured = lexer != nil
if lexer == nil {
lexer = lexers.Fallback
}
it, err := lexer.Tokenise(nil, string(code))
if err != nil {
it, _ = lexers.Fallback.Tokenise(nil, string(code))
coloured = false
}
end := from + count
line := 0
var cur strings.Builder
atEnd := true
tail := false // text since the last newline
for tok := it(); tok != chroma.EOF; tok = it() {
// A token may span lines (a block comment), so it is split at
// newlines and each piece attributed to the line it sits on.
text := tok.Value
for text != "" {
nl := strings.IndexByte(text, '\n')
piece := text
if nl >= 0 {
piece = text[:nl]
}
if line >= from && line < end {
writeToken(&cur, tok.Type, piece)
}
if nl < 0 {
tail = tail || piece != ""
break
}
tail = false
if line >= from && line < end {
lines = append(lines, cur.String())
cur.Reset()
}
line++
text = text[nl+1:]
if line >= end {
// Everything the caller asked for is in hand; the
// rest of the file is never tokenised.
atEnd = false
break
}
}
if !atEnd {
break
}
}
if atEnd {
// Text after the last newline is a line. Nothing after it is
// not: chroma ensures a final newline, so an empty tail is the
// lexer's, and numbering it would show a line the file lacks.
// An empty file is still one empty line, so the total says the
// end was reached.
if tail || line == 0 {
if line >= from && line < end {
lines = append(lines, cur.String())
}
line++
}
total = line
}
return lines, total, coloured
}
// writeToken writes one token's text, escaped, in the span its class
// names: the same classes the formatter emits, so highlight.CSS colours
// both.
// styledClass says whether CSS() colours tokens of class c. A token whose
// type and every parent are unstyled (punctuation, operators, plain names
// and whitespace in quarry's palette) is written as bare text: its span
// changed nothing on screen and was a third of a highlighted file's
// markup, 4,143 "p" spans alone on tcl's merge page (jva, 2026-10-30).
func styledClass(c string) bool {
styledOnce.Do(func() {
styled = map[string]bool{}
for _, m := range styledRule.FindAllStringSubmatch(CSS(), -1) {
styled[m[1]] = true
}
})
return styled[c]
}
var (
styledOnce sync.Once
styled map[string]bool
styledRule = regexp.MustCompile(`\.chroma \.([a-z0-9]+) *\{`)
)
func writeToken(b *strings.Builder, t chroma.TokenType, text string) {
if text == "" {
return
}
cls := ""
for tt := t; tt != 0; tt = tt.Parent() {
if c, ok := chroma.StandardTypes[tt]; ok && styledClass(c) {
cls = c
break
}
}
if cls == "" {
b.WriteString(html.EscapeString(text))
return
}
b.WriteString(`<span class="`)
b.WriteString(cls)
b.WriteString(`">`)
b.WriteString(html.EscapeString(text))
b.WriteString(`</span>`)
}
// The syntax colours are quarry's palette, not a stock chroma theme.
// quarryStyle colours each token with a placeholder, and CSS swaps each
// placeholder for the palette variable it stands for (--syn-*, and the
// diff and code tokens), so one set of rules serves both themes: the
// palette redefines the variables per theme, and its contrast tests
// check every syntax colour on the code ground and on the diff rows.
// It used to be chroma's "github" and "github-dark" — a near-black slab
// of saturated red and green in a warm Gruvbox page.
var quarryStyle = chroma.MustNewStyle("quarry", chroma.StyleEntries{
chroma.Background: "#010101 bg:#010100",
chroma.Keyword: "#010102",
chroma.KeywordType: "#010105",
chroma.OperatorWord: "#010102",
chroma.NameFunction: "#010104",
chroma.NameClass: "#010105",
chroma.NameBuiltin: "#010107",
chroma.NameDecorator: "#010107",
chroma.NameTag: "#010102",
chroma.NameAttribute: "#010108",
chroma.NameConstant: "#010106",
chroma.LiteralString: "#010103",
chroma.LiteralStringEscape: "#010107",
chroma.LiteralNumber: "#010106",
chroma.Comment: "italic #010109",
chroma.CommentPreproc: "noitalic #010102",
chroma.GenericDeleted: "#01010a bg:#01010b",
chroma.GenericInserted: "#01010c bg:#01010d",
chroma.GenericEmph: "italic",
chroma.GenericStrong: "bold",
chroma.GenericHeading: "bold",
chroma.GenericSubheading: "bold",
chroma.Error: "#01010a",
chroma.LineNumbers: "#01010e",
chroma.LineNumbersTable: "#01010e",
chroma.LineHighlight: "bg:#01010f",
})
// tokenOf maps each placeholder to the palette variable it stands for.
var tokenOf = strings.NewReplacer(
"#010100", "var(--code-block)", "#010101", "var(--fg)",
"#010102", "var(--syn-kw)", "#010103", "var(--syn-str)",
"#010104", "var(--syn-fn)", "#010105", "var(--syn-type)",
"#010106", "var(--syn-num)", "#010107", "var(--syn-builtin)",
"#010108", "var(--syn-attr)", "#010109", "var(--syn-com)",
"#01010a", "var(--del)", "#01010b", "var(--del-row)",
"#01010c", "var(--add)", "#01010d", "var(--add-row)",
"#01010e", "var(--muted)", "#01010f", "var(--mark)",
)
// CSS returns the chroma class rules, every colour a palette variable.
func CSS() string {
var b strings.Builder
formatter.WriteCSS(&b, quarryStyle)
return tokenOf.Replace(b.String())
}
// numbered is the formatter for a standalone viewer: the same classes,
// plus a line-number gutter in its own table cell so a reader selecting
// the code does not select the numbers with it.
var numbered = chromahtml.New(
chromahtml.WithClasses(true),
chromahtml.PreventSurroundingPre(false),
chromahtml.WithLineNumbers(true),
chromahtml.LineNumbersInTable(true),
)
// Numbered highlights a whole file with a line-number gutter. Unlike
// Code it always answers for content within MaxBytes: a file no lexer
// claims is still drawn, uncoloured, through chroma's fallback lexer,
// because the numbers are the point and grey text with a gutter is a
// better answer than a bare <pre>.
func Numbered(code []byte, filename string) (string, bool) {
if TooBig(code) {
return "", false
}
lexer := lexerFor(code, "", filename)
if lexer == nil {
lexer = lexers.Fallback
}
it, err := lexer.Tokenise(nil, string(code))
if err != nil {
return "", false
}
var buf bytes.Buffer
if err := numbered.Format(&buf, quarryStyle, it); err != nil {
return "", false
}
return buf.String(), true
}