mirror of
https://github.com/sbrow/thor.git
synced 2026-08-27 03:43:31 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 092f03d3e9 | |||
| ba2fac49b7 | |||
| 65568cef2c | |||
| d10425c0f7 | |||
| 2fd18d0108 | |||
| 23288d34cd | |||
| 68d349bdb6 | |||
| fec71bd80e |
@@ -13,14 +13,6 @@
|
|||||||
- [ ] Not sure whether to use temp allocator or site_allocator in opengraph.odin.
|
- [ ] Not sure whether to use temp allocator or site_allocator in opengraph.odin.
|
||||||
- [ ] Not sure whether to use temp allocator or site_allocator in `site_load_content`.
|
- [ ] Not sure whether to use temp allocator or site_allocator in `site_load_content`.
|
||||||
- [ ] Might not need to allocate in `strip_html_tags`
|
- [ ] Might not need to allocate in `strip_html_tags`
|
||||||
- [ ] Fix `apply_filter`'s `format` case (`mustache/pipes.odin`) boxing `apply_format`'s
|
|
||||||
`string` result into `any` via bare `return`, which materializes a hidden
|
|
||||||
header temp in `apply_filter`'s own stack frame. Dangling once the frame
|
|
||||||
returns; caused the `-o:speed` segfault in `write_value`. Fix: box explicitly
|
|
||||||
with `any{new_clone(formatted, context.temp_allocator), typeid_of(string)}`.
|
|
||||||
- [ ] Same pattern in `apply_group_by` (`mustache/pipes.odin`): `return groups, nil`
|
|
||||||
boxes a freshly-built `[dynamic]Group` as bare `any` — same latent
|
|
||||||
stack-temp UB, hasn't crashed yet but should get the same treatment.
|
|
||||||
|
|
||||||
## Markdown
|
## Markdown
|
||||||
- [ ] Add overloads for every extension - accept ^strings.Builder.
|
- [ ] Add overloads for every extension - accept ^strings.Builder.
|
||||||
@@ -28,21 +20,13 @@
|
|||||||
- [ ] Add heading ids as a default on extension.
|
- [ ] Add heading ids as a default on extension.
|
||||||
- [ ] Add opt-in deflist support.
|
- [ ] Add opt-in deflist support.
|
||||||
- [ ] Decide if lambdas actually provide any value.
|
- [ ] Decide if lambdas actually provide any value.
|
||||||
|
- [ ] configure date format as a partial
|
||||||
## Dates
|
|
||||||
- [x] Accept "strings"
|
|
||||||
- [x] Accept keys
|
|
||||||
- [ ] handle timezones
|
|
||||||
- [ ] display an error when no part of the date appears in the output.
|
|
||||||
- [x] use `date.format` as the default format.
|
|
||||||
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- [ ] Integrity hash
|
- [ ] Integrity hash
|
||||||
- Allows users to verify their output didn't change after upgrading to a new version
|
- Allows users to verify their output didn't change after upgrading to a new version
|
||||||
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
||||||
- [ ] Avoid `json.Value` / `json.Object` where possible.
|
- [ ] Avoid `json.Value` / `json.Object` where possible.
|
||||||
- [ ] Create a json schema file for `thor.json`.
|
|
||||||
- [ ] make `parse` an overload of `parse_text/parse_inline` and `parse_file`, or something.
|
- [ ] make `parse` an overload of `parse_text/parse_inline` and `parse_file`, or something.
|
||||||
- [ ] Add page params
|
- [ ] Add page params
|
||||||
- [ ] We must remove all mention of `posts` from the odin code.
|
- [ ] We must remove all mention of `posts` from the odin code.
|
||||||
@@ -62,22 +46,6 @@
|
|||||||
- [x] Block-override source-template tracking: warnings inside overrides point at the override's source file, not the parent template
|
- [x] Block-override source-template tracking: warnings inside overrides point at the override's source file, not the parent template
|
||||||
- [ ] Partial invocation stack in diagnostics: when an error fires inside a partial, show "invoked from" chain through `{{> name}}` calls. Currently warnings inside partials point at the partial (correct file) but don't show the invocation site.
|
- [ ] Partial invocation stack in diagnostics: when an error fires inside a partial, show "invoked from" chain through `{{> name}}` calls. Currently warnings inside partials point at the partial (correct file) but don't show the invocation site.
|
||||||
- [ ] Could be better error message when missing a closing (or opening) brace
|
- [ ] Could be better error message when missing a closing (or opening) brace
|
||||||
- [ ] Error message doesn't show position of faulty pipe name correctly.
|
|
||||||
- [ ] `render_template` (`render.odin`) blanks the *entire page* to `""` on any
|
|
||||||
mustache render error and only `log.errorf`s it — a single bad tag/pipe
|
|
||||||
anywhere on the page silently kills the whole output with no visible
|
|
||||||
signal outside the terminal log. Should at least be scoped to the
|
|
||||||
failing tag/section, or surfaced somewhere the person building the
|
|
||||||
site will actually see it.
|
|
||||||
```bash
|
|
||||||
[ERROR] --- [138:render_template()] unknown pipe op 'formats'
|
|
||||||
--> /home/spencer/github.com/sbrow.github.io/layouts/home.html:1:1
|
|
||||||
|
|
|
||||||
1 | {{<base}}
|
|
||||||
| ^^^^^^^^^
|
|
||||||
2 | {{$content}}
|
|
||||||
3 | <main>
|
|
||||||
```
|
|
||||||
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
||||||
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
||||||
- [ ] follow symlinks in `scan_content`?
|
- [ ] follow symlinks in `scan_content`?
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "base:runtime"
|
|
||||||
import "common"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:os"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
import "inlined"
|
|
||||||
import "original"
|
|
||||||
|
|
||||||
DATES :: #load(#directory + os.Path_Separator_String + "dates.txt")
|
|
||||||
FORMATS :: #load(#directory + os.Path_Separator_String + "formats.txt")
|
|
||||||
|
|
||||||
ITERATIONS :: 1_000
|
|
||||||
|
|
||||||
dates: []common.Date_Components
|
|
||||||
formats: []string
|
|
||||||
|
|
||||||
formatter :: #type proc(
|
|
||||||
date: common.Date_Components,
|
|
||||||
format: string,
|
|
||||||
allocator: runtime.Allocator,
|
|
||||||
) -> string
|
|
||||||
|
|
||||||
benchmark :: #type proc(
|
|
||||||
options: ^time.Benchmark_Options,
|
|
||||||
allocator: runtime.Allocator,
|
|
||||||
) -> (
|
|
||||||
err: time.Benchmark_Error,
|
|
||||||
)
|
|
||||||
|
|
||||||
Version :: struct {
|
|
||||||
name: string,
|
|
||||||
bench: benchmark,
|
|
||||||
}
|
|
||||||
|
|
||||||
init :: proc() {
|
|
||||||
raw_dates := strings.split_lines(string(DATES))
|
|
||||||
raw_dates = raw_dates[:len(raw_dates) - 1]
|
|
||||||
formats = strings.split_lines(string(FORMATS))
|
|
||||||
|
|
||||||
dates = make([]common.Date_Components, len(raw_dates))
|
|
||||||
|
|
||||||
assert(to_parsed(raw_dates, &dates))
|
|
||||||
}
|
|
||||||
|
|
||||||
to_parsed :: proc(raw_dates: []string, dates: ^[]common.Date_Components) -> bool {
|
|
||||||
for date, i in raw_dates {
|
|
||||||
// log.debugf("parsing '%s'", date)
|
|
||||||
dates[i] = common.parse_iso_date(date) or_return
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
main :: proc() {
|
|
||||||
logger_opts: log.Options =
|
|
||||||
(log.Default_Console_Logger_Opts - log.Full_Timestamp_Opts - {.Short_File_Path})
|
|
||||||
console_logger := log.create_console_logger(.Info, logger_opts)
|
|
||||||
context.logger = console_logger
|
|
||||||
defer log.destroy_console_logger(console_logger)
|
|
||||||
|
|
||||||
init()
|
|
||||||
|
|
||||||
versions := [?]Version {
|
|
||||||
{"original", to_benchmark(original.format_date)},
|
|
||||||
{"inlined", to_benchmark(inlined.format_date)},
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.printfln(
|
|
||||||
"%-10s %14s %10s %14s %12s %8s",
|
|
||||||
"version",
|
|
||||||
"total",
|
|
||||||
"calls",
|
|
||||||
"calls/s",
|
|
||||||
"time/call",
|
|
||||||
"MB/s",
|
|
||||||
)
|
|
||||||
|
|
||||||
for version in versions {
|
|
||||||
defer free_all(context.temp_allocator)
|
|
||||||
|
|
||||||
b: time.Benchmark_Options
|
|
||||||
b.bench = version.bench
|
|
||||||
|
|
||||||
if err := time.benchmark(&b, context.temp_allocator); err != nil {
|
|
||||||
fmt.panicf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmt.printfln("%v", b)
|
|
||||||
per_call := time.Duration(i64(b.duration) / i64(b.count))
|
|
||||||
fmt.printfln(
|
|
||||||
"%-10s %14v % 10d % 14.0f %12v % 8.2f",
|
|
||||||
version.name,
|
|
||||||
b.duration,
|
|
||||||
b.count,
|
|
||||||
b.rounds_per_second,
|
|
||||||
per_call,
|
|
||||||
b.megabytes_per_second,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
to_benchmark :: proc($f: formatter) -> benchmark {
|
|
||||||
return(
|
|
||||||
proc(
|
|
||||||
opts: ^time.Benchmark_Options,
|
|
||||||
allocator: runtime.Allocator,
|
|
||||||
) -> (
|
|
||||||
err: time.Benchmark_Error,
|
|
||||||
) {
|
|
||||||
for _ in 0 ..< ITERATIONS {
|
|
||||||
for date in dates {
|
|
||||||
for format in formats {
|
|
||||||
f(date, format, allocator)
|
|
||||||
opts.count += 1
|
|
||||||
opts.processed += size_of(date) + size_of(format)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.rounds += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
} \
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
Date_Components :: struct {
|
|
||||||
year: int,
|
|
||||||
month: int,
|
|
||||||
day: int,
|
|
||||||
hour: int,
|
|
||||||
minute: int,
|
|
||||||
second: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use some kind of scanner interface
|
|
||||||
parse_iso_date :: proc(iso: string) -> (c: Date_Components, ok: bool) {
|
|
||||||
if len(iso) < 10 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
c.year = parse_2_digits(iso, 0) * 100 + parse_2_digits(iso, 2)
|
|
||||||
c.month = parse_2_digits(iso, 5)
|
|
||||||
c.day = parse_2_digits(iso, 8)
|
|
||||||
|
|
||||||
if c.month < 1 || c.month > 12 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
if c.day < 1 || c.day > 31 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(iso) >= 19 && (iso[10] == 'T' || iso[10] == 't') {
|
|
||||||
c.hour = parse_2_digits(iso, 11)
|
|
||||||
c.minute = parse_2_digits(iso, 14)
|
|
||||||
c.second = parse_2_digits(iso, 17)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, true
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_2_digits :: proc(s: string, offset: int) -> int {
|
|
||||||
if offset + 1 >= len(s) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (int(s[offset]) - 0x30) * 10 + (int(s[offset + 1]) - 0x30)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
2024-01-05
|
|
||||||
2024-02-14
|
|
||||||
2024-03-08
|
|
||||||
2024-04-22
|
|
||||||
2024-05-01
|
|
||||||
2024-06-19
|
|
||||||
2024-07-04
|
|
||||||
2024-08-30
|
|
||||||
2024-09-11
|
|
||||||
2024-10-31
|
|
||||||
2024-11-27
|
|
||||||
2024-12-25
|
|
||||||
2023-01-15T00:00:00-05:00
|
|
||||||
2023-02-14T09:30:15+01:00
|
|
||||||
2023-03-08T14:45:22-08:00
|
|
||||||
2023-04-22T23:59:59+05:30
|
|
||||||
2023-05-01T06:05:09-04:00
|
|
||||||
2023-06-19T18:20:33+09:00
|
|
||||||
2023-07-04T03:04:05-07:00
|
|
||||||
2023-08-30T11:11:11+00:00
|
|
||||||
2023-09-11T15:04:05-04:00
|
|
||||||
2023-10-31T20:15:30+02:00
|
|
||||||
2023-11-27T07:07:07-06:00
|
|
||||||
2023-12-25T12:30:45+03:00
|
|
||||||
2025-01-15T00:15:00-0500
|
|
||||||
2025-02-14T09:30:15+0100
|
|
||||||
2025-03-08T14:45:22-0800
|
|
||||||
2025-04-22T23:59:59+0530
|
|
||||||
2025-05-01T06:05:09-0400
|
|
||||||
2025-06-19T18:20:33+0900
|
|
||||||
2025-07-04T03:04:05-0700
|
|
||||||
2025-08-30T11:11:11+0000
|
|
||||||
2025-09-11T15:04:05-0400
|
|
||||||
2025-10-31T20:15:30+0200
|
|
||||||
2025-11-27T07:07:07-0600
|
|
||||||
2025-12-25T12:30:45+0300
|
|
||||||
2022-01-01T00:00:00Z
|
|
||||||
2022-02-14T01:30:00Z
|
|
||||||
2022-03-08T11:59:59Z
|
|
||||||
2022-04-22T12:00:00Z
|
|
||||||
2022-05-01T12:00:01Z
|
|
||||||
2022-06-19T13:15:00Z
|
|
||||||
2022-07-04T15:04:05Z
|
|
||||||
2022-08-30T18:45:30Z
|
|
||||||
2022-09-11T20:20:20Z
|
|
||||||
2022-10-31T22:10:10Z
|
|
||||||
2022-11-27T23:59:59Z
|
|
||||||
2022-12-25T09:09:09Z
|
|
||||||
2021-03-14t09:26:53
|
|
||||||
2021-07-20t23:00:00
|
|
||||||
2021-11-05t00:00:00
|
|
||||||
2020-06-15T15:04:05
|
|
||||||
2020-12-31T23:59:59
|
|
||||||
2020-01-01T00:00:00
|
|
||||||
2024-02-29T12:00:00Z
|
|
||||||
2000-02-29T00:00:00Z
|
|
||||||
1999-12-31T23:59:59Z
|
|
||||||
2026-01-01T00:00:00Z
|
|
||||||
2026-07-23T14:56:07-04:00
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
2006
|
|
||||||
06
|
|
||||||
January
|
|
||||||
Jan
|
|
||||||
Monday
|
|
||||||
Mon
|
|
||||||
01
|
|
||||||
02
|
|
||||||
03
|
|
||||||
04
|
|
||||||
05
|
|
||||||
15
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
4
|
|
||||||
5
|
|
||||||
PM
|
|
||||||
pm
|
|
||||||
MST
|
|
||||||
2006-01-02
|
|
||||||
2006-01-02 15:04:05
|
|
||||||
2 Jan 2006
|
|
||||||
Jan 2, 2006
|
|
||||||
January 2, 2006
|
|
||||||
Monday, January 2, 2006
|
|
||||||
Mon Jan 2 2006
|
|
||||||
Mon Jan 02 2006
|
|
||||||
Mon, 02 Jan 2006 15:04:05 MST
|
|
||||||
01/02/2006
|
|
||||||
02/01/2006
|
|
||||||
2006/01/02
|
|
||||||
1/2/06
|
|
||||||
1/2/06 3:04PM
|
|
||||||
1/2/2006
|
|
||||||
3:04:05 PM
|
|
||||||
3:04 pm
|
|
||||||
15:04:05
|
|
||||||
15:04
|
|
||||||
15:04:05 MST
|
|
||||||
January 2006
|
|
||||||
Jan 06
|
|
||||||
2 January 2006
|
|
||||||
Monday 2 Jan 2006 at 15:04
|
|
||||||
06-01-02
|
|
||||||
3:4:5
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package inlined
|
|
||||||
|
|
||||||
import "../common"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
import "core:time/datetime"
|
|
||||||
|
|
||||||
format_date :: proc(
|
|
||||||
dt: common.Date_Components,
|
|
||||||
fmt: string,
|
|
||||||
allocator := context.temp_allocator,
|
|
||||||
) -> string {
|
|
||||||
b: strings.Builder
|
|
||||||
strings.builder_init_len(&b, len(fmt), allocator)
|
|
||||||
|
|
||||||
for i := 0; i < len(fmt); {
|
|
||||||
matched := match_token(&b, dt, fmt[i:])
|
|
||||||
if matched > 0 {
|
|
||||||
i += matched
|
|
||||||
} else {
|
|
||||||
strings.write_byte(&b, fmt[i])
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debugf("formatted date: '%s'", b.buf)
|
|
||||||
return strings.to_string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
match_token :: proc(b: ^strings.Builder, dt: common.Date_Components, s: string) -> int {
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"January",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%s", time.Month(dt.month))); return 7}
|
|
||||||
if strings.has_prefix(s, "Monday") {emit_weekday(b, dt, full = true); return 6}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"2006",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
|
||||||
if strings.has_prefix(s, "MST") {strings.write_string(b, "UTC"); return 3}
|
|
||||||
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
|
||||||
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"06",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.year % 100)); return 2}
|
|
||||||
if strings.has_prefix(s, "02") {strings.write_string(b, fmt.tprintf("%02d", dt.day)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"15",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.hour)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"04",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.minute)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"05",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.second)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"01",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.month)); return 2}
|
|
||||||
if strings.has_prefix(s, "03") {emit_hour_12(b, dt, pad = true); return 2}
|
|
||||||
if strings.has_prefix(s, "PM") {
|
|
||||||
strings.write_string(b, "PM" if dt.hour >= 12 else "AM")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if strings.has_prefix(s, "pm") {
|
|
||||||
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if len(s) >= 1 {
|
|
||||||
switch s[0] {
|
|
||||||
case '2':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.day)); return 1
|
|
||||||
case '1':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.month)); return 1
|
|
||||||
case '4':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.minute)); return 1
|
|
||||||
case '5':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.second)); return 1
|
|
||||||
case '3':
|
|
||||||
emit_hour_12(b, dt, pad = false); return 1
|
|
||||||
case:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_month_abbr :: proc(b: ^strings.Builder, dt: common.Date_Components) {
|
|
||||||
name := fmt.tprintf("%s", time.Month(dt.month))
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_weekday :: proc(b: ^strings.Builder, dt: common.Date_Components, full: bool) {
|
|
||||||
date := datetime.Date {
|
|
||||||
year = i64(dt.year),
|
|
||||||
month = i8(dt.month),
|
|
||||||
day = i8(dt.day),
|
|
||||||
}
|
|
||||||
ordinal, err := datetime.date_to_ordinal(date)
|
|
||||||
if err != .None {
|
|
||||||
strings.write_string(b, "???")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
weekday := datetime.day_of_week(ordinal)
|
|
||||||
name := fmt.tprintf("%s", weekday)
|
|
||||||
if full {
|
|
||||||
strings.write_string(b, name)
|
|
||||||
} else {
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_hour_12 :: proc(b: ^strings.Builder, dt: common.Date_Components, pad: bool) {
|
|
||||||
h12 := dt.hour % 12
|
|
||||||
if h12 == 0 {h12 = 12}
|
|
||||||
format := "%02d" if pad else "%d"
|
|
||||||
|
|
||||||
fmt.sbprintf(b, format, h12)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package inplace
|
|
||||||
|
|
||||||
import "../common"
|
|
||||||
import "core:bytes"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:mem"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
import "core:time/datetime"
|
|
||||||
|
|
||||||
format_date :: proc(
|
|
||||||
dt: common.Date_Components,
|
|
||||||
fmt: string,
|
|
||||||
allocator := context.temp_allocator,
|
|
||||||
) -> string {
|
|
||||||
b := make([]byte, len(fmt), context.temp_allocator)
|
|
||||||
x := transmute([]byte)(fmt)
|
|
||||||
mem.copy_non_overlapping(&b[0], &x, len(fmt))
|
|
||||||
|
|
||||||
|
|
||||||
return string(b)
|
|
||||||
// for i := 0; i < len(fmt); {
|
|
||||||
// matched := match_token(&b, dt, fmt[i:])
|
|
||||||
// if matched > 0 {
|
|
||||||
// i += matched
|
|
||||||
// } else {
|
|
||||||
// strings.write_byte(&b, fmt[i])
|
|
||||||
// i += 1
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// log.debugf("formatted date: '%s'", b.buf)
|
|
||||||
// return strings.to_string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
FULL_MONTH: string : "January"
|
|
||||||
|
|
||||||
match_token :: proc(s: []byte, dt: common.Date_Components) {
|
|
||||||
if bytes.equal(s[:len(FULL_MONTH)], transmute([]u8)FULL_MONTH) {
|
|
||||||
mo := fmt.tprintf("%s", time.Month(dt.month))
|
|
||||||
mem.copy_non_overlapping(&s[0], &(transmute([]u8)mo)[0], len(mo))
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
if strings.has_prefix(s, "Monday") {emit_weekday(b, dt, full = true); return 6}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"2006",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
|
||||||
if strings.has_prefix(s, "MST") {strings.write_string(b, "UTC"); return 3}
|
|
||||||
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
|
||||||
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"06",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.year % 100)); return 2}
|
|
||||||
if strings.has_prefix(s, "02") {strings.write_string(b, fmt.tprintf("%02d", dt.day)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"15",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.hour)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"04",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.minute)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"05",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.second)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"01",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.month)); return 2}
|
|
||||||
if strings.has_prefix(s, "03") {emit_hour_12(b, dt, pad = true); return 2}
|
|
||||||
if strings.has_prefix(s, "PM") {
|
|
||||||
strings.write_string(b, "PM" if dt.hour >= 12 else "AM")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if strings.has_prefix(s, "pm") {
|
|
||||||
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if len(s) >= 1 {
|
|
||||||
switch s[0] {
|
|
||||||
case '2':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.day)); return 1
|
|
||||||
case '1':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.month)); return 1
|
|
||||||
case '4':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.minute)); return 1
|
|
||||||
case '5':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.second)); return 1
|
|
||||||
case '3':
|
|
||||||
emit_hour_12(b, dt, pad = false); return 1
|
|
||||||
case:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_month_abbr :: proc(b: ^strings.Builder, dt: common.Date_Components) {
|
|
||||||
name := fmt.tprintf("%s", time.Month(dt.month))
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_weekday :: proc(b: ^strings.Builder, dt: common.Date_Components, full: bool) {
|
|
||||||
date := datetime.Date {
|
|
||||||
year = i64(dt.year),
|
|
||||||
month = i8(dt.month),
|
|
||||||
day = i8(dt.day),
|
|
||||||
}
|
|
||||||
ordinal, err := datetime.date_to_ordinal(date)
|
|
||||||
if err != .None {
|
|
||||||
strings.write_string(b, "???")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
weekday := datetime.day_of_week(ordinal)
|
|
||||||
name := fmt.tprintf("%s", weekday)
|
|
||||||
if full {
|
|
||||||
strings.write_string(b, name)
|
|
||||||
} else {
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_hour_12 :: proc(b: ^strings.Builder, dt: common.Date_Components, pad: bool) {
|
|
||||||
h12 := dt.hour % 12
|
|
||||||
if h12 == 0 {h12 = 12}
|
|
||||||
format := "%02d" if pad else "%d"
|
|
||||||
|
|
||||||
fmt.sbprintf(b, format, h12)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package original
|
|
||||||
|
|
||||||
import "../common"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
import "core:time/datetime"
|
|
||||||
|
|
||||||
format_date :: proc(
|
|
||||||
dt: common.Date_Components,
|
|
||||||
fmt: string,
|
|
||||||
allocator := context.temp_allocator,
|
|
||||||
) -> string {
|
|
||||||
b: strings.Builder
|
|
||||||
strings.builder_init(&b, allocator)
|
|
||||||
|
|
||||||
for i := 0; i < len(fmt); {
|
|
||||||
matched := match_token(&b, dt, fmt[i:])
|
|
||||||
if matched > 0 {
|
|
||||||
i += matched
|
|
||||||
} else {
|
|
||||||
strings.write_byte(&b, fmt[i])
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debugf("formatted date: '%s'", b.buf)
|
|
||||||
return strings.to_string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
match_token :: proc(b: ^strings.Builder, dt: common.Date_Components, s: string) -> int {
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"January",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%s", time.Month(dt.month))); return 7}
|
|
||||||
if strings.has_prefix(s, "Monday") {emit_weekday(b, dt, full = true); return 6}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"2006",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
|
||||||
if strings.has_prefix(s, "MST") {strings.write_string(b, "UTC"); return 3}
|
|
||||||
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
|
||||||
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"06",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.year % 100)); return 2}
|
|
||||||
if strings.has_prefix(s, "02") {strings.write_string(b, fmt.tprintf("%02d", dt.day)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"15",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.hour)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"04",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.minute)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"05",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.second)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"01",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.month)); return 2}
|
|
||||||
if strings.has_prefix(s, "03") {emit_hour_12(b, dt, pad = true); return 2}
|
|
||||||
if strings.has_prefix(s, "PM") {emit_am_pm(b, dt); return 2}
|
|
||||||
if strings.has_prefix(s, "pm") {emit_am_pm_lower(b, dt); return 2}
|
|
||||||
if len(s) >= 1 {
|
|
||||||
switch s[0] {
|
|
||||||
case '2':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.day)); return 1
|
|
||||||
case '1':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.month)); return 1
|
|
||||||
case '4':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.minute)); return 1
|
|
||||||
case '5':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.second)); return 1
|
|
||||||
case '3':
|
|
||||||
emit_hour_12(b, dt, pad = false); return 1
|
|
||||||
case:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_month_abbr :: proc(b: ^strings.Builder, dt: common.Date_Components) {
|
|
||||||
name := fmt.tprintf("%s", time.Month(dt.month))
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_weekday :: proc(b: ^strings.Builder, dt: common.Date_Components, full: bool) {
|
|
||||||
date := datetime.Date {
|
|
||||||
year = i64(dt.year),
|
|
||||||
month = i8(dt.month),
|
|
||||||
day = i8(dt.day),
|
|
||||||
}
|
|
||||||
ordinal, err := datetime.date_to_ordinal(date)
|
|
||||||
if err != .None {
|
|
||||||
strings.write_string(b, "???")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
weekday := datetime.day_of_week(ordinal)
|
|
||||||
name := fmt.tprintf("%s", weekday)
|
|
||||||
if full {
|
|
||||||
strings.write_string(b, name)
|
|
||||||
} else {
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_hour_12 :: proc(b: ^strings.Builder, dt: common.Date_Components, pad: bool) {
|
|
||||||
h12 := dt.hour % 12
|
|
||||||
if h12 == 0 {h12 = 12}
|
|
||||||
format := "%02d" if pad else "%d"
|
|
||||||
|
|
||||||
fmt.sbprintf(b, format, h12)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_am_pm :: proc(b: ^strings.Builder, dt: common.Date_Components) {
|
|
||||||
strings.write_string(b, "PM" if dt.hour >= 12 else "AM")
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_am_pm_lower :: proc(b: ^strings.Builder, dt: common.Date_Components) {
|
|
||||||
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
Date_Components :: struct {
|
|
||||||
year: int,
|
|
||||||
month: int,
|
|
||||||
day: int,
|
|
||||||
hour: int,
|
|
||||||
minute: int,
|
|
||||||
second: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use some kind of scanner interface
|
|
||||||
parse_iso_date :: proc(iso: string) -> (c: Date_Components, ok: bool) {
|
|
||||||
if len(iso) < 10 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
c.year = parse_2_digits(iso, 0) * 100 + parse_2_digits(iso, 2)
|
|
||||||
c.month = parse_2_digits(iso, 5)
|
|
||||||
c.day = parse_2_digits(iso, 8)
|
|
||||||
|
|
||||||
if c.month < 1 || c.month > 12 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
if c.day < 1 || c.day > 31 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(iso) >= 19 && (iso[10] == 'T' || iso[10] == 't') {
|
|
||||||
c.hour = parse_2_digits(iso, 11)
|
|
||||||
c.minute = parse_2_digits(iso, 14)
|
|
||||||
c.second = parse_2_digits(iso, 17)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, true
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_2_digits :: proc(s: string, offset: int) -> int {
|
|
||||||
if offset + 1 >= len(s) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (int(s[offset]) - 0x30) * 10 + (int(s[offset + 1]) - 0x30)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
package returned
|
|
||||||
|
|
||||||
import "../common"
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
import "core:time/datetime"
|
|
||||||
|
|
||||||
format_date :: proc(
|
|
||||||
dt: common.Date_Components,
|
|
||||||
fmt: string,
|
|
||||||
allocator := context.temp_allocator,
|
|
||||||
) -> string {
|
|
||||||
b: strings.Builder
|
|
||||||
// strings.builder_init_len(&b, len(fmt), allocator)
|
|
||||||
strings.builder_init(&b, allocator)
|
|
||||||
|
|
||||||
for i := 0; i < len(fmt); {
|
|
||||||
matched := match_token(&b, dt, fmt[i:])
|
|
||||||
if matched > 0 {
|
|
||||||
i += matched
|
|
||||||
} else {
|
|
||||||
strings.write_byte(&b, fmt[i])
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debugf("formatted date: '%s'", b.buf)
|
|
||||||
return strings.to_string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
match_token :: proc(b: ^strings.Builder, dt: common.Date_Components, s: string) -> int {
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"January",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%s", time.Month(dt.month))); return 7}
|
|
||||||
if strings.has_prefix(s, "Monday") {emit_weekday(b, dt, full = true); return 6}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"2006",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
|
||||||
if strings.has_prefix(s, "MST") {strings.write_string(b, "UTC"); return 3}
|
|
||||||
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
|
||||||
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"06",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.year % 100)); return 2}
|
|
||||||
if strings.has_prefix(s, "02") {strings.write_string(b, fmt.tprintf("%02d", dt.day)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"15",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.hour)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"04",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.minute)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"05",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.second)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"01",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.month)); return 2}
|
|
||||||
if strings.has_prefix(s, "03") {emit_hour_12(b, dt, pad = true); return 2}
|
|
||||||
if strings.has_prefix(s, "PM") {
|
|
||||||
strings.write_string(b, "PM" if dt.hour >= 12 else "AM")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if strings.has_prefix(s, "pm") {
|
|
||||||
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if len(s) >= 1 {
|
|
||||||
switch s[0] {
|
|
||||||
case '2':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.day)); return 1
|
|
||||||
case '1':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.month)); return 1
|
|
||||||
case '4':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.minute)); return 1
|
|
||||||
case '5':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.second)); return 1
|
|
||||||
case '3':
|
|
||||||
emit_hour_12(b, dt, pad = false); return 1
|
|
||||||
case:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_month_abbr :: proc(b: ^strings.Builder, dt: common.Date_Components) {
|
|
||||||
name := fmt.tprintf("%s", time.Month(dt.month))
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_weekday :: proc(b: ^strings.Builder, dt: common.Date_Components, full: bool) {
|
|
||||||
date := datetime.Date {
|
|
||||||
year = i64(dt.year),
|
|
||||||
month = i8(dt.month),
|
|
||||||
day = i8(dt.day),
|
|
||||||
}
|
|
||||||
ordinal, err := datetime.date_to_ordinal(date)
|
|
||||||
if err != .None {
|
|
||||||
strings.write_string(b, "???")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
weekday := datetime.day_of_week(ordinal)
|
|
||||||
name := fmt.tprintf("%s", weekday)
|
|
||||||
if full {
|
|
||||||
strings.write_string(b, name)
|
|
||||||
} else {
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_hour_12 :: proc(b: ^strings.Builder, dt: common.Date_Components, pad: bool) {
|
|
||||||
h12 := dt.hour % 12
|
|
||||||
if h12 == 0 {h12 = 12}
|
|
||||||
format := "%02d" if pad else "%d"
|
|
||||||
|
|
||||||
fmt.sbprintf(b, format, h12)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -149,8 +149,6 @@
|
|||||||
cmark
|
cmark
|
||||||
tree-sitter
|
tree-sitter
|
||||||
|
|
||||||
gdb
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
unstable.helix
|
unstable.helix
|
||||||
typescript-language-server
|
typescript-language-server
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:prof/spall"
|
import "core:prof/spall"
|
||||||
|
import "core:strings"
|
||||||
import "core:sync"
|
import "core:sync"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
|
|
||||||
@@ -32,9 +34,7 @@ main :: proc() {
|
|||||||
defer spall.buffer_destroy(&spall_ctx, &spall_buffer)
|
defer spall.buffer_destroy(&spall_ctx, &spall_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger_opts: log.Options =
|
console_logger := log.create_console_logger()
|
||||||
(log.Default_Console_Logger_Opts - log.Full_Timestamp_Opts - {.Short_File_Path})
|
|
||||||
console_logger := log.create_console_logger(.Info, logger_opts)
|
|
||||||
context.logger = console_logger
|
context.logger = console_logger
|
||||||
defer log.destroy_console_logger(console_logger)
|
defer log.destroy_console_logger(console_logger)
|
||||||
|
|
||||||
|
|||||||
@@ -56,16 +56,13 @@ Errors (returned as `Data_Error` at render time):
|
|||||||
- Any element is missing the named field.
|
- Any element is missing the named field.
|
||||||
- Any element has an empty value for the named field.
|
- Any element has an empty value for the named field.
|
||||||
|
|
||||||
#### `format`
|
#### `format` (no args yet)
|
||||||
|
|
||||||
Formats an ISO 8601 date string as a display string. Takes a string, returns a string (e.g. `"2026-03-15T08:49:54-04:00"` → `"15 Mar 2026"`). Invalid input (empty, too-short, non-string, or unparseable) returns a `Data_Error`. Templates that need to skip dateless pages should gate with a section — `{{#date}}<time datetime="{{.}}">{{. | format}}</time>{{/date}}` — so the section's truthiness check catches empty before the filter runs. Commonly used inline as `{{date | format}}` to render a display string while keeping the raw ISO available via `{{date}}` for the `datetime=` attribute.
|
Formats an ISO 8601 date string as a display string. Takes a string, returns a string (e.g. `"2026-03-15T08:49:54-04:00"` → `"15 Mar 2026"`). Invalid input (empty, too-short, non-string, or unparseable) returns a `Data_Error`. Templates that need to skip dateless pages should gate with a section — `{{#date}}<time datetime="{{.}}">{{. | format}}</time>{{/date}}` — so the section's truthiness check catches empty before the filter runs. Commonly used inline as `{{date | format}}` to render a display string while keeping the raw ISO available via `{{date}}` for the `datetime=` attribute.
|
||||||
|
|
||||||
Internally: parses the invariant `YYYY-MM-DD` prefix by char offset, stringifies `time.Month(month_num)` and slices `[:3]` for the abbreviation. Accepts any of these ISO 8601 forms (the date prefix is what matters): `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`.
|
Internally: parses the invariant `YYYY-MM-DD` prefix by char offset, stringifies `time.Month(month_num)` and slices `[:3]` for the abbreviation. Accepts any of these ISO 8601 forms (the date prefix is what matters): `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`.
|
||||||
|
|
||||||
Takes an optional arg for the Go reference-date layout to use:
|
Future: will accept Go reference-date format strings (e.g. `{{date | format "Mon Jan 2 2006"}}`) and pull default format/timezone from site configuration.
|
||||||
- A double-quoted literal, spaces allowed: `{{date | format "Mon Jan 2 2006"}}`.
|
|
||||||
- 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`).
|
|
||||||
|
|
||||||
### Memory ownership
|
### Memory ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
package mustache
|
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:time"
|
|
||||||
import "core:time/datetime"
|
|
||||||
|
|
||||||
Date_Components :: struct {
|
|
||||||
year: int,
|
|
||||||
month: int,
|
|
||||||
day: int,
|
|
||||||
hour: int,
|
|
||||||
minute: int,
|
|
||||||
second: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use some kind of scanner interface
|
|
||||||
parse_iso_date :: proc(iso: string) -> (c: Date_Components, ok: bool) {
|
|
||||||
if len(iso) < 10 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
c.year = parse_2_digits(iso, 0) * 100 + parse_2_digits(iso, 2)
|
|
||||||
c.month = parse_2_digits(iso, 5)
|
|
||||||
c.day = parse_2_digits(iso, 8)
|
|
||||||
|
|
||||||
if c.month < 1 || c.month > 12 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
if c.day < 1 || c.day > 31 {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(iso) >= 19 && (iso[10] == 'T' || iso[10] == 't') {
|
|
||||||
c.hour = parse_2_digits(iso, 11)
|
|
||||||
c.minute = parse_2_digits(iso, 14)
|
|
||||||
c.second = parse_2_digits(iso, 17)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, true
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_2_digits :: proc(s: string, offset: int) -> int {
|
|
||||||
if offset + 1 >= len(s) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (int(s[offset]) - 0x30) * 10 + (int(s[offset + 1]) - 0x30)
|
|
||||||
}
|
|
||||||
|
|
||||||
format_date :: proc(
|
|
||||||
dt: Date_Components,
|
|
||||||
fmt: string,
|
|
||||||
allocator := context.temp_allocator,
|
|
||||||
) -> string {
|
|
||||||
b: strings.Builder
|
|
||||||
strings.builder_init(&b, allocator)
|
|
||||||
|
|
||||||
for i := 0; i < len(fmt); {
|
|
||||||
matched := match_token(&b, dt, fmt[i:])
|
|
||||||
if matched > 0 {
|
|
||||||
i += matched
|
|
||||||
} else {
|
|
||||||
strings.write_byte(&b, fmt[i])
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debugf("formatted date: '%s'", b.buf)
|
|
||||||
return strings.to_string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
match_token :: proc(b: ^strings.Builder, dt: Date_Components, s: string) -> int {
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"January",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%s", time.Month(dt.month))); return 7}
|
|
||||||
if strings.has_prefix(s, "Monday") {emit_weekday(b, dt, full = true); return 6}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"2006",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%04d", dt.year)); return 4}
|
|
||||||
if strings.has_prefix(s, "MST") {strings.write_string(b, "UTC"); return 3}
|
|
||||||
if strings.has_prefix(s, "Jan") {emit_month_abbr(b, dt); return 3}
|
|
||||||
if strings.has_prefix(s, "Mon") {emit_weekday(b, dt, full = false); return 3}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"06",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.year % 100)); return 2}
|
|
||||||
if strings.has_prefix(s, "02") {strings.write_string(b, fmt.tprintf("%02d", dt.day)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"15",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.hour)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"04",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.minute)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"05",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.second)); return 2}
|
|
||||||
if strings.has_prefix(
|
|
||||||
s,
|
|
||||||
"01",
|
|
||||||
) {strings.write_string(b, fmt.tprintf("%02d", dt.month)); return 2}
|
|
||||||
if strings.has_prefix(s, "03") {emit_hour_12(b, dt, pad = true); return 2}
|
|
||||||
if strings.has_prefix(s, "PM") {emit_am_pm(b, dt); return 2}
|
|
||||||
if strings.has_prefix(s, "pm") {emit_am_pm_lower(b, dt); return 2}
|
|
||||||
if len(s) >= 1 {
|
|
||||||
switch s[0] {
|
|
||||||
case '2':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.day)); return 1
|
|
||||||
case '1':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.month)); return 1
|
|
||||||
case '4':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.minute)); return 1
|
|
||||||
case '5':
|
|
||||||
strings.write_string(b, fmt.tprintf("%d", dt.second)); return 1
|
|
||||||
case '3':
|
|
||||||
emit_hour_12(b, dt, pad = false); return 1
|
|
||||||
case:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_month_abbr :: proc(b: ^strings.Builder, dt: Date_Components) {
|
|
||||||
name := fmt.tprintf("%s", time.Month(dt.month))
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_weekday :: proc(b: ^strings.Builder, dt: Date_Components, full: bool) {
|
|
||||||
date := datetime.Date {
|
|
||||||
year = i64(dt.year),
|
|
||||||
month = i8(dt.month),
|
|
||||||
day = i8(dt.day),
|
|
||||||
}
|
|
||||||
ordinal, err := datetime.date_to_ordinal(date)
|
|
||||||
if err != .None {
|
|
||||||
strings.write_string(b, "???")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
weekday := datetime.day_of_week(ordinal)
|
|
||||||
name := fmt.tprintf("%s", weekday)
|
|
||||||
if full {
|
|
||||||
strings.write_string(b, name)
|
|
||||||
} else {
|
|
||||||
strings.write_string(b, name[:3 if len(name) >= 3 else len(name)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_hour_12 :: proc(b: ^strings.Builder, dt: Date_Components, pad: bool) {
|
|
||||||
h12 := dt.hour % 12
|
|
||||||
if h12 == 0 {h12 = 12}
|
|
||||||
format := "%02d" if pad else "%d"
|
|
||||||
|
|
||||||
fmt.sbprintf(b, format, h12)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_am_pm :: proc(b: ^strings.Builder, dt: Date_Components) {
|
|
||||||
strings.write_string(b, "PM" if dt.hour >= 12 else "AM")
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_am_pm_lower :: proc(b: ^strings.Builder, dt: Date_Components) {
|
|
||||||
strings.write_string(b, "pm" if dt.hour >= 12 else "am")
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
#+test
|
|
||||||
package mustache
|
|
||||||
|
|
||||||
import "core:testing"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// parse_iso_date
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_iso_date_extracts_time :: proc(t: ^testing.T) {
|
|
||||||
c, ok := parse_iso_date("2026-03-15T08:49:54-04:00")
|
|
||||||
testing.expect(t, ok, "should parse")
|
|
||||||
testing.expect_value(t, c.year, 2026)
|
|
||||||
testing.expect_value(t, c.month, 3)
|
|
||||||
testing.expect_value(t, c.day, 15)
|
|
||||||
testing.expect_value(t, c.hour, 8)
|
|
||||||
testing.expect_value(t, c.minute, 49)
|
|
||||||
testing.expect_value(t, c.second, 54)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_iso_date_lowercase_t :: proc(t: ^testing.T) {
|
|
||||||
c, ok := parse_iso_date("2026-03-15t08:49:54Z")
|
|
||||||
testing.expect(t, ok, "should parse lowercase t separator")
|
|
||||||
testing.expect_value(t, c.hour, 8)
|
|
||||||
testing.expect_value(t, c.minute, 49)
|
|
||||||
testing.expect_value(t, c.second, 54)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_iso_date_date_only_zero_time :: proc(t: ^testing.T) {
|
|
||||||
c, ok := parse_iso_date("2026-03-15")
|
|
||||||
testing.expect(t, ok, "should parse date-only")
|
|
||||||
testing.expect_value(t, c.hour, 0)
|
|
||||||
testing.expect_value(t, c.minute, 0)
|
|
||||||
testing.expect_value(t, c.second, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_iso_date_invalid_day_errors :: proc(t: ^testing.T) {
|
|
||||||
_, ok := parse_iso_date("2026-03-32")
|
|
||||||
testing.expect(t, !ok, "day > 31 should fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_iso_date_too_short_errors :: proc(t: ^testing.T) {
|
|
||||||
_, ok := parse_iso_date("2026-03")
|
|
||||||
testing.expect(t, !ok, "input shorter than 10 chars should fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// format_date / match_token
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_weekday_full :: proc(t: ^testing.T) {
|
|
||||||
// 2026-01-01 is a Thursday.
|
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1}
|
|
||||||
result := format_date(dt, "Monday")
|
|
||||||
testing.expect_value(t, result, "Thursday")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_weekday_abbr :: proc(t: ^testing.T) {
|
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1}
|
|
||||||
result := format_date(dt, "Mon")
|
|
||||||
testing.expect_value(t, result, "Thu")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_month_full_name :: proc(t: ^testing.T) {
|
|
||||||
dt := Date_Components{year = 2026, month = 3, day = 15}
|
|
||||||
result := format_date(dt, "January")
|
|
||||||
testing.expect_value(t, result, "March")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_two_digit_year :: proc(t: ^testing.T) {
|
|
||||||
dt := Date_Components{year = 2026, month = 3, day = 15}
|
|
||||||
result := format_date(dt, "06")
|
|
||||||
testing.expect_value(t, result, "26")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_hour24_padded :: proc(t: ^testing.T) {
|
|
||||||
midnight := Date_Components{year = 2026, month = 1, day = 1, hour = 0}
|
|
||||||
testing.expect_value(t, format_date(midnight, "15"), "00")
|
|
||||||
|
|
||||||
afternoon := Date_Components{year = 2026, month = 1, day = 1, hour = 13}
|
|
||||||
testing.expect_value(t, format_date(afternoon, "15"), "13")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_hour12_padded_am_pm_boundaries :: proc(t: ^testing.T) {
|
|
||||||
cases := [4]struct {
|
|
||||||
hour: int,
|
|
||||||
expected: string,
|
|
||||||
}{{0, "12 AM"}, {12, "12 PM"}, {13, "01 PM"}, {23, "11 PM"}}
|
|
||||||
|
|
||||||
for &c in cases {
|
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1, hour = c.hour}
|
|
||||||
result := format_date(dt, "03 PM")
|
|
||||||
testing.expect_value(t, result, c.expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_hour12_unpadded :: proc(t: ^testing.T) {
|
|
||||||
one_am := Date_Components{year = 2026, month = 1, day = 1, hour = 1}
|
|
||||||
testing.expect_value(t, format_date(one_am, "3"), "1")
|
|
||||||
|
|
||||||
one_pm := Date_Components{year = 2026, month = 1, day = 1, hour = 13}
|
|
||||||
testing.expect_value(t, format_date(one_pm, "3"), "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_am_pm_lowercase :: proc(t: ^testing.T) {
|
|
||||||
afternoon := Date_Components{year = 2026, month = 1, day = 1, hour = 13}
|
|
||||||
testing.expect_value(t, format_date(afternoon, "pm"), "pm")
|
|
||||||
|
|
||||||
morning := Date_Components{year = 2026, month = 1, day = 1, hour = 9}
|
|
||||||
testing.expect_value(t, format_date(morning, "pm"), "am")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_minute_second_padding :: proc(t: ^testing.T) {
|
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1, minute = 4, second = 5}
|
|
||||||
testing.expect_value(t, format_date(dt, "04:05"), "04:05")
|
|
||||||
testing.expect_value(t, format_date(dt, "4:5"), "4:5")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_month_day_numeric_padding :: proc(t: ^testing.T) {
|
|
||||||
dt := Date_Components{year = 2026, month = 3, day = 5}
|
|
||||||
testing.expect_value(t, format_date(dt, "01"), "03")
|
|
||||||
testing.expect_value(t, format_date(dt, "1"), "3")
|
|
||||||
testing.expect_value(t, format_date(dt, "02"), "05")
|
|
||||||
testing.expect_value(t, format_date(dt, "2"), "5")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_mst_always_utc :: proc(t: ^testing.T) {
|
|
||||||
// Date_Components carries no offset yet, so MST is a hardcoded
|
|
||||||
// placeholder until real timezone support lands.
|
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1, hour = 12}
|
|
||||||
result := format_date(dt, "MST")
|
|
||||||
testing.expect_value(t, result, "UTC")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_literal_passthrough :: proc(t: ^testing.T) {
|
|
||||||
dt := Date_Components{year = 2026, month = 1, day = 1}
|
|
||||||
result := format_date(dt, "Year: 2006!")
|
|
||||||
testing.expect_value(t, result, "Year: 2026!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_date_combined_go_reference_layout :: proc(t: ^testing.T) {
|
|
||||||
// 2023-10-15 is a Sunday.
|
|
||||||
dt := Date_Components{year = 2023, month = 10, day = 15, hour = 13, minute = 18, second = 50}
|
|
||||||
result := format_date(dt, "Mon Jan 2 15:04:05 MST 2006")
|
|
||||||
testing.expect_value(t, result, "Sun Oct 15 13:18:50 UTC 2023")
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package mustache
|
|||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:reflect"
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -557,7 +556,7 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:])
|
transformed, perr := apply_pipeline(val, node.filters[:], node.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return perr
|
return perr
|
||||||
}
|
}
|
||||||
@@ -599,7 +598,7 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:])
|
transformed, perr := apply_pipeline(val, node.filters[:], node.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return perr
|
return perr
|
||||||
}
|
}
|
||||||
@@ -637,7 +636,7 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:])
|
transformed, perr := apply_pipeline(val, node.filters[:], node.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return perr
|
return perr
|
||||||
}
|
}
|
||||||
@@ -693,7 +692,7 @@ render_nodes :: proc(
|
|||||||
warn_unknown_key(current, ctx[:], node)
|
warn_unknown_key(current, ctx[:], node)
|
||||||
}
|
}
|
||||||
if len(node.filters) > 0 {
|
if len(node.filters) > 0 {
|
||||||
transformed, perr := apply_pipeline(val, node.filters[:], node.pos, ctx[:])
|
transformed, perr := apply_pipeline(val, node.filters[:], node.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return perr
|
return perr
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-145
@@ -1,7 +1,6 @@
|
|||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
|
||||||
import "core:reflect"
|
import "core:reflect"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
import "core:time"
|
||||||
@@ -13,8 +12,6 @@ MAX_PIPES :: 8
|
|||||||
// No filter accepts more than 2 args.
|
// No filter accepts more than 2 args.
|
||||||
MAX_PIPE_ARGS :: 2
|
MAX_PIPE_ARGS :: 2
|
||||||
|
|
||||||
DEFAULT_DATE_FORMAT :: "2 Jan 2006"
|
|
||||||
|
|
||||||
Pipe_Filter :: struct {
|
Pipe_Filter :: struct {
|
||||||
op: string,
|
op: string,
|
||||||
args: [dynamic; MAX_PIPE_ARGS]string,
|
args: [dynamic; MAX_PIPE_ARGS]string,
|
||||||
@@ -25,51 +22,6 @@ Group :: struct {
|
|||||||
items: [dynamic]any,
|
items: [dynamic]any,
|
||||||
}
|
}
|
||||||
|
|
||||||
// is_pipe_space reports whether c is whitespace for the purposes of
|
|
||||||
// tokenizing a filter segment.
|
|
||||||
is_pipe_space :: proc(c: u8) -> bool {
|
|
||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenize_fields splits seg on whitespace like strings.fields, but a
|
|
||||||
// double-quoted span (spaces allowed inside) becomes a single token. The
|
|
||||||
// quote characters are kept in the token (not stripped) so callers can
|
|
||||||
// distinguish a quoted literal from a bare key name. No escape sequences.
|
|
||||||
tokenize_fields :: proc(seg: string, pos: int) -> (tokens: [dynamic]string, err: Error) {
|
|
||||||
i := 0
|
|
||||||
for i < len(seg) {
|
|
||||||
for i < len(seg) && is_pipe_space(seg[i]) {
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
if i >= len(seg) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if seg[i] == '"' {
|
|
||||||
start := i
|
|
||||||
j := i + 1
|
|
||||||
for j < len(seg) && seg[j] != '"' {
|
|
||||||
j += 1
|
|
||||||
}
|
|
||||||
if j >= len(seg) {
|
|
||||||
return tokens, Error_Body {
|
|
||||||
msg = fmt.tprintf("unterminated string literal: %s", seg),
|
|
||||||
pos = pos,
|
|
||||||
kind = .Syntax,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
append(&tokens, seg[start:j + 1])
|
|
||||||
i = j + 1
|
|
||||||
} else {
|
|
||||||
start := i
|
|
||||||
for i < len(seg) && !is_pipe_space(seg[i]) {
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
append(&tokens, seg[start:i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokens, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returned strings are slices into content — no cloning, lifetime bound to
|
// Returned strings are slices into content — no cloning, lifetime bound to
|
||||||
// the caller's source.
|
// the caller's source.
|
||||||
parse_pipeline :: proc(
|
parse_pipeline :: proc(
|
||||||
@@ -115,11 +67,7 @@ parse_pipeline :: proc(
|
|||||||
return "", Error_Body{msg = "empty filter", pos = pos, kind = .Syntax}
|
return "", Error_Body{msg = "empty filter", pos = pos, kind = .Syntax}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, terr := tokenize_fields(seg, pos)
|
tokens := strings.fields(seg)
|
||||||
if terr != nil {
|
|
||||||
delete(tokens)
|
|
||||||
return "", terr
|
|
||||||
}
|
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return "", Error_Body{msg = "filter missing op name", pos = pos, kind = .Syntax}
|
return "", Error_Body{msg = "filter missing op name", pos = pos, kind = .Syntax}
|
||||||
}
|
}
|
||||||
@@ -151,92 +99,35 @@ parse_pipeline :: proc(
|
|||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_pipeline :: proc(
|
apply_pipeline :: proc(value: any, filters: []Pipe_Filter, pos: int) -> (any, Error) {
|
||||||
value: any,
|
current := value
|
||||||
filters: []Pipe_Filter,
|
|
||||||
pos: int,
|
|
||||||
ctx: []any,
|
|
||||||
) -> (
|
|
||||||
current: any,
|
|
||||||
err: Error,
|
|
||||||
) {
|
|
||||||
current = value
|
|
||||||
for &filter in filters {
|
for &filter in filters {
|
||||||
current = apply_filter(current, &filter, pos, ctx) or_return
|
result, err := apply_filter(current, &filter, pos)
|
||||||
log.debugf("applied: filter=%v before=%s after=%s pos=%d", filter, value, current, pos)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
current = result
|
||||||
}
|
}
|
||||||
return
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve_format_string looks up name as a context key and returns its
|
apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int) -> (any, Error) {
|
||||||
// string value. Used both for an explicit bare-key filter arg (e.g.
|
|
||||||
// `format long`) and for the implicit "date_format" fallback when no arg
|
|
||||||
// is given.
|
|
||||||
resolve_format_string :: proc(name: string, ctx: []any, pos: int) -> (string, Error) {
|
|
||||||
raw := resolve_name(name, ctx)
|
|
||||||
if raw == nil {
|
|
||||||
return "", Error_Body {
|
|
||||||
msg = fmt.tprintf("unable to resolve date format key '%s'", name),
|
|
||||||
pos = pos,
|
|
||||||
kind = .Data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
str, ok := reflect.as_string(raw)
|
|
||||||
if !ok {
|
|
||||||
return "", Error_Body {
|
|
||||||
msg = fmt.tprintf("date format key '%s' is not a string", name),
|
|
||||||
pos = pos,
|
|
||||||
kind = .Data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: diagnostics don't show anything relevent
|
|
||||||
apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) -> (any, Error) {
|
|
||||||
switch filter.op {
|
switch filter.op {
|
||||||
case "group_by":
|
case "group_by":
|
||||||
return apply_group_by(value, filter.args[:], pos)
|
return apply_group_by(value, filter.args[:], pos)
|
||||||
case "format":
|
case "format":
|
||||||
str, ok := reflect.as_string(value)
|
str, ok := reflect.as_string(value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return value, Error_Body {
|
return value, Error_Body{
|
||||||
msg = "format may only be used on dates",
|
msg = "format may only be used on dates",
|
||||||
pos = pos,
|
pos = pos,
|
||||||
kind = .Data,
|
kind = .Data,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
date_format: string
|
|
||||||
|
|
||||||
if len(filter.args) > 0 {
|
|
||||||
arg := filter.args[0]
|
|
||||||
if len(arg) >= 2 && arg[0] == '"' && arg[len(arg) - 1] == '"' {
|
|
||||||
date_format = arg[1:len(arg) - 1]
|
|
||||||
} else {
|
|
||||||
df, ferr := resolve_format_string(arg, ctx, pos)
|
|
||||||
if ferr != nil {
|
|
||||||
return value, ferr
|
|
||||||
}
|
|
||||||
date_format = df
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
df, ferr := resolve_format_string("date_format", ctx, pos)
|
return apply_format(str, filter.args[:], pos)
|
||||||
if ferr != nil {
|
|
||||||
return value, ferr
|
|
||||||
}
|
|
||||||
date_format = df
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
case:
|
||||||
return nil, Error_Body {
|
return nil, Error_Body{
|
||||||
msg = fmt.tprintf("unknown pipe op '%s'", filter.op),
|
msg = fmt.tprintf("unknown pipe op '%s'", filter.op),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
kind = .Data,
|
kind = .Data,
|
||||||
@@ -244,40 +135,51 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_format :: proc(
|
// apply_format formats an ISO 8601 date string as a display string
|
||||||
iso: string,
|
// (e.g. "2026-03-15T08:49:54-04:00" → "15 Mar 2026"). Invalid input
|
||||||
args: []string,
|
// (empty, too-short, or unparseable) returns a Data-kind `Error`. Templates
|
||||||
pos: int,
|
// that need to skip dateless pages should gate with a section:
|
||||||
date_format: string,
|
// {{#date}}<time datetime="{{.}}">{{. | format}}</time>{{/date}}
|
||||||
) -> (
|
// The section's truthiness check catches empty before the filter runs.
|
||||||
result: string,
|
//
|
||||||
err: Error,
|
// Currently ignores args; planned to accept Go reference-date format
|
||||||
) {
|
// strings in the future.
|
||||||
fmt_str := date_format
|
//
|
||||||
if fmt_str == "" {
|
// Accepts any of these ISO 8601 forms (date prefix is invariant):
|
||||||
log.errorf(
|
// 2023-10-15T13:18:50-07:00
|
||||||
"format pipe used but no date format configured (set date.format in thor.json) Default will be used",
|
// 2023-10-15T13:18:50-0700
|
||||||
)
|
// 2023-10-15T13:18:50Z
|
||||||
fmt_str = DEFAULT_DATE_FORMAT
|
// 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
components, ok := parse_iso_date(iso)
|
year := iso[:4]
|
||||||
if !ok {
|
month_num := (int(iso[5]) - 0x30) * 10 + (int(iso[6]) - 0x30)
|
||||||
return "", Error_Body {
|
day_num := (int(iso[8]) - 0x30) * 10 + (int(iso[9]) - 0x30)
|
||||||
|
|
||||||
|
if month_num < 1 || month_num > 12 {
|
||||||
|
return nil, Error_Body{
|
||||||
msg = fmt.tprintf("invalid date: \"%s\"", iso),
|
msg = fmt.tprintf("invalid date: \"%s\"", iso),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
kind = .Data,
|
kind = .Data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debugf("date: '%s' format: '%s'", iso, date_format)
|
month := fmt.tprintf("%s", time.Month(month_num))[:3]
|
||||||
return format_date(components, fmt_str), nil
|
return fmt.tprintf("%d %s %s", day_num, month, year), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups preserve first-appearance order from the input list.
|
// Groups preserve first-appearance order from the input list.
|
||||||
apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, err: Error) {
|
apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, err: Error) {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return nil, Error_Body {
|
return nil, Error_Body{
|
||||||
msg = fmt.tprintf("group_by expects 1 argument, got %d", len(args)),
|
msg = fmt.tprintf("group_by expects 1 argument, got %d", len(args)),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
kind = .Data,
|
kind = .Data,
|
||||||
@@ -287,7 +189,11 @@ apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, er
|
|||||||
|
|
||||||
elem_info, count, data := list_info(value)
|
elem_info, count, data := list_info(value)
|
||||||
if elem_info == nil {
|
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)
|
groups := make([dynamic]Group, 0, 8, context.temp_allocator)
|
||||||
@@ -308,7 +214,7 @@ apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, er
|
|||||||
}
|
}
|
||||||
key_str := any_to_string(key_val)
|
key_str := any_to_string(key_val)
|
||||||
if len(key_str) == 0 {
|
if len(key_str) == 0 {
|
||||||
return nil, Error_Body {
|
return nil, Error_Body{
|
||||||
msg = fmt.tprintf("group_by: field '%s' is empty", field),
|
msg = fmt.tprintf("group_by: field '%s' is empty", field),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
kind = .Data,
|
kind = .Data,
|
||||||
|
|||||||
+4
-122
@@ -207,12 +207,10 @@ test_delete_template_doesnt_leak :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_interp_pipe_basic :: proc(t: ^testing.T) {
|
test_interp_pipe_basic :: proc(t: ^testing.T) {
|
||||||
Scalar_Data :: struct {
|
Scalar_Data :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
date_format: string,
|
|
||||||
}
|
}
|
||||||
data := Scalar_Data {
|
data := Scalar_Data {
|
||||||
name = "2026-03-15T08:49:54-04:00",
|
name = "2026-03-15T08:49:54-04:00",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("[{{name | format}}]", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("[{{name | format}}]", "<test>", allocator = context.temp_allocator)
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
@@ -222,12 +220,10 @@ 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,
|
|
||||||
}
|
}
|
||||||
data := Scalar_Data {
|
data := Scalar_Data {
|
||||||
name = "2025-12-25T00:00:00Z",
|
name = "2025-12-25T00:00:00Z",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("[{{&name | format}}]", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("[{{&name | format}}]", "<test>", allocator = context.temp_allocator)
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
@@ -237,12 +233,10 @@ test_interp_pipe_unescaped :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_interp_pipe_dot_current :: proc(t: ^testing.T) {
|
test_interp_pipe_dot_current :: proc(t: ^testing.T) {
|
||||||
List_Data :: struct {
|
List_Data :: struct {
|
||||||
items: [3]string,
|
items: [3]string,
|
||||||
date_format: string,
|
|
||||||
}
|
}
|
||||||
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"},
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("{{#items}}[{{. | format}}]{{/items}}", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("{{#items}}[{{. | format}}]{{/items}}", "<test>", allocator = context.temp_allocator)
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
@@ -254,15 +248,13 @@ test_interp_pipe_dot_current :: proc(t: ^testing.T) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Format_Data :: struct {
|
Format_Data :: struct {
|
||||||
date: string,
|
date: string,
|
||||||
date_format: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_format_typical_iso :: proc(t: ^testing.T) {
|
test_format_typical_iso :: proc(t: ^testing.T) {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "2026-03-15T08:49:54-04:00",
|
date = "2026-03-15T08:49:54-04:00",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -273,7 +265,6 @@ test_format_typical_iso :: proc(t: ^testing.T) {
|
|||||||
test_format_short_date_only :: proc(t: ^testing.T) {
|
test_format_short_date_only :: proc(t: ^testing.T) {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "2026-06-06",
|
date = "2026-06-06",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -284,7 +275,6 @@ test_format_short_date_only :: proc(t: ^testing.T) {
|
|||||||
test_format_empty_input_errors :: proc(t: ^testing.T) {
|
test_format_empty_input_errors :: proc(t: ^testing.T) {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "",
|
date = "",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("[{{date | format}}]", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("[{{date | format}}]", "<test>", allocator = context.temp_allocator)
|
||||||
defer delete_template(&tpl)
|
defer delete_template(&tpl)
|
||||||
@@ -296,7 +286,6 @@ test_format_empty_input_errors :: proc(t: ^testing.T) {
|
|||||||
test_format_non_date_string_errors :: proc(t: ^testing.T) {
|
test_format_non_date_string_errors :: proc(t: ^testing.T) {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "abc",
|
date = "abc",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("[{{date | format}}]", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("[{{date | format}}]", "<test>", allocator = context.temp_allocator)
|
||||||
defer delete_template(&tpl)
|
defer delete_template(&tpl)
|
||||||
@@ -322,7 +311,6 @@ test_format_non_string_value_errors :: proc(t: ^testing.T) {
|
|||||||
test_format_invalid_month_errors :: proc(t: ^testing.T) {
|
test_format_invalid_month_errors :: proc(t: ^testing.T) {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "2023-13-15",
|
date = "2023-13-15",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
||||||
defer delete_template(&tpl)
|
defer delete_template(&tpl)
|
||||||
@@ -336,7 +324,6 @@ test_format_inside_section_renders :: proc(t: ^testing.T) {
|
|||||||
// partial uses {{.}} for ISO attr and {{. | format}} for display.
|
// partial uses {{.}} for ISO attr and {{. | format}} for display.
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "2025-12-25T00:00:00Z",
|
date = "2025-12-25T00:00:00Z",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse(
|
tpl, _ := parse(
|
||||||
"{{#date}}<time datetime=\"{{.}}\">{{. | format}}</time>{{/date}}",
|
"{{#date}}<time datetime=\"{{.}}\">{{. | format}}</time>{{/date}}",
|
||||||
@@ -351,101 +338,12 @@ test_format_inside_section_renders :: proc(t: ^testing.T) {
|
|||||||
test_format_inside_section_skips_when_empty :: proc(t: ^testing.T) {
|
test_format_inside_section_skips_when_empty :: proc(t: ^testing.T) {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = "",
|
date = "",
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
tpl, _ := parse("[{{#date}}<time>{{. | format}}</time>{{/date}}]", "<test>", allocator = context.temp_allocator)
|
tpl, _ := parse("[{{#date}}<time>{{. | format}}</time>{{/date}}]", "<test>", allocator = context.temp_allocator)
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
result, _ := render(tpl, data, {}, context.temp_allocator)
|
||||||
testing.expect_value(t, result, "[]")
|
testing.expect_value(t, result, "[]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// format filter args: quoted literal strings and bare context keys
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_quoted_literal_arg :: proc(t: ^testing.T) {
|
|
||||||
data := Format_Data {
|
|
||||||
date = "2026-03-15T08:49:54-04:00",
|
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
|
||||||
tpl, _ := parse(`{{date | format "Jan 2, 2006"}}`, "<test>", allocator = context.temp_allocator)
|
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect_value(t, result, "Mar 15, 2026")
|
|
||||||
}
|
|
||||||
|
|
||||||
Key_Format_Data :: struct {
|
|
||||||
date: string,
|
|
||||||
long: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_bare_key_arg_resolves_from_context :: proc(t: ^testing.T) {
|
|
||||||
data := Key_Format_Data {
|
|
||||||
date = "2026-03-15T08:49:54-04:00",
|
|
||||||
long = "2 January 2006",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format long}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect_value(t, result, "15 March 2026")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_bare_key_arg_missing_errors :: proc(t: ^testing.T) {
|
|
||||||
data := Key_Format_Data {
|
|
||||||
date = "2026-03-15T08:49:54-04:00",
|
|
||||||
long = "2 January 2006",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format missing}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
defer delete_template(&tpl)
|
|
||||||
_, err := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect(t, err != nil, "unresolved format key should error")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_bare_key_arg_non_string_errors :: proc(t: ^testing.T) {
|
|
||||||
Int_Key_Data :: struct {
|
|
||||||
date: string,
|
|
||||||
count: int,
|
|
||||||
}
|
|
||||||
data := Int_Key_Data {
|
|
||||||
date = "2026-03-15T08:49:54-04:00",
|
|
||||||
count = 42,
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format count}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
defer delete_template(&tpl)
|
|
||||||
_, err := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect(t, err != nil, "non-string format key should error")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_unterminated_quote_arg_is_parse_error :: proc(t: ^testing.T) {
|
|
||||||
src := `{{date | format "Jan 2, 2006}}`
|
|
||||||
_, err := parse(src)
|
|
||||||
testing.expect(t, err != nil, "unterminated string literal should fail to parse")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_context_date_format_weekday :: proc(t: ^testing.T) {
|
|
||||||
data := Format_Data {
|
|
||||||
date = "2026-01-01T00:00:00Z",
|
|
||||||
date_format = "Monday, January 2, 2006",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect_value(t, result, "Thursday, January 1, 2026")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_context_date_format_time_of_day :: proc(t: ^testing.T) {
|
|
||||||
data := Format_Data {
|
|
||||||
date = "2026-01-01T15:30:00Z",
|
|
||||||
date_format = "3:04 PM",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
result, _ := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect_value(t, result, "3:30 PM")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_format_handles_all_iso8601_variants :: proc(t: ^testing.T) {
|
test_format_handles_all_iso8601_variants :: proc(t: ^testing.T) {
|
||||||
cases := [5]struct {
|
cases := [5]struct {
|
||||||
@@ -460,7 +358,6 @@ test_format_handles_all_iso8601_variants :: proc(t: ^testing.T) {
|
|||||||
for &c in cases {
|
for &c in cases {
|
||||||
data := Format_Data {
|
data := Format_Data {
|
||||||
date = c.input,
|
date = c.input,
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -468,18 +365,3 @@ test_format_handles_all_iso8601_variants :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_format_bare_numeric_arg_treated_as_key_not_literal :: proc(t: ^testing.T) {
|
|
||||||
// "2006" happens to also be a valid Go layout token — make sure an
|
|
||||||
// unquoted arg is still resolved as a context key (and fails, since
|
|
||||||
// no field is named "2006"), not silently used as the literal layout.
|
|
||||||
data := Format_Data {
|
|
||||||
date = "2026-03-15T08:49:54-04:00",
|
|
||||||
date_format = "2 Jan 2006",
|
|
||||||
}
|
|
||||||
tpl, _ := parse("{{date | format 2006}}", "<test>", allocator = context.temp_allocator)
|
|
||||||
defer delete_template(&tpl)
|
|
||||||
_, err := render(tpl, data, {}, context.temp_allocator)
|
|
||||||
testing.expect(t, err != nil, "bare numeric-looking arg should error as an unresolved key")
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-8
@@ -19,13 +19,12 @@ Page_Context :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Base_Data :: struct {
|
Base_Data :: struct {
|
||||||
now: string,
|
now: datetime.DateTime,
|
||||||
params: json.Value,
|
params: json.Value,
|
||||||
body: string,
|
body: string,
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
og: Open_Graph,
|
og: Open_Graph,
|
||||||
date_format: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Page_Data :: struct {
|
Page_Data :: struct {
|
||||||
@@ -145,7 +144,6 @@ render_template :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_site :: proc(site: ^Site) {
|
render_site :: proc(site: ^Site) {
|
||||||
allocator := site_allocator(site)
|
|
||||||
pages := site.pages[:]
|
pages := site.pages[:]
|
||||||
sort_pages_by_date(pages)
|
sort_pages_by_date(pages)
|
||||||
|
|
||||||
@@ -156,9 +154,7 @@ 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
|
now, ok := time.time_to_datetime(time.now())
|
||||||
offset := 0
|
|
||||||
now, ok := time.time_to_rfc3339(time.now(), offset, false, allocator)
|
|
||||||
assert(ok)
|
assert(ok)
|
||||||
|
|
||||||
// Build base data once
|
// Build base data once
|
||||||
@@ -167,7 +163,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find home page
|
// Find home page
|
||||||
@@ -261,7 +256,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
if !has_home {
|
if !has_home {
|
||||||
total += 1
|
total += 1
|
||||||
}
|
}
|
||||||
log.infof("Rendered %d pages to %s", total, site.output_dir)
|
fmt.printfln("Rendered %d pages to %s", total, site.output_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
render_page_html :: proc(
|
render_page_html :: proc(
|
||||||
|
|||||||
@@ -29,12 +29,6 @@ Site :: struct {
|
|||||||
features: bit_set[Feature],
|
features: bit_set[Feature],
|
||||||
markdown_extensions: bit_set[md.Extension],
|
markdown_extensions: bit_set[md.Extension],
|
||||||
og: Open_Graph,
|
og: Open_Graph,
|
||||||
date: Date_Preferences,
|
|
||||||
}
|
|
||||||
|
|
||||||
Date_Preferences :: struct {
|
|
||||||
format: string,
|
|
||||||
timezone: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Feature :: enum {
|
Feature :: enum {
|
||||||
@@ -57,7 +51,6 @@ Config_File :: struct {
|
|||||||
params: json.Value,
|
params: json.Value,
|
||||||
modules: json.Value,
|
modules: json.Value,
|
||||||
og: Open_Graph,
|
og: Open_Graph,
|
||||||
date: Date_Preferences,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration loaded from command line arguments. Gets folded in to Site
|
// Configuration loaded from command line arguments. Gets folded in to Site
|
||||||
@@ -170,7 +163,6 @@ site_apply_config :: proc(site: ^Site, config: Config_File, config_dir: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
site.og = config.og
|
site.og = config.og
|
||||||
site.date = config.date
|
|
||||||
}
|
}
|
||||||
|
|
||||||
site_apply_path_defaults :: proc(site: ^Site, config_dir: string) {
|
site_apply_path_defaults :: proc(site: ^Site, config_dir: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user