mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
refactor: Changed Render_Error from Union to struct.
Also renamed it to `Error`.
This commit is contained in:
@@ -307,26 +307,17 @@ write_gutter :: proc(sb: ^strings.Builder, width: int, faint: string, reset: str
|
|||||||
strings.write_byte(sb, '\n')
|
strings.write_byte(sb, '\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// format_render_error dispatches on Render_Error variant and produces a
|
// format_render_error produces a diagnostic for an Error value using the
|
||||||
// diagnostic for it. Returns "" for nil errors.
|
// template's path and source for context. Returns "" for nil errors.
|
||||||
format_render_error :: proc(err: Render_Error, tmpl: Template, colorize: bool = false) -> string {
|
format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false) -> string {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
switch e in err {
|
path := tmpl.path
|
||||||
case Syntax_Error:
|
if path == "" {
|
||||||
path := tmpl.path
|
path = "<input>"
|
||||||
if path == "" {
|
|
||||||
path = "<input>"
|
|
||||||
}
|
|
||||||
return format_error(path, tmpl.source, e.pos, e.msg, colorize = colorize)
|
|
||||||
case Data_Error:
|
|
||||||
path := tmpl.path
|
|
||||||
if path == "" {
|
|
||||||
path = "<input>"
|
|
||||||
}
|
|
||||||
return format_error(path, tmpl.source, e.pos, e.msg, colorize = colorize)
|
|
||||||
}
|
}
|
||||||
return ""
|
b := body(err)
|
||||||
|
return format_error(path, tmpl.source, b.pos, b.msg, colorize = colorize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -425,12 +425,10 @@ test_format_render_error_dispatch :: proc(t: ^testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
#partial switch e in parse_err {
|
b := body(parse_err)
|
||||||
case Syntax_Error:
|
out := format_error("test.html", src, b.pos, b.msg, colorize = false)
|
||||||
out := format_error("test.html", src, e.pos, e.msg, colorize = false)
|
testing.expect(t, strings.contains(out, "unclosed section"), out)
|
||||||
testing.expect(t, strings.contains(out, "unclosed section"), out)
|
testing.expect(t, strings.contains(out, "test.html:"), out)
|
||||||
testing.expect(t, strings.contains(out, "test.html:"), out)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -469,19 +467,17 @@ test_parse_error_expected_got_keeps_double_braces :: proc(t: ^testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#partial switch e in err {
|
b := body(err)
|
||||||
case Syntax_Error:
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{/content}}"),
|
||||||
strings.contains(e.msg, "{{/content}}"),
|
fmt.tprintf("msg should contain literal {{/content}}, got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal {{/content}}, got %q", e.msg),
|
)
|
||||||
)
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{/cotent}}"),
|
||||||
strings.contains(e.msg, "{{/cotent}}"),
|
fmt.tprintf("msg should contain literal {{/cotent}}, got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal {{/cotent}}, got %q", e.msg),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -492,14 +488,12 @@ test_parse_error_unclosed_section_keeps_double_braces :: proc(t: ^testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#partial switch e in err {
|
b := body(err)
|
||||||
case Syntax_Error:
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{#content}}"),
|
||||||
strings.contains(e.msg, "{{#content}}"),
|
fmt.tprintf("msg should contain literal {{#content}}, got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal {{#content}}, got %q", e.msg),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -510,14 +504,12 @@ test_parse_error_unexpected_close_keeps_double_braces :: proc(t: ^testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#partial switch e in err {
|
b := body(err)
|
||||||
case Syntax_Error:
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{/content}}"),
|
||||||
strings.contains(e.msg, "{{/content}}"),
|
fmt.tprintf("msg should contain literal {{/content}}, got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal {{/content}}, got %q", e.msg),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -528,14 +520,12 @@ test_parse_error_pipe_in_close_tag_keeps_double_braces :: proc(t: ^testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#partial switch e in err {
|
b := body(err)
|
||||||
case Syntax_Error:
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{/"),
|
||||||
strings.contains(e.msg, "{{/"),
|
fmt.tprintf("msg should contain literal '{{/', got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal '{{/', got %q", e.msg),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -546,14 +536,12 @@ test_parse_error_pipe_parse_in_section_keeps_double_braces :: proc(t: ^testing.T
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#partial switch e in err {
|
b := body(err)
|
||||||
case Syntax_Error:
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{#"),
|
||||||
strings.contains(e.msg, "{{#"),
|
fmt.tprintf("msg should contain literal '{{#', got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal '{{#', got %q", e.msg),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -564,13 +552,11 @@ test_parse_error_pipe_parse_in_inverted_keeps_double_braces :: proc(t: ^testing.
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#partial switch e in err {
|
b := body(err)
|
||||||
case Syntax_Error:
|
testing.expect(
|
||||||
testing.expect(
|
t,
|
||||||
t,
|
strings.contains(b.msg, "{{^"),
|
||||||
strings.contains(e.msg, "{{^"),
|
fmt.tprintf("msg should contain literal '{{^', got %q", b.msg),
|
||||||
fmt.tprintf("msg should contain literal '{{^', got %q", e.msg),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-30
@@ -8,20 +8,27 @@ import "core:strings"
|
|||||||
// Error types
|
// Error types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// TODO: I don't think this is the best way to do our errors.
|
Error_Kind :: enum {
|
||||||
Syntax_Error :: struct {
|
Syntax, // parse-time: malformed template
|
||||||
msg: string,
|
Data, // render-time: template fine, data wrong (e.g. filter misuse)
|
||||||
pos: int,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Data_Error :: struct {
|
Error_Body :: struct {
|
||||||
msg: string,
|
msg: string,
|
||||||
pos: int,
|
pos: int,
|
||||||
|
kind: Error_Kind,
|
||||||
}
|
}
|
||||||
|
|
||||||
Render_Error :: union {
|
// Error is nil when no error occurred.
|
||||||
Syntax_Error,
|
Error :: union { Error_Body }
|
||||||
Data_Error,
|
|
||||||
|
// body unwraps the Error_Body from a non-nil Error.
|
||||||
|
// Precondition: err != nil.
|
||||||
|
body :: proc(err: Error) -> Error_Body {
|
||||||
|
switch e in err {
|
||||||
|
case Error_Body: return e
|
||||||
|
case: return {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -101,7 +108,7 @@ parse :: proc(
|
|||||||
tokens_allocator := context.temp_allocator,
|
tokens_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
tmpl: Template,
|
tmpl: Template,
|
||||||
err: Render_Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
tokens, terr := tokenize(source, tokens_allocator)
|
tokens, terr := tokenize(source, tokens_allocator)
|
||||||
if terr != nil {
|
if terr != nil {
|
||||||
@@ -126,7 +133,7 @@ render :: proc(
|
|||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
) -> (
|
) -> (
|
||||||
result: string,
|
result: string,
|
||||||
err: Render_Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
builder: strings.Builder
|
builder: strings.Builder
|
||||||
strings.builder_init(&builder, allocator)
|
strings.builder_init(&builder, allocator)
|
||||||
@@ -158,7 +165,7 @@ parse_tokens :: proc(
|
|||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
) -> (
|
) -> (
|
||||||
nodes: [dynamic]Node,
|
nodes: [dynamic]Node,
|
||||||
err: Render_Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
nodes = make([dynamic]Node, 0, len(tokens), allocator)
|
nodes = make([dynamic]Node, 0, len(tokens), allocator)
|
||||||
pos := 0
|
pos := 0
|
||||||
@@ -174,7 +181,7 @@ parse_section :: proc(
|
|||||||
source: string,
|
source: string,
|
||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
open_pos: int = 0,
|
open_pos: int = 0,
|
||||||
) -> Render_Error {
|
) -> Error {
|
||||||
for pos^ < len(tokens) {
|
for pos^ < len(tokens) {
|
||||||
tok := tokens[pos^]
|
tok := tokens[pos^]
|
||||||
|
|
||||||
@@ -188,9 +195,10 @@ parse_section :: proc(
|
|||||||
append(nodes, Node{kind = .Variable, first_child = -1})
|
append(nodes, Node{kind = .Variable, first_child = -1})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{%s}}}}': %v", tok.value, perr),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes[idx].key = pipe_key
|
nodes[idx].key = pipe_key
|
||||||
@@ -201,9 +209,10 @@ parse_section :: proc(
|
|||||||
append(nodes, Node{kind = .Unescaped, first_child = -1})
|
append(nodes, Node{kind = .Unescaped, first_child = -1})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{&%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{&%s}}}}': %v", tok.value, perr),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes[idx].key = pipe_key
|
nodes[idx].key = pipe_key
|
||||||
@@ -220,9 +229,10 @@ parse_section :: proc(
|
|||||||
append(nodes, Node{kind = .Section, first_child = -1, pos = tok.pos})
|
append(nodes, Node{kind = .Section, first_child = -1, pos = tok.pos})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{#%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{#%s}}}}': %v", tok.value, perr),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes[idx].key = pipe_key
|
nodes[idx].key = pipe_key
|
||||||
@@ -241,9 +251,10 @@ parse_section :: proc(
|
|||||||
append(nodes, Node{kind = .Inverted, first_child = -1, pos = tok.pos})
|
append(nodes, Node{kind = .Inverted, first_child = -1, pos = tok.pos})
|
||||||
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
pipe_key, perr := parse_pipeline(tok.value, &nodes[idx].filters, tok.pos)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("pipe parse error in '{{{{^%s}}}}': %v", tok.value, perr),
|
msg = fmt.tprintf("pipe parse error in '{{{{^%s}}}}': %v", tok.value, perr),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes[idx].key = pipe_key
|
nodes[idx].key = pipe_key
|
||||||
@@ -256,27 +267,30 @@ parse_section :: proc(
|
|||||||
|
|
||||||
case .Section_Close:
|
case .Section_Close:
|
||||||
if strings.contains(tok.value, "|") {
|
if strings.contains(tok.value, "|") {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf(
|
msg = fmt.tprintf(
|
||||||
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
|
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
|
||||||
tok.value,
|
tok.value,
|
||||||
),
|
),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
}
|
kind = .Syntax,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if end_tag != "" && tok.value == end_tag {
|
if end_tag != "" && tok.value == end_tag {
|
||||||
pos^ += 1
|
pos^ += 1
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if end_tag == "" {
|
if end_tag == "" {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("unexpected {{{{/%s}}}}", tok.value),
|
msg = fmt.tprintf("unexpected {{{{/%s}}}}", tok.value),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("expected {{{{/%s}}}}, got {{{{/%s}}}}", end_tag, tok.value),
|
msg = fmt.tprintf("expected {{{{/%s}}}}, got {{{{/%s}}}}", end_tag, tok.value),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
|
|
||||||
case .Partial:
|
case .Partial:
|
||||||
@@ -330,9 +344,10 @@ parse_section :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if end_tag != "" {
|
if end_tag != "" {
|
||||||
return Syntax_Error {
|
return Error_Body {
|
||||||
msg = fmt.tprintf("unclosed section '{{{{#%s}}}}'", end_tag),
|
msg = fmt.tprintf("unclosed section '{{{{#%s}}}}'", end_tag),
|
||||||
pos = open_pos,
|
pos = open_pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -495,7 +510,7 @@ render_template :: proc(
|
|||||||
b: ^strings.Builder,
|
b: ^strings.Builder,
|
||||||
blocks: map[string]Block_Override,
|
blocks: map[string]Block_Override,
|
||||||
indent: string,
|
indent: string,
|
||||||
) -> Render_Error {
|
) -> Error {
|
||||||
if len(indent) > 0 && len(pt.source) > 0 {
|
if len(indent) > 0 && len(pt.source) > 0 {
|
||||||
// Per Mustache spec: the partial's source is indented before rendering,
|
// Per Mustache spec: the partial's source is indented before rendering,
|
||||||
// not its output. This is necessary so that data-injected newlines
|
// not its output. This is necessary so that data-injected newlines
|
||||||
@@ -525,7 +540,7 @@ render_nodes :: proc(
|
|||||||
partials: map[string]Template,
|
partials: map[string]Template,
|
||||||
b: ^strings.Builder,
|
b: ^strings.Builder,
|
||||||
blocks: map[string]Block_Override = nil,
|
blocks: map[string]Block_Override = nil,
|
||||||
) -> Render_Error {
|
) -> Error {
|
||||||
i := 0
|
i := 0
|
||||||
for i < len(nodes) {
|
for i < len(nodes) {
|
||||||
node := nodes[i]
|
node := nodes[i]
|
||||||
|
|||||||
+38
-19
@@ -30,7 +30,7 @@ parse_pipeline :: proc(
|
|||||||
pos: int,
|
pos: int,
|
||||||
) -> (
|
) -> (
|
||||||
key: string,
|
key: string,
|
||||||
err: Render_Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
if !strings.contains(content, "|") {
|
if !strings.contains(content, "|") {
|
||||||
key = strings.trim_space(content)
|
key = strings.trim_space(content)
|
||||||
@@ -41,19 +41,20 @@ parse_pipeline :: proc(
|
|||||||
|
|
||||||
filter_count := len(segments) - 1
|
filter_count := len(segments) - 1
|
||||||
if filter_count > MAX_PIPES {
|
if filter_count > MAX_PIPES {
|
||||||
return "", Syntax_Error {
|
return "", Error_Body {
|
||||||
msg = fmt.tprintf(
|
msg = fmt.tprintf(
|
||||||
"pipe expression has %d filters, max is %d",
|
"pipe expression has %d filters, max is %d",
|
||||||
filter_count,
|
filter_count,
|
||||||
MAX_PIPES,
|
MAX_PIPES,
|
||||||
),
|
),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
key = strings.trim_space(segments[0])
|
key = strings.trim_space(segments[0])
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return "", Syntax_Error{msg = "pipe expression missing key", pos = pos}
|
return "", Error_Body{msg = "pipe expression missing key", pos = pos, kind = .Syntax}
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter_count == 0 {
|
if filter_count == 0 {
|
||||||
@@ -63,17 +64,17 @@ parse_pipeline :: proc(
|
|||||||
for i in 0 ..< filter_count {
|
for i in 0 ..< filter_count {
|
||||||
seg := strings.trim_space(segments[i + 1])
|
seg := strings.trim_space(segments[i + 1])
|
||||||
if len(seg) == 0 {
|
if len(seg) == 0 {
|
||||||
return "", Syntax_Error{msg = "empty filter", pos = pos}
|
return "", Error_Body{msg = "empty filter", pos = pos, kind = .Syntax}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := strings.fields(seg)
|
tokens := strings.fields(seg)
|
||||||
if len(tokens) == 0 {
|
if len(tokens) == 0 {
|
||||||
return "", Syntax_Error{msg = "filter missing op name", pos = pos}
|
return "", Error_Body{msg = "filter missing op name", pos = pos, kind = .Syntax}
|
||||||
}
|
}
|
||||||
|
|
||||||
arg_count := len(tokens) - 1
|
arg_count := len(tokens) - 1
|
||||||
if arg_count > MAX_PIPE_ARGS {
|
if arg_count > MAX_PIPE_ARGS {
|
||||||
return "", Syntax_Error {
|
return "", Error_Body {
|
||||||
msg = fmt.tprintf(
|
msg = fmt.tprintf(
|
||||||
"filter '%s' has %d args, max is %d",
|
"filter '%s' has %d args, max is %d",
|
||||||
tokens[0],
|
tokens[0],
|
||||||
@@ -81,6 +82,7 @@ parse_pipeline :: proc(
|
|||||||
MAX_PIPE_ARGS,
|
MAX_PIPE_ARGS,
|
||||||
),
|
),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ parse_pipeline :: proc(
|
|||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_pipeline :: proc(value: any, filters: []Pipe_Filter, pos: int) -> (any, Render_Error) {
|
apply_pipeline :: proc(value: any, filters: []Pipe_Filter, pos: int) -> (any, Error) {
|
||||||
current := value
|
current := value
|
||||||
for &filter in filters {
|
for &filter in filters {
|
||||||
result, err := apply_filter(current, &filter, pos)
|
result, err := apply_filter(current, &filter, pos)
|
||||||
@@ -109,28 +111,33 @@ apply_pipeline :: proc(value: any, filters: []Pipe_Filter, pos: int) -> (any, Re
|
|||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int) -> (any, Render_Error) {
|
apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int) -> (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, Data_Error{msg = "format may only be used on dates", pos = pos}
|
return value, Error_Body{
|
||||||
|
msg = "format may only be used on dates",
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return apply_format(str, filter.args[:], pos)
|
return apply_format(str, filter.args[:], pos)
|
||||||
}
|
}
|
||||||
case:
|
case:
|
||||||
return nil, Data_Error{
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply_format formats an ISO 8601 date string as a display string
|
// 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
|
// (e.g. "2026-03-15T08:49:54-04:00" → "15 Mar 2026"). Invalid input
|
||||||
// (empty, too-short, or unparseable) returns a `Data_Error`. Templates
|
// (empty, too-short, or unparseable) returns a Data-kind `Error`. Templates
|
||||||
// that need to skip dateless pages should gate with a section:
|
// that need to skip dateless pages should gate with a section:
|
||||||
// {{#date}}<time datetime="{{.}}">{{. | format}}</time>{{/date}}
|
// {{#date}}<time datetime="{{.}}">{{. | format}}</time>{{/date}}
|
||||||
// The section's truthiness check catches empty before the filter runs.
|
// The section's truthiness check catches empty before the filter runs.
|
||||||
@@ -144,9 +151,13 @@ apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int) -> (any, Render
|
|||||||
// 2023-10-15T13:18:50Z
|
// 2023-10-15T13:18:50Z
|
||||||
// 2023-10-15T13:18:50
|
// 2023-10-15T13:18:50
|
||||||
// 2023-10-15
|
// 2023-10-15
|
||||||
apply_format :: proc(iso: string, args: []string, pos: int) -> (result: any, err: Render_Error) {
|
apply_format :: proc(iso: string, args: []string, pos: int) -> (result: any, err: Error) {
|
||||||
if len(iso) < 10 {
|
if len(iso) < 10 {
|
||||||
return nil, Data_Error{msg = "format may only be used on dates", pos = pos}
|
return nil, Error_Body{
|
||||||
|
msg = "format may only be used on dates",
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
year := iso[:4]
|
year := iso[:4]
|
||||||
@@ -154,7 +165,11 @@ apply_format :: proc(iso: string, args: []string, pos: int) -> (result: any, err
|
|||||||
day_num := (int(iso[8]) - 0x30) * 10 + (int(iso[9]) - 0x30)
|
day_num := (int(iso[8]) - 0x30) * 10 + (int(iso[9]) - 0x30)
|
||||||
|
|
||||||
if month_num < 1 || month_num > 12 {
|
if month_num < 1 || month_num > 12 {
|
||||||
return nil, Data_Error{msg = fmt.tprintf("invalid date: \"%s\"", iso), pos = pos}
|
return nil, Error_Body{
|
||||||
|
msg = fmt.tprintf("invalid date: \"%s\"", iso),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
month := fmt.tprintf("%s", time.Month(month_num))[:3]
|
month := fmt.tprintf("%s", time.Month(month_num))[:3]
|
||||||
@@ -162,20 +177,22 @@ apply_format :: proc(iso: string, args: []string, pos: int) -> (result: any, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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: Render_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, Data_Error{
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
field := args[0]
|
field := args[0]
|
||||||
|
|
||||||
elem_info, count, data := list_info(value)
|
elem_info, count, data := list_info(value)
|
||||||
if elem_info == nil {
|
if elem_info == nil {
|
||||||
return nil, Data_Error{
|
return nil, Error_Body{
|
||||||
msg = "group_by expects a list",
|
msg = "group_by expects a list",
|
||||||
pos = pos,
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,16 +206,18 @@ apply_group_by :: proc(value: any, args: []string, pos: int) -> (result: any, er
|
|||||||
|
|
||||||
key_val, found := lookup_in(elem, field)
|
key_val, found := lookup_in(elem, field)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, Data_Error {
|
return nil, Error_Body {
|
||||||
msg = fmt.tprintf("group_by: element missing field '%s'", field),
|
msg = fmt.tprintf("group_by: element missing field '%s'", field),
|
||||||
pos = pos,
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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, Data_Error{
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,13 +66,9 @@ test_pipe_group_by_missing_field_fails :: proc(t: ^testing.T) {
|
|||||||
defer delete_template(&tpl)
|
defer delete_template(&tpl)
|
||||||
_, err := render(tpl, data)
|
_, err := render(tpl, data)
|
||||||
testing.expect(t, err != nil, "missing field should error")
|
testing.expect(t, err != nil, "missing field should error")
|
||||||
|
b := body(err)
|
||||||
is_data_err := false
|
testing.expect(t, b.kind == .Data, "error should be Data kind")
|
||||||
#partial switch e in err {
|
testing.expect(t, len(b.msg) > 0, "error should have non-empty msg")
|
||||||
case Data_Error:
|
|
||||||
is_data_err = len(e.msg) > 0
|
|
||||||
}
|
|
||||||
testing.expect(t, is_data_err, "error should be Data_Error")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ tokenize :: proc(
|
|||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
) -> (
|
) -> (
|
||||||
tokens: [dynamic]Token,
|
tokens: [dynamic]Token,
|
||||||
err: Render_Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
tokens = make([dynamic]Token, 0, 8, allocator)
|
tokens = make([dynamic]Token, 0, 8, allocator)
|
||||||
|
|
||||||
@@ -47,9 +47,10 @@ tokenize :: proc(
|
|||||||
content_start := i + 3
|
content_start := i + 3
|
||||||
idx := strings.index(src[content_start:], "}}}")
|
idx := strings.index(src[content_start:], "}}}")
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return tokens, Syntax_Error {
|
return tokens, Error_Body {
|
||||||
msg = "unclosed triple mustache '{{{'",
|
msg = "unclosed triple mustache '{{{'",
|
||||||
pos = tag_pos,
|
pos = tag_pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close := content_start + idx
|
close := content_start + idx
|
||||||
@@ -89,9 +90,13 @@ tokenize :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
close_idx := strings.index(src[key_start:], "}}")
|
close_idx := strings.index(src[key_start:], "}}")
|
||||||
if close_idx < 0 {
|
if close_idx < 0 {
|
||||||
return tokens, Syntax_Error{msg = "unclosed tag '{{'", pos = tag_pos}
|
return tokens, Error_Body {
|
||||||
|
msg = "unclosed tag '{{'",
|
||||||
|
pos = tag_pos,
|
||||||
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
close := key_start + close_idx
|
close := key_start + close_idx
|
||||||
|
|
||||||
content := src[key_start:close]
|
content := src[key_start:close]
|
||||||
|
|||||||
+22
-50
@@ -63,31 +63,17 @@ load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
|||||||
source := string(data)
|
source := string(data)
|
||||||
tpl, err := mustache.parse(source, entry.fs_path)
|
tpl, err := mustache.parse(source, entry.fs_path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch e in err {
|
b := mustache.body(err)
|
||||||
// TODO: Find a way to merge these branches.
|
log.errorf(
|
||||||
case mustache.Syntax_Error:
|
"%s",
|
||||||
log.errorf(
|
mustache.format_error(
|
||||||
"%s",
|
entry.fs_path,
|
||||||
mustache.format_error(
|
source,
|
||||||
entry.fs_path,
|
b.pos,
|
||||||
source,
|
b.msg,
|
||||||
e.pos,
|
colorize = mustache.should_colorize(),
|
||||||
e.msg,
|
),
|
||||||
colorize = mustache.should_colorize(),
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
case mustache.Data_Error:
|
|
||||||
log.errorf(
|
|
||||||
"%s",
|
|
||||||
mustache.format_error(
|
|
||||||
entry.fs_path,
|
|
||||||
source,
|
|
||||||
e.pos,
|
|
||||||
e.msg,
|
|
||||||
colorize = mustache.should_colorize(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
}
|
}
|
||||||
return tpl
|
return tpl
|
||||||
@@ -375,31 +361,17 @@ load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template {
|
|||||||
source := string(data)
|
source := string(data)
|
||||||
tpl, err := mustache.parse(source, entry.fs_path)
|
tpl, err := mustache.parse(source, entry.fs_path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Find a way to merge these branches.
|
b := mustache.body(err)
|
||||||
switch e in err {
|
log.errorf(
|
||||||
case mustache.Syntax_Error:
|
"%s",
|
||||||
log.errorf(
|
mustache.format_error(
|
||||||
"%s",
|
entry.fs_path,
|
||||||
mustache.format_error(
|
source,
|
||||||
entry.fs_path,
|
b.pos,
|
||||||
source,
|
b.msg,
|
||||||
e.pos,
|
colorize = mustache.should_colorize(),
|
||||||
e.msg,
|
),
|
||||||
colorize = mustache.should_colorize(),
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
case mustache.Data_Error:
|
|
||||||
log.errorf(
|
|
||||||
"%s",
|
|
||||||
mustache.format_error(
|
|
||||||
entry.fs_path,
|
|
||||||
source,
|
|
||||||
e.pos,
|
|
||||||
e.msg,
|
|
||||||
colorize = mustache.should_colorize(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
}
|
}
|
||||||
partials[key] = tpl
|
partials[key] = tpl
|
||||||
|
|||||||
Reference in New Issue
Block a user