diff --git a/mustache/data.odin b/mustache/data.odin index 3053558..7a0f78e 100644 --- a/mustache/data.odin +++ b/mustache/data.odin @@ -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) { diff --git a/mustache/lambda_test.odin b/mustache/lambda_test.odin new file mode 100644 index 0000000..33d74a1 --- /dev/null +++ b/mustache/lambda_test.odin @@ -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, "") +} + +// --- 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, "<>") +} + diff --git a/mustache/mustache.odin b/mustache/mustache.odin index 9f4a956..7c050d2 100644 --- a/mustache/mustache.odin +++ b/mustache/mustache.odin @@ -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[:]) - write_value(b, val, escape = true) + 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[:]) - write_value(b, val, escape = false) + 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 {