internal/httpd/palette_test.go
96 lines · 3033 bytes
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}