fix: Auto mode is no longer disabled by explictly adding a menu.

A can of worms has been opened.
This commit is contained in:
Spencer Brower
2026-09-07 11:30:41 -04:00
parent 22dfa4c448
commit 83e488965e
4 changed files with 105 additions and 27 deletions
+3
View File
@@ -20,6 +20,9 @@
- [ ] instead of `warnings: [dynamic]Error` we should use `warnings: [dynamic; 8]Error` - [ ] instead of `warnings: [dynamic]Error` we should use `warnings: [dynamic; 8]Error`
- when limit reached, the template fails and stops rendering. - when limit reached, the template fails and stops rendering.
- [ ] Brainstorm plugin/tool/(postccs/less/tailwind/sass) support. - [ ] Brainstorm plugin/tool/(postccs/less/tailwind/sass) support.
- [ ] Reconsider `"menus": {}` opt-out: it currently discards all page frontmatter
menus (with only a warning). Decide whether empty config should instead just
suppress the auto-generated menu while still honoring frontmatter.
## General ## General
+24 -26
View File
@@ -4,7 +4,6 @@ import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:mem" import "core:mem"
import "core:os"
import "core:strings" import "core:strings"
DEFAULT_WEIGHT :: 10 DEFAULT_WEIGHT :: 10
@@ -112,42 +111,32 @@ parse_page_menus :: proc(
return result return result
} }
// build_menus populates site.menus: // build_menus populates site.menus by merging every source into one menu set:
// 1. Config menus (thor.json "menus" key present) — exclusive, preserves array order // 1. Auto-menus (sections + root-level pages)
// 2. Auto-menus (sections + root-level pages) + page frontmatter menus — merged, sorted // 2. Config menus (thor.json "menus" key), already parsed into site.menus
// 3. Page frontmatter menus
// All three are combined per menu name and sorted by weight, then name.
// //
// If "menus" is present but empty ({}) it means explicit opt-out: no menus. // If "menus" is present but empty ({}) it means explicit opt-out: no menus at
// Config menus cannot be mixed with page frontmatter menus (error). // all, and page frontmatter menus are ignored.
build_menus :: proc(site: ^Site) { build_menus :: proc(site: ^Site) {
if site.menus != nil { // Explicit opt-out: "menus": {} present but empty → no menus; page
has_menus := false // frontmatter menus are ignored (with a warning).
// TODO(TODOS.md): silently discarding frontmatter here is harsh; revisit.
if site.menus != nil && len(site.menus) == 0 {
for page in site.pages { for page in site.pages {
if len(page.menus) > 0 { if len(page.menus) > 0 {
has_menus = true
break
}
}
// Already populated from config in site_apply_config
if len(site.menus) == 0 {
// Explicit opt-out ("menus": {})
if has_menus {
log.warnf( log.warnf(
"menus: config has empty menus but pages have frontmatter menu entries; ignoring page menus", "menus: config has empty menus but pages have frontmatter menu entries; ignoring page menus",
) )
break
} }
return
} }
// Config menus active
if has_menus {
log.fatalf("menus: cannot mix config menus with frontmatter menus")
os.exit(1)
}
warn_all_duplicate_weights(site)
return return
} }
// No config menus — auto-generate, then merge page menus on top // Merge all sources: auto section/root menus, config menus (already in
// site.menus if present), then page frontmatter menus.
collect_auto_menus(site) collect_auto_menus(site)
merge_page_menus(site) merge_page_menus(site)
warn_all_duplicate_weights(site) warn_all_duplicate_weights(site)
@@ -253,8 +242,17 @@ collect_auto_menus :: proc(site: ^Site) {
return return
} }
sort_menu_entries(entries[:]) if site.menus == nil {
site.menus = make(map[string][]Menu_Entry, alloc) site.menus = make(map[string][]Menu_Entry, alloc)
}
// Merge into any existing "main" (e.g. config menus) rather than clobbering.
if existing, ok := site.menus["main"]; ok {
merged := make([dynamic]Menu_Entry, 0, len(existing) + len(entries), alloc)
append(&merged, ..existing)
append(&merged, ..entries[:])
entries = merged
}
sort_menu_entries(entries[:])
site.menus["main"] = entries[:] site.menus["main"] = entries[:]
} }
+65
View File
@@ -421,6 +421,71 @@ test_auto_menus_no_duplicate_with_frontmatter :: proc(t: ^testing.T) {
testing.expect_value(t, main[0].name, "Ideas") testing.expect_value(t, main[0].name, "Ideas")
} }
// --- config + frontmatter + auto merge tests ---
@(test)
test_build_menus_config_merges_frontmatter :: proc(t: ^testing.T) {
site: Site
mem.dynamic_arena_init(&site.arena)
defer mem.dynamic_arena_destroy(&site.arena)
context.allocator = site_allocator(&site)
// Simulate config menus as site_apply_config would populate them.
site.menus = make(map[string][]Menu_Entry, site_allocator(&site))
cfg := make([dynamic]Menu_Entry, 0, 1, site_allocator(&site))
append(&cfg, Menu_Entry{name = "GitHub", url = "https://example.com", weight = 20})
site.menus["main"] = cfg[:]
// A root-level page opting into "main" via frontmatter.
page := make_page("Ideas", "/ideas/")
page.menus = parse_page_menus(parse_raw(`"main"`), page, site_allocator(&site))
site.pages = make(#soa[dynamic]Page, 0, 1, site_allocator(&site))
append(&site.pages, page)
build_menus(&site)
main, ok := site.menus["main"]
testing.expect(t, ok)
// GitHub (config, weight 20) + Ideas (frontmatter, nil → DEFAULT_WEIGHT 10).
testing.expect(t, len(main) == 2, "expected config + frontmatter merged")
testing.expect_value(t, main[0].name, "Ideas")
testing.expect_value(t, main[1].name, "GitHub")
}
@(test)
test_build_menus_config_merges_auto :: proc(t: ^testing.T) {
site: Site
mem.dynamic_arena_init(&site.arena)
defer mem.dynamic_arena_destroy(&site.arena)
context.allocator = site_allocator(&site)
// Config menu in a different bucket ("footer"); auto-gen owns "main".
site.menus = make(map[string][]Menu_Entry, site_allocator(&site))
cfg := make([dynamic]Menu_Entry, 0, 1, site_allocator(&site))
append(&cfg, Menu_Entry{name = "GitHub", url = "https://example.com", weight = 20})
site.menus["footer"] = cfg[:]
// A section (non-index) page triggers an auto "main" section entry.
page := make_page("Post", "/blog/post/")
page.section = "blog"
site.pages = make(#soa[dynamic]Page, 0, 1, site_allocator(&site))
append(&site.pages, page)
build_menus(&site)
footer, ok_f := site.menus["footer"]
testing.expect(t, ok_f, "config footer menu should be preserved")
testing.expect(t, len(footer) == 1)
testing.expect_value(t, footer[0].name, "GitHub")
main, ok_m := site.menus["main"]
testing.expect(t, ok_m, "auto-generated main menu should exist alongside config")
testing.expect(t, len(main) == 1)
testing.expect_value(t, main[0].name, "Blog")
}
// --- warn_duplicate_weights tests --- // --- warn_duplicate_weights tests ---
@(test) @(test)
+13 -1
View File
@@ -6,9 +6,21 @@
"markdown_extensions": { "markdown_extensions": {
"footnotes": true "footnotes": true
}, },
"menus": {
"main": [
{
"name": "GitHub",
"url": "https://github.com/sbrow/thor",
"weight": 20
}
]
},
"params": { "params": {
"author": { "author": {
"name": "Spencer Brower" "name": "Spencer Brower"
} },
"stylesheets": [
"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.blue.min.css"
]
} }
} }