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
|
- [ ] Test menu diagnostics
|
||||||
- [ ] Honestly, Test **all** diagnostics
|
- [ ] Honestly, Test **all** diagnostics
|
||||||
- [ ] Need to be careful about diagnostics across module boundaries.
|
- [ ] 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] Simplify / unify template context stack. Come up with a name for it.
|
||||||
- [x] `render_template` should accept `Template_Context`, not `any`
|
- [x] `render_template` should accept `Template_Context`, not `any`
|
||||||
- [ ] Load grammars dynamically
|
- [ ] Load grammars dynamically
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
- i.e. force the user to choose one or the other.
|
- i.e. force the user to choose one or the other.
|
||||||
- [ ] Don't show annoying log output in tests.
|
- [ ] Don't show annoying log output in tests.
|
||||||
- [ ] improve home link customization.
|
- [ ] improve home link customization.
|
||||||
- [ ] currently an accessability issue.
|
- [ ] currently an accessibility issue.
|
||||||
- [ ] support JSON5 in in frontmatter
|
- [ ] support JSON5 in in frontmatter
|
||||||
- [ ] Create a json schema file for `thor.json`.
|
- [ ] Create a json schema file for `thor.json`.
|
||||||
- [ ] cleanup `#partial switch`es.
|
- [ ] cleanup `#partial switch`es.
|
||||||
|
|||||||
+36
-7
@@ -7,6 +7,8 @@ import "core:mem"
|
|||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
|
DEFAULT_WEIGHT :: 10
|
||||||
|
|
||||||
Menu_Entry :: struct {
|
Menu_Entry :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
url: string,
|
url: string,
|
||||||
@@ -35,6 +37,7 @@ 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:
|
||||||
@@ -44,6 +47,7 @@ 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)
|
||||||
@@ -53,7 +57,7 @@ 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 := 0
|
weight := DEFAULT_WEIGHT
|
||||||
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 {
|
#partial switch wval in w {
|
||||||
@@ -217,7 +221,7 @@ collect_auto_menus :: proc(site: ^Site) {
|
|||||||
break
|
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)
|
// 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 == "" {
|
if page._is_index || page.section != "" || page.title == "" {
|
||||||
continue
|
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 {
|
if len(entries) == 0 {
|
||||||
@@ -237,11 +244,16 @@ collect_auto_menus :: proc(site: ^Site) {
|
|||||||
site.menus["main"] = entries[:]
|
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) {
|
sort_menu_entries :: proc(entries: []Menu_Entry) {
|
||||||
for i in 1 ..< len(entries) {
|
for i in 1 ..< len(entries) {
|
||||||
key := entries[i]
|
key := entries[i]
|
||||||
j := i - 1
|
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]
|
entries[j + 1] = entries[j]
|
||||||
j -= 1
|
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.
|
// 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(
|
parse_config_menus :: proc(
|
||||||
raw: json.Value,
|
raw: json.Value,
|
||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
@@ -278,6 +290,7 @@ parse_config_menus :: proc(
|
|||||||
|
|
||||||
name := ""
|
name := ""
|
||||||
url := ""
|
url := ""
|
||||||
|
weight := DEFAULT_WEIGHT
|
||||||
|
|
||||||
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 {
|
||||||
@@ -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 == "" {
|
if name == "" {
|
||||||
log.warnf("menus: '%s' entry %d missing 'name', skipping", menu_name, idx)
|
log.warnf("menus: '%s' entry %d missing 'name', skipping", menu_name, idx)
|
||||||
continue
|
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[:]
|
result[menu_name] = entries[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+78
-5
@@ -16,7 +16,6 @@ parse_raw :: proc(s: string) -> json.Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_menus_string_form :: proc(t: ^testing.T) {
|
test_menus_string_form :: proc(t: ^testing.T) {
|
||||||
arena: mem.Dynamic_Arena
|
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(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, 0)
|
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -82,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, 0)
|
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(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")
|
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, 0)
|
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(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)
|
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, 0)
|
testing.expect_value(t, entry.weight, DEFAULT_WEIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -199,3 +198,77 @@ test_menus_null_json :: proc(t: ^testing.T) {
|
|||||||
menus := parse_page_menus(parse_raw(`null`), page, context.allocator)
|
menus := parse_page_menus(parse_raw(`null`), page, context.allocator)
|
||||||
testing.expect(t, menus == nil, "null JSON should return nil")
|
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