mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Added Go style date format configuration.
This commit is contained in:
@@ -13,6 +13,14 @@
|
||||
- [ ] 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`.
|
||||
- [ ] 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
|
||||
- [ ] Add overloads for every extension - accept ^strings.Builder.
|
||||
@@ -20,13 +28,20 @@
|
||||
- [ ] Add heading ids as a default on extension.
|
||||
- [ ] Add opt-in deflist support.
|
||||
- [ ] Decide if lambdas actually provide any value.
|
||||
- [ ] configure date format as a partial
|
||||
|
||||
## Dates
|
||||
- [ ] Accept "strings"
|
||||
- [ ] 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.
|
||||
|
||||
## General
|
||||
- [ ] Integrity hash
|
||||
- Allows users to verify their output didn't change after upgrading to a new version
|
||||
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
||||
- [ ] 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.
|
||||
- [ ] Add page params
|
||||
- [ ] We must remove all mention of `posts` from the odin code.
|
||||
@@ -46,6 +61,16 @@
|
||||
- [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.
|
||||
- [ ] Could be better error message when missing a closing (or opening) brace
|
||||
- [ ] Error message doesn't show position of faulty pipe name correctly.
|
||||
```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
|
||||
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
||||
- [ ] follow symlinks in `scan_content`?
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
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
|
||||
} \
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
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
|
||||
@@ -0,0 +1,46 @@
|
||||
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
|
||||
@@ -0,0 +1,126 @@
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
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,6 +149,8 @@
|
||||
cmark
|
||||
tree-sitter
|
||||
|
||||
gdb
|
||||
|
||||
# IDE
|
||||
unstable.helix
|
||||
typescript-language-server
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package main
|
||||
|
||||
import "base:runtime"
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:os"
|
||||
import "core:prof/spall"
|
||||
import "core:strings"
|
||||
import "core:sync"
|
||||
import "core:time"
|
||||
|
||||
@@ -36,7 +34,7 @@ main :: proc() {
|
||||
|
||||
logger_opts: log.Options =
|
||||
(log.Default_Console_Logger_Opts - log.Full_Timestamp_Opts - {.Short_File_Path})
|
||||
console_logger := log.create_console_logger(.Debug, logger_opts)
|
||||
console_logger := log.create_console_logger(.Info, logger_opts)
|
||||
context.logger = console_logger
|
||||
defer log.destroy_console_logger(console_logger)
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#+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")
|
||||
}
|
||||
+58
-38
@@ -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,9 +132,40 @@ 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 {
|
||||
msg = fmt.tprintf("unknown pipe op '%s'", filter.op),
|
||||
@@ -140,45 +175,34 @@ 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.
|
||||
@@ -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)
|
||||
|
||||
@@ -358,6 +358,28 @@ test_format_inside_section_skips_when_empty :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, result, "[]")
|
||||
}
|
||||
|
||||
@(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_format_handles_all_iso8601_variants :: proc(t: ^testing.T) {
|
||||
cases := [5]struct {
|
||||
|
||||
Reference in New Issue
Block a user