summaryrefslogtreecommitdiff
path: root/internal/generator/generator.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/generator/generator.go')
-rw-r--r--internal/generator/generator.go198
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
+}