mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Added menu configuration.
This commit is contained in:
+224
@@ -0,0 +1,224 @@
|
||||
package main
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
|
||||
Menu_Entry :: struct {
|
||||
name: string,
|
||||
url: string,
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// If "menus" is present but empty ({}) it means explicit opt-out: no menus.
|
||||
// Config menus cannot be mixed with page frontmatter menus (error).
|
||||
build_menus :: proc(site: ^Site) {
|
||||
if site.menus != nil {
|
||||
has_menus := false
|
||||
for page in site.pages {
|
||||
if page.menu != "" {
|
||||
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",
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Config menus active
|
||||
if has_menus {
|
||||
log.fatalf("menus: cannot mix config menus with frontmatter menus")
|
||||
os.exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// No config menus — auto-generate, then merge page menus on top
|
||||
collect_auto_menus(site)
|
||||
merge_page_menus(site)
|
||||
}
|
||||
|
||||
// merge_page_menus collects frontmatter "menu" entries from pages and merges
|
||||
// them into site.menus (which may already contain auto-generated entries).
|
||||
// If no pages have "menu" set, this is a no-op.
|
||||
merge_page_menus :: proc(site: ^Site) {
|
||||
alloc := site_allocator(site)
|
||||
|
||||
// Collect page entries by menu name
|
||||
page_entries := make(map[string][dynamic]Menu_Entry, 16, alloc)
|
||||
for page in site.pages {
|
||||
if page.menu == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := page_entries[page.menu]; !ok {
|
||||
page_entries[page.menu] = make([dynamic]Menu_Entry, 0, 4, alloc)
|
||||
}
|
||||
append(&page_entries[page.menu], Menu_Entry{name = page.title, url = page.permalink})
|
||||
}
|
||||
|
||||
if len(page_entries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if site.menus == nil {
|
||||
site.menus = make(map[string][]Menu_Entry, alloc)
|
||||
}
|
||||
|
||||
for menu_name, entries in page_entries {
|
||||
sort_menu_entries(entries[:])
|
||||
if existing, ok := site.menus[menu_name]; ok {
|
||||
// Merge with existing auto-generated entries
|
||||
merged := make([dynamic]Menu_Entry, 0, len(existing) + len(entries), alloc)
|
||||
append(&merged, ..existing)
|
||||
append(&merged, ..entries[:])
|
||||
sort_menu_entries(merged[:])
|
||||
site.menus[menu_name] = merged[:]
|
||||
} else {
|
||||
site.menus[menu_name] = entries[:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collect_auto_menus :: proc(site: ^Site) {
|
||||
alloc := site_allocator(site)
|
||||
sections: map[string]bool
|
||||
|
||||
for page in site.pages {
|
||||
if page._is_index {
|
||||
continue
|
||||
}
|
||||
if page.section != "" {
|
||||
sections[page.section] = true
|
||||
}
|
||||
}
|
||||
|
||||
entries := make([dynamic]Menu_Entry, 0, 8, alloc)
|
||||
|
||||
// Section entries (one per section directory)
|
||||
for section in sections {
|
||||
name := to_title_case(section, alloc)
|
||||
url := fmt.aprintf("/%s/", section, allocator = alloc)
|
||||
for page in site.pages {
|
||||
if page.section == section && page._is_index {
|
||||
url = page.permalink
|
||||
if page.title != "" {
|
||||
name = page.title
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
append(&entries, Menu_Entry{name = name, url = url})
|
||||
}
|
||||
|
||||
// Root-level page entries (section = "", not index)
|
||||
for page in site.pages {
|
||||
if page._is_index || page.section != "" || page.title == "" {
|
||||
continue
|
||||
}
|
||||
append(&entries, Menu_Entry{name = page.title, url = page.permalink})
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sort_menu_entries(entries[:])
|
||||
site.menus = make(map[string][]Menu_Entry, alloc)
|
||||
site.menus["main"] = entries[:]
|
||||
}
|
||||
|
||||
sort_menu_entries :: proc(entries: []Menu_Entry) {
|
||||
for i in 1 ..< len(entries) {
|
||||
key := entries[i]
|
||||
j := i - 1
|
||||
for j >= 0 && strings.compare(entries[j].name, key.name) > 0 {
|
||||
entries[j + 1] = entries[j]
|
||||
j -= 1
|
||||
}
|
||||
entries[j + 1] = key
|
||||
}
|
||||
}
|
||||
|
||||
// parse_config_menus converts raw JSON from thor.json into map[string][]Menu_Entry.
|
||||
// Preserves array order as-declared.
|
||||
parse_config_menus :: proc(
|
||||
raw: json.Value,
|
||||
allocator := context.allocator,
|
||||
) -> map[string][]Menu_Entry {
|
||||
obj, ok := raw.(json.Object)
|
||||
if !ok || len(obj) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string][]Menu_Entry, allocator)
|
||||
for menu_name, menu_val in obj {
|
||||
arr, ok := menu_val.(json.Array)
|
||||
if !ok {
|
||||
log.warnf("menus: '%s' is not an array, skipping", menu_name)
|
||||
continue
|
||||
}
|
||||
|
||||
entries := make([dynamic]Menu_Entry, 0, len(arr), allocator)
|
||||
for item, idx in arr {
|
||||
entry_obj, ok := item.(json.Object)
|
||||
if !ok {
|
||||
log.warnf("menus: '%s' entry %d is not an object, skipping", menu_name, idx)
|
||||
continue
|
||||
}
|
||||
|
||||
name := ""
|
||||
url := ""
|
||||
|
||||
if v, ok := entry_obj["name"]; ok {
|
||||
if s, ok2 := v.(json.String); ok2 {
|
||||
name = string(s)
|
||||
} else {
|
||||
log.warnf(
|
||||
"menus: '%s' entry %d: 'name' must be a string, got %v, skipping",
|
||||
menu_name,
|
||||
idx,
|
||||
v,
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := entry_obj["url"]; ok {
|
||||
if s, ok2 := v.(json.String); ok2 {
|
||||
url = string(s)
|
||||
} else {
|
||||
log.warnf(
|
||||
"menus: '%s' entry %d: 'url' must be a string, got %v, skipping",
|
||||
menu_name,
|
||||
idx,
|
||||
v,
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
log.warnf("menus: '%s' entry %d missing 'name', skipping", menu_name, idx)
|
||||
continue
|
||||
}
|
||||
|
||||
append(&entries, Menu_Entry{name = name, url = url})
|
||||
}
|
||||
result[menu_name] = entries[:]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user