summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-25 22:13:32 +0300
committerPaul Buetow <paul@buetow.org>2026-04-25 22:13:32 +0300
commita5cb9f7ad4b2bcb7ac367854147f661ab83c5ec1 (patch)
tree96db490ed72dec49c4bc2aba8539fd57f3246f47
parentcc9c69ce748416ad1d9b28592ec463434023d63c (diff)
can swap themes dynamicallyv0.12.0
-rw-r--r--PLAN.md85
-rw-r--r--integrationtests/integration_test.go33
-rw-r--r--internal/generator/doc.go33
-rw-r--r--internal/generator/favicon.go27
-rw-r--r--internal/generator/generator.go198
-rw-r--r--internal/generator/generator_test.go32
-rw-r--r--internal/generator/templates.go11
-rw-r--r--internal/generator/templates/embed.go65
-rw-r--r--internal/generator/templates/shared/favicon_head.tmpl2
-rw-r--r--internal/generator/templates/shared/nav.tmpl1772
-rw-r--r--internal/generator/templates/shared/shared.css925
-rw-r--r--internal/generator/templates/shared/shared.js1343
-rw-r--r--internal/generator/templates/shell.tmpl77
-rw-r--r--internal/generator/templates/themes/aurora.tmpl359
-rw-r--r--internal/generator/templates/themes/aurora/meta.json7
-rw-r--r--internal/generator/templates/themes/aurora/theme.css94
-rw-r--r--internal/generator/templates/themes/aurora/theme.js200
-rw-r--r--internal/generator/templates/themes/biomech.tmpl217
-rw-r--r--internal/generator/templates/themes/biomech/meta.json7
-rw-r--r--internal/generator/templates/themes/biomech/theme.css52
-rw-r--r--internal/generator/templates/themes/biomech/theme.js98
-rw-r--r--internal/generator/templates/themes/brutalist.tmpl260
-rw-r--r--internal/generator/templates/themes/brutalist/meta.json7
-rw-r--r--internal/generator/templates/themes/brutalist/theme.css65
-rw-r--r--internal/generator/templates/themes/brutalist/theme.js132
-rw-r--r--internal/generator/templates/themes/cathedral/meta.json7
-rw-r--r--internal/generator/templates/themes/cathedral/theme.css79
-rw-r--r--internal/generator/templates/themes/cathedral/theme.js (renamed from internal/generator/templates/themes/cathedral.tmpl)154
-rw-r--r--internal/generator/templates/themes/cosmos/meta.json7
-rw-r--r--internal/generator/templates/themes/cosmos/theme.css81
-rw-r--r--internal/generator/templates/themes/cosmos/theme.js (renamed from internal/generator/templates/themes/cosmos.tmpl)152
-rw-r--r--internal/generator/templates/themes/dos.tmpl302
-rw-r--r--internal/generator/templates/themes/dos/meta.json7
-rw-r--r--internal/generator/templates/themes/dos/theme.css79
-rw-r--r--internal/generator/templates/themes/dos/theme.js157
-rw-r--r--internal/generator/templates/themes/matrix/meta.json7
-rw-r--r--internal/generator/templates/themes/matrix/theme.css76
-rw-r--r--internal/generator/templates/themes/matrix/theme.js (renamed from internal/generator/templates/themes/matrix.tmpl)149
-rw-r--r--internal/generator/templates/themes/neon.tmpl348
-rw-r--r--internal/generator/templates/themes/neon/meta.json7
-rw-r--r--internal/generator/templates/themes/neon/theme.css94
-rw-r--r--internal/generator/templates/themes/neon/theme.js155
-rw-r--r--internal/generator/templates/themes/noir/meta.json7
-rw-r--r--internal/generator/templates/themes/noir/theme.css77
-rw-r--r--internal/generator/templates/themes/noir/theme.js (renamed from internal/generator/templates/themes/noir.tmpl)152
-rw-r--r--internal/generator/templates/themes/ocean/meta.json7
-rw-r--r--internal/generator/templates/themes/ocean/theme.css69
-rw-r--r--internal/generator/templates/themes/ocean/theme.js (renamed from internal/generator/templates/themes/ocean.tmpl)139
-rw-r--r--internal/generator/templates/themes/plasma/meta.json7
-rw-r--r--internal/generator/templates/themes/plasma/theme.css78
-rw-r--r--internal/generator/templates/themes/plasma/theme.js (renamed from internal/generator/templates/themes/plasma.tmpl)148
-rw-r--r--internal/generator/templates/themes/retro/meta.json7
-rw-r--r--internal/generator/templates/themes/retro/theme.css79
-rw-r--r--internal/generator/templates/themes/retro/theme.js (renamed from internal/generator/templates/themes/retro.tmpl)148
-rw-r--r--internal/generator/templates/themes/retrofuture.tmpl374
-rw-r--r--internal/generator/templates/themes/retrofuture/meta.json7
-rw-r--r--internal/generator/templates/themes/retrofuture/theme.css105
-rw-r--r--internal/generator/templates/themes/retrofuture/theme.js193
-rw-r--r--internal/generator/templates/themes/spaceage/meta.json7
-rw-r--r--internal/generator/templates/themes/spaceage/theme.css80
-rw-r--r--internal/generator/templates/themes/spaceage/theme.js (renamed from internal/generator/templates/themes/spaceage.tmpl)151
-rw-r--r--internal/generator/templates/themes/surveillance.tmpl224
-rw-r--r--internal/generator/templates/themes/surveillance/meta.json7
-rw-r--r--internal/generator/templates/themes/surveillance/theme.css50
-rw-r--r--internal/generator/templates/themes/surveillance/theme.js107
-rw-r--r--internal/generator/templates/themes/synthwave/meta.json7
-rw-r--r--internal/generator/templates/themes/synthwave/theme.css85
-rw-r--r--internal/generator/templates/themes/synthwave/theme.js (renamed from internal/generator/templates/themes/synthwave.tmpl)158
-rw-r--r--internal/generator/templates/themes/terminal.tmpl275
-rw-r--r--internal/generator/templates/themes/terminal/meta.json7
-rw-r--r--internal/generator/templates/themes/terminal/theme.css70
-rw-r--r--internal/generator/templates/themes/terminal/theme.js141
-rw-r--r--internal/generator/templates/themes/tropicale/meta.json7
-rw-r--r--internal/generator/templates/themes/tropicale/theme.css81
-rw-r--r--internal/generator/templates/themes/tropicale/theme.js (renamed from internal/generator/templates/themes/tropicale.tmpl)152
-rw-r--r--internal/generator/templates/themes/volcano/meta.json7
-rw-r--r--internal/generator/templates/themes/volcano/theme.css69
-rw-r--r--internal/generator/templates/themes/volcano/theme.js (renamed from internal/generator/templates/themes/volcano.tmpl)139
-rw-r--r--internal/generator/themes.go33
-rw-r--r--internal/version/version.go2
80 files changed, 5488 insertions, 5930 deletions
diff --git a/PLAN.md b/PLAN.md
deleted file mode 100644
index 3190323..0000000
--- a/PLAN.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Plan
-
-This is the plan for the microblog of snonux.foo
-
-## Style
-
-The style should be the same as in ./design.html
-
-* also link to foo.zone, my normal blog, at the top of every page of the microblog.
-* dont have an archive link like in the design.html example.
-* the "transmit to nexus" should just link to mailto:paul@nospan.buetow.org
-* every microblog entry should have date/timestamp linke in the design.html exampl
-* dont have the likes (hearts), comments, and rebroadcast buttons per blog post.
-* we want to be able to navigat between the blog posts with cursor up-down keys and jk (vi style up-down keys)
-* we also want to navigate with the left-right cursor keys to previous, next page (if any) and also hl(vi style up-down keys)
-* when navigating, we want to highlight the active entry (with a border or background), so it's clear which post is selected.and also play a sound.
-* when hit enter on a blog post, open it in a larger view. when hit ESC there, return.
-
-## Input dir
-
-I want to create a microblog. It should have an input directory, where i can put multiple source file formats:
-
-* Plain .txt
-* Markdown .md
-* Images (.png/.jpg/.gif)
-* Audio (.mp3 files)
-
-And the blog should automatically generate a post out of it. Furthermore, it should also support a Markdown with a reference to an image file in the same directory.
-
-Once we invoke the microblog generator, it should process all files in the input dir into blog assets.
-
-### Plain text input
-
-Just add this as a plain text as a blog post
-
-### Markdown .md
-
-Add this as a formatted HTML entry with styling etc. For this, we need to convert the Markdown to HTML.
-
-### Images
-
-Just have the image displayed as its own entry.
-
-### Audio
-
-Just have a playback button for the audio in the resulting entry
-
-### After being processed
-
-After an input was procssed, remove the files from the input dir.
-
-## Output dir
-
-The output dir should only contain static assets. That the directory to be published via a webbrowser.
-
-We expect one directory per blog post. The microblog generator then combines all of them together into multiple pages.
-
-So we need to keep the individual directories per blog post since the pages need to be re-generated according to the total blog post count and same for the atom.xml feed, so we need the directories as intermediate formats. We can link to images directly to them, though. So the output directory format will be like this:
-
-
-./outdir/index.html # Generated main page
-./outdir/pageN.html # Older pages
-./ourdir/posts/YYYY-MM-DD-HHmmss/ # Microblog post asset(s)
-
-Downscale any images to a maximum width of 1024px and compress to 80% JPEG quality
-
-## Page size limit
-
-Have max 42 entries on the single HTML page. Once more, allow paging (e.g. go to next 42 pages, etc).
-
-* Next page (if any) is only on the bottom of the page.
-* Previous page button (if any) is at the top of the page.
-
-## atom.xml feed
-
-We should have an atom.xml feed with the last 42 entries generated every run.
-
-## Comprehensive testing
-
-* All features should have integration tests.
-
-## Technologies used
-
-* Implemented in Google Go
-* Follow everything from the auditing-code-quality skill
diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go
index 91ae6af..4b96d4d 100644
--- a/integrationtests/integration_test.go
+++ b/integrationtests/integration_test.go
@@ -118,8 +118,16 @@ func TestTxtInput(t *testing.T) {
// index.html must contain the post text.
index := readFile(t, filepath.Join(outputDir, "index.html"))
assertContains(t, index, "Hello, Nexus!", "index.html")
+ // splash WebGL canvas is part of the per-theme splash markup baked in.
assertContains(t, index, "splash-gl-canvas", "index.html splash WebGL canvas")
assertContains(t, index, `href="atom.xml"`, "index.html atom feed link")
+ // shared bundles must also be written.
+ if _, err := os.Stat(filepath.Join(outputDir, "shared.css")); err != nil {
+ t.Fatalf("shared.css missing: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(outputDir, "shared.js")); err != nil {
+ t.Fatalf("shared.js missing: %v", err)
+ }
}
// TestMarkdownInput verifies Markdown files are converted to HTML.
@@ -356,8 +364,11 @@ func TestKeyboardNavJS(t *testing.T) {
index := readFile(t, filepath.Join(outputDir, "index.html"))
assertContains(t, index, `data-index="0"`, "index.html data-index attribute")
- assertContains(t, index, `.post-active`, "index.html .post-active CSS")
- assertContains(t, index, `playNavSound`, "index.html playNavSound function")
+ // Shared CSS and JS now live as separate files referenced from the shell.
+ sharedCSS := readFile(t, filepath.Join(outputDir, "shared.css"))
+ assertContains(t, sharedCSS, `.post-active`, "shared.css .post-active rule")
+ sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
+ assertContains(t, sharedJS, `playNavSound`, "shared.js playNavSound function")
}
// TestThemeSelection verifies that every registered theme renders a valid
@@ -395,8 +406,24 @@ func TestThemeSelection(t *testing.T) {
index := readFile(t, filepath.Join(outputDir, "index.html"))
assertContains(t, index, "theme test post", "post text")
- assertContains(t, index, "playNavSound", "nav JS")
assertContains(t, index, `data-index="0"`, "data-index attribute")
+ assertContains(t, index, `SNONUX_DEFAULT_THEME = "`+theme+`"`, "default theme baked in")
+
+ // Per-theme assets are written under themes/<theme>/.
+ for _, fname := range []string{"theme.css", "theme.js", "meta.json", "sounds.json"} {
+ path := filepath.Join(outputDir, "themes", theme, fname)
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("theme asset missing %s: %v", path, err)
+ }
+ if info.Size() == 0 {
+ t.Fatalf("theme asset %s is empty", path)
+ }
+ }
+
+ // shared.js holds the nav logic.
+ sharedJS := readFile(t, filepath.Join(outputDir, "shared.js"))
+ assertContains(t, sharedJS, "playNavSound", "shared.js playNavSound")
})
}
}
diff --git a/internal/generator/doc.go b/internal/generator/doc.go
index d7d4a53..be82bdc 100644
--- a/internal/generator/doc.go
+++ b/internal/generator/doc.go
@@ -4,21 +4,26 @@
// Responsibilities by area (file → role):
//
// - generator.go — Orchestration: load posts from disk, sort newest-first,
-// paginate, parse theme+nav templates, write index.html / pageN.html,
-// then call atom.Generate.
-// - themes.go — Theme registry (name → template string) and getTheme /
-// ListThemes for the CLI.
-// - theme_*.go — One file per visual theme: full-page HTML that invokes
-// {{template "navSharedCSSInner"}} inside <style>, then {{template "splashGate"}},
-// {{template "navhints" .}}, {{template "navmodal" .}},
-// {{template "navscript" .}}.
-// - shared.go — navDefs: shared {{define}} blocks merged at parse time with
-// the chosen theme so a single html/template parse sees every name.
-// - theme_sounds.go — Per-theme Web Audio parameters (splash arpeggio, nav blip,
-// modal open/close); embedded in pages as ThemeSoundsJSON for navscript.
-// - templates.go — Short pointer: where templates and registry live.
+// paginate, parse shell+nav templates, write index.html / pageN.html plus
+// 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
+// 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
+// also baked into shell.tmpl as window.SNONUX_SOUNDS so the splash chime
+// can fire instantly.
+// - favicon.go — Generates the 32×32 .ico binary written into the output dir.
+// - templates.go — Short pointer: where templates and helpers live.
//
-// Dependency direction: themes and shared nav templates are composed only for
+// Theme assets live as separate files under templates/themes/<name>/theme.css,
+// theme.js, and meta.json. The single shell.tmpl loads the active theme's CSS
+// synchronously in <head> (chosen from localStorage by a boot script) and
+// shared.js orchestrates the rest. Switching themes is a localStorage write
+// followed by location.reload().
+//
+// Dependency direction: shell and shared nav templates are composed only for
// the HTML path (generator.go). Package atom depends on config and post only,
// not on themes or html/template, so feed logic stays isolated from page chrome.
package generator
diff --git a/internal/generator/favicon.go b/internal/generator/favicon.go
index a4a8251..8664183 100644
--- a/internal/generator/favicon.go
+++ b/internal/generator/favicon.go
@@ -6,35 +6,12 @@ import (
"fmt"
"image"
"image/color"
- "log"
"os"
"path/filepath"
- "strings"
-
- "codeberg.org/snonux/snonux/internal/generator/templates"
)
-// faviconHeadHTML is the <link rel="icon"> fragment injected into every theme's
-// <head>. Loaded at startup from the embedded templates FS so no HTML lives in
-// the Go sources.
-var faviconHeadHTML = loadFaviconHeadHTML()
-
-func loadFaviconHeadHTML() string {
- s, err := templates.Shared("favicon_head")
- if err != nil {
- log.Printf("warning: could not load favicon_head template: %v", err)
- return ""
- }
- return s
-}
-
-func injectSharedHead(theme string) string {
- if strings.Contains(theme, `rel="icon"`) {
- return theme
- }
-
- return strings.Replace(theme, "</head>", faviconHeadHTML+"</head>", 1)
-}
+// shell.tmpl now embeds the favicon <link> directly, so no head-injection
+// helper is needed; favicon.go only generates and writes the .ico binary.
func writeFavicon(outputDir string) error {
data, err := generateFaviconICO()
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