diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-25 22:13:32 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-25 22:13:32 +0300 |
| commit | a5cb9f7ad4b2bcb7ac367854147f661ab83c5ec1 (patch) | |
| tree | 96db490ed72dec49c4bc2aba8539fd57f3246f47 /internal/generator/generator.go | |
| parent | cc9c69ce748416ad1d9b28592ec463434023d63c (diff) | |
can swap themes dynamicallyv0.12.0
Diffstat (limited to 'internal/generator/generator.go')
| -rw-r--r-- | internal/generator/generator.go | 198 |
1 files changed, 177 insertions, 21 deletions
diff --git a/internal/generator/generator.go b/internal/generator/generator.go index dc1f236..079cc38 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -12,17 +12,29 @@ import ( "codeberg.org/snonux/snonux/internal/config" "codeberg.org/snonux/snonux/internal/generator/atom" + "codeberg.org/snonux/snonux/internal/generator/templates" "codeberg.org/snonux/snonux/internal/post" ) // pageData holds the template variables for a single HTML page. +// Theme-specific values come from the default theme's meta.json — they are +// what the user sees on first paint. shared.js may swap them out at runtime +// when a non-default theme is saved in localStorage. type pageData struct { - Posts []postView - PrevPage string // URL of the newer page, empty if none - NextPage string // URL of the older page, empty if none - PrevPageJSON template.JS - NextPageJSON template.JS - ThemeSoundsJSON template.JS // Web Audio preset for this theme (splash + nav) + Posts []postView + PrevPage string // URL of the newer page, empty if none + NextPage string // URL of the older page, empty if none + PrevPageJSON template.JS + NextPageJSON template.JS + + DefaultTheme string // baked into the shell as SNONUX_DEFAULT_THEME + AllThemesJSON template.JS // [...] of all available theme names, JS literal + DefaultTitle string // <title> contents for the default theme + DefaultHeaderHTML template.HTML // <header> innerHTML for the default theme + DefaultSplashHTML template.HTML // #splash-overlay innerHTML for the default theme + DefaultPrevText template.HTML // pagination prev anchor text (theme-styled) + DefaultNextText template.HTML // pagination next anchor text + DefaultSoundsJSON template.JS // default theme's Web Audio preset (literal) } // postView is a render-friendly representation of a post for the HTML template. @@ -32,23 +44,62 @@ type postView struct { ContentHTML template.HTML // pre-rendered; trusted — generated by this tool } -// Run loads all posts, generates all HTML pages, and writes atom.xml. +// themeMeta mirrors the JSON structure written by the migration tool and used +// at runtime by shared.js to swap theme markup in. +type themeMeta struct { + Title string `json:"title"` + HeaderHTML string `json:"header_html"` + SplashInnerHTML string `json:"splash_inner_html"` + PrevPageText string `json:"prev_page_text"` + NextPageText string `json:"next_page_text"` +} + +func loadThemeMeta(name string) (themeMeta, error) { + var m themeMeta + b, err := templates.ThemeMeta(name) + if err != nil { + return m, fmt.Errorf("read theme meta %q: %w", name, err) + } + if err := json.Unmarshal(b, &m); err != nil { + return m, fmt.Errorf("parse theme meta %q: %w", name, err) + } + return m, nil +} + +// allThemesJSON returns a JS array literal of all theme names. +func allThemesJSON() (template.JS, error) { + names, err := templates.ThemeNames() + if err != nil { + return "", err + } + b, err := json.Marshal(names) + if err != nil { + return "", err + } + return template.JS(b), nil //nolint:gosec // marshalled from a fixed string slice +} + +// Run loads all posts, generates all HTML pages, and writes atom.xml plus the +// shared CSS/JS bundles and per-theme asset files. func Run(cfg *config.Config) error { posts, err := loadAllPosts(cfg.OutputDir) if err != nil { return err } - // Sort newest-first so page 1 (index.html) has the latest content. sort.Slice(posts, func(i, j int) bool { return posts[i].Timestamp.After(posts[j].Timestamp) }) pages := paginate(posts, config.PostsPerPage) - // Combine the theme HTML (which uses {{template "navhints"}} etc.) with the - // shared navDefs sub-templates so a single parse call resolves all references. - combined := injectSharedHead(getTheme(cfg.Theme)) + "\n" + navDefs + // Combine shell.tmpl with nav.tmpl partials so a single parse call resolves + // references to splashGate, navhints, navmodal. + shellSrc, err := templates.Shell() + if err != nil { + return fmt.Errorf("load shell template: %w", err) + } + combined := shellSrc + "\n" + navDefs tmpl, err := template.New("page").Parse(combined) if err != nil { return fmt.Errorf("parse page template: %w", err) @@ -57,9 +108,25 @@ func Run(cfg *config.Config) error { if err := writeFavicon(cfg.OutputDir); err != nil { return err } + if err := writeSharedAssets(cfg.OutputDir); err != nil { + return err + } + if err := writeAllThemeAssets(cfg.OutputDir); err != nil { + return err + } + + defaultTheme := validThemeName(cfg.Theme) + defaultMeta, err := loadThemeMeta(defaultTheme) + if err != nil { + return err + } + all, err := allThemesJSON() + if err != nil { + return err + } for i, page := range pages { - if err := writePage(tmpl, page, i, len(pages), cfg); err != nil { + if err := writePage(tmpl, page, i, len(pages), cfg, defaultTheme, defaultMeta, all); err != nil { return err } } @@ -126,8 +193,8 @@ func pageFilename(index int) string { const indexPageNavURL = "index.html?splash=0" // writePage renders one HTML page and writes it to cfg.OutputDir. -func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPages int, cfg *config.Config) error { - data := buildPageData(posts, pageIndex, totalPages, cfg.Theme) +func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPages int, cfg *config.Config, defaultTheme string, defaultMeta themeMeta, all template.JS) error { + data := buildPageData(posts, pageIndex, totalPages, defaultTheme, defaultMeta, all) filename := pageFilename(pageIndex) path := filepath.Join(cfg.OutputDir, filename) @@ -146,7 +213,7 @@ func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPage } // buildPageData constructs the template data for a single page. -func buildPageData(posts []*post.Post, pageIndex, totalPages int, theme string) pageData { +func buildPageData(posts []*post.Post, pageIndex, totalPages int, defaultTheme string, meta themeMeta, all template.JS) pageData { views := make([]postView, len(posts)) for i, p := range posts { views[i] = postView{ @@ -173,12 +240,19 @@ func buildPageData(posts []*post.Post, pageIndex, totalPages int, theme string) } return pageData{ - Posts: views, - PrevPage: prevPage, - NextPage: nextPage, - PrevPageJSON: jsonStringOrNull(prevPage), - NextPageJSON: jsonStringOrNull(nextPage), - ThemeSoundsJSON: themeSoundsJSON(theme), + Posts: views, + PrevPage: prevPage, + NextPage: nextPage, + PrevPageJSON: jsonStringOrNull(prevPage), + NextPageJSON: jsonStringOrNull(nextPage), + DefaultTheme: defaultTheme, + AllThemesJSON: all, + DefaultTitle: meta.Title, + DefaultHeaderHTML: template.HTML(meta.HeaderHTML), //nolint:gosec // source is in-tree theme metadata + DefaultSplashHTML: template.HTML(meta.SplashInnerHTML), //nolint:gosec // same + DefaultPrevText: template.HTML(meta.PrevPageText), //nolint:gosec // same + DefaultNextText: template.HTML(meta.NextPageText), //nolint:gosec // same + DefaultSoundsJSON: themeSoundsJSON(defaultTheme), } } @@ -202,3 +276,85 @@ func jsonStringOrNull(s string) template.JS { return template.JS(strings.TrimSpace(string(b))) //nolint:gosec // filename is tool-generated } + +// writeSharedAssets dumps shared.css and shared.js to the output dir. They are +// linked from every page and cached by browsers across navigations. +func writeSharedAssets(outputDir string) error { + css, err := templates.SharedCSS() + if err != nil { + return fmt.Errorf("read shared.css: %w", err) + } + if err := os.WriteFile(filepath.Join(outputDir, "shared.css"), css, 0o644); err != nil { + return fmt.Errorf("write shared.css: %w", err) + } + + js, err := templates.SharedJS() + if err != nil { + return fmt.Errorf("read shared.js: %w", err) + } + if err := os.WriteFile(filepath.Join(outputDir, "shared.js"), js, 0o644); err != nil { + return fmt.Errorf("write shared.js: %w", err) + } + + return nil +} + +// writeAllThemeAssets copies each theme's CSS, JS, meta.json, and writes its +// sounds.json to dist/themes/<name>/. shared.js fetches these on theme switch. +func writeAllThemeAssets(outputDir string) error { + names, err := templates.ThemeNames() + if err != nil { + return err + } + + root := filepath.Join(outputDir, "themes") + if err := os.MkdirAll(root, 0o755); err != nil { + return fmt.Errorf("create themes dir: %w", err) + } + + for _, name := range names { + dir := filepath.Join(root, name) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create theme dir %q: %w", name, err) + } + if err := writeThemeAsset(dir, name); err != nil { + return err + } + } + + return nil +} + +// writeThemeAsset copies the four per-theme files into dir. +func writeThemeAsset(dir, name string) error { + css, err := templates.ThemeCSS(name) + if err != nil { + return fmt.Errorf("read %s/theme.css: %w", name, err) + } + if err := os.WriteFile(filepath.Join(dir, "theme.css"), css, 0o644); err != nil { + return fmt.Errorf("write %s/theme.css: %w", name, err) + } + + js, err := templates.ThemeJS(name) + if err != nil { + return fmt.Errorf("read %s/theme.js: %w", name, err) + } + if err := os.WriteFile(filepath.Join(dir, "theme.js"), js, 0o644); err != nil { + return fmt.Errorf("write %s/theme.js: %w", name, err) + } + + meta, err := templates.ThemeMeta(name) + if err != nil { + return fmt.Errorf("read %s/meta.json: %w", name, err) + } + if err := os.WriteFile(filepath.Join(dir, "meta.json"), meta, 0o644); err != nil { + return fmt.Errorf("write %s/meta.json: %w", name, err) + } + + soundsJSON := themeSoundsJSON(name) + if err := os.WriteFile(filepath.Join(dir, "sounds.json"), []byte(soundsJSON), 0o644); err != nil { + return fmt.Errorf("write %s/sounds.json: %w", name, err) + } + + return nil +} |
