diff --git a/engine.go b/engine.go index b1db42e..ba90a4f 100644 --- a/engine.go +++ b/engine.go @@ -9,6 +9,7 @@ import ( "io" "github.com/osteele/liquid/chunks" + "github.com/osteele/liquid/expressions" "github.com/osteele/liquid/filters" "github.com/osteele/liquid/tags" ) @@ -23,6 +24,7 @@ func init() { // // In the future, it will be configured with additional tags, filters, and the {%include%} search path. type Engine interface { + DefineFilter(name string, fn interface{}) DefineTag(string, func(form string) (func(io.Writer, chunks.Context) error, error)) ParseTemplate(text []byte) (Template, error) @@ -43,7 +45,13 @@ func NewEngine() Engine { return engine{} } +func (e engine) DefineFilter(name string, fn interface{}) { + // TODO define this on the engine, not globally + expressions.DefineFilter(name, fn) +} + func (e engine) DefineTag(name string, td func(form string) (func(io.Writer, chunks.Context) error, error)) { + // TODO define this on the engine, not globally chunks.DefineTag(name, chunks.TagDefinition(td)) } diff --git a/expressions/filters.go b/expressions/filters.go index a9a660b..08641e0 100644 --- a/expressions/filters.go +++ b/expressions/filters.go @@ -3,6 +3,7 @@ package expressions import ( "fmt" "reflect" + "runtime/debug" "github.com/osteele/liquid/errors" "github.com/osteele/liquid/generics" @@ -48,7 +49,7 @@ func makeFilter(f valueFn, name string, param valueFn) valueFn { case generics.GenericError: panic(InterpreterError(e.Error())) default: - // fmt.Println(string(debug.Stack())) + fmt.Println(string(debug.Stack())) panic(e) } } @@ -57,7 +58,7 @@ func makeFilter(f valueFn, name string, param valueFn) valueFn { if param != nil { args = append(args, param(ctx)) } - out, err := generics.Apply(fr, args) + out, err := generics.Call(fr, args) if err != nil { panic(err) } diff --git a/filters/filter_test.go b/filters/filter_test.go index 9a7d0a4..b0454f3 100644 --- a/filters/filter_test.go +++ b/filters/filter_test.go @@ -3,6 +3,7 @@ package filters import ( "fmt" "testing" + "time" "github.com/osteele/liquid/expressions" "github.com/stretchr/testify/require" @@ -13,11 +14,18 @@ func init() { } var filterTests = []struct{ in, expected string }{ - // Jekyll extensions - {`obj | inspect`, `{"a":1}`}, + // values + {`4.99 | default: 2.99`, "4.99"}, + {`undefined | default: 2.99`, "2.99"}, + {`false | default: 2.99`, "2.99"}, + {`empty_list | default: 2.99`, "2.99"}, - // filters - // product_price | default: 2.99 }} + // date filters + {`article.published_at | date`, "Fri, Jul 17, 15"}, + // article.published_at | date: "%a, %b %d, %y" + // article.published_at | date: "%Y" + // "March 14, 2016" | date: "%b %d, %y" + // "now" | date: "%Y-%m-%d %H:%M" } // list filters // site.pages | map: 'category' | compact | join "," %} @@ -64,19 +72,28 @@ var filterTests = []struct{ in, expected string }{ // 183.357 | floor // minus, modulo, plus, round,times - // date filters - // article.published_at | date: "%a, %b %d, %y" - // article.published_at | date: "%Y" - // "March 14, 2016" | date: "%b %d, %y" - // "now" | date: "%Y-%m-%d %H:%M" } + // Jekyll extensions + {`obj | inspect`, `{"a":1}`}, +} + +func timeMustParse(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t } var filterTestContext = expressions.NewContext(map[string]interface{}{ - "x": 123, + "x": 123, + "empty_list": map[string]interface{}{}, "obj": map[string]interface{}{ "a": 1, }, "animals": []string{"zebra", "octopus", "giraffe", "Sally Snake"}, + "article": map[string]interface{}{ + "published_at": timeMustParse("2015-07-17T15:04:05Z"), + }, "pages": []map[string]interface{}{ {"category": "business"}, {"category": "celebrities"}, @@ -103,7 +120,7 @@ func TestFilters(t *testing.T) { t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { val, err := expressions.EvaluateExpr(test.in, filterTestContext) require.NoErrorf(t, err, test.in) - actual := fmt.Sprintf("%s", val) + actual := fmt.Sprintf("%v", val) require.Equalf(t, test.expected, actual, test.in) }) } diff --git a/filters/filters.go b/filters/filters.go index af2b8e9..7191302 100644 --- a/filters/filters.go +++ b/filters/filters.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/osteele/liquid/expressions" "github.com/osteele/liquid/generics" @@ -12,6 +13,27 @@ import ( // DefineStandardFilters defines the standard Liquid filters. func DefineStandardFilters() { + // values + expressions.DefineFilter("default", func(in, defaultValue interface{}) interface{} { + if in == nil || in == false || generics.IsEmpty(in) { + in = defaultValue + } + return in + }) + + // dates + expressions.DefineFilter("date", func(in, format interface{}) interface{} { + if format != nil { + panic("date conversion format is not implemented") + } + switch in := in.(type) { + case time.Time: + return in.Format("Mon, Jan 2, 06") + default: + panic("unimplemented date conversion") + } + }) + // lists expressions.DefineFilter("join", joinFilter) expressions.DefineFilter("sort", sortFilter) diff --git a/generics/call.go b/generics/call.go new file mode 100644 index 0000000..bd371ca --- /dev/null +++ b/generics/call.go @@ -0,0 +1,44 @@ +package generics + +import ( + "fmt" + "reflect" +) + +// Call applies a function to arguments, converting them as necessary. +// The conversion follows Liquid semantics, which are more aggressive than +// Go conversion. The function should return one or two values; the second value, +// if present, should be an error. +func Call(fn reflect.Value, args []interface{}) (interface{}, error) { + in := convertArguments(fn, args) + outs := fn.Call(in) + if len(outs) > 1 && outs[1].Interface() != nil { + switch e := outs[1].Interface().(type) { + case error: + fmt.Println("error") + return nil, e + default: + panic(e) + } + } + return outs[0].Interface(), nil +} + +// Convert args to match the input types of function fn. +func convertArguments(fn reflect.Value, in []interface{}) []reflect.Value { + rt := fn.Type() + out := make([]reflect.Value, rt.NumIn()) + for i, arg := range in { + if i < rt.NumIn() { + if arg == nil { + out[i] = reflect.Zero(rt.In(i)) + } else { + out[i] = convertType(arg, rt.In(i)) + } + } + } + for i := len(in); i < rt.NumIn(); i++ { + out[i] = reflect.Zero(rt.In(i)) + } + return out +} diff --git a/generics/generics.go b/generics/generics.go index dbd5f91..04dbd31 100644 --- a/generics/generics.go +++ b/generics/generics.go @@ -14,24 +14,6 @@ func genericErrorf(format string, a ...interface{}) error { return GenericError(fmt.Sprintf(format, a...)) } -// Apply applies a function to arguments, converting them as necessary. -// The conversion follows Liquid semantics, which are more aggressive than -// Go conversion. The function should return one or two values; the second value, -// if present, should be an error. -func Apply(fn reflect.Value, args []interface{}) (interface{}, error) { - in := convertArguments(fn, args) - outs := fn.Call(in) - if len(outs) > 1 && outs[1].Interface() != nil { - switch e := outs[1].Interface().(type) { - case error: - return nil, e - default: - panic(e) - } - } - return outs[0].Interface(), nil -} - // Convert val to the type. This is a more aggressive conversion, that will // recursively create new map and slice values as necessary. It doesn't // handle circular references. @@ -58,17 +40,18 @@ func convertType(val interface{}, t reflect.Type) reflect.Value { panic(genericErrorf("convertType: can't convert %#v<%s> to %v", val, r.Type(), t)) } -// Convert args to match the input types of function fn. -func convertArguments(fn reflect.Value, in []interface{}) []reflect.Value { - rt := fn.Type() - out := make([]reflect.Value, rt.NumIn()) - for i, arg := range in { - if i < rt.NumIn() { - out[i] = convertType(arg, rt.In(i)) - } +// IsEmpty returns a bool indicating whether the value is empty according to Liquid semantics. +func IsEmpty(in interface{}) bool { + if in == nil { + return false } - for i := len(in); i < rt.NumIn(); i++ { - out[i] = reflect.Zero(rt.In(i)) + r := reflect.ValueOf(in) + switch r.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return r.Len() == 0 + case reflect.Bool: + return r.Bool() == false + default: + return false } - return out }