feat: Added weight field to Page.

This commit is contained in:
Spencer Brower
2026-07-29 12:52:15 -04:00
parent c48afafd8a
commit df14d76a85
6 changed files with 176 additions and 6 deletions
+2
View File
@@ -485,6 +485,8 @@ These are things that are easy to get wrong:
- **`for` each loops use `item, idx` order**, not `idx, item`. Correct: `for item, idx in arr`. Wrong: `for idx, item in arr`.
- **`make([dynamic]T, n, allocator)` sets capacity, not length.** To get length=0 with capacity=n, use `make([dynamic]T, 0, n, allocator)`. Using `make([dynamic]T, n, allocator)` creates `len=n` with `n` zero-initialized elements.
- `#partial switch` is usually a code smell. prefer a `case all, extra, types:` branch.
- you don't usually need to create arena allocators in tests, instead use context.temp_allocator if you want to simplify cleanup.
- you don't need to manually set up a tracking allocator in tests. the context.allocator will warn you about leaks.
## TODO
+2
View File
@@ -20,6 +20,7 @@ Page :: struct {
description: string,
date: string,
year: string,
weight: int,
lastmod: string,
menus: map[string]Menu_Entry,
content: string,
@@ -253,6 +254,7 @@ load_page :: proc(
}
}
page.year = get_year(page.date)
page.weight = fm.weight if fm.weight != 0 else DEFAULT_WEIGHT
page.lastmod = fm.lastmod
page.draft = fm.draft
page.starred = fm.isStarred
+2
View File
@@ -10,6 +10,7 @@ Frontmatter :: struct {
date: string,
lastmod: string,
publishDate: string,
weight: int,
menus: json.Value,
layout: string,
og: Open_Graph,
@@ -53,6 +54,7 @@ parse_frontmatter :: proc(content: string) -> (fm: Frontmatter, body: string, ok
fm.date = json_get_string(obj, "date")
fm.lastmod = json_get_string(obj, "lastmod")
fm.publishDate = json_get_string(obj, "publishDate")
fm.weight = json_get_int(obj, "weight")
fm.draft = json_get_bool(obj, "draft")
fm.isStarred = json_get_bool(obj, "isStarred")
if v, ok := obj["menus"]; ok {
+21 -3
View File
@@ -166,7 +166,16 @@ merge_page_menus :: proc(site: ^Site) {
if _, ok := page_entries[menu_name]; !ok {
page_entries[menu_name] = make([dynamic]Menu_Entry, 0, 4, alloc)
}
append(&page_entries[menu_name], entry)
// Effective weight: per-menu weight if explicit, else page.weight
effective := entry.weight
if effective == DEFAULT_WEIGHT {
effective = page.weight
}
append(&page_entries[menu_name], Menu_Entry{
name = entry.name,
url = entry.url,
weight = effective,
})
}
}
@@ -212,16 +221,22 @@ collect_auto_menus :: proc(site: ^Site) {
for section in sections {
name := to_title_case(section, alloc)
url := fmt.aprintf("/%s/", section, allocator = alloc)
skip := false
for page in site.pages {
if page.section == section && page._is_index {
url = page.permalink
if page.title != "" {
name = page.title
}
if _, has_main := page.menus["main"]; has_main {
skip = true
}
break
}
}
append(&entries, Menu_Entry{name = name, url = url, weight = DEFAULT_WEIGHT})
if !skip {
append(&entries, Menu_Entry{name = name, url = url, weight = DEFAULT_WEIGHT})
}
}
// Root-level page entries (section = "", not index)
@@ -229,9 +244,12 @@ collect_auto_menus :: proc(site: ^Site) {
if page._is_index || page.section != "" || page.title == "" {
continue
}
if _, has_main := page.menus["main"]; has_main {
continue
}
append(
&entries,
Menu_Entry{name = page.title, url = page.permalink, weight = DEFAULT_WEIGHT},
Menu_Entry{name = page.title, url = page.permalink, weight = page.weight},
)
}
+135
View File
@@ -272,3 +272,138 @@ test_config_weight_parsing_and_sort :: proc(t: ^testing.T) {
testing.expect_value(t, main[2].name, "Heavy")
testing.expect_value(t, main[2].weight, 20)
}
// --- json_get_int tests ---
@(test)
test_json_get_int_integer :: 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"), 5)
}
@(test)
test_json_get_int_float :: proc(t: ^testing.T) {
obj, _ := json.parse_string(`{"weight": 5.0}`, spec = .JSON)
defer json.destroy_value(obj)
o, _ := obj.(json.Object)
testing.expect_value(t, json_get_int(o, "weight"), 5)
}
@(test)
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)
}
@(test)
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)
}
// --- sort_pages tests ---
@(test)
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 = "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
testing.expect_value(t, pages.title[0], "Beta")
testing.expect_value(t, pages.title[1], "Alpha")
testing.expect_value(t, pages.title[2], "Gamma")
}
@(test)
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})
sort_pages(pages[:])
// All same 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")
}
@(test)
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 = "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
testing.expect_value(t, pages.title[0], "Light")
testing.expect_value(t, pages.title[1], "DefaultNew")
testing.expect_value(t, pages.title[2], "DefaultOld")
testing.expect_value(t, pages.title[3], "Heavy")
}
// --- merge_page_menus effective weight tests ---
@(test)
test_merge_page_menus_weight_fallback :: proc(t: ^testing.T) {
site: Site
mem.dynamic_arena_init(&site.arena)
defer mem.dynamic_arena_destroy(&site.arena)
context.allocator = site_allocator(&site)
page := make_page("Test", "/test/")
page.weight = 3
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)
// Don't call collect_auto_menus — test merge_page_menus in isolation
merge_page_menus(&site)
main, ok := site.menus["main"]
testing.expect(t, ok)
testing.expect(t, len(main) == 1, "expected exactly 1 entry")
testing.expect_value(t, main[0].name, "Test")
testing.expect_value(t, main[0].weight, 3)
}
@(test)
test_auto_menus_no_duplicate_with_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)
// Root-level page with explicit "menus": "main"
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)
collect_auto_menus(&site)
merge_page_menus(&site)
main, ok := site.menus["main"]
testing.expect(t, ok)
testing.expect(t, len(main) == 1, "expected exactly 1 entry (no duplicate)")
testing.expect_value(t, main[0].name, "Ideas")
}
+14 -3
View File
@@ -136,7 +136,7 @@ render_template :: proc(
render_site :: proc(site: ^Site) {
allocator := site_allocator(site)
pages := site.pages[:]
sort_pages_by_date(pages)
sort_pages(pages)
// Load shared resources
partials := load_partials(&site.vfs)
@@ -373,11 +373,12 @@ get_year :: proc(iso: string) -> string {
return iso[:4]
}
sort_pages_by_date :: proc(pages: #soa[]Page) {
// Weight primary (ascending). Date secondary (descending) for equal weights.
sort_pages :: proc(pages: #soa[]Page) {
for i in 1 ..< len(pages) {
key := pages[i]
j := i - 1
for j >= 0 && pages.date[j] < key.date {
for j >= 0 && compare_pages(pages, j, key) > 0 {
pages[j + 1] = pages[j]
j -= 1
}
@@ -385,6 +386,16 @@ sort_pages_by_date :: proc(pages: #soa[]Page) {
}
}
compare_pages :: proc(pages: #soa[]Page, j: int, key: Page) -> int {
wj := pages.weight[j]
wk := key.weight
if wj != wk do return wj - wk
// Equal weight → date descending
if pages.date[j] < key.date do return 1
if pages.date[j] > key.date do return -1
return 0
}
write_page :: proc(output_dir: string, permalink: string, html: string) {
rel := permalink
if len(rel) > 0 && rel[0] == '/' {