feat/multi-repo
1package main
2
3import (
4 "fmt"
5 "html/template"
6 "net"
7 "net/http"
8 "sort"
9 "strings"
10
11 "github.com/alecthomas/chroma/v2/formatters/html"
12 "github.com/alecthomas/chroma/v2/lexers"
13 "github.com/alecthomas/chroma/v2/styles"
14
15 "github.com/antonmedv/gitmal/pkg/templates"
16)
17
18func resolveTheme(theme, themeLight, themeDark string) (string, string, error) {
19 switch {
20 case themeLight != "" && themeDark != "":
21 if _, ok := themeStyles[themeLight]; !ok {
22 return "", "", fmt.Errorf("invalid light theme: %s", themeLight)
23 }
24 if _, ok := themeStyles[themeDark]; !ok {
25 return "", "", fmt.Errorf("invalid dark theme: %s", themeDark)
26 }
27 return themeLight, themeDark, nil
28
29 case themeLight != "":
30 if _, ok := themeStyles[themeLight]; !ok {
31 return "", "", fmt.Errorf("invalid light theme: %s", themeLight)
32 }
33 if _, dark, ok := themePair(themeLight); ok {
34 return themeLight, dark, nil
35 }
36 echo(fmt.Sprintf("Warning: no dark counterpart for %q, using same theme", themeLight))
37 return themeLight, themeLight, nil
38
39 case themeDark != "":
40 if _, ok := themeStyles[themeDark]; !ok {
41 return "", "", fmt.Errorf("invalid dark theme: %s", themeDark)
42 }
43 if light, _, ok := themePair(themeDark); ok {
44 return light, themeDark, nil
45 }
46 echo(fmt.Sprintf("Warning: no light counterpart for %q, using same theme", themeDark))
47 return themeDark, themeDark, nil
48
49 default:
50 if _, ok := themeStyles[theme]; !ok {
51 return "", "", fmt.Errorf("invalid theme: %s", theme)
52 }
53 if light, dark, ok := themePair(theme); ok {
54 return light, dark, nil
55 }
56 echo(fmt.Sprintf("Warning: no %s counterpart for %q, using same theme for both", oppositeTone(theme), theme))
57 return theme, theme, nil
58 }
59}
60
61func oppositeTone(theme string) string {
62 if themeStyles[theme] == "dark" {
63 return "light"
64 }
65 return "dark"
66}
67
68var themeStyles = map[string]string{
69 "abap": "light",
70 "algol": "light",
71 "arduino": "light",
72 "autumn": "light",
73 "average": "dark",
74 "base16-snazzy": "dark",
75 "borland": "light",
76 "bw": "light",
77 "catppuccin-frappe": "dark",
78 "catppuccin-latte": "light",
79 "catppuccin-macchiato": "dark",
80 "catppuccin-mocha": "dark",
81 "colorful": "light",
82 "doom-one": "dark",
83 "doom-one2": "dark",
84 "dracula": "dark",
85 "emacs": "light",
86 "evergarden": "dark",
87 "friendly": "light",
88 "fruity": "dark",
89 "github-dark": "dark",
90 "github": "light",
91 "gruvbox-light": "light",
92 "gruvbox": "dark",
93 "hrdark": "dark",
94 "igor": "light",
95 "lovelace": "light",
96 "manni": "light",
97 "modus-operandi": "light",
98 "modus-vivendi": "dark",
99 "monokai": "dark",
100 "monokailight": "light",
101 "murphy": "light",
102 "native": "dark",
103 "nord": "dark",
104 "nordic": "dark",
105 "onedark": "dark",
106 "onesenterprise": "dark",
107 "paraiso-dark": "dark",
108 "paraiso-light": "light",
109 "pastie": "light",
110 "perldoc": "light",
111 "pygments": "light",
112 "rainbow_dash": "light",
113 "rose-pine-dawn": "light",
114 "rose-pine-moon": "dark",
115 "rose-pine": "dark",
116 "rpgle": "dark",
117 "rrt": "dark",
118 "solarized-dark": "dark",
119 "solarized-dark256": "dark",
120 "solarized-light": "light",
121 "swapoff": "dark",
122 "tango": "light",
123 "tokyonight-day": "light",
124 "tokyonight-moon": "dark",
125 "tokyonight-night": "dark",
126 "tokyonight-storm": "dark",
127 "trac": "light",
128 "vim": "dark",
129 "vs": "light",
130 "vulcan": "dark",
131 "witchhazel": "dark",
132 "xcode-dark": "dark",
133 "xcode": "light",
134}
135
136// knownPairs maps a theme name to its light/dark counterpart pair.
137// The key can be either the light or the dark theme name.
138var knownPairs = map[string]struct{ light, dark string }{
139 "github": {"github", "github-dark"},
140 "github-dark": {"github", "github-dark"},
141 "xcode": {"xcode", "xcode-dark"},
142 "xcode-dark": {"xcode", "xcode-dark"},
143 "catppuccin-latte": {"catppuccin-latte", "catppuccin-mocha"},
144 "catppuccin-mocha": {"catppuccin-latte", "catppuccin-mocha"},
145 "gruvbox-light": {"gruvbox-light", "gruvbox"},
146 "gruvbox": {"gruvbox-light", "gruvbox"},
147 "solarized-light": {"solarized-light", "solarized-dark"},
148 "solarized-dark": {"solarized-light", "solarized-dark"},
149 "paraiso-light": {"paraiso-light", "paraiso-dark"},
150 "paraiso-dark": {"paraiso-light", "paraiso-dark"},
151 "rose-pine-dawn": {"rose-pine-dawn", "rose-pine"},
152 "rose-pine": {"rose-pine-dawn", "rose-pine"},
153 "tokyonight-day": {"tokyonight-day", "tokyonight-night"},
154 "tokyonight-night": {"tokyonight-day", "tokyonight-night"},
155 "modus-operandi": {"modus-operandi", "modus-vivendi"},
156 "modus-vivendi": {"modus-operandi", "modus-vivendi"},
157 "monokailight": {"monokailight", "monokai"},
158 "monokai": {"monokailight", "monokai"},
159}
160
161// themePair returns the light and dark theme names for a given theme.
162// If no counterpart is found, it returns the same name for both and ok=false.
163func themePair(theme string) (light, dark string, ok bool) {
164 if p, found := knownPairs[theme]; found {
165 return p.light, p.dark, true
166 }
167 // Heuristic: swap -dark/-light or add/remove suffix
168 if _, exists := themeStyles[theme+"-dark"]; exists && themeStyles[theme] == "light" {
169 return theme, theme + "-dark", true
170 }
171 if _, exists := themeStyles[theme+"-light"]; exists && themeStyles[theme] == "dark" {
172 return theme + "-light", theme, true
173 }
174 // Try removing -dark/-light suffix
175 if strings.HasSuffix(theme, "-dark") {
176 base := strings.TrimSuffix(theme, "-dark")
177 if _, exists := themeStyles[base]; exists {
178 return base, theme, true
179 }
180 }
181 if strings.HasSuffix(theme, "-light") {
182 base := strings.TrimSuffix(theme, "-light")
183 if _, exists := themeStyles[base]; exists {
184 return theme, base, true
185 }
186 }
187 return theme, theme, false
188}
189
190func previewThemes() {
191 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
192 names := make([]string, 0, len(themeStyles))
193 for name := range themeStyles {
194 names = append(names, name)
195 }
196 sort.Strings(names)
197
198 sampleLang := "javascript"
199 sampleCode := `function fib(n) {
200 if (n <= 1) {
201 return n;
202 }
203 return fib(n - 1) + fib(n - 2);
204}
205
206// Print n Fibonacci numbers.
207const n = 10;
208
209for (let i = 0; i < n; i++) {
210 console.log(fib(i));
211}`
212
213 formatter := html.New(
214 html.WithClasses(false),
215 )
216
217 // Generate cards
218 cards := make([]templates.PreviewCard, 0, len(names))
219 for _, theme := range names {
220 style := styles.Get(theme)
221 if style == nil {
222 continue
223 }
224 lexer := lexers.Get(sampleLang)
225 if lexer == nil {
226 continue
227 }
228 it, err := lexer.Tokenise(nil, sampleCode)
229 if err != nil {
230 continue
231 }
232 var sb strings.Builder
233 if err := formatter.Format(&sb, style, it); err != nil {
234 continue
235 }
236 cards = append(cards, templates.PreviewCard{
237 Name: theme,
238 Tone: themeStyles[theme],
239 HTML: template.HTML(sb.String()),
240 })
241 }
242
243 w.Header().Set("Content-Type", "text/html; charset=utf-8")
244 _ = templates.PreviewTemplate.Execute(w, templates.PreviewParams{
245 Count: len(cards),
246 Themes: cards,
247 })
248 })
249
250 ln, err := net.Listen("tcp", "127.0.0.1:0")
251 if err != nil {
252 panic(err)
253 }
254
255 addr := ln.Addr().String()
256 echo("Preview themes at http://" + addr)
257
258 if err := http.Serve(ln, handler); err != nil && err != http.ErrServerClosed {
259 panic(err)
260 }
261}