refactor: weights are now wrapped in a Maybe.

This commit is contained in:
Spencer Brower
2026-07-30 09:21:58 -04:00
parent a420803b3e
commit b06cf6f8b1
6 changed files with 62 additions and 73 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ Page :: struct {
description: string, description: string,
date: string, date: string,
year: string, year: string,
weight: int, weight: Maybe(int),
lastmod: string, lastmod: string,
menus: map[string]Menu_Entry, menus: map[string]Menu_Entry,
content: string, content: string,
@@ -254,7 +254,7 @@ load_page :: proc(
} }
} }
page.year = get_year(page.date) page.year = get_year(page.date)
page.weight = fm.weight if fm.weight != 0 else DEFAULT_WEIGHT page.weight = fm.weight
page.lastmod = fm.lastmod page.lastmod = fm.lastmod
page.draft = fm.draft page.draft = fm.draft
page.starred = fm.isStarred page.starred = fm.isStarred
+4 -4
View File
@@ -10,7 +10,7 @@ Frontmatter :: struct {
date: string, date: string,
lastmod: string, lastmod: string,
publishDate: string, publishDate: string,
weight: int, weight: Maybe(int),
menus: json.Value, menus: json.Value,
layout: string, layout: string,
og: Open_Graph, og: Open_Graph,
@@ -85,7 +85,7 @@ json_get_bool :: proc(obj: json.Object, key: string) -> bool {
return false return false
} }
json_get_int :: proc(obj: json.Object, key: string) -> int { json_get_int :: proc(obj: json.Object, key: string) -> Maybe(int) {
if v, ok := obj[key]; ok { if v, ok := obj[key]; ok {
switch val in v { switch val in v {
case json.Integer: case json.Integer:
@@ -93,10 +93,10 @@ json_get_int :: proc(obj: json.Object, key: string) -> int {
case json.Float: case json.Float:
return int(val) return int(val)
case json.Boolean, json.String, json.Array, json.Object, json.Null: case json.Boolean, json.String, json.Array, json.Object, json.Null:
return 0 return nil
} }
} }
return 0 return nil
} }
json_get_open_graph :: proc(obj: json.Object, key: string) -> Open_Graph { json_get_open_graph :: proc(obj: json.Object, key: string) -> Open_Graph {
-27
View File
@@ -1,27 +0,0 @@
package main
import "core:fmt"
Ctx :: struct {
title: string,
using page: Page,
site: Site,
}
Page :: struct {
title: string,
}
Site :: struct {
title: string,
}
main :: proc() {
site := Ctx {
site = Site{title = "foo"},
page = Page{title = "bar"},
}
fmt.printfln("%+v", site)
fmt.printf("%+v", site)
}
+12 -12
View File
@@ -12,12 +12,12 @@ DEFAULT_WEIGHT :: 10
Menu_Entry :: struct { Menu_Entry :: struct {
name: string, name: string,
url: string, url: string,
weight: int, weight: Maybe(int),
} }
// parse_page_menus converts raw frontmatter JSON into map[string]Menu_Entry. // parse_page_menus converts raw frontmatter JSON into map[string]Menu_Entry.
// Supports three forms: // Supports three forms:
// "menus": "main" → {main: {name=title, url=permalink, weight=0}} // "menus": "main" → {main: {name=title, url=permalink, weight=nil}}
// "menus": ["main", "footer"] → {main: {...}, footer: {...}} // "menus": ["main", "footer"] → {main: {...}, footer: {...}}
// "menus": {"main": {"weight": 30}} → {main: {name=title, url=permalink, weight=30}} // "menus": {"main": {"weight": 30}} → {main: {name=title, url=permalink, weight=30}}
parse_page_menus :: proc( parse_page_menus :: proc(
@@ -37,7 +37,6 @@ parse_page_menus :: proc(
result[string(v)] = Menu_Entry { result[string(v)] = Menu_Entry {
name = page.title, name = page.title,
url = page.permalink, url = page.permalink,
weight = DEFAULT_WEIGHT,
} }
case json.Array: case json.Array:
@@ -47,7 +46,6 @@ parse_page_menus :: proc(
result[string(s)] = Menu_Entry { result[string(s)] = Menu_Entry {
name = page.title, name = page.title,
url = page.permalink, url = page.permalink,
weight = DEFAULT_WEIGHT,
} }
} else { } else {
log.warnf("menus: ignoring non-string item in menus array: %v", item) log.warnf("menus: ignoring non-string item in menus array: %v", item)
@@ -57,15 +55,15 @@ parse_page_menus :: proc(
case json.Object: case json.Object:
result = make(map[string]Menu_Entry, allocator) result = make(map[string]Menu_Entry, allocator)
for menu_name, entry_val in v { for menu_name, entry_val in v {
weight := DEFAULT_WEIGHT weight: Maybe(int) = nil
if entry_obj, ok := entry_val.(json.Object); ok { if entry_obj, ok := entry_val.(json.Object); ok {
if w, ok := entry_obj["weight"]; ok { if w, ok := entry_obj["weight"]; ok {
#partial switch wval in w { switch wval in w {
case json.Integer: case json.Integer:
weight = int(wval) weight = int(wval)
case json.Float: case json.Float:
weight = int(wval) weight = int(wval)
case: case json.Boolean, json.String, json.Array, json.Object, json.Null:
log.warnf( log.warnf(
"menus: '%s' entry 'weight' must be a number, got %v", "menus: '%s' entry 'weight' must be a number, got %v",
menu_name, menu_name,
@@ -166,9 +164,9 @@ merge_page_menus :: proc(site: ^Site) {
if _, ok := page_entries[menu_name]; !ok { if _, ok := page_entries[menu_name]; !ok {
page_entries[menu_name] = make([dynamic]Menu_Entry, 0, 4, alloc) page_entries[menu_name] = make([dynamic]Menu_Entry, 0, 4, alloc)
} }
// Effective weight: per-menu weight if explicit, else page.weight // Effective weight: per-menu weight if set, else page.weight
effective := entry.weight effective := entry.weight
if effective == DEFAULT_WEIGHT { if effective == nil {
effective = page.weight effective = page.weight
} }
append(&page_entries[menu_name], Menu_Entry{ append(&page_entries[menu_name], Menu_Entry{
@@ -235,7 +233,7 @@ collect_auto_menus :: proc(site: ^Site) {
} }
} }
if !skip { if !skip {
append(&entries, Menu_Entry{name = name, url = url, weight = DEFAULT_WEIGHT}) append(&entries, Menu_Entry{name = name, url = url})
} }
} }
@@ -263,7 +261,9 @@ collect_auto_menus :: proc(site: ^Site) {
} }
compare_menu_entries :: proc(a, b: Menu_Entry) -> int { compare_menu_entries :: proc(a, b: Menu_Entry) -> int {
if a.weight != b.weight do return a.weight - b.weight aw := a.weight.? or_else DEFAULT_WEIGHT
bw := b.weight.? or_else DEFAULT_WEIGHT
if aw != bw do return aw - bw
return strings.compare(a.name, b.name) return strings.compare(a.name, b.name)
} }
@@ -308,7 +308,7 @@ parse_config_menus :: proc(
name := "" name := ""
url := "" url := ""
weight := DEFAULT_WEIGHT weight: Maybe(int) = nil
if v, ok := entry_obj["name"]; ok { if v, ok := entry_obj["name"]; ok {
if s, ok2 := v.(json.String); ok2 { if s, ok2 := v.(json.String); ok2 {
+40 -24
View File
@@ -30,7 +30,7 @@ test_menus_string_form :: proc(t: ^testing.T) {
testing.expect(t, ok, "expected 'main' menu") testing.expect(t, ok, "expected 'main' menu")
testing.expect_value(t, entry.name, "About") testing.expect_value(t, entry.name, "About")
testing.expect_value(t, entry.url, "/about/") testing.expect_value(t, entry.url, "/about/")
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT) testing.expect(t, entry.weight == nil, "string form should have nil weight")
} }
@(test) @(test)
@@ -81,7 +81,7 @@ test_menus_object_no_weight :: proc(t: ^testing.T) {
testing.expect(t, len(menus) == 1) testing.expect(t, len(menus) == 1)
entry, ok := menus["main"] entry, ok := menus["main"]
testing.expect(t, ok) testing.expect(t, ok)
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT) testing.expect(t, entry.weight == nil, "object without weight key should have nil weight")
} }
@(test) @(test)
@@ -139,7 +139,7 @@ test_menus_object_non_object_value :: proc(t: ^testing.T) {
testing.expect(t, len(menus) == 1, "entry created with defaults") testing.expect(t, len(menus) == 1, "entry created with defaults")
entry, ok := menus["main"] entry, ok := menus["main"]
testing.expect(t, ok) testing.expect(t, ok)
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT) testing.expect(t, entry.weight == nil, "non-object value should have nil weight")
} }
@(test) @(test)
@@ -154,7 +154,7 @@ test_menus_non_numeric_weight :: proc(t: ^testing.T) {
menus := parse_page_menus(parse_raw(`{"main": {"weight": "30"}}`), page, context.allocator) menus := parse_page_menus(parse_raw(`{"main": {"weight": "30"}}`), page, context.allocator)
entry, ok := menus["main"] entry, ok := menus["main"]
testing.expect(t, ok) testing.expect(t, ok)
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT) testing.expect(t, entry.weight == nil, "non-numeric weight should have nil weight")
} }
@(test) @(test)
@@ -204,12 +204,12 @@ test_menus_null_json :: proc(t: ^testing.T) {
@(test) @(test)
test_sort_weight_orders_correctly :: proc(t: ^testing.T) { test_sort_weight_orders_correctly :: proc(t: ^testing.T) {
entries := []Menu_Entry { entries := []Menu_Entry {
{name = "Zeta", url = "/z/", weight = DEFAULT_WEIGHT}, {name = "Zeta", url = "/z/"},
{name = "Alpha", url = "/a/", weight = 5}, {name = "Alpha", url = "/a/", weight = 5},
{name = "Beta", url = "/b/", weight = 1}, {name = "Beta", url = "/b/", weight = 1},
} }
sort_menu_entries(entries) sort_menu_entries(entries)
// weight 1 first, then weight 5, then default weight 10 // weight 1 first, then weight 5, then nil (DEFAULT_WEIGHT)
testing.expect_value(t, entries[0].name, "Beta") testing.expect_value(t, entries[0].name, "Beta")
testing.expect_value(t, entries[1].name, "Alpha") testing.expect_value(t, entries[1].name, "Alpha")
testing.expect_value(t, entries[2].name, "Zeta") testing.expect_value(t, entries[2].name, "Zeta")
@@ -218,9 +218,9 @@ test_sort_weight_orders_correctly :: proc(t: ^testing.T) {
@(test) @(test)
test_sort_equal_weights_alphabetical :: proc(t: ^testing.T) { test_sort_equal_weights_alphabetical :: proc(t: ^testing.T) {
entries := []Menu_Entry { entries := []Menu_Entry {
{name = "Zebra", url = "/z/", weight = DEFAULT_WEIGHT}, {name = "Zebra", url = "/z/"},
{name = "Apple", url = "/a/", weight = DEFAULT_WEIGHT}, {name = "Apple", url = "/a/"},
{name = "Mango", url = "/m/", weight = DEFAULT_WEIGHT}, {name = "Mango", url = "/m/"},
} }
sort_menu_entries(entries) sort_menu_entries(entries)
testing.expect_value(t, entries[0].name, "Apple") testing.expect_value(t, entries[0].name, "Apple")
@@ -231,19 +231,35 @@ test_sort_equal_weights_alphabetical :: proc(t: ^testing.T) {
@(test) @(test)
test_sort_mixed_weights :: proc(t: ^testing.T) { test_sort_mixed_weights :: proc(t: ^testing.T) {
entries := []Menu_Entry { entries := []Menu_Entry {
{name = "Charlie", url = "/c/", weight = DEFAULT_WEIGHT}, {name = "Charlie", url = "/c/"},
{name = "Alpha", url = "/a/", weight = DEFAULT_WEIGHT}, {name = "Alpha", url = "/a/"},
{name = "Bravo", url = "/b/", weight = 3}, {name = "Bravo", url = "/b/", weight = 3},
{name = "Delta", url = "/d/", weight = 1}, {name = "Delta", url = "/d/", weight = 1},
} }
sort_menu_entries(entries) sort_menu_entries(entries)
// weight 1 (Delta), weight 3 (Bravo), then default weight alphabetical (Alpha, Charlie) // weight 1 (Delta), weight 3 (Bravo), then nil weight alphabetical (Alpha, Charlie)
testing.expect_value(t, entries[0].name, "Delta") testing.expect_value(t, entries[0].name, "Delta")
testing.expect_value(t, entries[1].name, "Bravo") testing.expect_value(t, entries[1].name, "Bravo")
testing.expect_value(t, entries[2].name, "Alpha") testing.expect_value(t, entries[2].name, "Alpha")
testing.expect_value(t, entries[3].name, "Charlie") testing.expect_value(t, entries[3].name, "Charlie")
} }
@(test)
test_sort_explicit_zero_before_nil :: proc(t: ^testing.T) {
// Explicit weight 0 is distinguishable from unset (nil → DEFAULT_WEIGHT).
// This is the key behavioral improvement of Maybe(int).
entries := []Menu_Entry {
{name = "Unset", url = "/u/"},
{name = "ExplicitZero", url = "/0/", weight = 0},
{name = "ExplicitFive", url = "/5/", weight = 5},
}
sort_menu_entries(entries)
// weight 0 first, then weight 5, then nil (DEFAULT_WEIGHT = 10)
testing.expect_value(t, entries[0].name, "ExplicitZero")
testing.expect_value(t, entries[1].name, "ExplicitFive")
testing.expect_value(t, entries[2].name, "Unset")
}
@(test) @(test)
test_config_weight_parsing_and_sort :: proc(t: ^testing.T) { test_config_weight_parsing_and_sort :: proc(t: ^testing.T) {
arena: mem.Dynamic_Arena arena: mem.Dynamic_Arena
@@ -268,7 +284,7 @@ test_config_weight_parsing_and_sort :: proc(t: ^testing.T) {
testing.expect_value(t, main[0].name, "Light") testing.expect_value(t, main[0].name, "Light")
testing.expect_value(t, main[0].weight, 1) testing.expect_value(t, main[0].weight, 1)
testing.expect_value(t, main[1].name, "Default") testing.expect_value(t, main[1].name, "Default")
testing.expect_value(t, main[1].weight, DEFAULT_WEIGHT) testing.expect(t, main[1].weight == nil, "entry without weight should be nil")
testing.expect_value(t, main[2].name, "Heavy") testing.expect_value(t, main[2].name, "Heavy")
testing.expect_value(t, main[2].weight, 20) testing.expect_value(t, main[2].weight, 20)
} }
@@ -296,7 +312,7 @@ test_json_get_int_missing :: proc(t: ^testing.T) {
obj, _ := json.parse_string(`{}`, spec = .JSON) obj, _ := json.parse_string(`{}`, spec = .JSON)
defer json.destroy_value(obj) defer json.destroy_value(obj)
o, _ := obj.(json.Object) o, _ := obj.(json.Object)
testing.expect_value(t, json_get_int(o, "weight"), 0) testing.expect(t, json_get_int(o, "weight") == nil, "missing key should return nil")
} }
@(test) @(test)
@@ -304,7 +320,7 @@ test_json_get_int_non_numeric :: proc(t: ^testing.T) {
obj, _ := json.parse_string(`{"weight": "5"}`, spec = .JSON) obj, _ := json.parse_string(`{"weight": "5"}`, spec = .JSON)
defer json.destroy_value(obj) defer json.destroy_value(obj)
o, _ := obj.(json.Object) o, _ := obj.(json.Object)
testing.expect_value(t, json_get_int(o, "weight"), 0) testing.expect(t, json_get_int(o, "weight") == nil, "non-numeric should return nil")
} }
// --- sort_pages tests --- // --- sort_pages tests ---
@@ -313,13 +329,13 @@ test_json_get_int_non_numeric :: proc(t: ^testing.T) {
test_sort_pages_weight_primary :: proc(t: ^testing.T) { test_sort_pages_weight_primary :: proc(t: ^testing.T) {
pages := make(#soa[dynamic]Page, 0, 3) pages := make(#soa[dynamic]Page, 0, 3)
defer delete(pages) defer delete(pages)
append(&pages, Page{title = "Gamma", date = "2025-01-03", weight = DEFAULT_WEIGHT}) append(&pages, Page{title = "Gamma", date = "2025-01-03"})
append(&pages, Page{title = "Alpha", date = "2025-01-01", weight = 5}) append(&pages, Page{title = "Alpha", date = "2025-01-01", weight = 5})
append(&pages, Page{title = "Beta", date = "2025-01-02", weight = 1}) append(&pages, Page{title = "Beta", date = "2025-01-02", weight = 1})
sort_pages(pages[:]) sort_pages(pages[:])
// weight 1, weight 5, then default weight 10 // weight 1, weight 5, then nil weight (DEFAULT_WEIGHT)
testing.expect_value(t, pages.title[0], "Beta") testing.expect_value(t, pages.title[0], "Beta")
testing.expect_value(t, pages.title[1], "Alpha") testing.expect_value(t, pages.title[1], "Alpha")
testing.expect_value(t, pages.title[2], "Gamma") testing.expect_value(t, pages.title[2], "Gamma")
@@ -329,13 +345,13 @@ test_sort_pages_weight_primary :: proc(t: ^testing.T) {
test_sort_pages_equal_weights_by_date :: proc(t: ^testing.T) { test_sort_pages_equal_weights_by_date :: proc(t: ^testing.T) {
pages := make(#soa[dynamic]Page, 0, 3) pages := make(#soa[dynamic]Page, 0, 3)
defer delete(pages) defer delete(pages)
append(&pages, Page{title = "Old", date = "2025-01-01", weight = DEFAULT_WEIGHT}) append(&pages, Page{title = "Old", date = "2025-01-01"})
append(&pages, Page{title = "New", date = "2025-06-01", weight = DEFAULT_WEIGHT}) append(&pages, Page{title = "New", date = "2025-06-01"})
append(&pages, Page{title = "Mid", date = "2025-03-01", weight = DEFAULT_WEIGHT}) append(&pages, Page{title = "Mid", date = "2025-03-01"})
sort_pages(pages[:]) sort_pages(pages[:])
// All same weight → date descending // All nil weight → date descending
testing.expect_value(t, pages.title[0], "New") testing.expect_value(t, pages.title[0], "New")
testing.expect_value(t, pages.title[1], "Mid") testing.expect_value(t, pages.title[1], "Mid")
testing.expect_value(t, pages.title[2], "Old") testing.expect_value(t, pages.title[2], "Old")
@@ -345,14 +361,14 @@ test_sort_pages_equal_weights_by_date :: proc(t: ^testing.T) {
test_sort_pages_mixed :: proc(t: ^testing.T) { test_sort_pages_mixed :: proc(t: ^testing.T) {
pages := make(#soa[dynamic]Page, 0, 4) pages := make(#soa[dynamic]Page, 0, 4)
defer delete(pages) defer delete(pages)
append(&pages, Page{title = "DefaultOld", date = "2025-01-01", weight = DEFAULT_WEIGHT}) append(&pages, Page{title = "DefaultOld", date = "2025-01-01"})
append(&pages, Page{title = "DefaultNew", date = "2025-06-01", weight = DEFAULT_WEIGHT}) append(&pages, Page{title = "DefaultNew", date = "2025-06-01"})
append(&pages, Page{title = "Heavy", date = "2025-03-01", weight = 20}) append(&pages, Page{title = "Heavy", date = "2025-03-01", weight = 20})
append(&pages, Page{title = "Light", date = "2025-02-01", weight = 1}) append(&pages, Page{title = "Light", date = "2025-02-01", weight = 1})
sort_pages(pages[:]) sort_pages(pages[:])
// weight 1, weight 10 (DefaultNew by date), weight 10 (DefaultOld by date), weight 20 // weight 1, nil weight (DefaultNew by date), nil weight (DefaultOld by date), weight 20
testing.expect_value(t, pages.title[0], "Light") testing.expect_value(t, pages.title[0], "Light")
testing.expect_value(t, pages.title[1], "DefaultNew") testing.expect_value(t, pages.title[1], "DefaultNew")
testing.expect_value(t, pages.title[2], "DefaultOld") testing.expect_value(t, pages.title[2], "DefaultOld")
+2 -2
View File
@@ -387,8 +387,8 @@ sort_pages :: proc(pages: #soa[]Page) {
} }
compare_pages :: proc(pages: #soa[]Page, j: int, key: Page) -> int { compare_pages :: proc(pages: #soa[]Page, j: int, key: Page) -> int {
wj := pages.weight[j] wj := pages.weight[j].? or_else DEFAULT_WEIGHT
wk := key.weight wk := key.weight.? or_else DEFAULT_WEIGHT
if wj != wk do return wj - wk if wj != wk do return wj - wk
// Equal weight → date descending // Equal weight → date descending
if pages.date[j] < key.date do return 1 if pages.date[j] < key.date do return 1