From d03e3469b36c4dbcdfa49295c1404262d7ede09b Mon Sep 17 00:00:00 2001 From: Spencer Brower <6729162+sbrow@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:21:58 -0400 Subject: [PATCH] refactor: weights are now wrapped in a Maybe. --- content.odin | 4 +-- frontmatter.odin | 8 +++--- it/it.odin | 27 -------------------- menus.odin | 28 ++++++++++----------- menus_test.odin | 64 ++++++++++++++++++++++++++++++------------------ render.odin | 4 +-- 6 files changed, 62 insertions(+), 73 deletions(-) delete mode 100644 it/it.odin diff --git a/content.odin b/content.odin index 3550d5f..7f448c2 100644 --- a/content.odin +++ b/content.odin @@ -20,7 +20,7 @@ Page :: struct { description: string, date: string, year: string, - weight: int, + weight: Maybe(int), lastmod: string, menus: map[string]Menu_Entry, content: string, @@ -254,7 +254,7 @@ load_page :: proc( } } 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.draft = fm.draft page.starred = fm.isStarred diff --git a/frontmatter.odin b/frontmatter.odin index 25eef77..908769f 100644 --- a/frontmatter.odin +++ b/frontmatter.odin @@ -10,7 +10,7 @@ Frontmatter :: struct { date: string, lastmod: string, publishDate: string, - weight: int, + weight: Maybe(int), menus: json.Value, layout: string, og: Open_Graph, @@ -85,7 +85,7 @@ json_get_bool :: proc(obj: json.Object, key: string) -> bool { 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 { switch val in v { case json.Integer: @@ -93,10 +93,10 @@ json_get_int :: proc(obj: json.Object, key: string) -> int { case json.Float: return int(val) 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 { diff --git a/it/it.odin b/it/it.odin deleted file mode 100644 index d9a15f1..0000000 --- a/it/it.odin +++ /dev/null @@ -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) -} diff --git a/menus.odin b/menus.odin index 812194b..33ba23e 100644 --- a/menus.odin +++ b/menus.odin @@ -12,12 +12,12 @@ DEFAULT_WEIGHT :: 10 Menu_Entry :: struct { name: string, url: string, - weight: int, + weight: Maybe(int), } // parse_page_menus converts raw frontmatter JSON into map[string]Menu_Entry. // 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": {"weight": 30}} → {main: {name=title, url=permalink, weight=30}} parse_page_menus :: proc( @@ -37,7 +37,6 @@ parse_page_menus :: proc( result[string(v)] = Menu_Entry { name = page.title, url = page.permalink, - weight = DEFAULT_WEIGHT, } case json.Array: @@ -45,9 +44,8 @@ parse_page_menus :: proc( for item in v { if s, ok := item.(json.String); ok { result[string(s)] = Menu_Entry { - name = page.title, - url = page.permalink, - weight = DEFAULT_WEIGHT, + name = page.title, + url = page.permalink, } } else { log.warnf("menus: ignoring non-string item in menus array: %v", item) @@ -57,15 +55,15 @@ parse_page_menus :: proc( case json.Object: result = make(map[string]Menu_Entry, allocator) for menu_name, entry_val in v { - weight := DEFAULT_WEIGHT + weight: Maybe(int) = nil if entry_obj, ok := entry_val.(json.Object); ok { if w, ok := entry_obj["weight"]; ok { - #partial switch wval in w { + switch wval in w { case json.Integer: weight = int(wval) case json.Float: weight = int(wval) - case: + case json.Boolean, json.String, json.Array, json.Object, json.Null: log.warnf( "menus: '%s' entry 'weight' must be a number, got %v", menu_name, @@ -166,9 +164,9 @@ merge_page_menus :: proc(site: ^Site) { if _, ok := page_entries[menu_name]; !ok { 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 - if effective == DEFAULT_WEIGHT { + if effective == nil { effective = page.weight } append(&page_entries[menu_name], Menu_Entry{ @@ -235,7 +233,7 @@ collect_auto_menus :: proc(site: ^Site) { } } 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 { - 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) } @@ -308,7 +308,7 @@ parse_config_menus :: proc( name := "" url := "" - weight := DEFAULT_WEIGHT + weight: Maybe(int) = nil if v, ok := entry_obj["name"]; ok { if s, ok2 := v.(json.String); ok2 { diff --git a/menus_test.odin b/menus_test.odin index fbd7937..afe620a 100644 --- a/menus_test.odin +++ b/menus_test.odin @@ -30,7 +30,7 @@ test_menus_string_form :: proc(t: ^testing.T) { testing.expect(t, ok, "expected 'main' menu") testing.expect_value(t, entry.name, "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) @@ -81,7 +81,7 @@ test_menus_object_no_weight :: proc(t: ^testing.T) { testing.expect(t, len(menus) == 1) entry, ok := menus["main"] 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) @@ -139,7 +139,7 @@ test_menus_object_non_object_value :: proc(t: ^testing.T) { testing.expect(t, len(menus) == 1, "entry created with defaults") entry, ok := menus["main"] 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) @@ -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) entry, ok := menus["main"] 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) @@ -204,12 +204,12 @@ test_menus_null_json :: proc(t: ^testing.T) { @(test) test_sort_weight_orders_correctly :: proc(t: ^testing.T) { entries := []Menu_Entry { - {name = "Zeta", url = "/z/", weight = DEFAULT_WEIGHT}, + {name = "Zeta", url = "/z/"}, {name = "Alpha", url = "/a/", weight = 5}, {name = "Beta", url = "/b/", weight = 1}, } 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[1].name, "Alpha") testing.expect_value(t, entries[2].name, "Zeta") @@ -218,9 +218,9 @@ test_sort_weight_orders_correctly :: proc(t: ^testing.T) { @(test) test_sort_equal_weights_alphabetical :: proc(t: ^testing.T) { entries := []Menu_Entry { - {name = "Zebra", url = "/z/", weight = DEFAULT_WEIGHT}, - {name = "Apple", url = "/a/", weight = DEFAULT_WEIGHT}, - {name = "Mango", url = "/m/", weight = DEFAULT_WEIGHT}, + {name = "Zebra", url = "/z/"}, + {name = "Apple", url = "/a/"}, + {name = "Mango", url = "/m/"}, } sort_menu_entries(entries) testing.expect_value(t, entries[0].name, "Apple") @@ -231,19 +231,35 @@ test_sort_equal_weights_alphabetical :: proc(t: ^testing.T) { @(test) test_sort_mixed_weights :: proc(t: ^testing.T) { entries := []Menu_Entry { - {name = "Charlie", url = "/c/", weight = DEFAULT_WEIGHT}, - {name = "Alpha", url = "/a/", weight = DEFAULT_WEIGHT}, + {name = "Charlie", url = "/c/"}, + {name = "Alpha", url = "/a/"}, {name = "Bravo", url = "/b/", weight = 3}, {name = "Delta", url = "/d/", weight = 1}, } 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[1].name, "Bravo") testing.expect_value(t, entries[2].name, "Alpha") 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_config_weight_parsing_and_sort :: proc(t: ^testing.T) { 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].weight, 1) 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].weight, 20) } @@ -296,7 +312,7 @@ test_json_get_int_missing :: proc(t: ^testing.T) { obj, _ := json.parse_string(`{}`, spec = .JSON) defer json.destroy_value(obj) 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) @@ -304,7 +320,7 @@ test_json_get_int_non_numeric :: proc(t: ^testing.T) { obj, _ := json.parse_string(`{"weight": "5"}`, spec = .JSON) defer json.destroy_value(obj) 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 --- @@ -313,13 +329,13 @@ test_json_get_int_non_numeric :: proc(t: ^testing.T) { test_sort_pages_weight_primary :: proc(t: ^testing.T) { pages := make(#soa[dynamic]Page, 0, 3) 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 = "Beta", date = "2025-01-02", weight = 1}) 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[1], "Alpha") 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) { pages := make(#soa[dynamic]Page, 0, 3) defer delete(pages) - append(&pages, Page{title = "Old", date = "2025-01-01", weight = DEFAULT_WEIGHT}) - append(&pages, Page{title = "New", date = "2025-06-01", weight = DEFAULT_WEIGHT}) - append(&pages, Page{title = "Mid", date = "2025-03-01", weight = DEFAULT_WEIGHT}) + append(&pages, Page{title = "Old", date = "2025-01-01"}) + append(&pages, Page{title = "New", date = "2025-06-01"}) + append(&pages, Page{title = "Mid", date = "2025-03-01"}) 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[1], "Mid") 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) { pages := make(#soa[dynamic]Page, 0, 4) defer delete(pages) - append(&pages, Page{title = "DefaultOld", date = "2025-01-01", weight = DEFAULT_WEIGHT}) - append(&pages, Page{title = "DefaultNew", date = "2025-06-01", weight = DEFAULT_WEIGHT}) + append(&pages, Page{title = "DefaultOld", date = "2025-01-01"}) + append(&pages, Page{title = "DefaultNew", date = "2025-06-01"}) append(&pages, Page{title = "Heavy", date = "2025-03-01", weight = 20}) append(&pages, Page{title = "Light", date = "2025-02-01", weight = 1}) 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[1], "DefaultNew") testing.expect_value(t, pages.title[2], "DefaultOld") diff --git a/render.odin b/render.odin index 295672e..15aa4de 100644 --- a/render.odin +++ b/render.odin @@ -387,8 +387,8 @@ sort_pages :: proc(pages: #soa[]Page) { } compare_pages :: proc(pages: #soa[]Page, j: int, key: Page) -> int { - wj := pages.weight[j] - wk := key.weight + wj := pages.weight[j].? or_else DEFAULT_WEIGHT + wk := key.weight.? or_else DEFAULT_WEIGHT if wj != wk do return wj - wk // Equal weight → date descending if pages.date[j] < key.date do return 1