diff --git a/mustache/diagnostic.odin b/mustache/diagnostic.odin index 88ad778..c5ef789 100644 --- a/mustache/diagnostic.odin +++ b/mustache/diagnostic.odin @@ -326,5 +326,5 @@ format_render_error :: proc(err: Error, tmpl: Template, colorize: bool = false) if path == "" { path = "" } - return format_error(path, source, b.pos, b.msg, colorize = colorize, span = b.span) + return format_error(path, source, b.pos, b.msg, hint = b.hint, colorize = colorize, span = b.span) } diff --git a/mustache/mustache.odin b/mustache/mustache.odin index 1d5c59c..4551ef2 100644 --- a/mustache/mustache.odin +++ b/mustache/mustache.odin @@ -32,6 +32,7 @@ Error_Body :: struct { source: string, path: string, span: int, + hint: string, } // Error is nil when no error occurred. @@ -59,6 +60,7 @@ tag_error :: proc(err: Error, tmpl: Template) -> Error { msg = b.msg, pos = b.pos, span = b.span, + hint = b.hint, kind = b.kind, source = tmpl.source, path = tmpl.path, diff --git a/mustache/pipes.odin b/mustache/pipes.odin index 0d32e5a..ed371fc 100644 --- a/mustache/pipes.odin +++ b/mustache/pipes.odin @@ -259,11 +259,17 @@ resolve_format_string :: proc(name: string, ctx: []any, pos: int) -> (string, Er apply_filter :: proc(value: any, filter: ^Pipe_Filter, pos: int, ctx: []any) -> (any, Error) { op, ok := pipe_op_from_string(filter.op) if !ok { + hint := "" + suggestion := suggest_correction(pipe_op_candidates(), filter.op) + if suggestion != "" { + hint = fmt.tprintf("did you mean '%s'?", suggestion) + } return nil, Error_Body { msg = fmt.tprintf("unknown pipe op '%s'", filter.op), pos = filter.op_pos, span = len(filter.op), kind = .Data, + hint = hint, } } diff --git a/mustache/pipes_test.odin b/mustache/pipes_test.odin index c45238c..50d0f5b 100644 --- a/mustache/pipes_test.odin +++ b/mustache/pipes_test.odin @@ -2,6 +2,7 @@ package mustache import "core:fmt" +import "core:strings" import "core:testing" import "core:time/datetime" import "core:time/timezone" @@ -561,3 +562,24 @@ test_format_mst_no_offset_no_timezone :: proc(t: ^testing.T) { result, _ := render(tpl, data, {}, context.temp_allocator) testing.expect_value(t, result, "UTC") } + +// --- pipe op suggestion tests --- + +@(test) +test_unknown_pipe_op_suggestion :: proc(t: ^testing.T) { + filter := Pipe_Filter{op = "formats", op_pos = 0} + _, err := apply_filter("2026-01-15", &filter, 0, nil) + testing.expect(t, err != nil, "should error on unknown op") + b := body(err) + testing.expect(t, strings.contains(b.hint, "format"), "hint should suggest 'format'") + testing.expect(t, strings.contains(b.hint, "did you mean"), "hint should be a suggestion") +} + +@(test) +test_unknown_pipe_op_no_suggestion :: proc(t: ^testing.T) { + filter := Pipe_Filter{op = "xyz", op_pos = 0} + _, err := apply_filter("2026-01-15", &filter, 0, nil) + testing.expect(t, err != nil, "should error on unknown op") + b := body(err) + testing.expect(t, b.hint == "", "no suggestion expected for 'xyz'") +}