mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Menu items get sorted by weight.
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
- [ ] Test menu diagnostics
|
||||
- [ ] Honestly, Test **all** diagnostics
|
||||
- [ ] Need to be careful about diagnostics across module boundaries.
|
||||
- we don't necessarilly want to warn users about theme designers mistakes. (though perhaps we do)
|
||||
- we don't necessarily want to warn users about theme designers mistakes. (though perhaps we do)
|
||||
- [x] Simplify / unify template context stack. Come up with a name for it.
|
||||
- [x] `render_template` should accept `Template_Context`, not `any`
|
||||
- [ ] Load grammars dynamically
|
||||
@@ -39,7 +39,7 @@
|
||||
- i.e. force the user to choose one or the other.
|
||||
- [ ] Don't show annoying log output in tests.
|
||||
- [ ] improve home link customization.
|
||||
- [ ] currently an accessability issue.
|
||||
- [ ] currently an accessibility issue.
|
||||
- [ ] support JSON5 in in frontmatter
|
||||
- [ ] Create a json schema file for `thor.json`.
|
||||
- [ ] cleanup `#partial switch`es.
|
||||
|
||||
+36
-7
@@ -7,6 +7,8 @@ import "core:mem"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
|
||||
DEFAULT_WEIGHT :: 10
|
||||
|
||||
Menu_Entry :: struct {
|
||||
name: string,
|
||||
url: string,
|
||||
@@ -35,6 +37,7 @@ parse_page_menus :: proc(
|
||||
result[string(v)] = Menu_Entry {
|
||||
name = page.title,
|
||||
url = page.permalink,
|
||||
weight = DEFAULT_WEIGHT,
|
||||
}
|
||||
|
||||
case json.Array:
|
||||
@@ -44,6 +47,7 @@ parse_page_menus :: proc(
|
||||
result[string(s)] = Menu_Entry {
|
||||
name = page.title,
|
||||
url = page.permalink,
|
||||
weight = DEFAULT_WEIGHT,
|
||||
}
|
||||
} else {
|
||||
log.warnf("menus: ignoring non-string item in menus array: %v", item)
|
||||
@@ -53,7 +57,7 @@ parse_page_menus :: proc(
|
||||
case json.Object:
|
||||
result = make(map[string]Menu_Entry, allocator)
|
||||
for menu_name, entry_val in v {
|
||||
weight := 0
|
||||
weight := DEFAULT_WEIGHT
|
||||
if entry_obj, ok := entry_val.(json.Object); ok {
|
||||
if w, ok := entry_obj["weight"]; ok {
|
||||
#partial switch wval in w {
|
||||
@@ -217,7 +221,7 @@ collect_auto_menus :: proc(site: ^Site) {
|
||||
break
|
||||
}
|
||||
}
|
||||
append(&entries, Menu_Entry{name = name, url = url})
|
||||
append(&entries, Menu_Entry{name = name, url = url, weight = DEFAULT_WEIGHT})
|
||||
}
|
||||
|
||||
// Root-level page entries (section = "", not index)
|
||||
@@ -225,7 +229,10 @@ collect_auto_menus :: proc(site: ^Site) {
|
||||
if page._is_index || page.section != "" || page.title == "" {
|
||||
continue
|
||||
}
|
||||
append(&entries, Menu_Entry{name = page.title, url = page.permalink})
|
||||
append(
|
||||
&entries,
|
||||
Menu_Entry{name = page.title, url = page.permalink, weight = DEFAULT_WEIGHT},
|
||||
)
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
@@ -237,11 +244,16 @@ collect_auto_menus :: proc(site: ^Site) {
|
||||
site.menus["main"] = entries[:]
|
||||
}
|
||||
|
||||
compare_menu_entries :: proc(a, b: Menu_Entry) -> int {
|
||||
if a.weight != b.weight do return a.weight - b.weight
|
||||
return strings.compare(a.name, b.name)
|
||||
}
|
||||
|
||||
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 {
|
||||
for j >= 0 && compare_menu_entries(entries[j], key) > 0 {
|
||||
entries[j + 1] = entries[j]
|
||||
j -= 1
|
||||
}
|
||||
@@ -250,7 +262,7 @@ sort_menu_entries :: proc(entries: []Menu_Entry) {
|
||||
}
|
||||
|
||||
// parse_config_menus converts raw JSON from thor.json into map[string][]Menu_Entry.
|
||||
// Preserves array order as-declared.
|
||||
// Entries are sorted by weight, then name.
|
||||
parse_config_menus :: proc(
|
||||
raw: json.Value,
|
||||
allocator := context.allocator,
|
||||
@@ -278,6 +290,7 @@ parse_config_menus :: proc(
|
||||
|
||||
name := ""
|
||||
url := ""
|
||||
weight := DEFAULT_WEIGHT
|
||||
|
||||
if v, ok := entry_obj["name"]; ok {
|
||||
if s, ok2 := v.(json.String); ok2 {
|
||||
@@ -307,16 +320,32 @@ parse_config_menus :: proc(
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := entry_obj["weight"]; ok {
|
||||
switch wval in v {
|
||||
case json.Integer:
|
||||
weight = int(wval)
|
||||
case json.Float:
|
||||
weight = int(wval)
|
||||
case json.Null, json.Boolean, json.String, json.Array, json.Object:
|
||||
log.warnf(
|
||||
"menus: '%s' entry %d: 'weight' must be a number, got %v",
|
||||
menu_name,
|
||||
idx,
|
||||
v,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
log.warnf("menus: '%s' entry %d missing 'name', skipping", menu_name, idx)
|
||||
continue
|
||||
}
|
||||
|
||||
append(&entries, Menu_Entry{name = name, url = url})
|
||||
append(&entries, Menu_Entry{name = name, url = url, weight = weight})
|
||||
}
|
||||
sort_menu_entries(entries[:])
|
||||
result[menu_name] = entries[:]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
+78
-5
@@ -16,7 +16,6 @@ parse_raw :: proc(s: string) -> json.Value {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@(test)
|
||||
test_menus_string_form :: proc(t: ^testing.T) {
|
||||
arena: mem.Dynamic_Arena
|
||||
@@ -31,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, 0)
|
||||
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -82,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, 0)
|
||||
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -140,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, 0)
|
||||
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -155,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, 0)
|
||||
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -199,3 +198,77 @@ test_menus_null_json :: proc(t: ^testing.T) {
|
||||
menus := parse_page_menus(parse_raw(`null`), page, context.allocator)
|
||||
testing.expect(t, menus == nil, "null JSON should return nil")
|
||||
}
|
||||
|
||||
// --- sort_menu_entries tests ---
|
||||
|
||||
@(test)
|
||||
test_sort_weight_orders_correctly :: proc(t: ^testing.T) {
|
||||
entries := []Menu_Entry {
|
||||
{name = "Zeta", url = "/z/", weight = DEFAULT_WEIGHT},
|
||||
{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
|
||||
testing.expect_value(t, entries[0].name, "Beta")
|
||||
testing.expect_value(t, entries[1].name, "Alpha")
|
||||
testing.expect_value(t, entries[2].name, "Zeta")
|
||||
}
|
||||
|
||||
@(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},
|
||||
}
|
||||
sort_menu_entries(entries)
|
||||
testing.expect_value(t, entries[0].name, "Apple")
|
||||
testing.expect_value(t, entries[1].name, "Mango")
|
||||
testing.expect_value(t, entries[2].name, "Zebra")
|
||||
}
|
||||
|
||||
@(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 = "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)
|
||||
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_config_weight_parsing_and_sort :: proc(t: ^testing.T) {
|
||||
arena: mem.Dynamic_Arena
|
||||
mem.dynamic_arena_init(&arena)
|
||||
defer mem.dynamic_arena_destroy(&arena)
|
||||
context.allocator = mem.dynamic_arena_allocator(&arena)
|
||||
|
||||
raw := parse_raw(
|
||||
`{
|
||||
"main": [
|
||||
{"name": "Heavy", "url": "/h/", "weight": 20},
|
||||
{"name": "Light", "url": "/l/", "weight": 1},
|
||||
{"name": "Default", "url": "/d/"}
|
||||
]
|
||||
}`,
|
||||
)
|
||||
|
||||
menus := parse_config_menus(raw, context.allocator)
|
||||
main, ok := menus["main"]
|
||||
testing.expect(t, ok)
|
||||
testing.expect(t, len(main) == 3)
|
||||
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_value(t, main[2].name, "Heavy")
|
||||
testing.expect_value(t, main[2].weight, 20)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user