Commit b7251fa5a1

b7251fa5a1e12135409fe387b1a27020bec4e986

parent: f493006847

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-17 06:18 UTC

web: every template class has a stylesheet rule

Fails today, listing classes with no rule in style.css:
  ".Issue.State" (issue.html)
  ".State" (issues.html, milestones.html, mr.html, orgmilestones.html)
  ".Verdict" (mr.html)
  activity (owner.html)
  authorlink (layout.html)
  chroma (layout.html)
  compareform (refs.html)
  eq (issue.html, issues.html, milestones.html, mr.html, orgmilestones.html)
  feedline (dashboard.html)
  msmain (milestones.html, orgmilestones.html)
  signupform (register.html)

Ref #218
internal/web/classes_test.go added +106
@@ -0,0 +1,106 @@
1package web
2
3import (
4 "io/fs"
5 "regexp"
6 "sort"
7 "strings"
8 "testing"
9)
10
11// TestEveryTemplateClassHasARule fails on a class a template uses that
12// no selector in style.css mentions. Class tokens containing template
13// actions ({{...}}) are composed at render time and skipped; their
14// prefixes (chip-, badge-, check-, lang-) are covered by the rules for
15// the concrete values.
16func TestEveryTemplateClassHasARule(t *testing.T) {
17 css := string(StyleCSS)
18 // Every ".name" that appears in a selector position: outside braces.
19 depth := 0
20 var sel strings.Builder
21 for _, r := range css {
22 switch r {
23 case '{':
24 depth++
25 case '}':
26 depth--
27 default:
28 if depth == 0 {
29 sel.WriteRune(r)
30 }
31 }
32 }
33 // Media queries wrap rules one level deeper; strip their headers and
34 // scan again at depth one.
35 depth = 0
36 inMedia := false
37 for i := 0; i < len(css); i++ {
38 if strings.HasPrefix(css[i:], "@media") {
39 inMedia = true
40 }
41 switch css[i] {
42 case '{':
43 depth++
44 if depth == 1 && !inMedia {
45 // ordinary rule; already scanned above
46 }
47 case '}':
48 depth--
49 if depth == 0 {
50 inMedia = false
51 }
52 default:
53 if inMedia && depth == 1 {
54 sel.WriteByte(css[i])
55 }
56 }
57 }
58 classRe := regexp.MustCompile(`\.([a-zA-Z_][a-zA-Z0-9_-]*)`)
59 styled := map[string]bool{}
60 for _, m := range classRe.FindAllStringSubmatch(sel.String(), -1) {
61 styled[m[1]] = true
62 }
63
64 attrRe := regexp.MustCompile(`class="([^"]*)"`)
65 missing := map[string][]string{}
66 entries, err := fs.ReadDir(templateFS, "templates")
67 if err != nil {
68 t.Fatal(err)
69 }
70 for _, e := range entries {
71 src, err := templateFS.ReadFile("templates/" + e.Name())
72 if err != nil {
73 t.Fatal(err)
74 }
75 for _, m := range attrRe.FindAllStringSubmatch(string(src), -1) {
76 for _, c := range strings.Fields(m[1]) {
77 if strings.Contains(c, "{{") || strings.Contains(c, "}}") {
78 continue
79 }
80 if !styled[c] {
81 missing[c] = append(missing[c], e.Name())
82 }
83 }
84 }
85 }
86 var names []string
87 for c := range missing {
88 names = append(names, c)
89 }
90 sort.Strings(names)
91 for _, c := range names {
92 t.Errorf("class %q in %s has no rule in style.css", c, strings.Join(uniq(missing[c]), ", "))
93 }
94}
95
96func uniq(in []string) []string {
97 seen := map[string]bool{}
98 var out []string
99 for _, s := range in {
100 if !seen[s] {
101 seen[s] = true
102 out = append(out, s)
103 }
104 }
105 return out
106}