master
1package main
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "regexp"
8 "sort"
9 "strings"
10
11 "github.com/alecthomas/chroma/v2"
12 "github.com/alecthomas/chroma/v2/formatters/html"
13 "github.com/alecthomas/chroma/v2/styles"
14
15 "github.com/antonmedv/gitmal/pkg/templates"
16)
17
18func generateCSSFiles(params Params) error {
19 out := params.OutputDir
20
21 if err := os.MkdirAll(out, 0o755); err != nil {
22 return err
23 }
24
25 if err := os.WriteFile(filepath.Join(out, "style.css"), []byte(templates.LayoutCSSAdaptive()), 0o644); err != nil {
26 return err
27 }
28
29 // markdown.css with both light and dark
30 var mdLight, mdDark string
31 mdLight = templates.CSSMarkdownLight
32 mdDark = templates.CSSMarkdownDark
33 combinedMD := mdLight + "\n@media (prefers-color-scheme: dark) {\n" + mdDark + "\n}\n"
34 if err := os.WriteFile(filepath.Join(out, "markdown.css"), []byte(combinedMD), 0o644); err != nil {
35 return err
36 }
37
38 // chroma.css with both light and dark themes
39 chromaLight, err := renderChromaCSS(params.StyleLight)
40 if err != nil {
41 return err
42 }
43 chromaDark, err := renderChromaCSS(params.StyleDark)
44 if err != nil {
45 return err
46 }
47 combinedChroma := combineThemeCSS(chromaLight, chromaDark)
48 if combinedChroma != "" {
49 if err := os.WriteFile(filepath.Join(out, "chroma.css"), []byte(combinedChroma), 0o644); err != nil {
50 return err
51 }
52 }
53
54 // diff.css with both light and dark themes
55 diffLight, err := renderDiffCSS(params.StyleLight)
56 if err != nil {
57 return err
58 }
59 diffDark, err := renderDiffCSS(params.StyleDark)
60 if err != nil {
61 return err
62 }
63 combinedDiff := combineThemeCSS(diffLight, diffDark)
64 if combinedDiff != "" {
65 if err := os.WriteFile(filepath.Join(out, "diff.css"), []byte(combinedDiff), 0o644); err != nil {
66 return err
67 }
68 }
69
70 return nil
71}
72
73func renderChromaCSS(theme string) (string, error) {
74 style := styles.Get(theme)
75 if style == nil {
76 return "", nil
77 }
78 formatter := html.New(
79 html.WithClasses(true),
80 html.WithCSSComments(false),
81 )
82 var buf strings.Builder
83 if err := formatter.WriteCSS(&buf, style); err != nil {
84 return "", err
85 }
86 return buf.String(), nil
87}
88
89// stripChromaBase removes background-color and color declarations from
90// top-level .chroma and .bg rules, so the code block background and default
91// text color are controlled by CSS variables from the layout theme instead.
92// Chroma outputs single-line rules like: .chroma { background-color: #fff; color: #000; }
93var reChromaBase = regexp.MustCompile(`(\.chroma|\.bg)\s*\{([^}]*)\}`)
94var reStripBGColor = regexp.MustCompile(`\s*background-color:\s*[^;]+;`)
95var reStripColor = regexp.MustCompile(`\s*color:\s*[^;]+;`)
96
97func stripChromaBase(css string) string {
98 return reChromaBase.ReplaceAllStringFunc(css, func(match string) string {
99 parts := reChromaBase.FindStringSubmatch(match)
100 body := reStripBGColor.ReplaceAllString(parts[2], "")
101 body = reStripColor.ReplaceAllString(body, "")
102 return parts[1] + " {" + body + " }"
103 })
104}
105
106var reChromaSelector = regexp.MustCompile(`\.chroma \.\w+`)
107
108func chromaSelectors(css string) map[string]struct{} {
109 sels := make(map[string]struct{})
110 for _, m := range reChromaSelector.FindAllString(css, -1) {
111 sels[m] = struct{}{}
112 }
113 return sels
114}
115
116// lightOnlySelectors returns selectors present in light but missing in dark.
117// These need to be reset in dark mode to prevent light colors from leaking.
118func lightOnlySelectors(light, dark string) []string {
119 lightSels := chromaSelectors(light)
120 darkSels := chromaSelectors(dark)
121 var only []string
122 for sel := range lightSels {
123 if _, ok := darkSels[sel]; !ok {
124 only = append(only, sel)
125 }
126 }
127 sort.Strings(only)
128 return only
129}
130
131func combineThemeCSS(light, dark string) string {
132 if light == "" && dark == "" {
133 return ""
134 }
135 if dark == "" || light == dark {
136 return light
137 }
138
139 var darkReset strings.Builder
140 for _, sel := range lightOnlySelectors(light, dark) {
141 darkReset.WriteString(sel + " { color: inherit; }\n")
142 }
143
144 return light +
145 "\n@media (prefers-color-scheme: dark) {\n" + dark + darkReset.String() + "\n}\n" +
146 "\n[data-theme=\"dark\"] {\n" + dark + darkReset.String() + "\n}\n"
147}
148
149func renderDiffCSS(theme string) (string, error) {
150 style := styles.Get(theme)
151 if style == nil {
152 return "", nil
153 }
154 formatter := html.New(
155 html.WithClasses(true),
156 html.WithCSSComments(false),
157 html.WithCustomCSS(map[chroma.TokenType]string{
158 chroma.GenericInserted: "display: block;",
159 chroma.GenericDeleted: "display: block;",
160 }),
161 )
162 var buf bytes.Buffer
163 if err := formatter.WriteCSS(&buf, style); err != nil {
164 return "", err
165 }
166 return buf.String(), nil
167}