A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 0e719f049c

0e719f049cb904a5aa8409fe3a260cab77fabd62

parent: b3b4b490f0

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-26T23:06:41Z

web: pick syntax palettes on measured contrast

friendly, chroma's default, put 61 token/ground pairs under 4.5:1 against
the grounds code actually sits on here — page, code block, and the two
diff tints. Comments came out at 2.5:1. Light uses xcode now, which puts
one pair under, overridden to clear it.

github-dark's line numbers were 4.31:1 on the page; lifted to the grey
its comments use.

TestSyntaxPaletteContrast holds both palettes to the floor so a future
style swap has to answer for it.
internal/httpd/diff.go +1 −1
@@ -227,7 +227,7 @@ func highlightLines(lexer chroma.Lexer, src string) []template.HTML {
227227 return nil
228228 }
229229 var buf bytes.Buffer
230 if err := diffFormatter.Format(&buf, styles.Get("friendly"), it); err != nil {
230 if err := diffFormatter.Format(&buf, styles.Get(lightStyle), it); err != nil {
231231 return nil
232232 }
233233 body := buf.String()
internal/httpd/palette_test.go added +96
@@ -0,0 +1,96 @@
1package httpd
2
3import (
4 "math"
5 "testing"
6
7 "github.com/alecthomas/chroma/v2"
8 "github.com/alecthomas/chroma/v2/styles"
9)
10
11func channel(v float64) float64 {
12 if v <= 0.03928 {
13 return v / 12.92
14 }
15 return math.Pow((v+0.055)/1.055, 2.4)
16}
17
18func luminance(c chroma.Colour) float64 {
19 return 0.2126*channel(float64(c.Red())/255) +
20 0.7152*channel(float64(c.Green())/255) +
21 0.0722*channel(float64(c.Blue())/255)
22}
23
24func contrast(a, b chroma.Colour) float64 {
25 hi, lo := luminance(a), luminance(b)
26 if hi < lo {
27 hi, lo = lo, hi
28 }
29 return (hi + 0.05) / (lo + 0.05)
30}
31
32// TestSyntaxPaletteContrast holds both palettes to WCAG AA for body text
33// against every ground code sits on here: the page, a code block, and the
34// two diff tints. The chroma default (friendly) put 61 token/ground pairs
35// under the floor, which is why the styles are chosen rather than assumed.
36//
37// Two tokens are exempted because the stylesheet overrides them after
38// writing the palette, which this test cannot see: NameAttribute in light
39// (#6f5a21) and the line-number tokens in dark (#8b949e).
40func TestSyntaxPaletteContrast(t *testing.T) {
41 const floor = 4.5
42 // chroma's own line-number gutter renders on blob pages only — the diff
43 // supplies its own gutter — so those tokens are checked against the page
44 // and code grounds, never against a diff tint.
45 gutterOnly := map[chroma.TokenType]bool{
46 chroma.LineNumbers: true, chroma.LineNumbersTable: true,
47 chroma.LineHighlight: true, chroma.LineTable: true, chroma.LineTableTD: true,
48 }
49 cases := []struct {
50 style string
51 grounds map[string]chroma.Colour
52 exempt map[chroma.TokenType]bool
53 }{
54 {lightStyle, map[string]chroma.Colour{
55 "page": chroma.NewColour(0xff, 0xff, 0xff),
56 "code": chroma.NewColour(0xf5, 0xf5, 0xf5),
57 "add": chroma.NewColour(0xe4, 0xf6, 0xea),
58 "del": chroma.NewColour(0xfd, 0xea, 0xea),
59 }, map[chroma.TokenType]bool{chroma.NameAttribute: true}},
60 {darkStyle, map[string]chroma.Colour{
61 "page": chroma.NewColour(0x0a, 0x0a, 0x0a),
62 "code": chroma.NewColour(0x05, 0x05, 0x05),
63 "add": chroma.NewColour(0x0d, 0x2a, 0x18),
64 "del": chroma.NewColour(0x2c, 0x11, 0x13),
65 }, map[chroma.TokenType]bool{
66 chroma.LineNumbers: true, chroma.LineNumbersTable: true,
67 }},
68 }
69
70 for _, tc := range cases {
71 st := styles.Get(tc.style)
72 if st == nil || st.Name != tc.style {
73 t.Fatalf("chroma style %q is not available", tc.style)
74 }
75 for _, tt := range st.Types() {
76 // Whitespace markers are meant to be near-invisible, and the
77 // background entry is a ground, not text.
78 if tt == chroma.TextWhitespace || tt == chroma.Background || tc.exempt[tt] {
79 continue
80 }
81 e := st.Get(tt)
82 if !e.Colour.IsSet() {
83 continue
84 }
85 for name, ground := range tc.grounds {
86 if gutterOnly[tt] && (name == "add" || name == "del") {
87 continue
88 }
89 if got := contrast(e.Colour, ground); got < floor {
90 t.Errorf("%s: %s (%s) on %s is %.2f:1, want >= %.1f",
91 tc.style, tt, e.Colour, name, got, floor)
92 }
93 }
94 }
95 }
96}
internal/httpd/web.go +19 −4
@@ -823,7 +823,7 @@ func highlight(filePath string, data []byte) template.HTML {
823823 return template.HTML("<pre>" + template.HTMLEscapeString(string(data)) + "</pre>")
824824 }
825825 var buf bytes.Buffer
826 if err := chromaFormatter.Format(&buf, styles.Get("friendly"), iterator); err != nil {
826 if err := chromaFormatter.Format(&buf, styles.Get(lightStyle), iterator); err != nil {
827827 return template.HTML("<pre>" + template.HTMLEscapeString(string(data)) + "</pre>")
828828 }
829829 return template.HTML(buf.String())
@@ -835,12 +835,27 @@ func highlight(filePath string, data []byte) template.HTML {
835835 // light-theme colour on a black ground — NameAttribute landed at 2.97:1.
836836 // Scoped, an unnamed token inherits the wrapper's colour instead, which is
837837 // readable in both. The site's --code-bg stays the background either way.
838// lightStyle and darkStyle are chosen on measured contrast against the
839// grounds code actually sits on here — page, code block, and the diff
840// tints. friendly, the chroma default, put 61 token/ground pairs under
841// 4.5:1; xcode puts one.
842const (
843 lightStyle = "xcode"
844 darkStyle = "github-dark"
845)
846
838847 var chromaCSS = func() []byte {
839848 var buf bytes.Buffer
840849 buf.WriteString("@media (prefers-color-scheme: light) {\n")
841 chromaFormatter.WriteCSS(&buf, styles.Get("friendly"))
850 chromaFormatter.WriteCSS(&buf, styles.Get(lightStyle))
851 // xcode's NameAttribute is its one token under 4.5:1 against the diff
852 // tints (4.51 on additions, 4.38 on deletions); darkened it clears both.
853 buf.WriteString(".chroma .na { color: #6f5a21 }\n")
842854 buf.WriteString("}\n@media (prefers-color-scheme: dark) {\n")
843 chromaFormatter.WriteCSS(&buf, styles.Get("github-dark"))
855 chromaFormatter.WriteCSS(&buf, styles.Get(darkStyle))
856 // github-dark's line numbers are #6e7681, 4.31:1 on the page; lifted to
857 // the same grey its comments use, which clears the floor.
858 buf.WriteString(".chroma .lnt, .chroma .ln { color: #8b949e }\n")
844859 buf.WriteString("}\n.chroma, .bg { background: transparent !important; }\n")
845860 return buf.Bytes()
846861 }()
@@ -923,7 +938,7 @@ func fenceHighlight(source, lang string) string {
923938 }
924939 var buf bytes.Buffer
925940 f := html.New(html.WithClasses(true))
926 if err := f.Format(&buf, styles.Get("friendly"), iterator); err != nil {
941 if err := f.Format(&buf, styles.Get(lightStyle), iterator); err != nil {
927942 return "<pre>" + template.HTMLEscapeString(source) + "</pre>"
928943 }
929944 return buf.String()