diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-27 08:40:32 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-27 08:40:32 +0300 |
| commit | ad0cc92e16aa98ee10bead846c0ff4c74c5244d6 (patch) | |
| tree | c405475e9744d0568b6ab3be96ba38021c731771 | |
| parent | 9ce439a217a8f2f5c48317bb5fb37e65d2f11ede (diff) | |
generator: lazy-init themeSet and navDefs with sync.Once
| -rw-r--r-- | internal/generator/doc.go | 2 | ||||
| -rw-r--r-- | internal/generator/generator.go | 2 | ||||
| -rw-r--r-- | internal/generator/generator_test.go | 12 | ||||
| -rw-r--r-- | internal/generator/shared.go | 23 | ||||
| -rw-r--r-- | internal/generator/templates.go | 2 | ||||
| -rw-r--r-- | internal/generator/themes.go | 42 |
6 files changed, 49 insertions, 34 deletions
diff --git a/internal/generator/doc.go b/internal/generator/doc.go index be82bdc..8eaf4db 100644 --- a/internal/generator/doc.go +++ b/internal/generator/doc.go @@ -8,7 +8,7 @@ // shared.css/shared.js and per-theme assets, then call atom.Generate. // - themes.go — ListThemes / validThemeName helpers backed by the embedded FS. // - shared.go — navDefs: shared {{define}} blocks (splashGate, navhints, -// navmodal) merged at parse time with shell.tmpl so a single html/template +// navSharedCSS) merged at parse time with shell.tmpl so a single html/template // parse sees every name. // - theme_sounds.go — Per-theme Web Audio parameters; one file is written per // theme to dist/themes/<name>/sounds.json and the default theme's preset is diff --git a/internal/generator/generator.go b/internal/generator/generator.go index 0b51edd..db9531b 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -99,7 +99,7 @@ func Run(cfg *config.Config) error { if err != nil { return fmt.Errorf("load shell template: %w", err) } - combined := shellSrc + "\n" + navDefs + combined := shellSrc + "\n" + getNavDefs() tmpl, err := template.New("page").Parse(combined) if err != nil { return fmt.Errorf("parse page template: %w", err) diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go index 0e3708b..56542a3 100644 --- a/internal/generator/generator_test.go +++ b/internal/generator/generator_test.go @@ -93,7 +93,7 @@ func TestJSONStringOrNull(t *testing.T) { func TestThemeSoundPresetsMatchRegistry(t *testing.T) { t.Parallel() - for name := range themeSet { + for name := range getThemeSet() { if _, ok := themeSoundPresets[name]; !ok { t.Errorf("theme %q has no sound preset in themeSoundPresets", name) } @@ -112,7 +112,7 @@ func TestThemeSoundsJSON_ambientSchema(t *testing.T) { t.Parallel() // Verify the ambient schema is present and valid for every registered theme. - for name := range themeSet { + for name := range getThemeSet() { j := themeSoundsJSON(name) if len(j) < 50 { t.Fatalf("themeSoundsJSON(%q) too short: %q", name, j) @@ -148,7 +148,7 @@ func TestThemeSoundsJSON_ambientSchema(t *testing.T) { func TestThemeSoundPresetsAmbientPopulated(t *testing.T) { t.Parallel() - for name := range themeSet { + for name := range getThemeSet() { preset, ok := themeSoundPresets[name] if !ok { t.Errorf("theme %q missing from themeSoundPresets", name) @@ -170,7 +170,7 @@ func TestThemeSoundPresetsAmbientPopulated(t *testing.T) { func TestThemeSoundPresetsAmbientValuesBounded(t *testing.T) { t.Parallel() - for name := range themeSet { + for name := range getThemeSet() { preset, ok := themeSoundPresets[name] if !ok { continue @@ -384,8 +384,8 @@ func TestLoadThemeMeta_neonHasFields(t *testing.T) { func TestListThemes_sortedAndComplete(t *testing.T) { t.Parallel() names := ListThemes() - if len(names) != len(themeSet) { - t.Fatalf("len=%d, want %d", len(names), len(themeSet)) + if len(names) != len(getThemeSet()) { + t.Fatalf("len=%d, want %d", len(names), len(getThemeSet())) } for i := 1; i < len(names); i++ { if names[i] <= names[i-1] { diff --git a/internal/generator/shared.go b/internal/generator/shared.go index d0badb8..378a01f 100644 --- a/internal/generator/shared.go +++ b/internal/generator/shared.go @@ -2,6 +2,7 @@ package generator import ( "log" + "sync" "codeberg.org/snonux/snonux/internal/generator/templates" ) @@ -18,13 +19,19 @@ import ( // Each theme ends its <style> with {{template "navSharedCSSInner"}} then calls // {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}}, // and {{template "navscript" .}} at the appropriate points in its HTML. -var navDefs = loadNavDefs() +var ( + navDefsCache string + navDefsOnce sync.Once +) -func loadNavDefs() string { - s, err := templates.Shared("nav") - if err != nil { - log.Printf("warning: could not load shared nav template: %v", err) - return "" - } - return s +func getNavDefs() string { + navDefsOnce.Do(func() { + s, err := templates.Shared("nav") + if err != nil { + log.Printf("warning: could not load shared nav template: %v", err) + return + } + navDefsCache = s + }) + return navDefsCache } diff --git a/internal/generator/templates.go b/internal/generator/templates.go index df854ab..fcb15f5 100644 --- a/internal/generator/templates.go +++ b/internal/generator/templates.go @@ -5,6 +5,6 @@ package generator // binary via embed.FS; see internal/generator/templates/embed.go. // // themes.go exposes ListThemes()/validThemeName(). -// shared.go loads templates/shared/nav.tmpl into navDefs (splashGate, navhints, +// shared.go loads templates/shared/nav.tmpl into getNavDefs() (splashGate, navhints, // navmodal partials called from shell.tmpl). // favicon.go generates the favicon.ico binary written into each output dir. diff --git a/internal/generator/themes.go b/internal/generator/themes.go index a3a9881..c0ba7ea 100644 --- a/internal/generator/themes.go +++ b/internal/generator/themes.go @@ -2,6 +2,7 @@ package generator import ( "log" + "sync" "codeberg.org/snonux/snonux/internal/generator/templates" ) @@ -12,30 +13,37 @@ const fallbackThemeName = "neon" // themeSet caches the list of theme names available in the embedded template FS // so ListThemes does not re-read the directory on every call. -var themeSet = loadThemeSet() - -func loadThemeSet() map[string]struct{} { - names, err := templates.ThemeNames() - if err != nil { - // At build time the embed //go:embed directive guarantees the FS is - // populated, so this should never happen; log and continue with an - // empty set so callers can fall back cleanly. - log.Printf("warning: could not enumerate themes from embedded FS: %v", err) - return map[string]struct{}{} - } +var ( + themeSetCache map[string]struct{} + themeSetOnce sync.Once +) - out := make(map[string]struct{}, len(names)) - for _, n := range names { - out[n] = struct{}{} - } - return out +func getThemeSet() map[string]struct{} { + themeSetOnce.Do(func() { + names, err := templates.ThemeNames() + if err != nil { + // At build time the embed //go:embed directive guarantees the FS is + // populated, so this should never happen; log and continue with an + // empty set so callers can fall back cleanly. + log.Printf("warning: could not enumerate themes from embedded FS: %v", err) + themeSetCache = map[string]struct{}{} + return + } + + out := make(map[string]struct{}, len(names)) + for _, n := range names { + out[n] = struct{}{} + } + themeSetCache = out + }) + return themeSetCache } // validThemeName returns name if it is a known theme, otherwise the fallback. // Callers use this to coerce CLI input ("--theme random" already resolves // upstream) so downstream lookups never miss. func validThemeName(name string) string { - if _, ok := themeSet[name]; ok { + if _, ok := getThemeSet()[name]; ok { return name } return fallbackThemeName |
