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}