From cbb987111c9b9b2750a853bb41ffc69e9c91c300 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Thu, 26 Feb 2026 19:22:06 +0800 Subject: [PATCH] feat: add LaxFilters option for undefined filter handling (fixes #9) --- cmd/liquid/main.go | 5 +++++ engine.go | 7 +++++++ engine_test.go | 20 ++++++++++++++++++++ expressions/config.go | 3 ++- expressions/filters.go | 5 ++++- 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cmd/liquid/main.go b/cmd/liquid/main.go index 6c2f7820..3af163fd 100644 --- a/cmd/liquid/main.go +++ b/cmd/liquid/main.go @@ -28,6 +28,7 @@ var ( env func() []string = os.Environ bindings map[string]any = map[string]any{} strictVars bool + laxFilters bool ) func main() { @@ -43,6 +44,7 @@ func main() { var bindEnvs bool cmdLine.BoolVar(&bindEnvs, "env", false, "bind environment variables") cmdLine.BoolVar(&strictVars, "strict", false, "enable strict variable mode in templates") + cmdLine.BoolVar(&laxFilters, "lax-filters", false, "ignore undefined filters instead of raising an error") err = cmdLine.Parse(os.Args[1:]) if err != nil { @@ -92,6 +94,9 @@ func render() error { if strictVars { e.StrictVariables() } + if laxFilters { + e.LaxFilters() + } tpl, err := e.ParseTemplate(buf) if err != nil { diff --git a/engine.go b/engine.go index e77591d2..abf1f0e4 100644 --- a/engine.go +++ b/engine.go @@ -84,6 +84,13 @@ func (e *Engine) StrictVariables() { e.cfg.StrictVariables = true } +// LaxFilters causes the renderer to silently pass through the input value +// when the template contains an undefined filter, matching Shopify Liquid behavior. +// By default, undefined filters cause an error. +func (e *Engine) LaxFilters() { + e.cfg.LaxFilters = true +} + // EnableJekyllExtensions enables Jekyll-specific extensions to Liquid. // This includes support for dot notation in assign tags (e.g., {% assign page.canonical_url = value %}). // Note: This is not part of the Shopify Liquid standard but is used in Jekyll and Gojekyll. diff --git a/engine_test.go b/engine_test.go index 82dc5d20..789a6136 100644 --- a/engine_test.go +++ b/engine_test.go @@ -185,6 +185,26 @@ func Test_template_store(t *testing.T) { require.Equal(t, "Message Text: filename from: template.liquid.", out) } +func TestEngine_LaxFilters(t *testing.T) { + // Default: undefined filters cause an error + engine := NewEngine() + _, err := engine.ParseAndRenderString(`{{ "hello" | nofilter }}`, emptyBindings) + require.Error(t, err) + require.Contains(t, err.Error(), "undefined filter") + + // LaxFilters: undefined filters pass through the value + engine = NewEngine() + engine.LaxFilters() + out, err := engine.ParseAndRenderString(`{{ "hello" | nofilter }}`, emptyBindings) + require.NoError(t, err) + require.Equal(t, "hello", out) + + // LaxFilters: defined filters still work + out, err = engine.ParseAndRenderString(`{{ "hello" | upcase }}`, emptyBindings) + require.NoError(t, err) + require.Equal(t, "HELLO", out) +} + func TestEngine_UnregisterTag(t *testing.T) { engine := NewEngine() engine.RegisterTag("echo", func(c render.Context) (string, error) { diff --git a/expressions/config.go b/expressions/config.go index 268a4be9..2378fe78 100644 --- a/expressions/config.go +++ b/expressions/config.go @@ -2,7 +2,8 @@ package expressions // Config holds configuration information for expression interpretation. type Config struct { - filters map[string]any + filters map[string]any + LaxFilters bool } // NewConfig creates a new Config. diff --git a/expressions/filters.go b/expressions/filters.go index 636ee5f2..adb79617 100644 --- a/expressions/filters.go +++ b/expressions/filters.go @@ -83,7 +83,10 @@ func isClosureInterfaceType(t reflect.Type) bool { func (ctx *context) ApplyFilter(name string, receiver valueFn, params []valueFn) (any, error) { filter, ok := ctx.filters[name] if !ok { - panic(UndefinedFilter(name)) + if !ctx.LaxFilters { + panic(UndefinedFilter(name)) + } + return receiver(ctx).Interface(), nil } fr := reflect.ValueOf(filter)