diff --git a/TODOS.md b/TODOS.md index 704fae7..792a6d0 100644 --- a/TODOS.md +++ b/TODOS.md @@ -20,6 +20,9 @@ - [ ] instead of `warnings: [dynamic]Error` we should use `warnings: [dynamic; 8]Error` - when limit reached, the template fails and stops rendering. - [ ] 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 diff --git a/menus.odin b/menus.odin index d102795..b0929df 100644 --- a/menus.odin +++ b/menus.odin @@ -4,7 +4,6 @@ import "core:encoding/json" import "core:fmt" import "core:log" import "core:mem" -import "core:os" import "core:strings" DEFAULT_WEIGHT :: 10 @@ -112,42 +111,32 @@ parse_page_menus :: proc( return result } -// build_menus populates site.menus: -// 1. Config menus (thor.json "menus" key present) — exclusive, preserves array order -// 2. Auto-menus (sections + root-level pages) + page frontmatter menus — merged, sorted +// build_menus populates site.menus by merging every source into one menu set: +// 1. Auto-menus (sections + root-level pages) +// 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. -// Config menus cannot be mixed with page frontmatter menus (error). +// If "menus" is present but empty ({}) it means explicit opt-out: no menus at +// all, and page frontmatter menus are ignored. build_menus :: proc(site: ^Site) { - if site.menus != nil { - has_menus := false + // Explicit opt-out: "menus": {} present but empty → no menus; page + // 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 { 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( "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 } - // 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) merge_page_menus(site) warn_all_duplicate_weights(site) @@ -253,8 +242,17 @@ collect_auto_menus :: proc(site: ^Site) { return } + if site.menus == nil { + 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 = make(map[string][]Menu_Entry, alloc) site.menus["main"] = entries[:] } diff --git a/menus_test.odin b/menus_test.odin index 220fa2e..0fdd6c0 100644 --- a/menus_test.odin +++ b/menus_test.odin @@ -421,6 +421,71 @@ test_auto_menus_no_duplicate_with_frontmatter :: proc(t: ^testing.T) { 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 --- @(test) diff --git a/site/thor.json b/site/thor.json index a76320c..00ce1b1 100644 --- a/site/thor.json +++ b/site/thor.json @@ -6,9 +6,21 @@ "markdown_extensions": { "footnotes": true }, + "menus": { + "main": [ + { + "name": "GitHub", + "url": "https://github.com/sbrow/thor", + "weight": 20 + } + ] + }, "params": { "author": { "name": "Spencer Brower" - } + }, + "stylesheets": [ + "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.blue.min.css" + ] } } \ No newline at end of file