Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/liquid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var (
env func() []string = os.Environ
bindings map[string]any = map[string]any{}
strictVars bool
laxFilters bool
)

func main() {
Expand All @@ -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 {
Expand Down Expand Up @@ -92,6 +94,9 @@ func render() error {
if strictVars {
e.StrictVariables()
}
if laxFilters {
e.LaxFilters()
}

tpl, err := e.ParseTemplate(buf)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion expressions/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion expressions/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down