1
0
mirror of https://github.com/danog/liquid.git synced 2024-11-30 07:08:58 +01:00

Implement filters: default; date (w/out format)

This commit is contained in:
Oliver Steele 2017-06-27 16:02:05 -04:00
parent 383db45707
commit d849e74d1d
6 changed files with 117 additions and 42 deletions

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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,
"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)
})
}

View File

@ -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)

44
generics/call.go Normal file
View File

@ -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
}

View File

@ -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
}
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
}
}
for i := len(in); i < rt.NumIn(); i++ {
out[i] = reflect.Zero(rt.In(i))
}
return out
}