From 00b73c04bad4e95ded7a449cf1234b9e29c6964b Mon Sep 17 00:00:00 2001 From: Spencer Brower <6729162+sbrow@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:47:12 -0400 Subject: [PATCH] refactor: Broke config/flags into a separate struct from `Site`. --- content.odin | 4 ++-- main.odin | 2 +- site.odin | 64 +++++++++++++++++++++++++++++++++++++++++--------- site_test.odin | 64 +++++++++++++++++++++++--------------------------- 4 files changed, 86 insertions(+), 48 deletions(-) diff --git a/content.odin b/content.odin index a6bcb14..462495f 100644 --- a/content.odin +++ b/content.odin @@ -34,8 +34,8 @@ Page :: struct { // TODO: What is the lifetime of pages? walk_content :: proc(site: ^Site) -> []Page { content_path := site.content_dir - include_drafts := site.drafts - sectionate := site.sectionate + include_drafts := .Drafts in site.features + sectionate := .Sections in site.features allocator := site_allocator(site) diff --git a/main.odin b/main.odin index f18f080..b3c1f0c 100644 --- a/main.odin +++ b/main.odin @@ -20,7 +20,7 @@ main :: proc() { pages := walk_content(&site) render_site(pages, site) - if !site.watch { + if !(.Watch in site.features) { break } time.sleep(5 * time.Second) diff --git a/site.odin b/site.odin index cfc62a1..a47f472 100644 --- a/site.odin +++ b/site.odin @@ -8,9 +8,29 @@ import "core:mem" import "core:os" import "core:strings" + Site :: struct { - // TODO: User can still technically try to set this - arena: mem.Dynamic_Arena `args:"hidden"`, + arena: mem.Dynamic_Arena, + title: string, + description: string, + author: string, + base_url: string, + config_path: string, + content_dir: string, + static_dir: string, + output_dir: string, + layouts_dir: string, + params: json.Value, + features: bit_set[Feature], +} + +Feature :: enum { + Sections, + Drafts, + Watch, +} + +Flags :: struct { config_path: string `args:"name=config"`, title: string, description: string, @@ -21,13 +41,13 @@ Site :: struct { layouts_dir: string, author: string, params: json.Value, - sectionate: bool, + sectionate: bool `args:"name=sections"`, drafts: bool `args:"name=drafts"`, watch: bool, } init_site :: proc(site: ^Site, args: []string) { - _flags: Site + _flags: Flags mem.dynamic_arena_init(&site.arena, alignment = 64) // FIXME: This is a hack alloc := site_allocator(site) flags.parse_or_exit(&_flags, args, .Odin, alloc) @@ -43,13 +63,15 @@ init_site :: proc(site: ^Site, args: []string) { } } - if load_site_config(site, path, alloc) { - site_merge(site, _flags) + cfg, cfg_ok := load_site_config(path, alloc) + if cfg_ok { + merge_flags(&cfg, _flags) } else { - _flags.arena = site.arena - site^ = _flags + cfg = _flags } + site_apply_flags(site, cfg) + // Determine config file's directory for relative defaults config_dir := "./" if idx := strings.last_index(path, "/"); idx >= 0 { @@ -76,10 +98,10 @@ init_site :: proc(site: ^Site, args: []string) { } load_site_config :: proc( - config: ^Site, path: string, allocator := context.allocator, ) -> ( + config: Flags, ok: bool, ) { data, err := os.read_entire_file_from_path(path, allocator) @@ -87,7 +109,7 @@ load_site_config :: proc( return } - unmarshal_err := json.unmarshal_string(string(data), config, allocator = allocator) + unmarshal_err := json.unmarshal_string(string(data), &config, allocator = allocator) if unmarshal_err != nil { log.warnf("thor: failed to parse %s: %v", path, unmarshal_err) return @@ -97,7 +119,7 @@ load_site_config :: proc( return } -site_merge :: proc(config: ^Site, flags: Site) { +merge_flags :: proc(config: ^Flags, flags: Flags) { if flags.base_url != "" { config.base_url = flags.base_url } @@ -116,9 +138,29 @@ site_merge :: proc(config: ^Site, flags: Site) { if flags.watch { config.watch = true } + if flags.sectionate { + config.sectionate = true + } config.config_path = flags.config_path } +site_apply_flags :: proc(site: ^Site, flags: Flags) { + site.title = flags.title + site.description = flags.description + site.author = flags.author + site.base_url = flags.base_url + site.config_path = flags.config_path + site.content_dir = flags.content_dir + site.static_dir = flags.static_dir + site.output_dir = flags.output_dir + site.layouts_dir = flags.layouts_dir + site.params = flags.params + + if flags.sectionate {site.features += {.Sections}} + if flags.drafts {site.features += {.Drafts}} + if flags.watch {site.features += {.Watch}} +} + site_allocator :: proc(site: ^Site) -> mem.Allocator { return mem.dynamic_arena_allocator(&site.arena) } diff --git a/site_test.odin b/site_test.odin index 8f4ac20..55cd900 100644 --- a/site_test.odin +++ b/site_test.odin @@ -34,16 +34,15 @@ test_load_site_config :: proc(t: ^testing.T) { ) defer os.remove(path) - site: Site - ok := load_site_config(&site, path, context.temp_allocator) + cfg, ok := load_site_config(path, context.temp_allocator) testing.expect(t, ok) - testing.expect_value(t, site.title, "Test Site") - testing.expect_value(t, site.description, "Test desc") - testing.expect_value(t, site.base_url, "https://example.com") - testing.expect_value(t, site.author, "Tester") + testing.expect_value(t, cfg.title, "Test Site") + testing.expect_value(t, cfg.description, "Test desc") + testing.expect_value(t, cfg.base_url, "https://example.com") + testing.expect_value(t, cfg.author, "Tester") - params, has_params := site.params.(json.Object) + params, has_params := cfg.params.(json.Object) testing.expect(t, has_params) social_val := params["social"] @@ -63,8 +62,7 @@ test_load_site_config :: proc(t: ^testing.T) { @(test) test_load_site_config_missing_file :: proc(t: ^testing.T) { - site: Site - ok := load_site_config(&site, "./nonexistent_thor_test.json", context.temp_allocator) + _, ok := load_site_config("./nonexistent_thor_test.json", context.temp_allocator) testing.expect(t, !ok) } @@ -73,8 +71,7 @@ test_load_site_config_invalid_json :: proc(t: ^testing.T) { path := write_temp_config("invalid", `{not valid json}`) defer os.remove(path) - site: Site - ok := load_site_config(&site, path, context.temp_allocator) + _, ok := load_site_config(path, context.temp_allocator) testing.expect(t, !ok) } @@ -83,27 +80,26 @@ test_load_site_config_partial :: proc(t: ^testing.T) { path := write_temp_config("partial", `{"title":"Partial"}`) defer os.remove(path) - site: Site - ok := load_site_config(&site, path, context.temp_allocator) + cfg, ok := load_site_config(path, context.temp_allocator) testing.expect(t, ok) - testing.expect_value(t, site.title, "Partial") - testing.expect_value(t, site.description, "") - testing.expect_value(t, site.author, "") - testing.expect(t, site.params == nil) + testing.expect_value(t, cfg.title, "Partial") + testing.expect_value(t, cfg.description, "") + testing.expect_value(t, cfg.author, "") + testing.expect(t, cfg.params == nil) } @(test) test_site_merge_overrides :: proc(t: ^testing.T) { - config := Site { + config := Flags { base_url = "https://original.com", content_dir = "./content", } - flags := Site { + flags := Flags { base_url = "https://override.com", } - site_merge(&config, flags) + merge_flags(&config, flags) testing.expect_value(t, config.base_url, "https://override.com") testing.expect_value(t, config.content_dir, "./content") @@ -111,13 +107,13 @@ test_site_merge_overrides :: proc(t: ^testing.T) { @(test) test_site_merge_empty_flags_keep_config :: proc(t: ^testing.T) { - config := Site { + config := Flags { base_url = "https://keep.com", content_dir = "./keep", } - flags := Site{} + flags := Flags{} - site_merge(&config, flags) + merge_flags(&config, flags) testing.expect_value(t, config.base_url, "https://keep.com") testing.expect_value(t, config.content_dir, "./keep") @@ -125,38 +121,38 @@ test_site_merge_empty_flags_keep_config :: proc(t: ^testing.T) { @(test) test_site_merge_drafts_true :: proc(t: ^testing.T) { - config := Site { + config := Flags { drafts = false, } - flags := Site { + flags := Flags { drafts = true, } - site_merge(&config, flags) + merge_flags(&config, flags) testing.expect(t, config.drafts) } @(test) test_site_merge_drafts_false_preserves :: proc(t: ^testing.T) { - config := Site { + config := Flags { drafts = false, } - flags := Site { + flags := Flags { drafts = false, } - site_merge(&config, flags) + merge_flags(&config, flags) testing.expect(t, !config.drafts) } @(test) test_site_merge_config_path :: proc(t: ^testing.T) { - config := Site{} - flags := Site { + config := Flags{} + flags := Flags { config_path = "./custom/thor.json", } - site_merge(&config, flags) + merge_flags(&config, flags) testing.expect_value(t, config.config_path, "./custom/thor.json") } @@ -192,7 +188,7 @@ test_init_site_flag_overrides_default :: proc(t: ^testing.T) { init_site(&site, args) defer destroy_site(&site) - testing.expect(t, site.drafts) + testing.expect(t, .Drafts in site.features) testing.expect_value(t, site.base_url, "https://flag.com") } @@ -212,7 +208,7 @@ test_init_site_full_pipeline :: proc(t: ^testing.T) { testing.expect_value(t, site.title, "Pipeline Test") testing.expect_value(t, site.description, "Full") testing.expect_value(t, site.author, "Author") - testing.expect(t, site.drafts) + testing.expect(t, .Drafts in site.features) testing.expect_value(t, site.base_url, "https://config.com") }