Commit 0e719f049c
Verified · cmc ci/build: success
internal/httpd/diff.go +1 −1
| @@ -227,7 +227,7 @@ func highlightLines(lexer chroma.Lexer, src string) []template.HTML { | ||
| 227 | 227 | return nil |
| 228 | 228 | } |
| 229 | 229 | 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 { | |
| 231 | 231 | return nil |
| 232 | 232 | } |
| 233 | 233 | body := buf.String() |
internal/httpd/palette_test.go added +96
| @@ -0,0 +1,96 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 4 | "math" | |
| 5 | "testing" | |
| 6 | ||
| 7 | "github.com/alecthomas/chroma/v2" | |
| 8 | "github.com/alecthomas/chroma/v2/styles" | |
| 9 | ) | |
| 10 | ||
| 11 | func 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 | ||
| 18 | func 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 | ||
| 24 | func 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). | |
| 40 | func 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 { | ||
| 823 | 823 | return template.HTML("<pre>" + template.HTMLEscapeString(string(data)) + "</pre>") |
| 824 | 824 | } |
| 825 | 825 | 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 { | |
| 827 | 827 | return template.HTML("<pre>" + template.HTMLEscapeString(string(data)) + "</pre>") |
| 828 | 828 | } |
| 829 | 829 | return template.HTML(buf.String()) |
| @@ -835,12 +835,27 @@ func highlight(filePath string, data []byte) template.HTML { | ||
| 835 | 835 | // light-theme colour on a black ground — NameAttribute landed at 2.97:1. |
| 836 | 836 | // Scoped, an unnamed token inherits the wrapper's colour instead, which is |
| 837 | 837 | // 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. | |
| 842 | const ( | |
| 843 | lightStyle = "xcode" | |
| 844 | darkStyle = "github-dark" | |
| 845 | ) | |
| 846 | ||
| 838 | 847 | var chromaCSS = func() []byte { |
| 839 | 848 | var buf bytes.Buffer |
| 840 | 849 | 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") | |
| 842 | 854 | 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") | |
| 844 | 859 | buf.WriteString("}\n.chroma, .bg { background: transparent !important; }\n") |
| 845 | 860 | return buf.Bytes() |
| 846 | 861 | }() |
| @@ -923,7 +938,7 @@ func fenceHighlight(source, lang string) string { | ||
| 923 | 938 | } |
| 924 | 939 | var buf bytes.Buffer |
| 925 | 940 | 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 { | |
| 927 | 942 | return "<pre>" + template.HTMLEscapeString(source) + "</pre>" |
| 928 | 943 | } |
| 929 | 944 | return buf.String() |