feat: Added Go style date format configuration.

This commit is contained in:
Spencer Brower
2026-07-21 17:46:03 -04:00
parent 4560014954
commit d35da93dcf
16 changed files with 1282 additions and 45 deletions
+61 -41
View File
@@ -1,6 +1,7 @@
package mustache
import "core:fmt"
import "core:log"
import "core:reflect"
import "core:strings"
import "core:time"
@@ -12,6 +13,8 @@ MAX_PIPES :: 8
// No filter accepts more than 2 args.
MAX_PIPE_ARGS :: 2
DEFAULT_DATE_FORMAT :: "2 Jan 2006"
Pipe_Filter :: struct {
op: string,
args: [dynamic; MAX_PIPE_ARGS]string,
@@ -111,6 +114,7 @@ apply_pipeline :: proc(
current = value
for &filter in filters {
current = apply_filter(current, &filter, pos, ctx) or_return
log.debugf("applied: filter=%v before=%s after=%s pos=%d", filter, value, current, pos)
}
return
}
@@ -128,11 +132,42 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
pos = pos,
kind = .Data,
}
} else {
return apply_format(str, filter.args[:], pos)
}
date_format: string
if len(filter.args) > 0 {
date_format = filter.args[0]
} else {
date_format_raw := resolve_name("date_format", ctx)
if date_format_raw == nil {
return value, Error_Body {
msg = "Unable to determine date format",
pos = pos,
kind = .Data,
}
} else {
valid: bool
date_format, valid = reflect.as_string(date_format_raw)
if !valid {
return value, Error_Body {
msg = "date format is not a string",
pos = pos,
kind = .Data,
}
}
}
}
str2, err := apply_format(str, filter.args[:], pos, date_format)
if err != nil {
return value, err
} else {
return any{new_clone(str2, context.temp_allocator), typeid_of(string)}, nil
}
case:
return nil, Error_Body{
return nil, Error_Body {
msg = fmt.tprintf("unknown pipe op '%s'", filter.op),
pos = pos,
kind = .Data,
@@ -140,51 +175,40 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
}
}
// apply_format formats an ISO 8601 date string as a display string
// (e.g. "2026-03-15T08:49:54-04:00" → "15 Mar 2026"). Invalid input
// (empty, too-short, or unparseable) returns a Data-kind `Error`. Templates
// that need to skip dateless pages should gate with a section:
// {{#date}}<time datetime="{{.}}">{{. | format}}</time>{{/date}}
// The section's truthiness check catches empty before the filter runs.
//
// Currently ignores args; planned to accept Go reference-date format
// strings in the future.
//
// Accepts any of these ISO 8601 forms (date prefix is invariant):
// 2023-10-15T13:18:50-07:00
// 2023-10-15T13:18:50-0700
// 2023-10-15T13:18:50Z
// 2023-10-15T13:18:50
// 2023-10-15
apply_format :: proc(iso: string, args: []string, pos: int) -> (result: any, err: Error) {
if len(iso) < 10 {
return nil, Error_Body{
msg = "format may only be used on dates",
pos = pos,
kind = .Data,
}
apply_format :: proc(
iso: string,
args: []string,
pos: int,
date_format: string,
) -> (
result: string,
err: Error,
) {
fmt_str := date_format
if fmt_str == "" {
log.errorf(
"format pipe used but no date format configured (set date.format in thor.json) Default will be used",
)
fmt_str = DEFAULT_DATE_FORMAT
}
year := iso[:4]
month_num := (int(iso[5]) - 0x30) * 10 + (int(iso[6]) - 0x30)
day_num := (int(iso[8]) - 0x30) * 10 + (int(iso[9]) - 0x30)
if month_num < 1 || month_num > 12 {
return nil, Error_Body{
components, ok := parse_iso_date(iso)
if !ok {
return "", Error_Body {
msg = fmt.tprintf("invalid date: \"%s\"", iso),
pos = pos,
kind = .Data,
}
}
month := fmt.tprintf("%s", time.Month(month_num))[:3]
return fmt.tprintf("%d %s %s", day_num, month, year), nil
log.debugf("date: '%s' format: '%s'", iso, date_format)
return format_date(components, fmt_str), nil
}
// Groups preserve first-appearance order from the input list.
apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, err: Error) {
if len(args) != 1 {
return nil, Error_Body{
return nil, Error_Body {
msg = fmt.tprintf("group_by expects 1 argument, got %d", len(args)),
pos = pos,
kind = .Data,
@@ -194,11 +218,7 @@ apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, er
elem_info, count, data := list_info(value)
if elem_info == nil {
return nil, Error_Body{
msg = "group_by expects a list",
pos = pos,
kind = .Data,
}
return nil, Error_Body{msg = "group_by expects a list", pos = pos, kind = .Data}
}
groups := make([dynamic]Group, 0, 8, context.temp_allocator)
@@ -219,7 +239,7 @@ apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, er
}
key_str := any_to_string(key_val)
if len(key_str) == 0 {
return nil, Error_Body{
return nil, Error_Body {
msg = fmt.tprintf("group_by: field '%s' is empty", field),
pos = pos,
kind = .Data,