mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat(mustache): Added spec-compliant lambda support.
This commit is contained in:
@@ -155,6 +155,32 @@ is_truthy :: proc(a: any) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
call_interp_lambda :: proc(val: any) -> (result: string, ok: bool) {
|
||||
switch v in val {
|
||||
case proc() -> string:
|
||||
return v(), true
|
||||
case proc() -> int:
|
||||
return fmt.tprintf("%d", v()), true
|
||||
case proc() -> bool:
|
||||
return "true" if v() else "false", true
|
||||
case:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
call_section_lambda :: proc(val: any, text: string) -> (result: string, ok: bool) {
|
||||
switch v in val {
|
||||
case proc(string) -> string:
|
||||
return v(text), true
|
||||
case proc(string) -> int:
|
||||
return fmt.tprintf("%d", v(text)), true
|
||||
case proc(string) -> bool:
|
||||
return "true" if v(text) else "false", true
|
||||
case:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// list_info returns element type info, count, and data pointer for a list value.
|
||||
// Returns elem_info=nil if the value is not a list.
|
||||
list_info :: proc(a: any) -> (elem_info: ^runtime.Type_Info, count: int, data: rawptr) {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#+test
|
||||
package mustache
|
||||
|
||||
import "core:fmt"
|
||||
import "core:testing"
|
||||
|
||||
// --- Spec test 1: Interpolation ---
|
||||
// A lambda's return value should be interpolated.
|
||||
|
||||
Interp_Data :: struct {
|
||||
lambda: proc() -> string,
|
||||
planet: string,
|
||||
}
|
||||
|
||||
Interp_Data_Int :: struct {
|
||||
lambda: proc() -> int,
|
||||
planet: string,
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_interpolation :: proc(t: ^testing.T) {
|
||||
data := Interp_Data {
|
||||
lambda = proc() -> string {return "world"},
|
||||
}
|
||||
tpl, _ := parse("Hello, {{lambda}}!", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "Hello, world!")
|
||||
}
|
||||
|
||||
// --- Spec test 2: Interpolation - Expansion ---
|
||||
// A lambda's return value should be parsed.
|
||||
|
||||
@(test)
|
||||
test_lambda_interpolation_expansion :: proc(t: ^testing.T) {
|
||||
data := Interp_Data {
|
||||
lambda = proc() -> string {return "{{planet}}"},
|
||||
planet = "world",
|
||||
}
|
||||
tpl, _ := parse("Hello, {{lambda}}!", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "Hello, world!")
|
||||
}
|
||||
|
||||
// --- Spec test 4: Interpolation - Multiple Calls ---
|
||||
// Interpolated lambdas should not be cached.
|
||||
|
||||
counter_lambda :: proc() -> int {
|
||||
@(static) call_count := 0
|
||||
call_count += 1
|
||||
return call_count
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_interpolation_multiple_calls :: proc(t: ^testing.T) {
|
||||
data := Interp_Data_Int {
|
||||
lambda = counter_lambda,
|
||||
}
|
||||
tpl, _ := parse("{{lambda}} == {{{lambda}}} == {{lambda}}", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "1 == 2 == 3")
|
||||
}
|
||||
|
||||
// --- Spec test 5: Escaping ---
|
||||
// Lambda results should be appropriately escaped.
|
||||
|
||||
@(test)
|
||||
test_lambda_escaping :: proc(t: ^testing.T) {
|
||||
data := Interp_Data {
|
||||
lambda = proc() -> string {return ">"},
|
||||
}
|
||||
tpl, _ := parse("<{{lambda}}{{{lambda}}}", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<>>")
|
||||
}
|
||||
|
||||
// --- Spec test 6: Section ---
|
||||
// Lambdas used for sections should receive the raw section string.
|
||||
|
||||
Section_Data :: struct {
|
||||
lambda: proc(_: string) -> string,
|
||||
x: string,
|
||||
planet: string,
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_section :: proc(t: ^testing.T) {
|
||||
data := Section_Data {
|
||||
lambda = proc(text: string) -> string {
|
||||
if text == "{{x}}" {return "yes"} else {return "no"}
|
||||
},
|
||||
x = "Error!",
|
||||
}
|
||||
tpl, _ := parse("<{{#lambda}}{{x}}{{/lambda}}>", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<yes>")
|
||||
}
|
||||
|
||||
// --- Spec test 7: Section - Expansion ---
|
||||
// Lambdas used for sections should have their results parsed.
|
||||
|
||||
@(test)
|
||||
test_lambda_section_expansion :: proc(t: ^testing.T) {
|
||||
data := Section_Data {
|
||||
lambda = proc(text: string) -> string {
|
||||
return fmt.tprintf("%s{{{{planet}}}}%s", text, text)
|
||||
},
|
||||
planet = "Earth",
|
||||
}
|
||||
tpl, _ := parse("<{{#lambda}}-{{/lambda}}>", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<-Earth->")
|
||||
}
|
||||
|
||||
// --- Spec test 9: Section - Multiple Calls ---
|
||||
// Lambdas used for sections should not be cached.
|
||||
|
||||
@(test)
|
||||
test_lambda_section_multiple_calls :: proc(t: ^testing.T) {
|
||||
data := Section_Data {
|
||||
lambda = proc(text: string) -> string {
|
||||
return fmt.tprintf("__%s__", text)
|
||||
},
|
||||
}
|
||||
tpl, _ := parse("{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "__FILE__ != __LINE__")
|
||||
}
|
||||
|
||||
// --- Spec test 10: Inverted Section ---
|
||||
// Lambdas used for inverted sections should be considered truthy.
|
||||
|
||||
Inverted_Data :: struct {
|
||||
lambda: proc(_: string) -> bool,
|
||||
static: string,
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_inverted_section :: proc(t: ^testing.T) {
|
||||
data := Inverted_Data {
|
||||
lambda = proc(text: string) -> bool {return false},
|
||||
static = "static",
|
||||
}
|
||||
tpl, _ := parse("<{{^lambda}}{{static}}{{/lambda}}>", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<>")
|
||||
}
|
||||
|
||||
+45
-7
@@ -48,6 +48,7 @@ Node :: struct {
|
||||
indent: string,
|
||||
first_child: int,
|
||||
child_count: int,
|
||||
content: string,
|
||||
}
|
||||
|
||||
// node_span returns the number of flat-array entries a node occupies:
|
||||
@@ -103,7 +104,7 @@ parse :: proc(
|
||||
return {}, terr
|
||||
}
|
||||
|
||||
tmpl.nodes, err = parse_tokens(tokens[:], allocator)
|
||||
tmpl.nodes, err = parse_tokens(tokens[:], source, allocator)
|
||||
if err != nil {
|
||||
delete(tmpl.nodes)
|
||||
return {}, err
|
||||
@@ -148,6 +149,7 @@ render :: proc(
|
||||
|
||||
parse_tokens :: proc(
|
||||
tokens: []Token,
|
||||
source: string,
|
||||
allocator := context.allocator,
|
||||
) -> (
|
||||
nodes: [dynamic]Node,
|
||||
@@ -155,7 +157,7 @@ parse_tokens :: proc(
|
||||
) {
|
||||
nodes = make([dynamic]Node, 0, len(tokens), allocator)
|
||||
pos := 0
|
||||
err = parse_section(tokens, &pos, &nodes, "")
|
||||
err = parse_section(tokens, &pos, &nodes, "", source)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,6 +166,7 @@ parse_section :: proc(
|
||||
pos: ^int,
|
||||
nodes: ^[dynamic]Node,
|
||||
end_tag: string,
|
||||
source: string,
|
||||
) -> Render_Error {
|
||||
for pos^ < len(tokens) {
|
||||
tok := tokens[pos^]
|
||||
@@ -187,18 +190,28 @@ parse_section :: proc(
|
||||
case .Section_Open:
|
||||
pos^ += 1
|
||||
idx := len(nodes)
|
||||
content_start := 0
|
||||
if pos^ < len(tokens) { content_start = tokens[pos^].pos }
|
||||
append(nodes, Node{kind = .Section, key = tok.value, first_child = -1})
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
close_pos := 0
|
||||
if pos^ - 1 >= 0 && pos^ - 1 < len(tokens) { close_pos = tokens[pos^ - 1].pos }
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
nodes[idx].content = source[content_start:close_pos]
|
||||
|
||||
case .Inverted_Open:
|
||||
pos^ += 1
|
||||
idx := len(nodes)
|
||||
content_start := 0
|
||||
if pos^ < len(tokens) { content_start = tokens[pos^].pos }
|
||||
append(nodes, Node{kind = .Inverted, key = tok.value, first_child = -1})
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
close_pos := 0
|
||||
if pos^ - 1 >= 0 && pos^ - 1 < len(tokens) { close_pos = tokens[pos^ - 1].pos }
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
nodes[idx].content = source[content_start:close_pos]
|
||||
|
||||
case .Section_Close:
|
||||
if end_tag != "" && tok.value == end_tag {
|
||||
@@ -236,7 +249,7 @@ parse_section :: proc(
|
||||
nodes,
|
||||
Node{kind = .Parent, key = tok.value, indent = tok.indent, first_child = -1},
|
||||
)
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
|
||||
@@ -247,7 +260,7 @@ parse_section :: proc(
|
||||
nodes,
|
||||
Node{kind = .Block, key = tok.value, indent = tok.indent, first_child = -1},
|
||||
)
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
}
|
||||
@@ -447,17 +460,42 @@ render_nodes :: proc(
|
||||
|
||||
case .Variable:
|
||||
val := resolve_name(node.key, ctx[:])
|
||||
if result_str, ok := call_interp_lambda(val); ok {
|
||||
sub_tpl, perr := parse(result_str, context.temp_allocator, context.temp_allocator)
|
||||
if perr == nil {
|
||||
temp: strings.Builder
|
||||
strings.builder_init(&temp, context.temp_allocator)
|
||||
render_nodes(sub_tpl.nodes[:], sub_tpl.nodes[:], ctx, partials, &temp, blocks) or_return
|
||||
write_value(b, strings.to_string(temp), escape = true)
|
||||
}
|
||||
} else {
|
||||
write_value(b, val, escape = true)
|
||||
}
|
||||
i += 1
|
||||
|
||||
case .Unescaped:
|
||||
val := resolve_name(node.key, ctx[:])
|
||||
if result_str, ok := call_interp_lambda(val); ok {
|
||||
sub_tpl, perr := parse(result_str, context.temp_allocator, context.temp_allocator)
|
||||
if perr == nil {
|
||||
temp: strings.Builder
|
||||
strings.builder_init(&temp, context.temp_allocator)
|
||||
render_nodes(sub_tpl.nodes[:], sub_tpl.nodes[:], ctx, partials, &temp, blocks) or_return
|
||||
write_value(b, strings.to_string(temp), escape = false)
|
||||
}
|
||||
} else {
|
||||
write_value(b, val, escape = false)
|
||||
}
|
||||
i += 1
|
||||
|
||||
case .Section:
|
||||
val := resolve_name(node.key, ctx[:])
|
||||
if is_truthy(val) {
|
||||
if result_str, ok := call_section_lambda(val, node.content); ok {
|
||||
sub_tpl, perr := parse(result_str, context.temp_allocator, context.temp_allocator)
|
||||
if perr == nil {
|
||||
render_nodes(sub_tpl.nodes[:], sub_tpl.nodes[:], ctx, partials, b, blocks) or_return
|
||||
}
|
||||
} else if is_truthy(val) {
|
||||
children := all_nodes[node.first_child:node.first_child + node.child_count]
|
||||
elem_info, count, data := list_info(val)
|
||||
if elem_info != nil {
|
||||
|
||||
Reference in New Issue
Block a user