mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
refactor: Eliminated global TZ cache
The configured timezone (or local) is now set on Site directly.
This commit is contained in:
@@ -222,11 +222,14 @@ Data is passed as **typed structs** (not `map[string]any`). Mustache resolves st
|
|||||||
|
|
||||||
```odin
|
```odin
|
||||||
Base_Data :: struct {
|
Base_Data :: struct {
|
||||||
now: datetime.DateTime,
|
now: string, // UTC ISO 8601 build timestamp
|
||||||
params: json.Value,
|
params: json.Value,
|
||||||
body: string,
|
body: string,
|
||||||
title: string,
|
title: string,
|
||||||
og: Open_Graph,
|
description: string,
|
||||||
|
og: Open_Graph,
|
||||||
|
date_format: string, // from site.date.format (thor.json)
|
||||||
|
timezone: ^datetime.TZ_Region, // loaded from site.date.timezone or local, owned by Site
|
||||||
}
|
}
|
||||||
Page_Data :: struct {
|
Page_Data :: struct {
|
||||||
using base: Base_Data, // fields promoted via reflection fallback
|
using base: Base_Data, // fields promoted via reflection fallback
|
||||||
@@ -259,7 +262,7 @@ Section tags and interpolation tags may transform the resolved value before rend
|
|||||||
<time datetime="{{date}}">{{date | format}}</time>
|
<time datetime="{{date}}">{{date | format}}</time>
|
||||||
```
|
```
|
||||||
|
|
||||||
Currently implemented: `group_by <field>` (list → list-of-groups) and `format` (ISO date string → display string like "15 Mar 2026"). Filter results live in `context.temp_allocator` (render-scoped). See `mustache/EXTENSIONS.md` for syntax details, caps (`MAX_PIPES`, `MAX_PIPE_ARGS`), and the `Group` struct shape.
|
Currently implemented: `group_by <field>` (list → list-of-groups) and `format` (ISO date string → display string like "15 Mar 2026"). The `format` pipe resolves `date_format` (string) and `timezone` (`^datetime.TZ_Region`) from the data context. When `timezone` is non-nil, dates are DST-aware converted before formatting. The `MST` token reflects the active timezone abbreviation (e.g. `"EST"`/`"EDT"`) or the source offset (e.g. `"UTC-04:00"`) when no target tz is configured. TZ data is loaded once by `init_site` via `timezone.region_load` using the site arena allocator, stored on `Site.tz`, and freed when the arena is destroyed. Filter results live in `context.temp_allocator` (render-scoped). See `mustache/EXTENSIONS.md` for syntax details, caps (`MAX_PIPES`, `MAX_PIPE_ARGS`), and the `Group` struct shape.
|
||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,13 @@
|
|||||||
- [x] Accept "strings"
|
- [x] Accept "strings"
|
||||||
- [x] Accept keys
|
- [x] Accept keys
|
||||||
- [x] handle timezones
|
- [x] handle timezones
|
||||||
- [ ] Fix TZ cache leak in parallel tests: `tz_cache` uses `context.allocator` (tracking allocator in tests), but it's global state shared across parallel test threads. Race causes leak warnings. Fix: use heap allocator for TZ cache.
|
|
||||||
- [ ] display an error when no part of the date appears in the output.
|
- [ ] display an error when no part of the date appears in the output.
|
||||||
- [x] use `date.format` as the default format.
|
- [x] use `date.format` as the default format.
|
||||||
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
||||||
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
||||||
|
- [ ] show a proper diagnostic for timezones
|
||||||
|
- currently "unable to load timezone 'America/New_Yorkskie'"
|
||||||
|
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- [ ] Integrity hash
|
- [ ] Integrity hash
|
||||||
|
|||||||
@@ -67,6 +67,26 @@ Takes an optional arg for the Go reference-date layout to use:
|
|||||||
- A bare key, resolved from context like any other field: `{{date | format long}}` uses the value of `long` (e.g. a site-config field) as the layout.
|
- A bare key, resolved from context like any other field: `{{date | format long}}` uses the value of `long` (e.g. a site-config field) as the layout.
|
||||||
- No arg: falls back to the `date_format` context key (typically `date.format` from `thor.json`).
|
- No arg: falls back to the `date_format` context key (typically `date.format` from `thor.json`).
|
||||||
|
|
||||||
|
#### Timezone conversion
|
||||||
|
|
||||||
|
When the `timezone` context key is set (typically `date.timezone` from `thor.json`, an IANA name like `"America/New_York"`), the `format` pipe converts dates to that timezone before formatting:
|
||||||
|
|
||||||
|
- **Date has offset + target tz**: adjusts to true UTC, then converts to the target timezone (DST-aware).
|
||||||
|
- **Date has no offset + target tz**: assumes the date is already in the target timezone — no conversion, only resolves the abbreviation.
|
||||||
|
- **Date has offset + no target tz**: displays in the source offset.
|
||||||
|
- **Date has no offset + no target tz**: displays as-is, assumes UTC.
|
||||||
|
|
||||||
|
The `MST` token reflects the active timezone:
|
||||||
|
|
||||||
|
| Config tz | ISO has offset | `MST` output |
|
||||||
|
|---|---|---|
|
||||||
|
| `"America/New_York"` | yes | `"EST"` or `"EDT"` (DST-aware) |
|
||||||
|
| `"America/New_York"` | no | `"EST"` or `"EDT"` |
|
||||||
|
| not set | yes | `"UTC-04:00"` (from source offset) |
|
||||||
|
| not set | no | `"UTC"` |
|
||||||
|
|
||||||
|
Timezone data is loaded once by `init_site` via `core:time/timezone.region_load`, using the site arena allocator. The resolved `^datetime.TZ_Region` pointer is stored on `Site.tz` and passed to templates through `Base_Data.timezone`. The arena frees it automatically on `destroy_site`.
|
||||||
|
|
||||||
### Memory ownership
|
### Memory ownership
|
||||||
|
|
||||||
- **Parsed pipe filters** (`Pipe_Filter` values) are stored inline on each `Node` via `[dynamic; MAX_PIPES]Pipe_Filter`, and `args` is inline on each `Pipe_Filter` via `[dynamic; MAX_PIPE_ARGS]string`. Both use Odin's fixed-capacity dynamic array type, so no per-tag heap allocations occur at parse time. The storage dies with the `Template` when `delete_template` is called.
|
- **Parsed pipe filters** (`Pipe_Filter` values) are stored inline on each `Node` via `[dynamic; MAX_PIPES]Pipe_Filter`, and `args` is inline on each `Pipe_Filter` via `[dynamic; MAX_PIPE_ARGS]string`. Both use Odin's fixed-capacity dynamic array type, so no per-tag heap allocations occur at parse time. The storage dies with the `Template` when `delete_template` is called.
|
||||||
|
|||||||
+21
-24
@@ -240,33 +240,30 @@ format_offset :: proc(offset_seconds: int) -> string {
|
|||||||
return fmt.tprintf("UTC%s%02d:%02d", sign, hours, minutes)
|
return fmt.tprintf("UTC%s%02d:%02d", sign, hours, minutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tz_cache caches loaded TZ_Region pointers by timezone name.
|
// resolve_tz looks up the `tz` field from the context stack and returns
|
||||||
// Entries persist until destroy_tz_cache is called.
|
// the ^TZ_Region pointer, or nil if not set.
|
||||||
tz_cache: map[string]^datetime.TZ_Region
|
resolve_tz :: proc(ctx: []any) -> ^datetime.TZ_Region {
|
||||||
|
raw := resolve_name("timezone", ctx)
|
||||||
get_cached_tz :: proc(name: string) -> (^datetime.TZ_Region, bool) {
|
if raw == nil do return nil
|
||||||
if name == "" || name == "UTC" {
|
switch v in raw {
|
||||||
return nil, true
|
case ^datetime.TZ_Region:
|
||||||
|
return v
|
||||||
|
case:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if cached, ok := tz_cache[name]; ok {
|
|
||||||
return cached, true
|
|
||||||
}
|
|
||||||
tz, ok := timezone.region_load(name)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
tz_cache[name] = tz
|
|
||||||
return tz, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy_tz_cache :: proc() {
|
// compute_utc_offset returns the UTC offset (in seconds) for the given
|
||||||
for _, tz in tz_cache {
|
// timezone at the current time. Returns (0, true) if tz is nil.
|
||||||
if tz != nil {
|
compute_utc_offset :: proc(tz: ^datetime.TZ_Region) -> (offset: int, ok: bool) {
|
||||||
timezone.region_destroy(tz)
|
if tz == nil do return 0, true
|
||||||
}
|
tm := time.now()
|
||||||
}
|
dt_utc := time.time_to_datetime(tm) or_return
|
||||||
delete(tz_cache)
|
dt_local := timezone.datetime_to_tz(dt_utc, tz) or_return
|
||||||
tz_cache = nil
|
tm_utc := time.datetime_to_time(dt_utc) or_return
|
||||||
|
tm_local := time.datetime_to_time(dt_local) or_return
|
||||||
|
offset = int(time.time_to_unix(tm_local) - time.time_to_unix(tm_utc))
|
||||||
|
return offset, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert_to_tz converts date components from their source timezone to a
|
// convert_to_tz converts date components from their source timezone to a
|
||||||
|
|||||||
+11
-29
@@ -2,6 +2,7 @@
|
|||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
import "core:time/timezone"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// parse_iso_date
|
// parse_iso_date
|
||||||
@@ -230,36 +231,15 @@ test_format_offset_positive :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// get_cached_tz / convert_to_tz (require system zoneinfo)
|
// convert_to_tz (require system zoneinfo)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_cached_tz_utc_returns_nil :: proc(t: ^testing.T) {
|
|
||||||
tz, ok := get_cached_tz("UTC")
|
|
||||||
testing.expect_value(t, ok, true)
|
|
||||||
testing.expect_value(t, tz == nil, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_cached_tz_empty_returns_nil :: proc(t: ^testing.T) {
|
|
||||||
tz, ok := get_cached_tz("")
|
|
||||||
testing.expect_value(t, ok, true)
|
|
||||||
testing.expect_value(t, tz == nil, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_cached_tz_loads_named_region :: proc(t: ^testing.T) {
|
|
||||||
tz, ok := get_cached_tz("America/New_York")
|
|
||||||
defer destroy_tz_cache()
|
|
||||||
if !ok do return
|
|
||||||
testing.expect(t, tz != nil, "should load TZ_Region for America/New_York")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_convert_to_tz_no_offset_assumes_target :: proc(t: ^testing.T) {
|
test_convert_to_tz_no_offset_assumes_target :: proc(t: ^testing.T) {
|
||||||
tz, tz_ok := get_cached_tz("America/New_York")
|
tz, tz_ok := timezone.region_load("America/New_York", context.temp_allocator)
|
||||||
defer destroy_tz_cache()
|
testing.expect(t, tz_ok, "should load timezone")
|
||||||
if !tz_ok do return
|
if !tz_ok do return
|
||||||
|
defer timezone.region_destroy(tz, context.temp_allocator)
|
||||||
|
|
||||||
c := Date_Components{
|
c := Date_Components{
|
||||||
year = 2026, month = 3, day = 15,
|
year = 2026, month = 3, day = 15,
|
||||||
@@ -274,9 +254,10 @@ test_convert_to_tz_no_offset_assumes_target :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_convert_to_tz_with_offset_converts :: proc(t: ^testing.T) {
|
test_convert_to_tz_with_offset_converts :: proc(t: ^testing.T) {
|
||||||
tz, tz_ok := get_cached_tz("America/New_York")
|
tz, tz_ok := timezone.region_load("America/New_York", context.temp_allocator)
|
||||||
defer destroy_tz_cache()
|
testing.expect(t, tz_ok, "should load timezone")
|
||||||
if !tz_ok do return
|
if !tz_ok do return
|
||||||
|
defer timezone.region_destroy(tz, context.temp_allocator)
|
||||||
|
|
||||||
// 2026-03-15T12:49:54Z (UTC) → 08:49:54 EDT (UTC-4)
|
// 2026-03-15T12:49:54Z (UTC) → 08:49:54 EDT (UTC-4)
|
||||||
c := Date_Components{
|
c := Date_Components{
|
||||||
@@ -294,9 +275,10 @@ test_convert_to_tz_with_offset_converts :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_convert_to_tz_with_negative_offset_converts :: proc(t: ^testing.T) {
|
test_convert_to_tz_with_negative_offset_converts :: proc(t: ^testing.T) {
|
||||||
tz, tz_ok := get_cached_tz("America/New_York")
|
tz, tz_ok := timezone.region_load("America/New_York", context.temp_allocator)
|
||||||
defer destroy_tz_cache()
|
testing.expect(t, tz_ok, "should load timezone")
|
||||||
if !tz_ok do return
|
if !tz_ok do return
|
||||||
|
defer timezone.region_destroy(tz, context.temp_allocator)
|
||||||
|
|
||||||
// 2026-03-15T08:49:54-04:00 → UTC 12:49:54 → EDT 08:49:54
|
// 2026-03-15T08:49:54-04:00 → UTC 12:49:54 → EDT 08:49:54
|
||||||
c := Date_Components{
|
c := Date_Components{
|
||||||
|
|||||||
+6
-22
@@ -5,6 +5,7 @@ import "core:log"
|
|||||||
import "core:reflect"
|
import "core:reflect"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
import "core:time/datetime"
|
||||||
|
|
||||||
// MAX_PIPES was chosen arbitrarily. It holds no performance or logical
|
// MAX_PIPES was chosen arbitrarily. It holds no performance or logical
|
||||||
// significance.
|
// significance.
|
||||||
@@ -228,15 +229,8 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
|
|||||||
date_format = df
|
date_format = df
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve timezone (optional — empty is fine, means display as-is)
|
tz := resolve_tz(ctx)
|
||||||
tz_name := ""
|
str2, err := apply_format(str, filter.args[:], pos, date_format, tz)
|
||||||
if raw := resolve_name("timezone", ctx); raw != nil {
|
|
||||||
if s, s_ok := reflect.as_string(raw); s_ok {
|
|
||||||
tz_name = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str2, err := apply_format(str, filter.args[:], pos, date_format, tz_name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return value, err
|
return value, err
|
||||||
} else {
|
} else {
|
||||||
@@ -257,7 +251,7 @@ apply_format :: proc(
|
|||||||
args: []string,
|
args: []string,
|
||||||
pos: int,
|
pos: int,
|
||||||
date_format: string,
|
date_format: string,
|
||||||
timezone_name: string,
|
tz: ^datetime.TZ_Region,
|
||||||
) -> (
|
) -> (
|
||||||
result: string,
|
result: string,
|
||||||
err: Error,
|
err: Error,
|
||||||
@@ -279,22 +273,12 @@ apply_format :: proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target_tz, tz_ok := get_cached_tz(timezone_name)
|
if tz != nil {
|
||||||
if !tz_ok {
|
components, _ = convert_to_tz(components, tz)
|
||||||
return "", Error_Body {
|
|
||||||
msg = fmt.tprintf("unable to load timezone '%s'", timezone_name),
|
|
||||||
pos = pos,
|
|
||||||
kind = .Data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if target_tz != nil {
|
|
||||||
components, _ = convert_to_tz(components, target_tz)
|
|
||||||
} else if components.has_offset {
|
} else if components.has_offset {
|
||||||
components.tz_abbr = format_offset(components.offset_seconds)
|
components.tz_abbr = format_offset(components.offset_seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debugf("date: '%s' format: '%s' tz: '%s'", iso, date_format, timezone_name)
|
|
||||||
return format_date(components, fmt_str), nil
|
return format_date(components, fmt_str), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-54
@@ -3,6 +3,8 @@ package mustache
|
|||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
import "core:time/datetime"
|
||||||
|
import "core:time/timezone"
|
||||||
|
|
||||||
Pipe_Post :: struct {
|
Pipe_Post :: struct {
|
||||||
title: string,
|
title: string,
|
||||||
@@ -209,7 +211,7 @@ test_interp_pipe_basic :: proc(t: ^testing.T) {
|
|||||||
Scalar_Data :: struct {
|
Scalar_Data :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
date_format: string,
|
date_format: string,
|
||||||
timezone: string,
|
timezone: ^datetime.TZ_Region,
|
||||||
}
|
}
|
||||||
data := Scalar_Data {
|
data := Scalar_Data {
|
||||||
name = "2026-03-15T08:49:54-04:00",
|
name = "2026-03-15T08:49:54-04:00",
|
||||||
@@ -223,9 +225,9 @@ test_interp_pipe_basic :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_interp_pipe_unescaped :: proc(t: ^testing.T) {
|
test_interp_pipe_unescaped :: proc(t: ^testing.T) {
|
||||||
Scalar_Data :: struct {
|
Scalar_Data :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
date_format: string,
|
date_format: string,
|
||||||
date_timezone: string,
|
timezone: ^datetime.TZ_Region,
|
||||||
}
|
}
|
||||||
data := Scalar_Data {
|
data := Scalar_Data {
|
||||||
name = "2025-12-25T00:00:00Z",
|
name = "2025-12-25T00:00:00Z",
|
||||||
@@ -241,7 +243,7 @@ test_interp_pipe_dot_current :: proc(t: ^testing.T) {
|
|||||||
List_Data :: struct {
|
List_Data :: struct {
|
||||||
items: [3]string,
|
items: [3]string,
|
||||||
date_format: string,
|
date_format: string,
|
||||||
timezone: string,
|
timezone: ^datetime.TZ_Region,
|
||||||
}
|
}
|
||||||
data := List_Data {
|
data := List_Data {
|
||||||
items = {"2026-01-06T00:00:00Z", "2026-06-15T00:00:00Z", "2026-10-15T00:00:00Z"},
|
items = {"2026-01-06T00:00:00Z", "2026-06-15T00:00:00Z", "2026-10-15T00:00:00Z"},
|
||||||
@@ -257,9 +259,9 @@ test_interp_pipe_dot_current :: proc(t: ^testing.T) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Format_Data :: struct {
|
Format_Data :: struct {
|
||||||
date: string,
|
date: string,
|
||||||
date_format: string,
|
date_format: string,
|
||||||
timezone: string,
|
timezone: ^datetime.TZ_Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -488,44 +490,21 @@ test_format_bare_numeric_arg_treated_as_key_not_literal :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, err != nil, "bare numeric-looking arg should error as an unresolved key")
|
testing.expect(t, err != nil, "bare numeric-looking arg should error as an unresolved key")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_timezone_resolves_from_context :: proc(t: ^testing.T) {
|
|
||||||
TZ_Data :: struct {
|
|
||||||
timezone: string,
|
|
||||||
}
|
|
||||||
data := TZ_Data {
|
|
||||||
timezone = "America/New_York",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("[{{timezone}}]", "<test>", allocator = context.temp_allocator)
|
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect_value(t, result, "[America/New_York]")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_timezone_empty_when_not_set :: proc(t: ^testing.T) {
|
|
||||||
TZ_Data :: struct {
|
|
||||||
timezone: string,
|
|
||||||
}
|
|
||||||
data := TZ_Data {}
|
|
||||||
tpl, _ := parse("[{{timezone}}]", "<test>", allocator = context.temp_allocator)
|
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect_value(t, result, "[]")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// format pipe with timezone conversion
|
// format pipe with timezone conversion
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_format_with_timezone_summer :: proc(t: ^testing.T) {
|
test_format_with_timezone_summer :: proc(t: ^testing.T) {
|
||||||
tz, tz_ok := get_cached_tz("America/New_York")
|
tz, ok := timezone.region_load("America/New_York", context.temp_allocator)
|
||||||
defer destroy_tz_cache()
|
testing.expect(t, ok, "should load timezone")
|
||||||
if !tz_ok || tz == nil do return
|
if !ok do return
|
||||||
|
defer timezone.region_destroy(tz, context.temp_allocator)
|
||||||
|
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "2026-03-15T12:49:54Z",
|
date = "2026-03-15T12:49:54Z",
|
||||||
date_format = "15:04 MST",
|
date_format = "15:04 MST",
|
||||||
timezone = "America/New_York",
|
timezone = tz,
|
||||||
}
|
}
|
||||||
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
@@ -534,14 +513,15 @@ test_format_with_timezone_summer :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_format_with_timezone_winter :: proc(t: ^testing.T) {
|
test_format_with_timezone_winter :: proc(t: ^testing.T) {
|
||||||
tz, tz_ok := get_cached_tz("America/New_York")
|
tz, ok := timezone.region_load("America/New_York", context.temp_allocator)
|
||||||
defer destroy_tz_cache()
|
testing.expect(t, ok, "should load timezone")
|
||||||
if !tz_ok || tz == nil do return
|
if !ok do return
|
||||||
|
defer timezone.region_destroy(tz, context.temp_allocator)
|
||||||
|
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "2026-01-15T12:49:54Z",
|
date = "2026-01-15T12:49:54Z",
|
||||||
date_format = "15:04 MST",
|
date_format = "15:04 MST",
|
||||||
timezone = "America/New_York",
|
timezone = tz,
|
||||||
}
|
}
|
||||||
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
@@ -569,18 +549,3 @@ test_format_mst_no_offset_no_timezone :: proc(t: ^testing.T) {
|
|||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
testing.expect_value(t, result, "UTC")
|
testing.expect_value(t, result, "UTC")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_invalid_timezone_errors :: proc(t: ^testing.T) {
|
|
||||||
defer destroy_tz_cache()
|
|
||||||
|
|
||||||
data := Format_Data {
|
|
||||||
date = "2026-03-15",
|
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
timezone = "Invalid/Zone",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
defer delete_template(&tpl)
|
|
||||||
_, err := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect(t, err != nil, "invalid timezone should error")
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-7
@@ -25,8 +25,8 @@ Base_Data :: struct {
|
|||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
og: Open_Graph,
|
og: Open_Graph,
|
||||||
date_format: string,
|
date_format: string,
|
||||||
timezone: string,
|
timezone: ^datetime.TZ_Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
Page_Data :: struct {
|
Page_Data :: struct {
|
||||||
@@ -157,10 +157,10 @@ render_site :: proc(site: ^Site) {
|
|||||||
template_cache: map[string]mustache.Template
|
template_cache: map[string]mustache.Template
|
||||||
defer delete(template_cache)
|
defer delete(template_cache)
|
||||||
|
|
||||||
// TODO: CAlculate offset
|
offset, ok := mustache.compute_utc_offset(site.tz)
|
||||||
offset := 0
|
|
||||||
now, ok := time.time_to_rfc3339(time.now(), offset, false, allocator)
|
|
||||||
assert(ok)
|
assert(ok)
|
||||||
|
now, ok2 := time.time_to_rfc3339(time.now(), offset, false, allocator)
|
||||||
|
assert(ok2)
|
||||||
|
|
||||||
// Build base data once
|
// Build base data once
|
||||||
base := Base_Data {
|
base := Base_Data {
|
||||||
@@ -168,8 +168,8 @@ render_site :: proc(site: ^Site) {
|
|||||||
params = site.params,
|
params = site.params,
|
||||||
description = site.description,
|
description = site.description,
|
||||||
og = site.og,
|
og = site.og,
|
||||||
date_format = site.date.format,
|
date_format = site.date.format,
|
||||||
timezone = site.date.timezone,
|
timezone = site.tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find home page
|
// Find home page
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import "core:log"
|
|||||||
import "core:mem"
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
import "core:time/datetime"
|
||||||
|
import "core:time/timezone"
|
||||||
|
|
||||||
import md "markdown"
|
import md "markdown"
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ Site :: struct {
|
|||||||
markdown_extensions: bit_set[md.Extension],
|
markdown_extensions: bit_set[md.Extension],
|
||||||
og: Open_Graph,
|
og: Open_Graph,
|
||||||
date: Date_Preferences,
|
date: Date_Preferences,
|
||||||
|
tz: ^datetime.TZ_Region,
|
||||||
}
|
}
|
||||||
|
|
||||||
Date_Preferences :: struct {
|
Date_Preferences :: struct {
|
||||||
@@ -115,8 +118,22 @@ init_site :: proc(site: ^Site, args: []string) {
|
|||||||
site_apply_cli_flags(site, _flags)
|
site_apply_cli_flags(site, _flags)
|
||||||
site.config_path = path
|
site.config_path = path
|
||||||
|
|
||||||
// Build the resolved site-level OG now that every other field is set.
|
|
||||||
site.og = og_for_site(site)
|
site.og = og_for_site(site)
|
||||||
|
|
||||||
|
tz_name := site.date.timezone
|
||||||
|
if tz_name == "" {
|
||||||
|
log.warnf("no timezone configured, falling back to local system timezone")
|
||||||
|
tz_name = "local"
|
||||||
|
}
|
||||||
|
tz, tz_ok := timezone.region_load(tz_name, alloc)
|
||||||
|
if tz_ok {
|
||||||
|
site.tz = tz
|
||||||
|
if site.date.timezone == "" {
|
||||||
|
log.debugf("detected local timezone: %s", tz.name)
|
||||||
|
}
|
||||||
|
} else if site.date.timezone != "" {
|
||||||
|
log.warnf("unable to load timezone '%s'", site.date.timezone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load_config_file :: proc(
|
load_config_file :: proc(
|
||||||
|
|||||||
Reference in New Issue
Block a user