A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit af7479c3da

af7479c3da05687175f211b836c3e428233a65ab

parent: e278948b45

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-25T20:34:38Z

Scheme-aware highlighting everywhere code renders

Follow-up to the class-based chroma switch: drop the light-pinned .code
colors (the pin existed because inline styles were light-only — the
blinding dark-mode block), let the container use --code-bg/--fg, and
make chroma blocks transparent inside it.

Markdown fences highlight through goldmark-highlighting and org src
blocks through a chroma HighlightCodeBlock hook, both class-based so
the palette follows the scheme. The UGC sanitizer admits class
attributes matching chroma's short token codes only — the site's own
class names stay unusable from repo-authored HTML. Ref #10
e2e/design_test.go +4 −2
@@ -28,7 +28,8 @@ func TestReadmeRelativeLinks(t *testing.T) {
2828 "# site\n\n[guide](docs/guide.md) and [export](docs/paper.html) and "+
2929 "[abs](https://example.org/x) here\n\n![logo](img/logo.png)\n"+
3030 "![ext](https://example.org/pic.png)\n\n"+
31 "| flag | effect |\n|------|--------|\n| `-v` | verbose |\n"), 0o644)
31 "| flag | effect |\n|------|--------|\n| `-v` | verbose |\n\n"+
32 "```go\nfunc main() {}\n```\n"), 0o644)
3233 os.WriteFile(filepath.Join(dir, "docs", "guide.md"), []byte("# guide\n"), 0o644)
3334 os.WriteFile(filepath.Join(dir, "docs", "paper.org"), []byte("* paper\n"), 0o644)
3435 os.WriteFile(filepath.Join(dir, "img", "logo.png"), []byte{0x89, 0x50}, 0o644)
@@ -48,6 +49,7 @@ func TestReadmeRelativeLinks(t *testing.T) {
4849 `src="https://example.org/pic.png"`, // remote image untouched
4950 `href="https://example.org/x"`, // absolute untouched
5051 "<table>", "<td>verbose</td>", // GFM table renders
52 `<span class="kd">func</span>`, // fenced code highlighted via classes
5153 `href="/alice/site/blob/main/README.md">README.md</a>`, // clickable card header
5254 `<th>name</th>`, // file table column headers
5355 } {
@@ -108,7 +110,7 @@ func TestReadmeRelativeLinks(t *testing.T) {
108110 t.Errorf("highlighting not class-based:\n%.2000s", body)
109111 }
110112 if _, css := inst.get(t, "/static/style.css"); strings.Count(css, "/* Background */") < 2 ||
111 !strings.Contains(css, ".chroma, .bg { background: var(--code-bg)") {
113 !strings.Contains(css, ".chroma, .bg { background: transparent") {
112114 t.Error("stylesheet missing dual syntax palettes")
113115 }
114116 // Explore rows carry topics, license, and updated date.
go.mod +2 −1
@@ -10,6 +10,8 @@ require (
1010 github.com/niklasfasching/go-org v1.9.1
1111 github.com/spf13/cobra v1.10.2
1212 github.com/yuin/goldmark v1.8.5
13 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
14 go.yaml.in/yaml/v3 v3.0.4
1315 golang.org/x/crypto v0.55.0
1416 golang.org/x/net v0.57.0
1517 golang.org/x/term v0.45.0
@@ -30,7 +32,6 @@ require (
3032 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
3133 github.com/russross/blackfriday/v2 v2.1.0 // indirect
3234 github.com/spf13/pflag v1.0.9 // indirect
33 go.yaml.in/yaml/v3 v3.0.4 // indirect
3435 golang.org/x/sys v0.47.0 // indirect
3536 golang.org/x/text v0.41.0 // indirect
3637 modernc.org/libc v1.74.4 // indirect
go.sum +12
@@ -4,8 +4,10 @@ github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67
44github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
55github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
66github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
7github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
78github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
89github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
10github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
911github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
1012github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
1113github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -14,6 +16,10 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
1416github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
1517github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
1618github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
19github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
22github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
1723github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
1824github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
1925github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -48,8 +54,13 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
4854github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
4955github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
5056github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
57github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
58github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
59github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
5160github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
5261github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
62github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
63github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
5364go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
5465go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
5566golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
@@ -70,6 +81,7 @@ golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
7081golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
7182gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
7283gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
84gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
7385modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
7486modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
7587modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
internal/httpd/web.go +45 −7
@@ -23,6 +23,7 @@ import (
2323 "github.com/microcosm-cc/bluemonday"
2424 "github.com/niklasfasching/go-org/org"
2525 "github.com/yuin/goldmark"
26 highlighting "github.com/yuin/goldmark-highlighting/v2"
2627 "github.com/yuin/goldmark/extension"
2728
2829 "gitbay.org/gitbay/internal/autolink"
@@ -765,7 +766,7 @@ var chromaCSS = func() []byte {
765766 chromaFormatter.WriteCSS(&buf, styles.Get("friendly"))
766767 buf.WriteString("\n@media (prefers-color-scheme: dark) {\n")
767768 chromaFormatter.WriteCSS(&buf, styles.Get("github-dark"))
768 buf.WriteString("}\n.chroma, .bg { background: var(--code-bg) !important; }\n")
769 buf.WriteString("}\n.chroma, .bg { background: transparent !important; }\n")
769770 return buf.Bytes()
770771 }()
771772
@@ -828,8 +829,30 @@ func pickReadme(entries []gitutil.TreeEntry) string {
828829 }
829830
830831 // markdown is the shared renderer: GFM (tables, strikethrough, autolinks,
831// task lists) on top of CommonMark. Raw HTML is still dropped.
832var markdown = goldmark.New(goldmark.WithExtensions(extension.GFM))
832// task lists) on top of CommonMark, with class-based fence highlighting
833// (the palette lives in the stylesheet, per scheme). Raw HTML is still
834// dropped.
835var markdown = goldmark.New(goldmark.WithExtensions(extension.GFM,
836 highlighting.NewHighlighting(highlighting.WithFormatOptions(html.WithClasses(true)))))
837
838// fenceHighlight renders one code block with chroma classes, for org and
839// anything else outside goldmark. Unknown languages fall back to plain.
840func fenceHighlight(source, lang string) string {
841 lexer := lexers.Get(lang)
842 if lexer == nil {
843 lexer = lexers.Fallback
844 }
845 iterator, err := lexer.Tokenise(nil, source)
846 if err != nil {
847 return "<pre>" + template.HTMLEscapeString(source) + "</pre>"
848 }
849 var buf bytes.Buffer
850 f := html.New(html.WithClasses(true))
851 if err := f.Format(&buf, styles.Get("friendly"), iterator); err != nil {
852 return "<pre>" + template.HTMLEscapeString(source) + "</pre>"
853 }
854 return buf.String()
855}
833856
834857 // mdHTML renders user-authored markdown (issue and MR bodies, comments).
835858 // goldmark's default renderer drops raw HTML, so this is safe as-is.
@@ -921,8 +944,16 @@ func renderComments(cs []store.IssueComment, md func(string) template.HTML) []re
921944
922945 // ugcPolicy sanitizes rendered repo content before it enters the forge's
923946 // origin: markdown is already safe (goldmark drops raw HTML), but org-mode
924// output and repo-authored HTML are not.
925var ugcPolicy = bluemonday.UGCPolicy()
947// output and repo-authored HTML are not. Chroma's highlighting classes
948// must survive; the pattern admits only short token codes, not the site's
949// own class names.
950var ugcPolicy = func() *bluemonday.Policy {
951 p := bluemonday.UGCPolicy()
952 p.AllowAttrs("class").
953 Matching(regexp.MustCompile(`^(chroma|[a-z0-9]{1,3})( (chroma|[a-z0-9]{1,3}))*$`)).
954 OnElements("span", "pre", "code", "div")
955 return p
956}()
926957
927958 // renderReadme renders a README by extension: markdown, org-mode, and
928959 // (sanitized) HTML richly; everything else as escaped plaintext.
@@ -942,11 +973,18 @@ func renderReadme(name string, raw []byte) template.HTML {
942973 return template.HTML(buf.String())
943974 case ".org":
944975 doc := org.New().Parse(bytes.NewReader(raw), name)
945 html, err := doc.Write(org.NewHTMLWriter())
976 writer := org.NewHTMLWriter()
977 writer.HighlightCodeBlock = func(source, lang string, inline bool, params map[string]string) string {
978 if inline {
979 return "<code>" + template.HTMLEscapeString(source) + "</code>"
980 }
981 return fenceHighlight(source, lang)
982 }
983 out, err := doc.Write(writer)
946984 if err != nil {
947985 return plain()
948986 }
949 return template.HTML(ugcPolicy.Sanitize(html))
987 return template.HTML(ugcPolicy.Sanitize(out))
950988 case ".html", ".htm":
951989 return template.HTML(ugcPolicy.Sanitize(string(raw)))
952990 default:
internal/web/static/style.css +3 −3
@@ -457,9 +457,9 @@ button.linklike:hover { text-decoration: underline; filter: none; }
457457 margin-top: var(--sp-4);
458458 overflow-x: auto;
459459 }
460/* chroma emits inline styles for a light background; pin code blocks to
461 light colors in both schemes so unstyled tokens stay legible. */
462.code { background: #f8f8f8; color: #1a1a1a; padding: var(--sp-3) var(--sp-4); color-scheme: light; }
460/* Highlighting is class-based with a palette per scheme, so code blocks
461 follow the site's own colors. */
462.code { background: var(--code-bg); color: var(--fg); padding: var(--sp-3) var(--sp-4); }
463463 .code pre { margin: 0; background: transparent !important; }
464464 @media (prefers-color-scheme: dark) {
465465 .code { border-color: var(--line); }