diff --git a/AGENTS.md b/AGENTS.md index 2f19ca8..7f7b947 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -416,7 +416,6 @@ Spec-compliant implementation at `mustache/`. See `mustache/SPEC.md` for the imp | `diagnostic.odin` | Rust-style error formatter: `format_error` (multi-line context, ANSI colors via `core:terminal/ansi`, `colorize` param), `format_render_error` (formats `Error`), `line_col`, `line_text`, `context_extent`, `count_lines`, `digit_count`, `should_colorize`. | | `suggest.odin` | Strict-warning helpers: `validate_key_path` (walks dotted path, crosses maps silently), `suggest_correction` (Levenshtein via `core:strings/levenshtein_distance`), `collect_struct_keys` (via reflection, recurses into `using`), `struct_has_field` (distinguishes missing field from nil value — needed for `Maybe(bool)`), `collect_partial_names`, `collect_block_names`. | | `spec_test.odin` | JSON spec test runner — loads `spec/specs/*.json`, runs each test case. Uses `log.nil_logger()` to suppress expected warnings. | -| `lambda_test.odin` | Spec lambda tests | | `pipes_test.odin` | Pipe filter tests (`group_by` + `format`) | | `diagnostic_test.odin` | Golden-output tests for `format_error` (multi-line context, edge cases, alignment, caret position, hint) + parser error message brace-escaping | | `suggest_test.odin` | Tests for `validate_key_path`, `suggest_correction`, `struct_has_field` with `Maybe(bool)` and `using`-promoted fields | @@ -460,13 +459,6 @@ Rust-style error messages with multi-line source context, caret underlines, and **Block override source tracking**: `Block_Override.source: Template` ensures warnings inside block overrides point at the override's source file (e.g., `page.html`), not the parent template (`base.html`). -### Lambdas - -Spec-compliant. Stored as `any` values in the data context. - -- **Interpolation lambdas**: `proc() -> string`, `proc() -> int`, `proc() -> bool` — called via `call_interp_lambda`, result stringified and escaped. -- **Section lambdas**: `proc(string) -> string`, `proc(string) -> int`, `proc(string) -> bool` — called via `call_section_lambda` with the raw section text (`node.content`). String result is re-parsed as mustache and rendered against the current context stack. - ### Pipes `{{key | op args…}}` for interpolation, `{{#key | op args…}}…{{/key}}` for sections. Stored as `[dynamic; MAX_PIPES]Pipe_Filter` on each `Node`. Applied in the renderer via `apply_pipeline` before truthiness/interpolation. Implemented filters: diff --git a/TODOS.md b/TODOS.md index 9313920..d0478f1 100644 --- a/TODOS.md +++ b/TODOS.md @@ -121,6 +121,9 @@ - [ ] configure opt-out of automatic sections being added to menu. - [ ] nested menus (i.e. `parent` support) - [ ] get rid of the global variables in the `treesitter` package. +- [ ] enforce heading structure. + - [ ] Either frontmatter.title set, or 1 h1 tag at top, not both + - [ ] No skipping. - [ ] Consider using `or_else` when applying default values to structs. i.e. ```odin package main diff --git a/mustache/data.odin b/mustache/data.odin index dc741c8..72dbd80 100644 --- a/mustache/data.odin +++ b/mustache/data.odin @@ -175,32 +175,6 @@ 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 deleted file mode 100644 index ffd31f8..0000000 --- a/mustache/lambda_test.odin +++ /dev/null @@ -1,154 +0,0 @@ -#+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, {}, 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, {}, 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, {}, 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, {}, 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, {}, 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, {}, 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, {}, 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, {}, context.temp_allocator) - testing.expect_value(t, result, "<>") -} - -*/ diff --git a/mustache/mustache.odin b/mustache/mustache.odin index 4551ef2..b46200e 100644 --- a/mustache/mustache.odin +++ b/mustache/mustache.odin @@ -625,30 +625,7 @@ render_nodes :: proc( } val = transformed } - if result_str, ok := call_interp_lambda(val); ok { - sub_tpl, perr := parse( - result_str, - fmt.tprintf("", node.key), - context.temp_allocator, - context.temp_allocator, - ) - if perr == nil { - temp: strings.Builder - strings.builder_init(&temp, context.temp_allocator) - render_nodes( - sub_tpl, - sub_tpl.nodes[:], - ctx, - partials, - &temp, - blocks, - nil, - ) or_return - write_value(b, strings.to_string(temp), escape = true) - } - } else { - write_value(b, val, escape = true) - } + write_value(b, val, escape = true) i += 1 case .Unescaped: @@ -667,30 +644,7 @@ render_nodes :: proc( } val = transformed } - if result_str, ok := call_interp_lambda(val); ok { - sub_tpl, perr := parse( - result_str, - fmt.tprintf("", node.key), - context.temp_allocator, - context.temp_allocator, - ) - if perr == nil { - temp: strings.Builder - strings.builder_init(&temp, context.temp_allocator) - render_nodes( - sub_tpl, - sub_tpl.nodes[:], - ctx, - partials, - &temp, - blocks, - nil, - ) or_return - write_value(b, strings.to_string(temp), escape = false) - } - } else { - write_value(b, val, escape = false) - } + write_value(b, val, escape = false) i += 1 case .Section: @@ -705,25 +659,7 @@ render_nodes :: proc( } val = transformed } - if result_str, ok := call_section_lambda(val, node.content); ok { - sub_tpl, perr := parse( - result_str, - fmt.tprintf("", node.key), - context.temp_allocator, - context.temp_allocator, - ) - if perr == nil { - render_nodes( - sub_tpl, - sub_tpl.nodes[:], - ctx, - partials, - b, - blocks, - nil, - ) or_return - } - } else if is_truthy(val) { + if is_truthy(val) { children := node.children elem_info, count, data := list_info(val) if elem_info != nil {