diff --git a/helpers/general.go b/helpers/general.go index ea3620119..4fd91133b 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -17,12 +17,10 @@ import ( "bytes" "crypto/md5" "encoding/hex" - "errors" "fmt" "io" "net" "path/filepath" - "reflect" "strings" "sync" "unicode" @@ -320,198 +318,6 @@ func IsWhitespace(r rune) bool { return r == ' ' || r == '\t' || r == '\n' || r == '\r' } -// Seq creates a sequence of integers. -// It's named and used as GNU's seq. -// Examples: -// 3 => 1, 2, 3 -// 1 2 4 => 1, 3 -// -3 => -1, -2, -3 -// 1 4 => 1, 2, 3, 4 -// 1 -2 => 1, 0, -1, -2 -func Seq(args ...interface{}) ([]int, error) { - if len(args) < 1 || len(args) > 3 { - return nil, errors.New("Seq, invalid number of args: 'first' 'increment' (optional) 'last' (optional)") - } - - intArgs := cast.ToIntSlice(args) - - if len(intArgs) < 1 || len(intArgs) > 3 { - return nil, errors.New("Invalid argument(s) to Seq") - } - - var inc = 1 - var last int - var first = intArgs[0] - - if len(intArgs) == 1 { - last = first - if last == 0 { - return []int{}, nil - } else if last > 0 { - first = 1 - } else { - first = -1 - inc = -1 - } - } else if len(intArgs) == 2 { - last = intArgs[1] - if last < first { - inc = -1 - } - } else { - inc = intArgs[1] - last = intArgs[2] - if inc == 0 { - return nil, errors.New("'increment' must not be 0") - } - if first < last && inc < 0 { - return nil, errors.New("'increment' must be > 0") - } - if first > last && inc > 0 { - return nil, errors.New("'increment' must be < 0") - } - } - - // sanity check - if last < -100000 { - return nil, errors.New("size of result exceeds limit") - } - size := ((last - first) / inc) + 1 - - // sanity check - if size <= 0 || size > 2000 { - return nil, errors.New("size of result exceeds limit") - } - - seq := make([]int, size) - val := first - for i := 0; ; i++ { - seq[i] = val - val += inc - if (inc < 0 && val < last) || (inc > 0 && val > last) { - break - } - } - - return seq, nil -} - -// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to -// determine the type of the two terms. -func DoArithmetic(a, b interface{}, op rune) (interface{}, error) { - av := reflect.ValueOf(a) - bv := reflect.ValueOf(b) - var ai, bi int64 - var af, bf float64 - var au, bu uint64 - switch av.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - ai = av.Int() - switch bv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bi = bv.Int() - case reflect.Float32, reflect.Float64: - af = float64(ai) // may overflow - ai = 0 - bf = bv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bu = bv.Uint() - if ai >= 0 { - au = uint64(ai) - ai = 0 - } else { - bi = int64(bu) // may overflow - bu = 0 - } - default: - return nil, errors.New("Can't apply the operator to the values") - } - case reflect.Float32, reflect.Float64: - af = av.Float() - switch bv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bf = float64(bv.Int()) // may overflow - case reflect.Float32, reflect.Float64: - bf = bv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bf = float64(bv.Uint()) // may overflow - default: - return nil, errors.New("Can't apply the operator to the values") - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - au = av.Uint() - switch bv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bi = bv.Int() - if bi >= 0 { - bu = uint64(bi) - bi = 0 - } else { - ai = int64(au) // may overflow - au = 0 - } - case reflect.Float32, reflect.Float64: - af = float64(au) // may overflow - au = 0 - bf = bv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bu = bv.Uint() - default: - return nil, errors.New("Can't apply the operator to the values") - } - case reflect.String: - as := av.String() - if bv.Kind() == reflect.String && op == '+' { - bs := bv.String() - return as + bs, nil - } - return nil, errors.New("Can't apply the operator to the values") - default: - return nil, errors.New("Can't apply the operator to the values") - } - - switch op { - case '+': - if ai != 0 || bi != 0 { - return ai + bi, nil - } else if af != 0 || bf != 0 { - return af + bf, nil - } else if au != 0 || bu != 0 { - return au + bu, nil - } - return 0, nil - case '-': - if ai != 0 || bi != 0 { - return ai - bi, nil - } else if af != 0 || bf != 0 { - return af - bf, nil - } else if au != 0 || bu != 0 { - return au - bu, nil - } - return 0, nil - case '*': - if ai != 0 || bi != 0 { - return ai * bi, nil - } else if af != 0 || bf != 0 { - return af * bf, nil - } else if au != 0 || bu != 0 { - return au * bu, nil - } - return 0, nil - case '/': - if bi != 0 { - return ai / bi, nil - } else if bf != 0 { - return af / bf, nil - } else if bu != 0 { - return au / bu, nil - } - return nil, errors.New("Can't divide the value by 0") - default: - return nil, errors.New("There is no such an operation") - } -} - // NormalizeHugoFlags facilitates transitions of Hugo command-line flags, // e.g. --baseUrl to --baseURL, --uglyUrls to --uglyURLs func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { diff --git a/helpers/general_test.go b/helpers/general_test.go index 3fa587e78..ee4ed2370 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -162,137 +162,6 @@ func TestFindAvailablePort(t *testing.T) { assert.True(t, addr.Port > 0) } -func TestSeq(t *testing.T) { - for i, this := range []struct { - in []interface{} - expect interface{} - }{ - {[]interface{}{-2, 5}, []int{-2, -1, 0, 1, 2, 3, 4, 5}}, - {[]interface{}{1, 2, 4}, []int{1, 3}}, - {[]interface{}{1}, []int{1}}, - {[]interface{}{3}, []int{1, 2, 3}}, - {[]interface{}{3.2}, []int{1, 2, 3}}, - {[]interface{}{0}, []int{}}, - {[]interface{}{-1}, []int{-1}}, - {[]interface{}{-3}, []int{-1, -2, -3}}, - {[]interface{}{3, -2}, []int{3, 2, 1, 0, -1, -2}}, - {[]interface{}{6, -2, 2}, []int{6, 4, 2}}, - {[]interface{}{1, 0, 2}, false}, - {[]interface{}{1, -1, 2}, false}, - {[]interface{}{2, 1, 1}, false}, - {[]interface{}{2, 1, 1, 1}, false}, - {[]interface{}{2001}, false}, - {[]interface{}{}, false}, - // TODO(bep) {[]interface{}{t}, false}, - {nil, false}, - } { - - result, err := Seq(this.in...) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] TestSeq didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] TestSeq got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestDoArithmetic(t *testing.T) { - for i, this := range []struct { - a interface{} - b interface{} - op rune - expect interface{} - }{ - {3, 2, '+', int64(5)}, - {3, 2, '-', int64(1)}, - {3, 2, '*', int64(6)}, - {3, 2, '/', int64(1)}, - {3.0, 2, '+', float64(5)}, - {3.0, 2, '-', float64(1)}, - {3.0, 2, '*', float64(6)}, - {3.0, 2, '/', float64(1.5)}, - {3, 2.0, '+', float64(5)}, - {3, 2.0, '-', float64(1)}, - {3, 2.0, '*', float64(6)}, - {3, 2.0, '/', float64(1.5)}, - {3.0, 2.0, '+', float64(5)}, - {3.0, 2.0, '-', float64(1)}, - {3.0, 2.0, '*', float64(6)}, - {3.0, 2.0, '/', float64(1.5)}, - {uint(3), uint(2), '+', uint64(5)}, - {uint(3), uint(2), '-', uint64(1)}, - {uint(3), uint(2), '*', uint64(6)}, - {uint(3), uint(2), '/', uint64(1)}, - {uint(3), 2, '+', uint64(5)}, - {uint(3), 2, '-', uint64(1)}, - {uint(3), 2, '*', uint64(6)}, - {uint(3), 2, '/', uint64(1)}, - {3, uint(2), '+', uint64(5)}, - {3, uint(2), '-', uint64(1)}, - {3, uint(2), '*', uint64(6)}, - {3, uint(2), '/', uint64(1)}, - {uint(3), -2, '+', int64(1)}, - {uint(3), -2, '-', int64(5)}, - {uint(3), -2, '*', int64(-6)}, - {uint(3), -2, '/', int64(-1)}, - {-3, uint(2), '+', int64(-1)}, - {-3, uint(2), '-', int64(-5)}, - {-3, uint(2), '*', int64(-6)}, - {-3, uint(2), '/', int64(-1)}, - {uint(3), 2.0, '+', float64(5)}, - {uint(3), 2.0, '-', float64(1)}, - {uint(3), 2.0, '*', float64(6)}, - {uint(3), 2.0, '/', float64(1.5)}, - {3.0, uint(2), '+', float64(5)}, - {3.0, uint(2), '-', float64(1)}, - {3.0, uint(2), '*', float64(6)}, - {3.0, uint(2), '/', float64(1.5)}, - {0, 0, '+', 0}, - {0, 0, '-', 0}, - {0, 0, '*', 0}, - {"foo", "bar", '+', "foobar"}, - {3, 0, '/', false}, - {3.0, 0, '/', false}, - {3, 0.0, '/', false}, - {uint(3), uint(0), '/', false}, - {3, uint(0), '/', false}, - {-3, uint(0), '/', false}, - {uint(3), 0, '/', false}, - {3.0, uint(0), '/', false}, - {uint(3), 0.0, '/', false}, - {3, "foo", '+', false}, - {3.0, "foo", '+', false}, - {uint(3), "foo", '+', false}, - {"foo", 3, '+', false}, - {"foo", "bar", '-', false}, - {3, 2, '%', false}, - } { - result, err := DoArithmetic(this.a, this.b, this.op) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] doArithmetic didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] doArithmetic got %v but expected %v", i, result, this.expect) - } - } - } -} - func TestToLowerMap(t *testing.T) { tests := []struct { diff --git a/hugolib/scratch.go b/hugolib/scratch.go index b6a06cd79..4a80416f1 100644 --- a/hugolib/scratch.go +++ b/hugolib/scratch.go @@ -14,10 +14,11 @@ package hugolib import ( - "github.com/spf13/hugo/helpers" "reflect" "sort" "sync" + + "github.com/spf13/hugo/tpl/math" ) // Scratch is a writable context used for stateful operations in Page/Node rendering. @@ -49,7 +50,7 @@ func (c *Scratch) Add(key string, newAddend interface{}) (string, error) { newVal = reflect.Append(addendV, nav).Interface() } } else { - newVal, err = helpers.DoArithmetic(existingAddend, newAddend, '+') + newVal, err = math.DoArithmetic(existingAddend, newAddend, '+') if err != nil { return "", err } diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go new file mode 100644 index 000000000..cb4dfa64e --- /dev/null +++ b/tpl/collections/apply.go @@ -0,0 +1,143 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +// Apply takes a map, array, or slice and returns a new slice with the function fname applied over it. +func (ns *Namespace) Apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { + if seq == nil { + return make([]interface{}, 0), nil + } + + if fname == "apply" { + return nil, errors.New("can't apply myself (no turtles allowed)") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + fnv, found := ns.lookupFunc(fname) + if !found { + return nil, errors.New("can't find function " + fname) + } + + // fnv := reflect.ValueOf(fn) + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + r := make([]interface{}, seqv.Len()) + for i := 0; i < seqv.Len(); i++ { + vv := seqv.Index(i) + + vvv, err := applyFnToThis(fnv, vv, args...) + + if err != nil { + return nil, err + } + + r[i] = vvv.Interface() + } + + return r, nil + default: + return nil, fmt.Errorf("can't apply over %v", seq) + } +} + +func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, error) { + n := make([]reflect.Value, len(args)) + for i, arg := range args { + if arg == "." { + n[i] = this + } else { + n[i] = reflect.ValueOf(arg) + } + } + + num := fn.Type().NumIn() + + if fn.Type().IsVariadic() { + num-- + } + + // TODO(bep) see #1098 - also see template_tests.go + /*if len(args) < num { + return reflect.ValueOf(nil), errors.New("Too few arguments") + } else if len(args) > num { + return reflect.ValueOf(nil), errors.New("Too many arguments") + }*/ + + for i := 0; i < num; i++ { + // AssignableTo reports whether xt is assignable to type targ. + if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) { + return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String()) + } + } + + res := fn.Call(n) + + if len(res) == 1 || res[1].IsNil() { + return res[0], nil + } + return reflect.ValueOf(nil), res[1].Interface().(error) +} + +func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) { + if !strings.ContainsRune(fname, '.') { + fn, found := ns.funcMap[fname] + if !found { + return reflect.Value{}, false + } + + return reflect.ValueOf(fn), true + } + + ss := strings.SplitN(fname, ".", 2) + + // namespace + nv, found := ns.lookupFunc(ss[0]) + if !found { + return reflect.Value{}, false + } + + // method + m := nv.MethodByName(ss[1]) + // if reflect.DeepEqual(m, reflect.Value{}) { + if m.Kind() == reflect.Invalid { + return reflect.Value{}, false + } + return m, true +} + +// indirect is taken from 'text/template/exec.go' +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go new file mode 100644 index 000000000..9718570fd --- /dev/null +++ b/tpl/collections/apply_test.go @@ -0,0 +1,92 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "fmt" + "html/template" + "testing" + + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/tpl/strings" + "github.com/stretchr/testify/assert" +) + +func TestApply(t *testing.T) { + t.Parallel() + + hstrings := strings.New(&deps.Deps{}) + + ns := New(&deps.Deps{}) + ns.Funcs(template.FuncMap{ + "apply": ns.Apply, + "chomp": hstrings.Chomp, + "strings": hstrings, + "print": fmt.Sprint, + }) + + strings := []interface{}{"a\n", "b\n"} + noStringers := []interface{}{tstNoStringer{}, tstNoStringer{}} + + result, _ := ns.Apply(strings, "chomp", ".") + assert.Equal(t, []interface{}{template.HTML("a"), template.HTML("b")}, result) + + result, _ = ns.Apply(strings, "chomp", "c\n") + assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, result) + + result, _ = ns.Apply(strings, "strings.Chomp", "c\n") + assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, result) + + result, _ = ns.Apply(strings, "print", "a", "b", "c") + assert.Equal(t, []interface{}{"abc", "abc"}, result, "testing variadic") + + result, _ = ns.Apply(nil, "chomp", ".") + assert.Equal(t, []interface{}{}, result) + + _, err := ns.Apply(strings, "apply", ".") + if err == nil { + t.Errorf("apply with apply should fail") + } + + var nilErr *error + _, err = ns.Apply(nilErr, "chomp", ".") + if err == nil { + t.Errorf("apply with nil in seq should fail") + } + + _, err = ns.Apply(strings, "dobedobedo", ".") + if err == nil { + t.Errorf("apply with unknown func should fail") + } + + _, err = ns.Apply(noStringers, "chomp", ".") + if err == nil { + t.Errorf("apply when func fails should fail") + } + + _, err = ns.Apply(tstNoStringer{}, "chomp", ".") + if err == nil { + t.Errorf("apply with non-sequence should fail") + } + + _, err = ns.Apply(strings, "foo.Chomp", "c\n") + if err == nil { + t.Errorf("apply with unknown namespace should fail") + } + + _, err = ns.Apply(strings, "strings.Foo", "c\n") + if err == nil { + t.Errorf("apply with unknown namespace method should fail") + } +} diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go new file mode 100644 index 000000000..95d1df642 --- /dev/null +++ b/tpl/collections/collections.go @@ -0,0 +1,574 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "html/template" + "math/rand" + "net/url" + "reflect" + "strings" + "sync" + "time" + + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" +) + +// New returns a new instance of the collections-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "collections" namespace. +type Namespace struct { + sync.Mutex + funcMap template.FuncMap + + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// Funcs sets the internal funcMap for the collections namespace. +func (ns *Namespace) Funcs(fm template.FuncMap) *Namespace { + ns.Lock() + ns.funcMap = fm + ns.Unlock() + + return ns +} + +// After returns all the items after the first N in a rangeable list. +func (ns *Namespace) After(index interface{}, seq interface{}) (interface{}, error) { + if index == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + indexv, err := cast.ToIntE(index) + if err != nil { + return nil, err + } + + if indexv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + if indexv >= seqv.Len() { + return nil, errors.New("no items left") + } + + return seqv.Slice(indexv, seqv.Len()).Interface(), nil +} + +// Delimit takes a given sequence and returns a delimited HTML string. +// If last is passed to the function, it will be used as the final delimiter. +func (ns *Namespace) Delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) { + d, err := cast.ToStringE(delimiter) + if err != nil { + return "", err + } + + var dLast *string + if len(last) > 0 { + l := last[0] + dStr, err := cast.ToStringE(l) + if err != nil { + dLast = nil + } + dLast = &dStr + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return "", errors.New("can't iterate over a nil value") + } + + var str string + switch seqv.Kind() { + case reflect.Map: + sortSeq, err := ns.Sort(seq) + if err != nil { + return "", err + } + seqv = reflect.ValueOf(sortSeq) + fallthrough + case reflect.Array, reflect.Slice, reflect.String: + for i := 0; i < seqv.Len(); i++ { + val := seqv.Index(i).Interface() + valStr, err := cast.ToStringE(val) + if err != nil { + continue + } + switch { + case i == seqv.Len()-2 && dLast != nil: + str += valStr + *dLast + case i == seqv.Len()-1: + str += valStr + default: + str += valStr + d + } + } + + default: + return "", fmt.Errorf("can't iterate over %v", seq) + } + + return template.HTML(str), nil +} + +// Dictionary creates a map[string]interface{} from the given parameters by +// walking the parameters and treating them as key-value pairs. The number +// of parameters must be even. +func (ns *Namespace) Dictionary(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dictionary call") + } + + dict := make(map[string]interface{}, len(values)/2) + + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dictionary keys must be strings") + } + dict[key] = values[i+1] + } + + return dict, nil +} + +// EchoParam returns a given value if it is set; otherwise, it returns an +// empty string. +func (ns *Namespace) EchoParam(a, key interface{}) interface{} { + av, isNil := indirect(reflect.ValueOf(a)) + if isNil { + return "" + } + + var avv reflect.Value + switch av.Kind() { + case reflect.Array, reflect.Slice: + index, ok := key.(int) + if ok && av.Len() > index { + avv = av.Index(index) + } + case reflect.Map: + kv := reflect.ValueOf(key) + if kv.Type().AssignableTo(av.Type().Key()) { + avv = av.MapIndex(kv) + } + } + + avv, isNil = indirect(avv) + + if isNil { + return "" + } + + if avv.IsValid() { + switch avv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return avv.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return avv.Uint() + case reflect.Float32, reflect.Float64: + return avv.Float() + case reflect.String: + return avv.String() + } + } + + return "" +} + +// First returns the first N items in a rangeable list. +func (ns *Namespace) First(limit interface{}, seq interface{}) (interface{}, error) { + if limit == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + limitv, err := cast.ToIntE(limit) + if err != nil { + return nil, err + } + + if limitv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + if limitv > seqv.Len() { + limitv = seqv.Len() + } + + return seqv.Slice(0, limitv).Interface(), nil +} + +// In returns whether v is in the set l. l may be an array or slice. +func (ns *Namespace) In(l interface{}, v interface{}) bool { + if l == nil || v == nil { + return false + } + + lv := reflect.ValueOf(l) + vv := reflect.ValueOf(v) + + switch lv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < lv.Len(); i++ { + lvv := lv.Index(i) + lvv, isNil := indirect(lvv) + if isNil { + continue + } + switch lvv.Kind() { + case reflect.String: + if vv.Type() == lvv.Type() && vv.String() == lvv.String() { + return true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if vv.Int() == lvv.Int() { + return true + } + } + case reflect.Float32, reflect.Float64: + switch vv.Kind() { + case reflect.Float32, reflect.Float64: + if vv.Float() == lvv.Float() { + return true + } + } + } + } + case reflect.String: + if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) { + return true + } + } + return false +} + +// Intersect returns the common elements in the given sets, l1 and l2. l1 and +// l2 must be of the same type and may be either arrays or slices. +func (ns *Namespace) Intersect(l1, l2 interface{}) (interface{}, error) { + if l1 == nil || l2 == nil { + return make([]interface{}, 0), nil + } + + l1v := reflect.ValueOf(l1) + l2v := reflect.ValueOf(l2) + + switch l1v.Kind() { + case reflect.Array, reflect.Slice: + switch l2v.Kind() { + case reflect.Array, reflect.Slice: + r := reflect.MakeSlice(l1v.Type(), 0, 0) + for i := 0; i < l1v.Len(); i++ { + l1vv := l1v.Index(i) + for j := 0; j < l2v.Len(); j++ { + l2vv := l2v.Index(j) + switch l1vv.Kind() { + case reflect.String: + if l1vv.Type() == l2vv.Type() && l1vv.String() == l2vv.String() && !ns.In(r.Interface(), l2vv.Interface()) { + r = reflect.Append(r, l2vv) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch l2vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if l1vv.Int() == l2vv.Int() && !ns.In(r.Interface(), l2vv.Interface()) { + r = reflect.Append(r, l2vv) + } + } + case reflect.Float32, reflect.Float64: + switch l2vv.Kind() { + case reflect.Float32, reflect.Float64: + if l1vv.Float() == l2vv.Float() && !ns.In(r.Interface(), l2vv.Interface()) { + r = reflect.Append(r, l2vv) + } + } + } + } + } + return r.Interface(), nil + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) + } + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) + } +} + +// IsSet returns whether a given array, channel, slice, or map has a key +// defined. +func (ns *Namespace) IsSet(a interface{}, key interface{}) (bool, error) { + av := reflect.ValueOf(a) + kv := reflect.ValueOf(key) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Slice: + if int64(av.Len()) > kv.Int() { + return true, nil + } + case reflect.Map: + if kv.Type() == av.Type().Key() { + return av.MapIndex(kv).IsValid(), nil + } + } + + return false, nil +} + +// Last returns the last N items in a rangeable list. +func (ns *Namespace) Last(limit interface{}, seq interface{}) (interface{}, error) { + if limit == nil || seq == nil { + return nil, errors.New("both limit and seq must be provided") + } + + limitv, err := cast.ToIntE(limit) + if err != nil { + return nil, err + } + + if limitv < 1 { + return nil, errors.New("can't return negative/empty count of items from sequence") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + if limitv > seqv.Len() { + limitv = seqv.Len() + } + + return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil +} + +// Querify encodes the given parameters in URL-encoded form ("bar=baz&foo=quux") sorted by key. +func (ns *Namespace) Querify(params ...interface{}) (string, error) { + qs := url.Values{} + vals, err := ns.Dictionary(params...) + if err != nil { + return "", errors.New("querify keys must be strings") + } + + for name, value := range vals { + qs.Add(name, fmt.Sprintf("%v", value)) + } + + return qs.Encode(), nil +} + +// Seq creates a sequence of integers. It's named and used as GNU's seq. +// +// Examples: +// 3 => 1, 2, 3 +// 1 2 4 => 1, 3 +// -3 => -1, -2, -3 +// 1 4 => 1, 2, 3, 4 +// 1 -2 => 1, 0, -1, -2 +func (ns *Namespace) Seq(args ...interface{}) ([]int, error) { + if len(args) < 1 || len(args) > 3 { + return nil, errors.New("invalid number of arguments to Seq") + } + + intArgs := cast.ToIntSlice(args) + if len(intArgs) < 1 || len(intArgs) > 3 { + return nil, errors.New("invalid arguments to Seq") + } + + var inc = 1 + var last int + var first = intArgs[0] + + if len(intArgs) == 1 { + last = first + if last == 0 { + return []int{}, nil + } else if last > 0 { + first = 1 + } else { + first = -1 + inc = -1 + } + } else if len(intArgs) == 2 { + last = intArgs[1] + if last < first { + inc = -1 + } + } else { + inc = intArgs[1] + last = intArgs[2] + if inc == 0 { + return nil, errors.New("'increment' must not be 0") + } + if first < last && inc < 0 { + return nil, errors.New("'increment' must be > 0") + } + if first > last && inc > 0 { + return nil, errors.New("'increment' must be < 0") + } + } + + // sanity check + if last < -100000 { + return nil, errors.New("size of result exceeds limit") + } + size := ((last - first) / inc) + 1 + + // sanity check + if size <= 0 || size > 2000 { + return nil, errors.New("size of result exceeds limit") + } + + seq := make([]int, size) + val := first + for i := 0; ; i++ { + seq[i] = val + val += inc + if (inc < 0 && val < last) || (inc > 0 && val > last) { + break + } + } + + return seq, nil +} + +// Shuffle returns the given rangeable list in a randomised order. +func (ns *Namespace) Shuffle(seq interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("both count and seq must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + // okay + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) + } + + shuffled := reflect.MakeSlice(reflect.TypeOf(seq), seqv.Len(), seqv.Len()) + + rand.Seed(time.Now().UTC().UnixNano()) + randomIndices := rand.Perm(seqv.Len()) + + for index, value := range randomIndices { + shuffled.Index(value).Set(seqv.Index(index)) + } + + return shuffled.Interface(), nil +} + +// Slice returns a slice of all passed arguments. +func (ns *Namespace) Slice(args ...interface{}) []interface{} { + return args +} + +// Union returns the union of the given sets, l1 and l2. l1 and +// l2 must be of the same type and may be either arrays or slices. +// If l1 and l2 aren't of the same type then l1 will be returned. +// If either l1 or l2 is nil then the non-nil list will be returned. +func (ns *Namespace) Union(l1, l2 interface{}) (interface{}, error) { + if l1 == nil && l2 == nil { + return nil, errors.New("both arrays/slices have to be of the same type") + } else if l1 == nil && l2 != nil { + return l2, nil + } else if l1 != nil && l2 == nil { + return l1, nil + } + + l1v := reflect.ValueOf(l1) + l2v := reflect.ValueOf(l2) + + switch l1v.Kind() { + case reflect.Array, reflect.Slice: + switch l2v.Kind() { + case reflect.Array, reflect.Slice: + r := reflect.MakeSlice(l1v.Type(), 0, 0) + + if l1v.Type() != l2v.Type() { + return r.Interface(), nil + } + + for i := 0; i < l1v.Len(); i++ { + elem := l1v.Index(i) + if !ns.In(r.Interface(), elem.Interface()) { + r = reflect.Append(r, elem) + } + } + + for j := 0; j < l2v.Len(); j++ { + elem := l2v.Index(j) + if !ns.In(r.Interface(), elem.Interface()) { + r = reflect.Append(r, elem) + } + } + + return r.Interface(), nil + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) + } + default: + return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) + } +} diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go new file mode 100644 index 000000000..9d34d3be0 --- /dev/null +++ b/tpl/collections/collections_test.go @@ -0,0 +1,610 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "html/template" + "math/rand" + "reflect" + "testing" + "time" + + "github.com/spf13/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestAfter(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + index interface{} + seq interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c", "d"}, []string{"c", "d"}}, + {int32(3), []string{"a", "b"}, false}, + {int64(2), []int{100, 200, 300}, []int{300}}, + {100, []int{100, 200}, false}, + {"1", []int{100, 200, 300}, []int{200, 300}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.After(test.index, test.seq) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestDelimit(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + seq interface{} + delimiter interface{} + last interface{} + expect template.HTML + }{ + {[]string{"class1", "class2", "class3"}, " ", nil, "class1 class2 class3"}, + {[]int{1, 2, 3, 4, 5}, ",", nil, "1,2,3,4,5"}, + {[]int{1, 2, 3, 4, 5}, ", ", nil, "1, 2, 3, 4, 5"}, + {[]string{"class1", "class2", "class3"}, " ", " and ", "class1 class2 and class3"}, + {[]int{1, 2, 3, 4, 5}, ",", ",", "1,2,3,4,5"}, + {[]int{1, 2, 3, 4, 5}, ", ", ", and ", "1, 2, 3, 4, and 5"}, + // test maps with and without sorting required + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", nil, "10--20--30--40--50"}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", nil, "30--20--10--40--50"}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", nil, "10--20--30--40--50"}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", nil, "30--20--10--40--50"}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", nil, "50--40--10--30--20"}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", nil, "10--20--30--40--50"}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", nil, "30--20--10--40--50"}, + {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, "--", nil, "30--20--10--40--50"}, + // test maps with a last delimiter + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", "--and--", "10--20--30--40--and--50"}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", "--and--", "30--20--10--40--and--50"}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", "--and--", "10--20--30--40--and--50"}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", "--and--", "30--20--10--40--and--50"}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", "--and--", "50--40--10--30--and--20"}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", "--and--", "10--20--30--40--and--50"}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, + {map[float64]string{3.5: "10", 2.5: "20", 1.5: "30", 4.5: "40", 5.5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + var result template.HTML + var err error + + if test.last == nil { + result, err = ns.Delimit(test.seq, test.delimiter) + } else { + result, err = ns.Delimit(test.seq, test.delimiter, test.last) + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestDictionary(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + values []interface{} + expect interface{} + }{ + {[]interface{}{"a", "b"}, map[string]interface{}{"a": "b"}}, + {[]interface{}{"a", 12, "b", []int{4}}, map[string]interface{}{"a": 12, "b": []int{4}}}, + // errors + {[]interface{}{5, "b"}, false}, + {[]interface{}{"a", "b", "c"}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.values) + + result, err := ns.Dictionary(test.values...) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestEchoParam(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + a interface{} + key interface{} + expect interface{} + }{ + {[]int{1, 2, 3}, 1, int64(2)}, + {[]uint{1, 2, 3}, 1, uint64(2)}, + {[]float64{1.1, 2.2, 3.3}, 1, float64(2.2)}, + {[]string{"foo", "bar", "baz"}, 1, "bar"}, + {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, 1, ""}, + {map[string]int{"foo": 1, "bar": 2, "baz": 3}, "bar", int64(2)}, + {map[string]uint{"foo": 1, "bar": 2, "baz": 3}, "bar", uint64(2)}, + {map[string]float64{"foo": 1.1, "bar": 2.2, "baz": 3.3}, "bar", float64(2.2)}, + {map[string]string{"foo": "FOO", "bar": "BAR", "baz": "BAZ"}, "bar", "BAR"}, + {map[string]TstX{"foo": {A: "a", B: "b"}, "bar": {A: "c", B: "d"}, "baz": {A: "e", B: "f"}}, "bar", ""}, + {map[string]interface{}{"foo": nil}, "foo", ""}, + {(*[]string)(nil), "bar", ""}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result := ns.EchoParam(test.a, test.key) + + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestFirst(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + limit interface{} + seq interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c"}, []string{"a", "b"}}, + {int32(3), []string{"a", "b"}, []string{"a", "b"}}, + {int64(2), []int{100, 200, 300}, []int{100, 200}}, + {100, []int{100, 200}, []int{100, 200}}, + {"1", []int{100, 200, 300}, []int{100}}, + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.First(test.limit, test.seq) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestIn(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + l1 interface{} + l2 interface{} + expect bool + }{ + {[]string{"a", "b", "c"}, "b", true}, + {[]interface{}{"a", "b", "c"}, "b", true}, + {[]interface{}{"a", "b", "c"}, "d", false}, + {[]string{"a", "b", "c"}, "d", false}, + {[]string{"a", "12", "c"}, 12, false}, + {[]string{"a", "b", "c"}, nil, false}, + {[]int{1, 2, 4}, 2, true}, + {[]interface{}{1, 2, 4}, 2, true}, + {[]interface{}{1, 2, 4}, nil, false}, + {[]interface{}{nil}, nil, false}, + {[]int{1, 2, 4}, 3, false}, + {[]float64{1.23, 2.45, 4.67}, 1.23, true}, + {[]float64{1.234567, 2.45, 4.67}, 1.234568, false}, + {"this substring should be found", "substring", true}, + {"this substring should not be found", "subseastring", false}, + {nil, "foo", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result := ns.In(test.l1, test.l2) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestIntersect(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + l1, l2 interface{} + expect interface{} + }{ + {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b"}}, + {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b"}}, + {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{}}, + {[]string{}, []string{}, []string{}}, + {nil, nil, make([]interface{}, 0)}, + {[]string{"1", "2"}, []int{1, 2}, []string{}}, + {[]int{1, 2}, []string{"1", "2"}, []int{}}, + {[]int{1, 2, 4}, []int{2, 4}, []int{2, 4}}, + {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4}}, + {[]int{1, 2, 4}, []int{3, 6}, []int{}}, + {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4}}, + // errors + {"not array or slice", []string{"a"}, false}, + {[]string{"a"}, "not array or slice", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Intersect(test.l1, test.l2) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestIsSet(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + a interface{} + key interface{} + expect bool + isErr bool + errStr string + }{ + {[]interface{}{1, 2, 3, 5}, 2, true, false, ""}, + {[]interface{}{1, 2, 3, 5}, 22, false, false, ""}, + + {map[string]interface{}{"a": 1, "b": 2}, "b", true, false, ""}, + {map[string]interface{}{"a": 1, "b": 2}, "bc", false, false, ""}, + + {time.Now(), "Day", false, false, ""}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.IsSet(test.a, test.key) + if test.isErr { + assert.EqualError(t, err, test.errStr, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestLast(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + limit interface{} + seq interface{} + expect interface{} + }{ + {int(2), []string{"a", "b", "c"}, []string{"b", "c"}}, + {int32(3), []string{"a", "b"}, []string{"a", "b"}}, + {int64(2), []int{100, 200, 300}, []int{200, 300}}, + {100, []int{100, 200}, []int{100, 200}}, + {"1", []int{100, 200, 300}, []int{300}}, + // errors + {int64(-1), []int{100, 200, 300}, false}, + {"noint", []int{100, 200, 300}, false}, + {1, nil, false}, + {nil, []int{100}, false}, + {1, t, false}, + {1, (*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Last(test.limit, test.seq) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestQuerify(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + params []interface{} + expect interface{} + }{ + {[]interface{}{"a", "b"}, "a=b"}, + {[]interface{}{"a", "b", "c", "d", "f", " &"}, `a=b&c=d&f=+%26`}, + // errors + {[]interface{}{5, "b"}, false}, + {[]interface{}{"a", "b", "c"}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.params) + + result, err := ns.Querify(test.params...) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSeq(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + args []interface{} + expect interface{} + }{ + {[]interface{}{-2, 5}, []int{-2, -1, 0, 1, 2, 3, 4, 5}}, + {[]interface{}{1, 2, 4}, []int{1, 3}}, + {[]interface{}{1}, []int{1}}, + {[]interface{}{3}, []int{1, 2, 3}}, + {[]interface{}{3.2}, []int{1, 2, 3}}, + {[]interface{}{0}, []int{}}, + {[]interface{}{-1}, []int{-1}}, + {[]interface{}{-3}, []int{-1, -2, -3}}, + {[]interface{}{3, -2}, []int{3, 2, 1, 0, -1, -2}}, + {[]interface{}{6, -2, 2}, []int{6, 4, 2}}, + // errors + {[]interface{}{1, 0, 2}, false}, + {[]interface{}{1, -1, 2}, false}, + {[]interface{}{2, 1, 1}, false}, + {[]interface{}{2, 1, 1, 1}, false}, + {[]interface{}{2001}, false}, + {[]interface{}{}, false}, + {[]interface{}{0, -1000000}, false}, + {[]interface{}{tstNoStringer{}}, false}, + {nil, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Seq(test.args...) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestShuffle(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + seq interface{} + success bool + }{ + {[]string{"a", "b", "c", "d"}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200}, true}, + {[]string{"a", "b"}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100, 200, 300}, true}, + {[]int{100}, true}, + // errors + {nil, false}, + {t, false}, + {(*string)(nil), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Shuffle(test.seq) + + if !test.success { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + + resultv := reflect.ValueOf(result) + seqv := reflect.ValueOf(test.seq) + + assert.Equal(t, resultv.Len(), seqv.Len(), errMsg) + } +} + +func TestShuffleRandomising(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + // Note that this test can fail with false negative result if the shuffle + // of the sequence happens to be the same as the original sequence. However + // the propability of the event is 10^-158 which is negligible. + seqLen := 100 + rand.Seed(time.Now().UTC().UnixNano()) + + for _, test := range []struct { + seq []int + }{ + {rand.Perm(seqLen)}, + } { + result, err := ns.Shuffle(test.seq) + resultv := reflect.ValueOf(result) + + require.NoError(t, err) + + allSame := true + for i, v := range test.seq { + allSame = allSame && (resultv.Index(i).Interface() == v) + } + + assert.False(t, allSame, "Expected sequence to be shuffled but was in the same order") + } +} + +func TestSlice(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + args []interface{} + }{ + {[]interface{}{"a", "b"}}, + // errors + {[]interface{}{5, "b"}}, + {[]interface{}{tstNoStringer{}}}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.args) + + result := ns.Slice(test.args...) + + assert.Equal(t, test.args, result, errMsg) + } +} + +func TestUnion(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + l1 interface{} + l2 interface{} + expect interface{} + isErr bool + }{ + {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b", "c"}, false}, + {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{"a", "b", "c", "d", "e"}, false}, + {[]string{}, []string{}, []string{}, false}, + {[]string{"a", "b"}, nil, []string{"a", "b"}, false}, + {nil, []string{"a", "b"}, []string{"a", "b"}, false}, + {nil, nil, make([]interface{}, 0), true}, + {[]string{"1", "2"}, []int{1, 2}, make([]string, 0), false}, + {[]int{1, 2}, []string{"1", "2"}, make([]int, 0), false}, + {[]int{1, 2, 3}, []int{3, 4, 5}, []int{1, 2, 3, 4, 5}, false}, + {[]int{1, 2, 3}, []int{1, 2, 3}, []int{1, 2, 3}, false}, + {[]int{1, 2, 4}, []int{2, 4}, []int{1, 2, 4}, false}, + {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4, 1}, false}, + {[]int{1, 2, 4}, []int{3, 6}, []int{1, 2, 4, 3, 6}, false}, + {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4, 1.1}, false}, + // errors + {"not array or slice", []string{"a"}, false, true}, + {[]string{"a"}, "not array or slice", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Union(test.l1, test.l2) + if test.isErr { + assert.Error(t, err, errMsg) + continue + } + + assert.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func (x *TstX) TstRp() string { + return "r" + x.A +} + +func (x TstX) TstRv() string { + return "r" + x.B +} + +func (x TstX) unexportedMethod() string { + return x.unexported +} + +func (x TstX) MethodWithArg(s string) string { + return s +} + +func (x TstX) MethodReturnNothing() {} + +func (x TstX) MethodReturnErrorOnly() error { + return errors.New("some error occurred") +} + +func (x TstX) MethodReturnTwoValues() (string, string) { + return "foo", "bar" +} + +func (x TstX) MethodReturnValueWithError() (string, error) { + return "", errors.New("some error occurred") +} + +func (x TstX) String() string { + return fmt.Sprintf("A: %s, B: %s", x.A, x.B) +} + +type TstX struct { + A, B string + unexported string +} diff --git a/tpl/collections/index.go b/tpl/collections/index.go new file mode 100644 index 000000000..b08151188 --- /dev/null +++ b/tpl/collections/index.go @@ -0,0 +1,107 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" +) + +// Index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +// +// Copied from Go stdlib src/text/template/funcs.go. +// +// We deviate from the stdlib due to https://github.com/golang/go/issues/14751. +// +// TODO(moorereason): merge upstream changes. +func (ns *Namespace) Index(item interface{}, indices ...interface{}) (interface{}, error) { + v := reflect.ValueOf(item) + if !v.IsValid() { + return nil, errors.New("index of untyped nil") + } + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return nil, errors.New("index of nil pointer") + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + case reflect.Invalid: + return nil, errors.New("cannot index slice/array with nil") + default: + return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) + } + if x < 0 || x >= int64(v.Len()) { + // We deviate from stdlib here. Don't return an error if the + // index is out of range. + return nil, nil + } + v = v.Index(int(x)) + case reflect.Map: + index, err := prepareArg(index, v.Type().Key()) + if err != nil { + return nil, err + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + case reflect.Invalid: + // the loop holds invariant: v.IsValid() + panic("unreachable") + default: + return nil, fmt.Errorf("can't index item of type %s", v.Type()) + } + } + return v.Interface(), nil +} + +// prepareArg checks if value can be used as an argument of type argType, and +// converts an invalid value to appropriate zero if possible. +// +// Copied from Go stdlib src/text/template/funcs.go. +func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { + if !value.IsValid() { + if !canBeNil(argType) { + return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) + } + value = reflect.Zero(argType) + } + if !value.Type().AssignableTo(argType) { + return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) + } + return value, nil +} + +// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. +// +// Copied from Go stdlib src/text/template/exec.go. +func canBeNil(typ reflect.Type) bool { + switch typ.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return true + } + return false +} diff --git a/tpl/collections/index_test.go b/tpl/collections/index_test.go new file mode 100644 index 000000000..211a24e12 --- /dev/null +++ b/tpl/collections/index_test.go @@ -0,0 +1,60 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "fmt" + "testing" + + "github.com/spf13/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIndex(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + for i, test := range []struct { + item interface{} + indices []interface{} + expect interface{} + isErr bool + }{ + {[]int{0, 1}, []interface{}{0}, 0, false}, + {[]int{0, 1}, []interface{}{9}, nil, false}, // index out of range + {[]uint{0, 1}, nil, []uint{0, 1}, false}, + {[][]int{[]int{1, 2}, []int{3, 4}}, []interface{}{0, 0}, 1, false}, + {map[int]int{1: 10, 2: 20}, []interface{}{1}, 10, false}, + {map[int]int{1: 10, 2: 20}, []interface{}{0}, 0, false}, + // errors + {nil, nil, nil, true}, + {[]int{0, 1}, []interface{}{"1"}, nil, true}, + {[]int{0, 1}, []interface{}{nil}, nil, true}, + {tstNoStringer{}, []interface{}{0}, nil, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Index(test.item, test.indices...) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go new file mode 100644 index 000000000..313ba1e83 --- /dev/null +++ b/tpl/collections/sort.go @@ -0,0 +1,159 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "reflect" + "sort" + "strings" + + "github.com/spf13/cast" + "github.com/spf13/hugo/tpl/compare" +) + +// Sort returns a sorted sequence. +func (ns *Namespace) Sort(seq interface{}, args ...interface{}) (interface{}, error) { + if seq == nil { + return nil, errors.New("sequence must be provided") + } + + seqv := reflect.ValueOf(seq) + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice, reflect.Map: + // ok + default: + return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String()) + } + + // Create a list of pairs that will be used to do the sort + p := pairList{SortAsc: true, SliceType: reflect.SliceOf(seqv.Type().Elem())} + p.Pairs = make([]pair, seqv.Len()) + + var sortByField string + for i, l := range args { + dStr, err := cast.ToStringE(l) + switch { + case i == 0 && err != nil: + sortByField = "" + case i == 0 && err == nil: + sortByField = dStr + case i == 1 && err == nil && dStr == "desc": + p.SortAsc = false + case i == 1: + p.SortAsc = true + } + } + path := strings.Split(strings.Trim(sortByField, "."), ".") + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < seqv.Len(); i++ { + p.Pairs[i].Value = seqv.Index(i) + if sortByField == "" || sortByField == "value" { + p.Pairs[i].Key = p.Pairs[i].Value + } else { + v := p.Pairs[i].Value + var err error + for _, elemName := range path { + v, err = evaluateSubElem(v, elemName) + if err != nil { + return nil, err + } + } + p.Pairs[i].Key = v + } + } + + case reflect.Map: + keys := seqv.MapKeys() + for i := 0; i < seqv.Len(); i++ { + p.Pairs[i].Value = seqv.MapIndex(keys[i]) + if sortByField == "" { + p.Pairs[i].Key = keys[i] + } else if sortByField == "value" { + p.Pairs[i].Key = p.Pairs[i].Value + } else { + v := p.Pairs[i].Value + var err error + for _, elemName := range path { + v, err = evaluateSubElem(v, elemName) + if err != nil { + return nil, err + } + } + p.Pairs[i].Key = v + } + } + } + return p.sort(), nil +} + +// Credit for pair sorting method goes to Andrew Gerrand +// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw +// A data structure to hold a key/value pair. +type pair struct { + Key reflect.Value + Value reflect.Value +} + +// A slice of pairs that implements sort.Interface to sort by Value. +type pairList struct { + Pairs []pair + SortAsc bool + SliceType reflect.Type +} + +func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] } +func (p pairList) Len() int { return len(p.Pairs) } +func (p pairList) Less(i, j int) bool { + iv := p.Pairs[i].Key + jv := p.Pairs[j].Key + + if iv.IsValid() { + if jv.IsValid() { + // can only call Interface() on valid reflect Values + return compare.Lt(iv.Interface(), jv.Interface()) + } + // if j is invalid, test i against i's zero value + return compare.Lt(iv.Interface(), reflect.Zero(iv.Type())) + } + + if jv.IsValid() { + // if i is invalid, test j against j's zero value + return compare.Lt(reflect.Zero(jv.Type()), jv.Interface()) + } + + return false +} + +// sorts a pairList and returns a slice of sorted values +func (p pairList) sort() interface{} { + if p.SortAsc { + sort.Sort(p) + } else { + sort.Sort(sort.Reverse(p)) + } + sorted := reflect.MakeSlice(p.SliceType, len(p.Pairs), len(p.Pairs)) + for i, v := range p.Pairs { + sorted.Index(i).Set(v.Value) + } + + return sorted.Interface() +} diff --git a/tpl/collections/sort_test.go b/tpl/collections/sort_test.go new file mode 100644 index 000000000..93a4d9da2 --- /dev/null +++ b/tpl/collections/sort_test.go @@ -0,0 +1,237 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "reflect" + "testing" + + "github.com/spf13/hugo/deps" +) + +func TestSort(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + type ts struct { + MyInt int + MyFloat float64 + MyString string + } + type mid struct { + Tst TstX + } + + for i, test := range []struct { + seq interface{} + sortByField interface{} + sortAsc string + expect interface{} + }{ + {[]string{"class1", "class2", "class3"}, nil, "asc", []string{"class1", "class2", "class3"}}, + {[]string{"class3", "class1", "class2"}, nil, "asc", []string{"class1", "class2", "class3"}}, + {[]int{1, 2, 3, 4, 5}, nil, "asc", []int{1, 2, 3, 4, 5}}, + {[]int{5, 4, 3, 1, 2}, nil, "asc", []int{1, 2, 3, 4, 5}}, + // test sort key parameter is focibly set empty + {[]string{"class3", "class1", "class2"}, map[int]string{1: "a"}, "asc", []string{"class1", "class2", "class3"}}, + // test map sorting by keys + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []int{10, 20, 30, 40, 50}}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []int{30, 20, 10, 40, 50}}, + {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, + {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, nil, "asc", []string{"50", "40", "10", "30", "20"}}, + {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, + {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, + // test map sorting by value + {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, + {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, + // test map sorting by field value + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyInt", + "asc", + []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, + }, + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyFloat", + "asc", + []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, + }, + { + map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, + "MyString", + "asc", + []ts{{50, 50.5, "fifty"}, {40, 40.5, "forty"}, {10, 10.5, "ten"}, {30, 30.5, "thirty"}, {20, 20.5, "twenty"}}, + }, + // test sort desc + {[]string{"class1", "class2", "class3"}, "value", "desc", []string{"class3", "class2", "class1"}}, + {[]string{"class3", "class1", "class2"}, "value", "desc", []string{"class3", "class2", "class1"}}, + // test sort by struct's method + { + []TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, + "TstRv", + "asc", + []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + { + []*TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, + "TstRp", + "asc", + []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + // test map sorting by struct's method + { + map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, + "TstRv", + "asc", + []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + { + map[string]*TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, + "TstRp", + "asc", + []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, + }, + // test sort by dot chaining key argument + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + ".foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.TstRv", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + []map[string]*TstX{{"foo": &TstX{A: "e", B: "f"}}, {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}}, + "foo.TstRp", + "asc", + []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, + }, + { + []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.A", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + { + []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.TstRv", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + // test map sorting by dot chaining key argument + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + ".foo.A", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.TstRv", + "asc", + []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]*TstX{"1": {"foo": &TstX{A: "e", B: "f"}}, "2": {"foo": &TstX{A: "a", B: "b"}}, "3": {"foo": &TstX{A: "c", B: "d"}}}, + "foo.TstRp", + "asc", + []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, + }, + { + map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.A", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + { + map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, + "foo.Tst.TstRv", + "asc", + []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, + }, + // interface slice with missing elements + { + []interface{}{ + map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, + map[interface{}]interface{}{"Title": "Bar"}, + map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, + }, + "Weight", + "asc", + []interface{}{ + map[interface{}]interface{}{"Title": "Bar"}, + map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, + map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, + }, + }, + // test error cases + {(*[]TstX)(nil), nil, "asc", false}, + {TstX{A: "a", B: "b"}, nil, "asc", false}, + { + []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, + "foo.NotAvailable", + "asc", + false, + }, + { + map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, + "foo.NotAvailable", + "asc", + false, + }, + {nil, nil, "asc", false}, + } { + var result interface{} + var err error + if test.sortByField == nil { + result, err = ns.Sort(test.seq) + } else { + result, err = ns.Sort(test.seq, test.sortByField, test.sortAsc) + } + + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Sort didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(result, test.expect) { + t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, test.seq, test.sortByField, result, test.expect) + } + } + } +} diff --git a/tpl/collections/where.go b/tpl/collections/where.go new file mode 100644 index 000000000..f34494eb3 --- /dev/null +++ b/tpl/collections/where.go @@ -0,0 +1,421 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "errors" + "fmt" + "reflect" + "strings" + "time" +) + +// Where returns a filtered subset of a given data type. +func (ns *Namespace) Where(seq, key interface{}, args ...interface{}) (interface{}, error) { + seqv, isNil := indirect(reflect.ValueOf(seq)) + if isNil { + return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) + } + + mv, op, err := parseWhereArgs(args...) + if err != nil { + return nil, err + } + + var path []string + kv := reflect.ValueOf(key) + if kv.Kind() == reflect.String { + path = strings.Split(strings.Trim(kv.String(), "."), ".") + } + + switch seqv.Kind() { + case reflect.Array, reflect.Slice: + return ns.checkWhereArray(seqv, kv, mv, path, op) + case reflect.Map: + return ns.checkWhereMap(seqv, kv, mv, path, op) + default: + return nil, fmt.Errorf("can't iterate over %v", seq) + } +} + +func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error) { + v, vIsNil := indirect(v) + if !v.IsValid() { + vIsNil = true + } + mv, mvIsNil := indirect(mv) + if !mv.IsValid() { + mvIsNil = true + } + if vIsNil || mvIsNil { + switch op { + case "", "=", "==", "eq": + return vIsNil == mvIsNil, nil + case "!=", "<>", "ne": + return vIsNil != mvIsNil, nil + } + return false, nil + } + + if v.Kind() == reflect.Bool && mv.Kind() == reflect.Bool { + switch op { + case "", "=", "==", "eq": + return v.Bool() == mv.Bool(), nil + case "!=", "<>", "ne": + return v.Bool() != mv.Bool(), nil + } + return false, nil + } + + var ivp, imvp *int64 + var svp, smvp *string + var slv, slmv interface{} + var ima []int64 + var sma []string + if mv.Type() == v.Type() { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + imv := mv.Int() + imvp = &imv + case reflect.String: + sv := v.String() + svp = &sv + smv := mv.String() + smvp = &smv + case reflect.Struct: + switch v.Type() { + case timeType: + iv := toTimeUnix(v) + ivp = &iv + imv := toTimeUnix(mv) + imvp = &imv + } + case reflect.Array, reflect.Slice: + slv = v.Interface() + slmv = mv.Interface() + } + } else { + if mv.Kind() != reflect.Array && mv.Kind() != reflect.Slice { + return false, nil + } + + if mv.Len() == 0 { + return false, nil + } + + if v.Kind() != reflect.Interface && mv.Type().Elem().Kind() != reflect.Interface && mv.Type().Elem() != v.Type() { + return false, nil + } + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + for i := 0; i < mv.Len(); i++ { + if anInt := toInt(mv.Index(i)); anInt != -1 { + ima = append(ima, anInt) + } + + } + case reflect.String: + sv := v.String() + svp = &sv + for i := 0; i < mv.Len(); i++ { + if aString := toString(mv.Index(i)); aString != "" { + sma = append(sma, aString) + } + } + case reflect.Struct: + switch v.Type() { + case timeType: + iv := toTimeUnix(v) + ivp = &iv + for i := 0; i < mv.Len(); i++ { + ima = append(ima, toTimeUnix(mv.Index(i))) + } + } + } + } + + switch op { + case "", "=", "==", "eq": + if ivp != nil && imvp != nil { + return *ivp == *imvp, nil + } else if svp != nil && smvp != nil { + return *svp == *smvp, nil + } + case "!=", "<>", "ne": + if ivp != nil && imvp != nil { + return *ivp != *imvp, nil + } else if svp != nil && smvp != nil { + return *svp != *smvp, nil + } + case ">=", "ge": + if ivp != nil && imvp != nil { + return *ivp >= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp >= *smvp, nil + } + case ">", "gt": + if ivp != nil && imvp != nil { + return *ivp > *imvp, nil + } else if svp != nil && smvp != nil { + return *svp > *smvp, nil + } + case "<=", "le": + if ivp != nil && imvp != nil { + return *ivp <= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp <= *smvp, nil + } + case "<", "lt": + if ivp != nil && imvp != nil { + return *ivp < *imvp, nil + } else if svp != nil && smvp != nil { + return *svp < *smvp, nil + } + case "in", "not in": + var r bool + if ivp != nil && len(ima) > 0 { + r = ns.In(ima, *ivp) + } else if svp != nil { + if len(sma) > 0 { + r = ns.In(sma, *svp) + } else if smvp != nil { + r = ns.In(*smvp, *svp) + } + } else { + return false, nil + } + if op == "not in" { + return !r, nil + } + return r, nil + case "intersect": + r, err := ns.Intersect(slv, slmv) + if err != nil { + return false, err + } + + if reflect.TypeOf(r).Kind() == reflect.Slice { + s := reflect.ValueOf(r) + + if s.Len() > 0 { + return true, nil + } + return false, nil + } + return false, errors.New("invalid intersect values") + default: + return false, errors.New("no such operator") + } + return false, nil +} + +func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) { + if !obj.IsValid() { + return zero, errors.New("can't evaluate an invalid value") + } + typ := obj.Type() + obj, isNil := indirect(obj) + + // first, check whether obj has a method. In this case, obj is + // an interface, a struct or its pointer. If obj is a struct, + // to check all T and *T method, use obj pointer type Value + objPtr := obj + if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { + objPtr = objPtr.Addr() + } + mt, ok := objPtr.Type().MethodByName(elemName) + if ok { + if mt.PkgPath != "" { + return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ) + } + // struct pointer has one receiver argument and interface doesn't have an argument + if mt.Type.NumIn() > 1 || mt.Type.NumOut() == 0 || mt.Type.NumOut() > 2 { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + res := objPtr.Method(mt.Index).Call([]reflect.Value{}) + if len(res) == 2 && !res[1].IsNil() { + return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error)) + } + return res[0], nil + } + + // elemName isn't a method so next start to check whether it is + // a struct field or a map value. In both cases, it mustn't be + // a nil value + if isNil { + return zero, fmt.Errorf("can't evaluate a nil pointer of type %s by a struct field or map key name %s", typ, elemName) + } + switch obj.Kind() { + case reflect.Struct: + ft, ok := obj.Type().FieldByName(elemName) + if ok { + if ft.PkgPath != "" && !ft.Anonymous { + return zero, fmt.Errorf("%s is an unexported field of struct type %s", elemName, typ) + } + return obj.FieldByIndex(ft.Index), nil + } + return zero, fmt.Errorf("%s isn't a field of struct type %s", elemName, typ) + case reflect.Map: + kv := reflect.ValueOf(elemName) + if kv.Type().AssignableTo(obj.Type().Key()) { + return obj.MapIndex(kv), nil + } + return zero, fmt.Errorf("%s isn't a key of map type %s", elemName, typ) + } + return zero, fmt.Errorf("%s is neither a struct field, a method nor a map element of type %s", elemName, typ) +} + +// parseWhereArgs parses the end arguments to the where function. Return a +// match value and an operator, if one is defined. +func parseWhereArgs(args ...interface{}) (mv reflect.Value, op string, err error) { + switch len(args) { + case 1: + mv = reflect.ValueOf(args[0]) + case 2: + var ok bool + if op, ok = args[0].(string); !ok { + err = errors.New("operator argument must be string type") + return + } + op = strings.TrimSpace(strings.ToLower(op)) + mv = reflect.ValueOf(args[1]) + default: + err = errors.New("can't evaluate the array by no match argument or more than or equal to two arguments") + } + return +} + +// checkWhereArray handles the where-matching logic when the seqv value is an +// Array or Slice. +func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { + rv := reflect.MakeSlice(seqv.Type(), 0, 0) + for i := 0; i < seqv.Len(); i++ { + var vvv reflect.Value + rvv := seqv.Index(i) + if kv.Kind() == reflect.String { + vvv = rvv + for _, elemName := range path { + var err error + vvv, err = evaluateSubElem(vvv, elemName) + if err != nil { + return nil, err + } + } + } else { + vv, _ := indirect(rvv) + if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { + vvv = vv.MapIndex(kv) + } + } + + if ok, err := ns.checkCondition(vvv, mv, op); ok { + rv = reflect.Append(rv, rvv) + } else if err != nil { + return nil, err + } + } + return rv.Interface(), nil +} + +// checkWhereMap handles the where-matching logic when the seqv value is a Map. +func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { + rv := reflect.MakeMap(seqv.Type()) + keys := seqv.MapKeys() + for _, k := range keys { + elemv := seqv.MapIndex(k) + switch elemv.Kind() { + case reflect.Array, reflect.Slice: + r, err := ns.checkWhereArray(elemv, kv, mv, path, op) + if err != nil { + return nil, err + } + + switch rr := reflect.ValueOf(r); rr.Kind() { + case reflect.Slice: + if rr.Len() > 0 { + rv.SetMapIndex(k, elemv) + } + } + case reflect.Interface: + elemvv, isNil := indirect(elemv) + if isNil { + continue + } + + switch elemvv.Kind() { + case reflect.Array, reflect.Slice: + r, err := ns.checkWhereArray(elemvv, kv, mv, path, op) + if err != nil { + return nil, err + } + + switch rr := reflect.ValueOf(r); rr.Kind() { + case reflect.Slice: + if rr.Len() > 0 { + rv.SetMapIndex(k, elemv) + } + } + } + } + } + return rv.Interface(), nil +} + +// toInt returns the int value if possible, -1 if not. +func toInt(v reflect.Value) int64 { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() + case reflect.Interface: + return toInt(v.Elem()) + } + return -1 +} + +// toString returns the string value if possible, "" if not. +func toString(v reflect.Value) string { + switch v.Kind() { + case reflect.String: + return v.String() + case reflect.Interface: + return toString(v.Elem()) + } + return "" +} + +var ( + zero reflect.Value + errorType = reflect.TypeOf((*error)(nil)).Elem() + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() +) + +func toTimeUnix(v reflect.Value) int64 { + if v.Kind() == reflect.Interface { + return toTimeUnix(v.Elem()) + } + if v.Type() != timeType { + panic("coding error: argument must be time.Time type reflect Value") + } + return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() +} diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go new file mode 100644 index 000000000..aee56757e --- /dev/null +++ b/tpl/collections/where_test.go @@ -0,0 +1,606 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/spf13/hugo/deps" +) + +func TestWhere(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + type Mid struct { + Tst TstX + } + + d1 := time.Now() + d2 := d1.Add(1 * time.Hour) + d3 := d2.Add(1 * time.Hour) + d4 := d3.Add(1 * time.Hour) + d5 := d4.Add(1 * time.Hour) + d6 := d5.Add(1 * time.Hour) + + for i, test := range []struct { + seq interface{} + key interface{} + op string + match interface{} + expect interface{} + }{ + { + seq: []map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}, + }, + key: "b", match: 4, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []TstX{ + {A: "e", B: "f"}, + }, + }, + { + seq: []*map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []*map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + seq: []*TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []*TstX{ + {A: "e", B: "f"}, + }, + }, + { + seq: []*TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, + }, + key: "TstRp", match: "rc", + expect: []*TstX{ + {A: "c", B: "d"}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, + }, + key: "TstRv", match: "rc", + expect: []TstX{ + {A: "e", B: "c"}, + }, + }, + { + seq: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: ".foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRv", match: "rd", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]*TstX{ + {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRp", match: "rc", + expect: []map[string]*TstX{ + {"foo": &TstX{A: "c", B: "d"}}, + }, + }, + { + seq: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.B", match: "d", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + seq: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.TstRv", match: "rd", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + seq: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": &Mid{Tst: TstX{A: "e", B: "f"}}}, + }, + key: "foo.Tst.TstRp", match: "rc", + expect: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: 3, + expect: []map[string]int{ + {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "!=", match: "f", + expect: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: []int{3, 4, 5}, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + seq: []map[string][]string{ + {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"G", "H", "I"}, "b": []string{"J", "K", "L"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, + }, + key: "b", op: "intersect", match: []string{"D", "P", "Q"}, + expect: []map[string][]string{ + {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, + }, + }, + { + seq: []map[string][]int{ + {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, {"a": []int{13, 14, 15}, "b": []int{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int{4, 10, 12}, + expect: []map[string][]int{ + {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int8{ + {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, {"a": []int8{13, 14, 15}, "b": []int8{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int8{4, 10, 12}, + expect: []map[string][]int8{ + {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int16{ + {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, {"a": []int16{13, 14, 15}, "b": []int16{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int16{4, 10, 12}, + expect: []map[string][]int16{ + {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int32{ + {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, {"a": []int32{13, 14, 15}, "b": []int32{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int32{4, 10, 12}, + expect: []map[string][]int32{ + {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, + }, + }, + { + seq: []map[string][]int64{ + {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, {"a": []int64{13, 14, 15}, "b": []int64{16, 17, 18}}, + }, + key: "b", op: "intersect", match: []int64{4, 10, 12}, + expect: []map[string][]int64{ + {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, + }, + }, + { + seq: []map[string][]float32{ + {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, {"a": []float32{13.0, 14.0, 15.0}, "b": []float32{16.0, 17.0, 18.0}}, + }, + key: "b", op: "intersect", match: []float32{4, 10, 12}, + expect: []map[string][]float32{ + {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, + }, + }, + { + seq: []map[string][]float64{ + {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, {"a": []float64{13.0, 14.0, 15.0}, "b": []float64{16.0, 17.0, 18.0}}, + }, + key: "b", op: "intersect", match: []float64{4, 10, 12}, + expect: []map[string][]float64{ + {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: ns.Slice(3, 4, 5), + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + seq: []map[string]time.Time{ + {"a": d1, "b": d2}, {"a": d3, "b": d4}, {"a": d5, "b": d6}, + }, + key: "b", op: "in", match: ns.Slice(d3, d4, d5), + expect: []map[string]time.Time{ + {"a": d3, "b": d4}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: []string{"c", "d", "e"}, + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: ns.Slice("c", t, "d", "e"), + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: "", match: nil, + expect: []map[string]int{ + {"a": 3}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: "!=", match: nil, + expect: []map[string]int{ + {"a": 1, "b": 2}, {"a": 5, "b": 6}, + }, + }, + { + seq: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: nil, + expect: []map[string]int{}, + }, + { + seq: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: "", match: true, + expect: []map[string]bool{ + {"c": true, "b": true}, + }, + }, + { + seq: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: "!=", match: true, + expect: []map[string]bool{ + {"a": true, "b": false}, {"d": true, "b": false}, + }, + }, + { + seq: []map[string]bool{ + {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, + }, + key: "b", op: ">", match: false, + expect: []map[string]bool{}, + }, + {seq: (*[]TstX)(nil), key: "A", match: "a", expect: false}, + {seq: TstX{A: "a", B: "b"}, key: "A", match: "a", expect: false}, + {seq: []map[string]*TstX{{"foo": nil}}, key: "foo.B", match: "d", expect: false}, + { + seq: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "op", match: "f", + expect: false, + }, + { + seq: map[string]interface{}{ + "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + key: "b", op: "in", match: ns.Slice(3, 4, 5), + expect: map[string]interface{}{ + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + }, + }, + { + seq: map[string]interface{}{ + "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + key: "b", op: ">", match: 3, + expect: map[string]interface{}{ + "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, + "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, + }, + }, + } { + var results interface{} + var err error + + if len(test.op) > 0 { + results, err = ns.Where(test.seq, test.key, test.op, test.match) + } else { + results, err = ns.Where(test.seq, test.key, test.match) + } + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Where didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, test.expect) { + t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, test.key, test.match, results, test.expect) + } + } + } + + var err error + _, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1) + if err == nil { + t.Errorf("Where called with none string op value didn't return an expected error") + } + + _, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2) + if err == nil { + t.Errorf("Where called with more than two variable arguments didn't return an expected error") + } + + _, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a") + if err == nil { + t.Errorf("Where called with no variable arguments didn't return an expected error") + } +} + +func TestCheckCondition(t *testing.T) { + t.Parallel() + + ns := New(&deps.Deps{}) + + type expect struct { + result bool + isError bool + } + + for i, test := range []struct { + value reflect.Value + match reflect.Value + op string + expect + }{ + {reflect.ValueOf(123), reflect.ValueOf(123), "", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("foo"), "", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "", + expect{true, false}, + }, + {reflect.ValueOf(true), reflect.ValueOf(true), "", expect{true, false}}, + {reflect.ValueOf(nil), reflect.ValueOf(nil), "", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(456), "!=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), "!=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + "!=", + expect{true, false}, + }, + {reflect.ValueOf(true), reflect.ValueOf(false), "!=", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(nil), "!=", expect{true, false}}, + {reflect.ValueOf(456), reflect.ValueOf(123), ">=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + ">=", + expect{true, false}, + }, + {reflect.ValueOf(456), reflect.ValueOf(123), ">", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + ">", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf(456), "<=", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<=", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "<=", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf(456), "<", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + "<", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf([]int{123, 45, 678}), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"foo", "bar", "baz"}), "in", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf([]time.Time{ + time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.June, 26, 19, 18, 56, 12345, time.UTC), + }), + "in", + expect{true, false}, + }, + {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, + { + reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), + reflect.ValueOf([]time.Time{ + time.Date(2015, time.February, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.March, 26, 19, 18, 56, 12345, time.UTC), + time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), + }), + "not in", + expect{true, false}, + }, + {reflect.ValueOf("foo"), reflect.ValueOf("bar-foo-baz"), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar--baz"), "not in", expect{true, false}}, + {reflect.Value{}, reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.Value{}, "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf((*TstX)(nil)), "", expect{false, false}}, + {reflect.ValueOf(true), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(true), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(map[int]string{}), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]int{1, 2}), "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf((*TstX)(nil)), ">", expect{false, false}}, + {reflect.ValueOf(true), reflect.ValueOf(false), ">", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{}), "in", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf(123), "op", expect{false, true}}, + } { + result, err := ns.checkCondition(test.value, test.match, test.op) + if test.expect.isError { + if err == nil { + t.Errorf("[%d] checkCondition didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result != test.expect.result { + t.Errorf("[%d] check condition %v %s %v, got %v but expected %v", i, test.value, test.op, test.match, result, test.expect.result) + } + } + } +} + +func TestEvaluateSubElem(t *testing.T) { + t.Parallel() + tstx := TstX{A: "foo", B: "bar"} + var inner struct { + S fmt.Stringer + } + inner.S = tstx + interfaceValue := reflect.ValueOf(&inner).Elem().Field(0) + + for i, test := range []struct { + value reflect.Value + key string + expect interface{} + }{ + {reflect.ValueOf(tstx), "A", "foo"}, + {reflect.ValueOf(&tstx), "TstRp", "rfoo"}, + {reflect.ValueOf(tstx), "TstRv", "rbar"}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1, "foo"}, + {reflect.ValueOf(map[string]string{"key1": "foo", "key2": "bar"}), "key1", "foo"}, + {interfaceValue, "String", "A: foo, B: bar"}, + {reflect.Value{}, "foo", false}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1.2, false}, + {reflect.ValueOf(tstx), "unexported", false}, + {reflect.ValueOf(tstx), "unexportedMethod", false}, + {reflect.ValueOf(tstx), "MethodWithArg", false}, + {reflect.ValueOf(tstx), "MethodReturnNothing", false}, + {reflect.ValueOf(tstx), "MethodReturnErrorOnly", false}, + {reflect.ValueOf(tstx), "MethodReturnTwoValues", false}, + {reflect.ValueOf(tstx), "MethodReturnValueWithError", false}, + {reflect.ValueOf((*TstX)(nil)), "A", false}, + {reflect.ValueOf(tstx), "C", false}, + {reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false}, + {reflect.ValueOf([]string{"foo", "bar"}), "1", false}, + } { + result, err := evaluateSubElem(test.value, test.key) + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] evaluateSubElem didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result.Kind() != reflect.String || result.String() != test.expect { + t.Errorf("[%d] evaluateSubElem with %v got %v but expected %v", i, test.key, result, test.expect) + } + } + } +} diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go new file mode 100644 index 000000000..8b7a96bf0 --- /dev/null +++ b/tpl/compare/compare.go @@ -0,0 +1,198 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "fmt" + "reflect" + "strconv" + "time" +) + +// Default checks whether a given value is set and returns a default value if it +// is not. "Set" in this context means non-zero for numeric types and times; +// non-zero length for strings, arrays, slices, and maps; +// any boolean or struct value; or non-nil for any other types. +func Default(dflt interface{}, given ...interface{}) (interface{}, error) { + // given is variadic because the following construct will not pass a piped + // argument when the key is missing: {{ index . "key" | default "foo" }} + // The Go template will complain that we got 1 argument when we expectd 2. + + if len(given) == 0 { + return dflt, nil + } + if len(given) != 1 { + return nil, fmt.Errorf("wrong number of args for default: want 2 got %d", len(given)+1) + } + + g := reflect.ValueOf(given[0]) + if !g.IsValid() { + return dflt, nil + } + + set := false + + switch g.Kind() { + case reflect.Bool: + set = true + case reflect.String, reflect.Array, reflect.Slice, reflect.Map: + set = g.Len() != 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + set = g.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + set = g.Uint() != 0 + case reflect.Float32, reflect.Float64: + set = g.Float() != 0 + case reflect.Complex64, reflect.Complex128: + set = g.Complex() != 0 + case reflect.Struct: + switch actual := given[0].(type) { + case time.Time: + set = !actual.IsZero() + default: + set = true + } + default: + set = !g.IsNil() + } + + if set { + return given[0], nil + } + + return dflt, nil +} + +// Eq returns the boolean truth of arg1 == arg2. +func Eq(x, y interface{}) bool { + normalize := func(v interface{}) interface{} { + vv := reflect.ValueOf(v) + switch vv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return vv.Int() + case reflect.Float32, reflect.Float64: + return vv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return vv.Uint() + default: + return v + } + } + x = normalize(x) + y = normalize(y) + return reflect.DeepEqual(x, y) +} + +// Ne returns the boolean truth of arg1 != arg2. +func Ne(x, y interface{}) bool { + return !Eq(x, y) +} + +// Ge returns the boolean truth of arg1 >= arg2. +func Ge(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left >= right +} + +// Gt returns the boolean truth of arg1 > arg2. +func Gt(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left > right +} + +// Le returns the boolean truth of arg1 <= arg2. +func Le(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left <= right +} + +// Lt returns the boolean truth of arg1 < arg2. +func Lt(a, b interface{}) bool { + left, right := compareGetFloat(a, b) + return left < right +} + +func compareGetFloat(a interface{}, b interface{}) (float64, float64) { + var left, right float64 + var leftStr, rightStr *string + av := reflect.ValueOf(a) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + left = float64(av.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + left = float64(av.Int()) + case reflect.Float32, reflect.Float64: + left = av.Float() + case reflect.String: + var err error + left, err = strconv.ParseFloat(av.String(), 64) + if err != nil { + str := av.String() + leftStr = &str + } + case reflect.Struct: + switch av.Type() { + case timeType: + left = float64(toTimeUnix(av)) + } + } + + bv := reflect.ValueOf(b) + + switch bv.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + right = float64(bv.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + right = float64(bv.Int()) + case reflect.Float32, reflect.Float64: + right = bv.Float() + case reflect.String: + var err error + right, err = strconv.ParseFloat(bv.String(), 64) + if err != nil { + str := bv.String() + rightStr = &str + } + case reflect.Struct: + switch bv.Type() { + case timeType: + right = float64(toTimeUnix(bv)) + } + } + + switch { + case leftStr == nil || rightStr == nil: + case *leftStr < *rightStr: + return 0, 1 + case *leftStr > *rightStr: + return 1, 0 + default: + return 0, 0 + } + + return left, right +} + +var timeType = reflect.TypeOf((*time.Time)(nil)).Elem() + +func toTimeUnix(v reflect.Value) int64 { + if v.Kind() == reflect.Interface { + return toTimeUnix(v.Elem()) + } + if v.Type() != timeType { + panic("coding error: argument must be time.Time type reflect Value") + } + return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() +} diff --git a/tpl/compare/compare_test.go b/tpl/compare/compare_test.go new file mode 100644 index 000000000..d40a6fe5f --- /dev/null +++ b/tpl/compare/compare_test.go @@ -0,0 +1,197 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "fmt" + "path" + "reflect" + "runtime" + "testing" + "time" + + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstCompareType int + +const ( + tstEq tstCompareType = iota + tstNe + tstGt + tstGe + tstLt + tstLe +) + +func tstIsEq(tp tstCompareType) bool { return tp == tstEq || tp == tstGe || tp == tstLe } +func tstIsGt(tp tstCompareType) bool { return tp == tstGt || tp == tstGe } +func tstIsLt(tp tstCompareType) bool { return tp == tstLt || tp == tstLe } + +func TestDefaultFunc(t *testing.T) { + t.Parallel() + + then := time.Now() + now := time.Now() + + for i, test := range []struct { + dflt interface{} + given interface{} + expect interface{} + }{ + {true, false, false}, + {"5", 0, "5"}, + + {"test1", "set", "set"}, + {"test2", "", "test2"}, + {"test3", nil, "test3"}, + + {[2]int{10, 20}, [2]int{1, 2}, [2]int{1, 2}}, + {[2]int{10, 20}, [0]int{}, [2]int{10, 20}}, + {[2]int{100, 200}, nil, [2]int{100, 200}}, + + {[]string{"one"}, []string{"uno"}, []string{"uno"}}, + {[]string{"two"}, []string{}, []string{"two"}}, + {[]string{"three"}, nil, []string{"three"}}, + + {map[string]int{"one": 1}, map[string]int{"uno": 1}, map[string]int{"uno": 1}}, + {map[string]int{"one": 1}, map[string]int{}, map[string]int{"one": 1}}, + {map[string]int{"two": 2}, nil, map[string]int{"two": 2}}, + + {10, 1, 1}, + {10, 0, 10}, + {20, nil, 20}, + + {float32(10), float32(1), float32(1)}, + {float32(10), 0, float32(10)}, + {float32(20), nil, float32(20)}, + + {complex(2, -2), complex(1, -1), complex(1, -1)}, + {complex(2, -2), complex(0, 0), complex(2, -2)}, + {complex(3, -3), nil, complex(3, -3)}, + + {struct{ f string }{f: "one"}, struct{}{}, struct{}{}}, + {struct{ f string }{f: "two"}, nil, struct{ f string }{f: "two"}}, + + {then, now, now}, + {then, time.Time{}, then}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := Default(test.dflt, test.given) + + require.NoError(t, err, errMsg) + assert.Equal(t, result, test.expect, errMsg) + } +} + +func TestCompare(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + tstCompareType + funcUnderTest func(a, b interface{}) bool + }{ + {tstGt, Gt}, + {tstLt, Lt}, + {tstGe, Ge}, + {tstLe, Le}, + {tstEq, Eq}, + {tstNe, Ne}, + } { + doTestCompare(t, test.tstCompareType, test.funcUnderTest) + } +} + +func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b interface{}) bool) { + for i, test := range []struct { + left interface{} + right interface{} + expectIndicator int + }{ + {5, 8, -1}, + {8, 5, 1}, + {5, 5, 0}, + {int(5), int64(5), 0}, + {int32(5), int(5), 0}, + {int16(4), int(5), -1}, + {uint(15), uint64(15), 0}, + {-2, 1, -1}, + {2, -5, 1}, + {0.0, 1.23, -1}, + {1.1, 1.1, 0}, + {float32(1.0), float64(1.0), 0}, + {1.23, 0.0, 1}, + {"5", "5", 0}, + {"8", "5", 1}, + {"5", "0001", 1}, + {[]int{100, 99}, []int{1, 2, 3, 4}, -1}, + {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-20"), 0}, + {cast.ToTime("2015-11-19"), cast.ToTime("2015-11-20"), -1}, + {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-19"), 1}, + {"a", "a", 0}, + {"a", "b", -1}, + {"b", "a", 1}, + } { + result := funcUnderTest(test.left, test.right) + success := false + + if test.expectIndicator == 0 { + if tstIsEq(tp) { + success = result + } else { + success = !result + } + } + + if test.expectIndicator < 0 { + success = result && (tstIsLt(tp) || tp == tstNe) + success = success || (!result && !tstIsLt(tp)) + } + + if test.expectIndicator > 0 { + success = result && (tstIsGt(tp) || tp == tstNe) + success = success || (!result && (!tstIsGt(tp) || tp != tstNe)) + } + + if !success { + t.Errorf("[%d][%s] %v compared to %v: %t", i, path.Base(runtime.FuncForPC(reflect.ValueOf(funcUnderTest).Pointer()).Name()), test.left, test.right, result) + } + } +} + +func TestTimeUnix(t *testing.T) { + t.Parallel() + var sec int64 = 1234567890 + tv := reflect.ValueOf(time.Unix(sec, 0)) + i := 1 + + res := toTimeUnix(tv) + if sec != res { + t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec) + } + + i++ + func(t *testing.T) { + defer func() { + if err := recover(); err == nil { + t.Errorf("[%d] timeUnix didn't return an expected error", i) + } + }() + iv := reflect.ValueOf(sec) + toTimeUnix(iv) + }(t) +} diff --git a/tpl/crypto/crypto.go b/tpl/crypto/crypto.go new file mode 100644 index 000000000..207e4df39 --- /dev/null +++ b/tpl/crypto/crypto.go @@ -0,0 +1,67 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + + "github.com/spf13/cast" +) + +// New returns a new instance of the crypto-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "crypto" namespace. +type Namespace struct{} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// MD5 hashes the given input and returns its MD5 checksum. +func (ns *Namespace) MD5(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := md5.Sum([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// SHA1 hashes the given input and returns its SHA1 checksum. +func (ns *Namespace) SHA1(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := sha1.Sum([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} + +// SHA256 hashes the given input and returns its SHA256 checksum. +func (ns *Namespace) SHA256(in interface{}) (string, error) { + conv, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + hash := sha256.Sum256([]byte(conv)) + return hex.EncodeToString(hash[:]), nil +} diff --git a/tpl/crypto/crypto_test.go b/tpl/crypto/crypto_test.go new file mode 100644 index 000000000..53b41bd26 --- /dev/null +++ b/tpl/crypto/crypto_test.go @@ -0,0 +1,111 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New() + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestMD5(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + in interface{} + expect interface{} + }{ + {"Hello world, gophers!", "b3029f756f98f79e7f1b7f1d1f0dd53b"}, + {"Lorem ipsum dolor", "06ce65ac476fc656bea3fca5d02cfd81"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.in) + + result, err := ns.MD5(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSHA1(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + in interface{} + expect interface{} + }{ + {"Hello world, gophers!", "c8b5b0e33d408246e30f53e32b8f7627a7a649d4"}, + {"Lorem ipsum dolor", "45f75b844be4d17b3394c6701768daf39419c99b"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.in) + + result, err := ns.SHA1(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSHA256(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + in interface{} + expect interface{} + }{ + {"Hello world, gophers!", "6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46"}, + {"Lorem ipsum dolor", "9b3e1beb7053e0f900a674dd1c99aca3355e1275e1b03d3cb1bc977f5154e196"}, + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.in) + + result, err := ns.SHA256(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/data/data.go b/tpl/data/data.go new file mode 100644 index 000000000..39cbc9b19 --- /dev/null +++ b/tpl/data/data.go @@ -0,0 +1,31 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package data + +import "github.com/spf13/hugo/deps" + +// New returns a new instance of the data-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "data" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } diff --git a/tpl/tplimpl/template_resources.go b/tpl/data/resources.go similarity index 91% rename from tpl/tplimpl/template_resources.go rename to tpl/data/resources.go index cfec816ad..272e2474e 100644 --- a/tpl/tplimpl/template_resources.go +++ b/tpl/data/resources.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tplimpl +package data import ( "bytes" @@ -176,25 +176,25 @@ func resGetLocal(url string, fs afero.Fs, cfg config.Provider) ([]byte, error) { } // resGetResource loads the content of a local or remote file -func (t *templateFuncster) resGetResource(url string) ([]byte, error) { +func (ns *Namespace) resGetResource(url string) ([]byte, error) { if url == "" { return nil, nil } if strings.Contains(url, "://") { - return resGetRemote(url, t.Fs.Source, t.Cfg, http.DefaultClient) + return resGetRemote(url, ns.deps.Fs.Source, ns.deps.Cfg, http.DefaultClient) } - return resGetLocal(url, t.Fs.Source, t.Cfg) + return resGetLocal(url, ns.deps.Fs.Source, ns.deps.Cfg) } -// getJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one. +// GetJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one. // If you provide multiple parts they will be joined together to the final URL. // GetJSON returns nil or parsed JSON to use in a short code. -func (t *templateFuncster) getJSON(urlParts ...string) interface{} { +func (ns *Namespace) GetJSON(urlParts ...string) interface{} { var v interface{} url := strings.Join(urlParts, "") for i := 0; i <= resRetries; i++ { - c, err := t.resGetResource(url) + c, err := ns.resGetResource(url) if err != nil { jww.ERROR.Printf("Failed to get json resource %s with error message %s", url, err) return nil @@ -205,7 +205,7 @@ func (t *templateFuncster) getJSON(urlParts ...string) interface{} { jww.ERROR.Printf("Cannot read json from resource %s with error message %s", url, err) jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) time.Sleep(resSleep) - resDeleteCache(url, t.Fs.Source, t.Cfg) + resDeleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg) continue } break @@ -226,23 +226,23 @@ func parseCSV(c []byte, sep string) ([][]string, error) { return r.ReadAll() } -// getCSV expects a data separator and one or n-parts of a URL to a resource which +// GetCSV expects a data separator and one or n-parts of a URL to a resource which // can either be a local or a remote one. // The data separator can be a comma, semi-colon, pipe, etc, but only one character. // If you provide multiple parts for the URL they will be joined together to the final URL. // GetCSV returns nil or a slice slice to use in a short code. -func (t *templateFuncster) getCSV(sep string, urlParts ...string) [][]string { +func (ns *Namespace) GetCSV(sep string, urlParts ...string) [][]string { var d [][]string url := strings.Join(urlParts, "") var clearCacheSleep = func(i int, u string) { jww.ERROR.Printf("Retry #%d for %s and sleeping for %s", i, url, resSleep) time.Sleep(resSleep) - resDeleteCache(url, t.Fs.Source, t.Cfg) + resDeleteCache(url, ns.deps.Fs.Source, ns.deps.Cfg) } for i := 0; i <= resRetries; i++ { - c, err := t.resGetResource(url) + c, err := ns.resGetResource(url) if err == nil && !bytes.Contains(c, []byte(sep)) { err = errors.New("Cannot find separator " + sep + " in CSV.") diff --git a/tpl/tplimpl/template_resources_test.go b/tpl/data/resources_test.go similarity index 94% rename from tpl/tplimpl/template_resources_test.go rename to tpl/data/resources_test.go index e7e7f7782..f0d7cfd17 100644 --- a/tpl/tplimpl/template_resources_test.go +++ b/tpl/data/resources_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tplimpl +package data import ( "bytes" @@ -25,6 +25,8 @@ import ( "time" "github.com/spf13/afero" + "github.com/spf13/hugo/config" + "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/viper" @@ -259,7 +261,7 @@ func TestParseCSV(t *testing.T) { func TestGetJSONFailParse(t *testing.T) { t.Parallel() - f := newTestFuncster() + ns := New(newDeps(viper.New())) reqCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -276,7 +278,7 @@ func TestGetJSONFailParse(t *testing.T) { url := ts.URL + "/test.json" want := map[string]interface{}{"gomeetup": []interface{}{"Sydney", "San Francisco", "Stockholm"}} - have := f.getJSON(url) + have := ns.GetJSON(url) assert.NotNil(t, have) if have != nil { assert.EqualValues(t, want, have) @@ -285,7 +287,8 @@ func TestGetJSONFailParse(t *testing.T) { func TestGetCSVFailParseSep(t *testing.T) { t.Parallel() - f := newTestFuncster() + + ns := New(newDeps(viper.New())) reqCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -305,7 +308,7 @@ func TestGetCSVFailParseSep(t *testing.T) { url := ts.URL + "/test.csv" want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} - have := f.getCSV(",", url) + have := ns.GetCSV(",", url) assert.NotNil(t, have) if have != nil { assert.EqualValues(t, want, have) @@ -315,7 +318,7 @@ func TestGetCSVFailParseSep(t *testing.T) { func TestGetCSVFailParse(t *testing.T) { t.Parallel() - f := newTestFuncster() + ns := New(newDeps(viper.New())) reqCount := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -337,9 +340,19 @@ func TestGetCSVFailParse(t *testing.T) { url := ts.URL + "/test.csv" want := [][]string{{"gomeetup", "city"}, {"yes", "Sydney"}, {"yes", "San Francisco"}, {"yes", "Stockholm"}} - have := f.getCSV(",", url) + have := ns.GetCSV(",", url) assert.NotNil(t, have) if have != nil { assert.EqualValues(t, want, have) } } + +func newDeps(cfg config.Provider) *deps.Deps { + l := helpers.NewLanguage("en", cfg) + l.Set("i18nDir", "i18n") + return &deps.Deps{ + Cfg: cfg, + Fs: hugofs.NewMem(l), + ContentSpec: helpers.NewContentSpec(l), + } +} diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go new file mode 100644 index 000000000..311edb209 --- /dev/null +++ b/tpl/encoding/encoding.go @@ -0,0 +1,64 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "encoding/base64" + "encoding/json" + "html/template" + + "github.com/spf13/cast" +) + +// New returns a new instance of the encoding-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "encoding" namespace. +type Namespace struct{} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// Base64Decode returns the base64 decoding of the given content. +func (ns *Namespace) Base64Decode(content interface{}) (string, error) { + conv, err := cast.ToStringE(content) + if err != nil { + return "", err + } + + dec, err := base64.StdEncoding.DecodeString(conv) + return string(dec), err +} + +// Base64Encode returns the base64 encoding of the given content. +func (ns *Namespace) Base64Encode(content interface{}) (string, error) { + conv, err := cast.ToStringE(content) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString([]byte(conv)), nil +} + +// Jsonify encodes a given object to JSON. +func (ns *Namespace) Jsonify(v interface{}) (template.HTML, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + + return template.HTML(b), nil +} diff --git a/tpl/encoding/encoding_test.go b/tpl/encoding/encoding_test.go new file mode 100644 index 000000000..d03362866 --- /dev/null +++ b/tpl/encoding/encoding_test.go @@ -0,0 +1,117 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encoding + +import ( + "fmt" + "html/template" + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New() + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestBase64Decode(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {"YWJjMTIzIT8kKiYoKSctPUB+", "abc123!?$*&()'-=@~"}, + // errors + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.Base64Decode(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestBase64Encode(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {"YWJjMTIzIT8kKiYoKSctPUB+", "WVdKak1USXpJVDhrS2lZb0tTY3RQVUIr"}, + // errors + {t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.Base64Encode(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestJsonify(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + v interface{} + expect interface{} + }{ + {[]string{"a", "b"}, template.HTML(`["a","b"]`)}, + {tstNoStringer{}, template.HTML("{}")}, + {nil, template.HTML("null")}, + // errors + {math.NaN(), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.v) + + result, err := ns.Jsonify(test.v) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/images/images.go b/tpl/images/images.go new file mode 100644 index 000000000..6700603dd --- /dev/null +++ b/tpl/images/images.go @@ -0,0 +1,82 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "errors" + "image" + "sync" + + // Importing image codecs for image.DecodeConfig + _ "image/gif" + _ "image/jpeg" + _ "image/png" + + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" +) + +// New returns a new instance of the images-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + cache: map[string]image.Config{}, + deps: deps, + } +} + +// Namespace provides template functions for the "images" namespace. +type Namespace struct { + sync.RWMutex + cache map[string]image.Config + + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// Config returns the image.Config for the specified path relative to the +// working directory. +func (ns *Namespace) Config(path interface{}) (image.Config, error) { + filename, err := cast.ToStringE(path) + if err != nil { + return image.Config{}, err + } + + if filename == "" { + return image.Config{}, errors.New("config needs a filename") + } + + // Check cache for image config. + ns.RLock() + config, ok := ns.cache[filename] + ns.RUnlock() + + if ok { + return config, nil + } + + f, err := ns.deps.Fs.WorkingDir.Open(filename) + if err != nil { + return image.Config{}, err + } + + config, _, err = image.DecodeConfig(f) + + ns.Lock() + ns.cache[filename] = config + ns.Unlock() + + return config, err +} diff --git a/tpl/images/images_test.go b/tpl/images/images_test.go new file mode 100644 index 000000000..740a469af --- /dev/null +++ b/tpl/images/images_test.go @@ -0,0 +1,132 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package images + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/png" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +var configTests = []struct { + path interface{} + input []byte + expect interface{} +}{ + { + path: "a.png", + input: blankImage(10, 10), + expect: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + path: "a.png", + input: blankImage(10, 10), + expect: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + { + path: "b.png", + input: blankImage(20, 15), + expect: image.Config{ + Width: 20, + Height: 15, + ColorModel: color.NRGBAModel, + }, + }, + { + path: "a.png", + input: blankImage(20, 15), + expect: image.Config{ + Width: 10, + Height: 10, + ColorModel: color.NRGBAModel, + }, + }, + // errors + {path: tstNoStringer{}, expect: false}, + {path: "non-existent.png", expect: false}, + {path: "", expect: false}, +} + +func TestNamespace(t *testing.T) { + t.Parallel() + + v := viper.New() + v.Set("workingDir", "/a/b") + + ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestNSConfig(t *testing.T) { + t.Parallel() + + v := viper.New() + v.Set("workingDir", "/a/b") + + ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) + + for i, test := range configTests { + errMsg := fmt.Sprintf("[%d] %s", i, test.path) + + // check for expected errors early to avoid writing files + if b, ok := test.expect.(bool); ok && !b { + _, err := ns.Config(interface{}(test.path)) + require.Error(t, err, errMsg) + continue + } + + // cast path to string for afero.WriteFile + sp, err := cast.ToStringE(test.path) + require.NoError(t, err, errMsg) + afero.WriteFile(ns.deps.Fs.Source, filepath.Join(v.GetString("workingDir"), sp), test.input, 0755) + + result, err := ns.Config(test.path) + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + assert.NotEqual(t, 0, len(ns.cache), errMsg) + } +} + +func blankImage(width, height int) []byte { + var buf bytes.Buffer + img := image.NewRGBA(image.Rect(0, 0, width, height)) + if err := png.Encode(&buf, img); err != nil { + panic(err) + } + return buf.Bytes() +} diff --git a/tpl/inflect/inflect.go b/tpl/inflect/inflect.go new file mode 100644 index 000000000..9c13238b5 --- /dev/null +++ b/tpl/inflect/inflect.go @@ -0,0 +1,79 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package inflect + +import ( + "strconv" + + _inflect "github.com/bep/inflect" + "github.com/spf13/cast" +) + +// New returns a new instance of the inflect-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "inflect" namespace. +type Namespace struct{} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// Humanize returns the humanized form of a single parameter. +// +// If the parameter is either an integer or a string containing an integer +// value, the behavior is to add the appropriate ordinal. +// +// Example: "my-first-post" -> "My first post" +// Example: "103" -> "103rd" +// Example: 52 -> "52nd" +func (ns *Namespace) Humanize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + if word == "" { + return "", nil + } + + _, ok := in.(int) // original param was literal int value + _, err = strconv.Atoi(word) // original param was string containing an int value + if ok || err == nil { + return _inflect.Ordinalize(word), nil + } + + return _inflect.Humanize(word), nil +} + +// Pluralize returns the plural form of a single word. +func (ns *Namespace) Pluralize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + return _inflect.Pluralize(word), nil +} + +// Singularize returns the singular form of a single word. +func (ns *Namespace) Singularize(in interface{}) (string, error) { + word, err := cast.ToStringE(in) + if err != nil { + return "", err + } + + return _inflect.Singularize(word), nil +} diff --git a/tpl/inflect/inflect_test.go b/tpl/inflect/inflect_test.go new file mode 100644 index 000000000..028d5d9af --- /dev/null +++ b/tpl/inflect/inflect_test.go @@ -0,0 +1,56 @@ +package inflect + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New() + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestInflect(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + fn func(i interface{}) (string, error) + in interface{} + expect interface{} + }{ + {ns.Humanize, "MyCamel", "My camel"}, + {ns.Humanize, "", ""}, + {ns.Humanize, "103", "103rd"}, + {ns.Humanize, "41", "41st"}, + {ns.Humanize, 103, "103rd"}, + {ns.Humanize, int64(92), "92nd"}, + {ns.Humanize, "5.5", "5.5"}, + {ns.Humanize, t, false}, + {ns.Pluralize, "cat", "cats"}, + {ns.Pluralize, "", ""}, + {ns.Pluralize, t, false}, + {ns.Singularize, "cats", "cat"}, + {ns.Singularize, "", ""}, + {ns.Singularize, t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := test.fn(test.in) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/lang/lang.go b/tpl/lang/lang.go new file mode 100644 index 000000000..04d187603 --- /dev/null +++ b/tpl/lang/lang.go @@ -0,0 +1,49 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lang + +import ( + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" +) + +// New returns a new instance of the lang-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "lang" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// Translate ... +func (ns *Namespace) Translate(id interface{}, args ...interface{}) (string, error) { + sid, err := cast.ToStringE(id) + if err != nil { + return "", nil + } + + return ns.deps.Translate(sid, args...), nil +} + +// T is an alias to Translate. +func (ns *Namespace) T(id interface{}, args ...interface{}) (string, error) { + return ns.Translate(id, args...) +} diff --git a/tpl/math/math.go b/tpl/math/math.go new file mode 100644 index 000000000..47b7b8306 --- /dev/null +++ b/tpl/math/math.go @@ -0,0 +1,199 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "errors" + "reflect" +) + +// New returns a new instance of the math-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "math" namespace. +type Namespace struct{} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +func (ns *Namespace) Add(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '+') +} + +func (ns *Namespace) Div(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '/') +} + +// Mod returns a % b. +func (ns *Namespace) Mod(a, b interface{}) (int64, error) { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + var ai, bi int64 + + switch av.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ai = av.Int() + default: + return 0, errors.New("Modulo operator can't be used with non integer value") + } + + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + default: + return 0, errors.New("Modulo operator can't be used with non integer value") + } + + if bi == 0 { + return 0, errors.New("The number can't be divided by zero at modulo operation") + } + + return ai % bi, nil +} + +// ModBool returns the boolean of a % b. If a % b == 0, return true. +func (ns *Namespace) ModBool(a, b interface{}) (bool, error) { + res, err := ns.Mod(a, b) + if err != nil { + return false, err + } + + return res == int64(0), nil +} + +func (ns *Namespace) Mul(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '*') +} + +func (ns *Namespace) Sub(a, b interface{}) (interface{}, error) { + return DoArithmetic(a, b, '-') +} + +// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to +// determine the type of the two terms. +func DoArithmetic(a, b interface{}, op rune) (interface{}, error) { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + var ai, bi int64 + var af, bf float64 + var au, bu uint64 + switch av.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ai = av.Int() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + case reflect.Float32, reflect.Float64: + af = float64(ai) // may overflow + ai = 0 + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bu = bv.Uint() + if ai >= 0 { + au = uint64(ai) + ai = 0 + } else { + bi = int64(bu) // may overflow + bu = 0 + } + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.Float32, reflect.Float64: + af = av.Float() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bf = float64(bv.Int()) // may overflow + case reflect.Float32, reflect.Float64: + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bf = float64(bv.Uint()) // may overflow + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + au = av.Uint() + switch bv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bi = bv.Int() + if bi >= 0 { + bu = uint64(bi) + bi = 0 + } else { + ai = int64(au) // may overflow + au = 0 + } + case reflect.Float32, reflect.Float64: + af = float64(au) // may overflow + au = 0 + bf = bv.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bu = bv.Uint() + default: + return nil, errors.New("Can't apply the operator to the values") + } + case reflect.String: + as := av.String() + if bv.Kind() == reflect.String && op == '+' { + bs := bv.String() + return as + bs, nil + } + return nil, errors.New("Can't apply the operator to the values") + default: + return nil, errors.New("Can't apply the operator to the values") + } + + switch op { + case '+': + if ai != 0 || bi != 0 { + return ai + bi, nil + } else if af != 0 || bf != 0 { + return af + bf, nil + } else if au != 0 || bu != 0 { + return au + bu, nil + } + return 0, nil + case '-': + if ai != 0 || bi != 0 { + return ai - bi, nil + } else if af != 0 || bf != 0 { + return af - bf, nil + } else if au != 0 || bu != 0 { + return au - bu, nil + } + return 0, nil + case '*': + if ai != 0 || bi != 0 { + return ai * bi, nil + } else if af != 0 || bf != 0 { + return af * bf, nil + } else if au != 0 || bu != 0 { + return au * bu, nil + } + return 0, nil + case '/': + if bi != 0 { + return ai / bi, nil + } else if bf != 0 { + return af / bf, nil + } else if bu != 0 { + return au / bu, nil + } + return nil, errors.New("Can't divide the value by 0") + default: + return nil, errors.New("There is no such an operation") + } +} diff --git a/tpl/math/math_test.go b/tpl/math/math_test.go new file mode 100644 index 000000000..649a2756e --- /dev/null +++ b/tpl/math/math_test.go @@ -0,0 +1,228 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package math + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New() + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestBasicNSArithmetic(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + fn func(a, b interface{}) (interface{}, error) + a interface{} + b interface{} + expect interface{} + }{ + {ns.Add, 4, 2, int64(6)}, + {ns.Add, 1.0, "foo", false}, + {ns.Sub, 4, 2, int64(2)}, + {ns.Sub, 1.0, "foo", false}, + {ns.Mul, 4, 2, int64(8)}, + {ns.Mul, 1.0, "foo", false}, + {ns.Div, 4, 2, int64(2)}, + {ns.Div, 1.0, "foo", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := test.fn(test.a, test.b) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestDoArithmetic(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + a interface{} + b interface{} + op rune + expect interface{} + }{ + {3, 2, '+', int64(5)}, + {3, 2, '-', int64(1)}, + {3, 2, '*', int64(6)}, + {3, 2, '/', int64(1)}, + {3.0, 2, '+', float64(5)}, + {3.0, 2, '-', float64(1)}, + {3.0, 2, '*', float64(6)}, + {3.0, 2, '/', float64(1.5)}, + {3, 2.0, '+', float64(5)}, + {3, 2.0, '-', float64(1)}, + {3, 2.0, '*', float64(6)}, + {3, 2.0, '/', float64(1.5)}, + {3.0, 2.0, '+', float64(5)}, + {3.0, 2.0, '-', float64(1)}, + {3.0, 2.0, '*', float64(6)}, + {3.0, 2.0, '/', float64(1.5)}, + {uint(3), uint(2), '+', uint64(5)}, + {uint(3), uint(2), '-', uint64(1)}, + {uint(3), uint(2), '*', uint64(6)}, + {uint(3), uint(2), '/', uint64(1)}, + {uint(3), 2, '+', uint64(5)}, + {uint(3), 2, '-', uint64(1)}, + {uint(3), 2, '*', uint64(6)}, + {uint(3), 2, '/', uint64(1)}, + {3, uint(2), '+', uint64(5)}, + {3, uint(2), '-', uint64(1)}, + {3, uint(2), '*', uint64(6)}, + {3, uint(2), '/', uint64(1)}, + {uint(3), -2, '+', int64(1)}, + {uint(3), -2, '-', int64(5)}, + {uint(3), -2, '*', int64(-6)}, + {uint(3), -2, '/', int64(-1)}, + {-3, uint(2), '+', int64(-1)}, + {-3, uint(2), '-', int64(-5)}, + {-3, uint(2), '*', int64(-6)}, + {-3, uint(2), '/', int64(-1)}, + {uint(3), 2.0, '+', float64(5)}, + {uint(3), 2.0, '-', float64(1)}, + {uint(3), 2.0, '*', float64(6)}, + {uint(3), 2.0, '/', float64(1.5)}, + {3.0, uint(2), '+', float64(5)}, + {3.0, uint(2), '-', float64(1)}, + {3.0, uint(2), '*', float64(6)}, + {3.0, uint(2), '/', float64(1.5)}, + {0, 0, '+', 0}, + {0, 0, '-', 0}, + {0, 0, '*', 0}, + {"foo", "bar", '+', "foobar"}, + {3, 0, '/', false}, + {3.0, 0, '/', false}, + {3, 0.0, '/', false}, + {uint(3), uint(0), '/', false}, + {3, uint(0), '/', false}, + {-3, uint(0), '/', false}, + {uint(3), 0, '/', false}, + {3.0, uint(0), '/', false}, + {uint(3), 0.0, '/', false}, + {3, "foo", '+', false}, + {3.0, "foo", '+', false}, + {uint(3), "foo", '+', false}, + {"foo", 3, '+', false}, + {"foo", "bar", '-', false}, + {3, 2, '%', false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := DoArithmetic(test.a, test.b, test.op) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestMod(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + b interface{} + expect interface{} + }{ + {3, 2, int64(1)}, + {3, 1, int64(0)}, + {3, 0, false}, + {0, 3, int64(0)}, + {3.1, 2, false}, + {3, 2.1, false}, + {3.1, 2.1, false}, + {int8(3), int8(2), int64(1)}, + {int16(3), int16(2), int64(1)}, + {int32(3), int32(2), int64(1)}, + {int64(3), int64(2), int64(1)}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Mod(test.a, test.b) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestModBool(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + b interface{} + expect interface{} + }{ + {3, 3, true}, + {3, 2, false}, + {3, 1, true}, + {3, 0, nil}, + {0, 3, true}, + {3.1, 2, nil}, + {3, 2.1, nil}, + {3.1, 2.1, nil}, + {int8(3), int8(3), true}, + {int8(3), int8(2), false}, + {int16(3), int16(3), true}, + {int16(3), int16(2), false}, + {int32(3), int32(3), true}, + {int32(3), int32(2), false}, + {int64(3), int64(3), true}, + {int64(3), int64(2), false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ModBool(test.a, test.b) + + if test.expect == nil { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/os/os.go b/tpl/os/os.go new file mode 100644 index 000000000..91d6e14f6 --- /dev/null +++ b/tpl/os/os.go @@ -0,0 +1,101 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package os + +import ( + "errors" + "fmt" + _os "os" + + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" +) + +// New returns a new instance of the os-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "os" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// Getenv retrieves the value of the environment variable named by the key. +// It returns the value, which will be empty if the variable is not present. +func (ns *Namespace) Getenv(key interface{}) (string, error) { + skey, err := cast.ToStringE(key) + if err != nil { + return "", nil + } + + return _os.Getenv(skey), nil +} + +// readFile reads the file named by filename relative to the given basepath +// and returns the contents as a string. +// There is a upper size limit set at 1 megabytes. +func readFile(fs *afero.BasePathFs, filename string) (string, error) { + if filename == "" { + return "", errors.New("readFile needs a filename") + } + + if info, err := fs.Stat(filename); err == nil { + if info.Size() > 1000000 { + return "", fmt.Errorf("File %q is too big", filename) + } + } else { + return "", err + } + b, err := afero.ReadFile(fs, filename) + + if err != nil { + return "", err + } + + return string(b), nil +} + +// ReadFilereads the file named by filename relative to the configured +// WorkingDir. It returns the contents as a string. There is a upper size +// limit set at 1 megabytes. +func (ns *Namespace) ReadFile(i interface{}) (string, error) { + s, err := cast.ToStringE(i) + if err != nil { + return "", err + } + + return readFile(ns.deps.Fs.WorkingDir, s) +} + +// ReadDir lists the directory contents relative to the configured WorkingDir. +func (ns *Namespace) ReadDir(i interface{}) ([]_os.FileInfo, error) { + path, err := cast.ToStringE(i) + if err != nil { + return nil, err + } + + list, err := afero.ReadDir(ns.deps.Fs.WorkingDir, path) + if err != nil { + return nil, fmt.Errorf("Failed to read Directory %s with error message %s", path, err) + } + + return list, nil +} diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go new file mode 100644 index 000000000..166df5e51 --- /dev/null +++ b/tpl/os/os_test.go @@ -0,0 +1,65 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package os + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadFile(t *testing.T) { + t.Parallel() + + workingDir := "/home/hugo" + + v := viper.New() + v.Set("workingDir", workingDir) + + // f := newTestFuncsterWithViper(v) + ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) + + afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) + afero.WriteFile(ns.deps.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) + + for i, test := range []struct { + filename string + expect interface{} + }{ + {filepath.FromSlash("/f/f1.txt"), "f1-content"}, + {filepath.FromSlash("f/f1.txt"), "f1-content"}, + {filepath.FromSlash("../f2.txt"), false}, + {"", false}, + {"b", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ReadFile(test.filename) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/safe/safe.go b/tpl/safe/safe.go new file mode 100644 index 000000000..64f383703 --- /dev/null +++ b/tpl/safe/safe.go @@ -0,0 +1,74 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safe + +import ( + "html/template" + + "github.com/spf13/cast" + "github.com/spf13/hugo/helpers" +) + +// New returns a new instance of the safe-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "safe" namespace. +type Namespace struct{} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// CSS returns a given string as html/template CSS content. +func (ns *Namespace) CSS(a interface{}) (template.CSS, error) { + s, err := cast.ToStringE(a) + return template.CSS(s), err +} + +// HTML returns a given string as html/template HTML content. +func (ns *Namespace) HTML(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + return template.HTML(s), err +} + +// HTMLAttr returns a given string as html/template HTMLAttr content. +func (ns *Namespace) HTMLAttr(a interface{}) (template.HTMLAttr, error) { + s, err := cast.ToStringE(a) + return template.HTMLAttr(s), err +} + +// JS returns the given string as a html/template JS content. +func (ns *Namespace) JS(a interface{}) (template.JS, error) { + s, err := cast.ToStringE(a) + return template.JS(s), err +} + +// JSStr returns the given string as a html/template JSStr content. +func (ns *Namespace) JSStr(a interface{}) (template.JSStr, error) { + s, err := cast.ToStringE(a) + return template.JSStr(s), err +} + +// URL returns a given string as html/template URL content. +func (ns *Namespace) URL(a interface{}) (template.URL, error) { + s, err := cast.ToStringE(a) + return template.URL(s), err +} + +// SanitizeURL returns a given string as html/template URL content. +func (ns *Namespace) SanitizeURL(a interface{}) (string, error) { + s, err := cast.ToStringE(a) + return helpers.SanitizeURL(s), err +} diff --git a/tpl/safe/safe_test.go b/tpl/safe/safe_test.go new file mode 100644 index 000000000..ae58d9784 --- /dev/null +++ b/tpl/safe/safe_test.go @@ -0,0 +1,222 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safe + +import ( + "fmt" + "html/template" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New() + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestCSS(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`a[href =~ "//example.com"]#foo`, template.CSS(`a[href =~ "//example.com"]#foo`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.CSS(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHTML(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`Hello, World &tc!`, template.HTML(`Hello, World &tc!`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HTML(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHTMLAttr(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {` dir="ltr"`, template.HTMLAttr(` dir="ltr"`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HTMLAttr(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestJS(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`c && alert("Hello, World!");`, template.JS(`c && alert("Hello, World!");`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.JS(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestJSStr(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`Hello, World & O'Reilly\x21`, template.JSStr(`Hello, World & O'Reilly\x21`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.JSStr(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestURL(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {`greeting=H%69&addressee=(World)`, template.URL(`greeting=H%69&addressee=(World)`)}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.URL(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSanitizeURL(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + a interface{} + expect interface{} + }{ + {"http://foo/../../bar", "http://foo/bar"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.SanitizeURL(test.a) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/strings/regexp.go b/tpl/strings/regexp.go new file mode 100644 index 000000000..7b52c9f6e --- /dev/null +++ b/tpl/strings/regexp.go @@ -0,0 +1,109 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "regexp" + "sync" + + "github.com/spf13/cast" +) + +// FindRE returns a list of strings that match the regular expression. By default all matches +// will be included. The number of matches can be limited with an optional third parameter. +func (ns *Namespace) FindRE(expr string, content interface{}, limit ...interface{}) ([]string, error) { + re, err := reCache.Get(expr) + if err != nil { + return nil, err + } + + conv, err := cast.ToStringE(content) + if err != nil { + return nil, err + } + + if len(limit) == 0 { + return re.FindAllString(conv, -1), nil + } + + lim, err := cast.ToIntE(limit[0]) + if err != nil { + return nil, err + } + + return re.FindAllString(conv, lim), nil +} + +// ReplaceRE returns a copy of s, replacing all matches of the regular +// expression pattern with the replacement text repl. +func (ns *Namespace) ReplaceRE(pattern, repl, s interface{}) (_ string, err error) { + sp, err := cast.ToStringE(pattern) + if err != nil { + return + } + + sr, err := cast.ToStringE(repl) + if err != nil { + return + } + + ss, err := cast.ToStringE(s) + if err != nil { + return + } + + re, err := reCache.Get(sp) + if err != nil { + return "", err + } + + return re.ReplaceAllString(ss, sr), nil +} + +// regexpCache represents a cache of regexp objects protected by a mutex. +type regexpCache struct { + mu sync.RWMutex + re map[string]*regexp.Regexp +} + +// Get retrieves a regexp object from the cache based upon the pattern. +// If the pattern is not found in the cache, create one +func (rc *regexpCache) Get(pattern string) (re *regexp.Regexp, err error) { + var ok bool + + if re, ok = rc.get(pattern); !ok { + re, err = regexp.Compile(pattern) + if err != nil { + return nil, err + } + rc.set(pattern, re) + } + + return re, nil +} + +func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) { + rc.mu.RLock() + re, ok = rc.re[key] + rc.mu.RUnlock() + return +} + +func (rc *regexpCache) set(key string, re *regexp.Regexp) { + rc.mu.Lock() + rc.re[key] = re + rc.mu.Unlock() +} + +var reCache = regexpCache{re: make(map[string]*regexp.Regexp)} diff --git a/tpl/strings/regexp_test.go b/tpl/strings/regexp_test.go new file mode 100644 index 000000000..3bacd2018 --- /dev/null +++ b/tpl/strings/regexp_test.go @@ -0,0 +1,86 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindRE(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + expr string + content interface{} + limit interface{} + expect interface{} + }{ + {"[G|g]o", "Hugo is a static site generator written in Go.", 2, []string{"go", "Go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", -1, []string{"go", "Go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", 1, []string{"go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", "1", []string{"go"}}, + {"[G|g]o", "Hugo is a static site generator written in Go.", nil, []string(nil)}, + // errors + {"[G|go", "Hugo is a static site generator written in Go.", nil, false}, + {"[G|g]o", t, nil, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.FindRE(test.expr, test.content, test.limit) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestReplaceRE(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + pattern interface{} + repl interface{} + s interface{} + expect interface{} + }{ + {"^https?://([^/]+).*", "$1", "http://gohugo.io/docs", "gohugo.io"}, + {"^https?://([^/]+).*", "$2", "http://gohugo.io/docs", ""}, + {"(ab)", "AB", "aabbaab", "aABbaAB"}, + // errors + {"(ab", "AB", "aabb", false}, // invalid re + {tstNoStringer{}, "$2", "http://gohugo.io/docs", false}, + {"^https?://([^/]+).*", tstNoStringer{}, "http://gohugo.io/docs", false}, + {"^https?://([^/]+).*", "$2", tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ReplaceRE(test.pattern, test.repl, test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go new file mode 100644 index 000000000..32c5c00ae --- /dev/null +++ b/tpl/strings/strings.go @@ -0,0 +1,380 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "errors" + "fmt" + "html/template" + _strings "strings" + "unicode/utf8" + + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" +) + +// New returns a new instance of the strings-namespaced template functions. +func New(d *deps.Deps) *Namespace { + return &Namespace{deps: d} +} + +// Namespace provides template functions for the "strings" namespace. +// Most functions mimic the Go stdlib, but the order of the parameters may be +// different to ease their use in the Go template system. +type Namespace struct { + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// CountRunes returns the number of runes in s, excluding whitepace. +func (ns *Namespace) CountRunes(s interface{}) (int, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return 0, fmt.Errorf("Failed to convert content to string: %s", err) + } + + counter := 0 + for _, r := range helpers.StripHTML(ss) { + if !helpers.IsWhitespace(r) { + counter++ + } + } + + return counter, nil +} + +// CountWords returns the approximate word count in s. +func (ns *Namespace) CountWords(s interface{}) (int, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return 0, fmt.Errorf("Failed to convert content to string: %s", err) + } + + counter := 0 + for _, word := range _strings.Fields(helpers.StripHTML(ss)) { + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + counter++ + } else { + counter += runeCount + } + } + + return counter, nil +} + +// Chomp returns a copy of s with all trailing newline characters removed. +func (ns *Namespace) Chomp(s interface{}) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return template.HTML(_strings.TrimRight(ss, "\r\n")), nil +} + +// Contains reports whether substr is in s. +func (ns *Namespace) Contains(s, substr interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + su, err := cast.ToStringE(substr) + if err != nil { + return false, err + } + + return _strings.Contains(ss, su), nil +} + +// ContainsAny reports whether any Unicode code points in chars are within s. +func (ns *Namespace) ContainsAny(s, chars interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sc, err := cast.ToStringE(chars) + if err != nil { + return false, err + } + + return _strings.ContainsAny(ss, sc), nil +} + +// HasPrefix tests whether the input s begins with prefix. +func (ns *Namespace) HasPrefix(s, prefix interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sx, err := cast.ToStringE(prefix) + if err != nil { + return false, err + } + + return _strings.HasPrefix(ss, sx), nil +} + +// HasSuffix tests whether the input s begins with suffix. +func (ns *Namespace) HasSuffix(s, suffix interface{}) (bool, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return false, err + } + + sx, err := cast.ToStringE(suffix) + if err != nil { + return false, err + } + + return _strings.HasSuffix(ss, sx), nil +} + +// Replace returns a copy of the string s with all occurrences of old replaced +// with new. +func (ns *Namespace) Replace(s, old, new interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + so, err := cast.ToStringE(old) + if err != nil { + return "", err + } + + sn, err := cast.ToStringE(new) + if err != nil { + return "", err + } + + return _strings.Replace(ss, so, sn, -1), nil +} + +// SliceString slices a string by specifying a half-open range with +// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. +// The end index can be omitted, it defaults to the string's length. +func (ns *Namespace) SliceString(a interface{}, startEnd ...interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var argStart, argEnd int + + argNum := len(startEnd) + + if argNum > 0 { + if argStart, err = cast.ToIntE(startEnd[0]); err != nil { + return "", errors.New("start argument must be integer") + } + } + if argNum > 1 { + if argEnd, err = cast.ToIntE(startEnd[1]); err != nil { + return "", errors.New("end argument must be integer") + } + } + + if argNum > 2 { + return "", errors.New("too many arguments") + } + + asRunes := []rune(aStr) + + if argNum > 0 && (argStart < 0 || argStart >= len(asRunes)) { + return "", errors.New("slice bounds out of range") + } + + if argNum == 2 { + if argEnd < 0 || argEnd > len(asRunes) { + return "", errors.New("slice bounds out of range") + } + return string(asRunes[argStart:argEnd]), nil + } else if argNum == 1 { + return string(asRunes[argStart:]), nil + } else { + return string(asRunes[:]), nil + } + +} + +// Split slices an input string into all substrings separated by delimiter. +func (ns *Namespace) Split(a interface{}, delimiter string) ([]string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return []string{}, err + } + + return _strings.Split(aStr, delimiter), nil +} + +// Substr extracts parts of a string, beginning at the character at the specified +// position, and returns the specified number of characters. +// +// It normally takes two parameters: start and length. +// It can also take one parameter: start, i.e. length is omitted, in which case +// the substring starting from start until the end of the string will be returned. +// +// To extract characters from the end of the string, use a negative start number. +// +// In addition, borrowing from the extended behavior described at http://php.net/substr, +// if length is given and is negative, then that many characters will be omitted from +// the end of string. +func (ns *Namespace) Substr(a interface{}, nums ...interface{}) (string, error) { + aStr, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + var start, length int + + asRunes := []rune(aStr) + + switch len(nums) { + case 0: + return "", errors.New("too less arguments") + case 1: + if start, err = cast.ToIntE(nums[0]); err != nil { + return "", errors.New("start argument must be integer") + } + length = len(asRunes) + case 2: + if start, err = cast.ToIntE(nums[0]); err != nil { + return "", errors.New("start argument must be integer") + } + if length, err = cast.ToIntE(nums[1]); err != nil { + return "", errors.New("length argument must be integer") + } + default: + return "", errors.New("too many arguments") + } + + if start < -len(asRunes) { + start = 0 + } + if start > len(asRunes) { + return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr)) + } + + var s, e int + if start >= 0 && length >= 0 { + s = start + e = start + length + } else if start < 0 && length >= 0 { + s = len(asRunes) + start - length + 1 + e = len(asRunes) + start + 1 + } else if start >= 0 && length < 0 { + s = start + e = len(asRunes) + length + } else { + s = len(asRunes) + start + e = len(asRunes) + length + } + + if s > e { + return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e) + } + if e > len(asRunes) { + e = len(asRunes) + } + + return string(asRunes[s:e]), nil +} + +// Title returns a copy of the input s with all Unicode letters that begin words +// mapped to their title case. +func (ns *Namespace) Title(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return _strings.Title(ss), nil +} + +// ToLower returns a copy of the input s with all Unicode letters mapped to their +// lower case. +func (ns *Namespace) ToLower(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return _strings.ToLower(ss), nil +} + +// ToUpper returns a copy of the input s with all Unicode letters mapped to their +// upper case. +func (ns *Namespace) ToUpper(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return _strings.ToUpper(ss), nil +} + +// Trim returns a string with all leading and trailing characters defined +// contained in cutset removed. +func (ns *Namespace) Trim(s, cutset interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + sc, err := cast.ToStringE(cutset) + if err != nil { + return "", err + } + + return _strings.Trim(ss, sc), nil +} + +// TrimPrefix returns s without the provided leading prefix string. If s doesn't +// start with prefix, s is returned unchanged. +func (ns *Namespace) TrimPrefix(s, prefix interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + sx, err := cast.ToStringE(prefix) + if err != nil { + return "", err + } + + return _strings.TrimPrefix(ss, sx), nil +} + +// TrimSuffix returns s without the provided trailing suffix string. If s +// doesn't end with suffix, s is returned unchanged. +func (ns *Namespace) TrimSuffix(s, suffix interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + sx, err := cast.ToStringE(suffix) + if err != nil { + return "", err + } + + return _strings.TrimSuffix(ss, sx), nil +} diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go new file mode 100644 index 000000000..9164729fe --- /dev/null +++ b/tpl/strings/strings_test.go @@ -0,0 +1,639 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package strings + +import ( + "fmt" + "html/template" + "testing" + + "github.com/spf13/hugo/deps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ns = New(&deps.Deps{}) + +type tstNoStringer struct{} + +func TestNamespace(t *testing.T) { + t.Parallel() + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestChomp(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"\n a\n", template.HTML("\n a")}, + {"\n a\n\n", template.HTML("\n a")}, + {"\n a\r\n", template.HTML("\n a")}, + {"\n a\n\r\n", template.HTML("\n a")}, + {"\n a\r\r", template.HTML("\n a")}, + {"\n a\r", template.HTML("\n a")}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Chomp(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestContains(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + substr interface{} + expect bool + isErr bool + }{ + {"", "", true, false}, + {"123", "23", true, false}, + {"123", "234", false, false}, + {"123", "", true, false}, + {"", "a", false, false}, + {123, "23", true, false}, + {123, "234", false, false}, + {123, "", true, false}, + {template.HTML("123"), []byte("23"), true, false}, + {template.HTML("123"), []byte("234"), false, false}, + {template.HTML("123"), []byte(""), true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Contains(test.s, test.substr) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestContainsAny(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + substr interface{} + expect bool + isErr bool + }{ + {"", "", false, false}, + {"", "1", false, false}, + {"", "123", false, false}, + {"1", "", false, false}, + {"1", "1", true, false}, + {"111", "1", true, false}, + {"123", "789", false, false}, + {"123", "729", true, false}, + {"a☺b☻c☹d", "uvw☻xyz", true, false}, + {1, "", false, false}, + {1, "1", true, false}, + {111, "1", true, false}, + {123, "789", false, false}, + {123, "729", true, false}, + {[]byte("123"), template.HTML("789"), false, false}, + {[]byte("123"), template.HTML("729"), true, false}, + {[]byte("a☺b☻c☹d"), template.HTML("uvw☻xyz"), true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ContainsAny(test.s, test.substr) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestCountRunes(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"foo bar", 6}, + {"旁边", 2}, + {`
旁边
`, 2}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.s) + + result, err := ns.CountRunes(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestCountWords(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"Do Be Do Be Do", 5}, + {"旁边", 2}, + {`
旁边
`, 2}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test.s) + + result, err := ns.CountWords(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHasPrefix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + prefix interface{} + expect interface{} + isErr bool + }{ + {"abcd", "ab", true, false}, + {"abcd", "cd", false, false}, + {template.HTML("abcd"), "ab", true, false}, + {template.HTML("abcd"), "cd", false, false}, + {template.HTML("1234"), 12, true, false}, + {template.HTML("1234"), 34, false, false}, + {[]byte("abcd"), "ab", true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HasPrefix(test.s, test.prefix) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestHasSuffix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + suffix interface{} + expect interface{} + isErr bool + }{ + {"abcd", "cd", true, false}, + {"abcd", "ab", false, false}, + {template.HTML("abcd"), "cd", true, false}, + {template.HTML("abcd"), "ab", false, false}, + {template.HTML("1234"), 34, true, false}, + {template.HTML("1234"), 12, false, false}, + {[]byte("abcd"), "cd", true, false}, + // errors + {"", tstNoStringer{}, false, true}, + {tstNoStringer{}, "", false, true}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.HasSuffix(test.s, test.suffix) + + if test.isErr { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestReplace(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + old interface{} + new interface{} + expect interface{} + }{ + {"aab", "a", "b", "bbb"}, + {"11a11", 1, 2, "22a22"}, + {12345, 1, 2, "22345"}, + // errors + {tstNoStringer{}, "a", "b", false}, + {"a", tstNoStringer{}, "b", false}, + {"a", "b", tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Replace(test.s, test.old, test.new) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSliceString(t *testing.T) { + t.Parallel() + + var err error + for i, test := range []struct { + v1 interface{} + v2 interface{} + v3 interface{} + expect interface{} + }{ + {"abc", 1, 2, "b"}, + {"abc", 1, 3, "bc"}, + {"abcdef", 1, int8(3), "bc"}, + {"abcdef", 1, int16(3), "bc"}, + {"abcdef", 1, int32(3), "bc"}, + {"abcdef", 1, int64(3), "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", nil, nil, "abcdef"}, + {"abcdef", 0, 6, "abcdef"}, + {"abcdef", 0, 2, "ab"}, + {"abcdef", 2, nil, "cdef"}, + {"abcdef", int8(2), nil, "cdef"}, + {"abcdef", int16(2), nil, "cdef"}, + {"abcdef", int32(2), nil, "cdef"}, + {"abcdef", int64(2), nil, "cdef"}, + {123, 1, 3, "23"}, + {"abcdef", 6, nil, false}, + {"abcdef", 4, 7, false}, + {"abcdef", -1, nil, false}, + {"abcdef", -1, 7, false}, + {"abcdef", 1, -1, false}, + {tstNoStringer{}, 0, 1, false}, + {"ĀĀĀ", 0, 1, "Ā"}, // issue #1333 + {"a", t, nil, false}, + {"a", 1, t, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + var result string + if test.v2 == nil { + result, err = ns.SliceString(test.v1) + } else if test.v3 == nil { + result, err = ns.SliceString(test.v1, test.v2) + } else { + result, err = ns.SliceString(test.v1, test.v2, test.v3) + } + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } + + // Too many arguments + _, err = ns.SliceString("a", 1, 2, 3) + if err == nil { + t.Errorf("Should have errored") + } +} + +func TestSplit(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + v1 interface{} + v2 string + expect interface{} + }{ + {"a, b", ", ", []string{"a", "b"}}, + {"a & b & c", " & ", []string{"a", "b", "c"}}, + {"http://example.com", "http://", []string{"", "example.com"}}, + {123, "2", []string{"1", "3"}}, + {tstNoStringer{}, ",", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Split(test.v1, test.v2) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestSubstr(t *testing.T) { + t.Parallel() + + var err error + var n int + for i, test := range []struct { + v1 interface{} + v2 interface{} + v3 interface{} + expect interface{} + }{ + {"abc", 1, 2, "bc"}, + {"abc", 0, 1, "a"}, + {"abcdef", -1, 2, "ef"}, + {"abcdef", -3, 3, "bcd"}, + {"abcdef", 0, -1, "abcde"}, + {"abcdef", 2, -1, "cde"}, + {"abcdef", 4, -4, false}, + {"abcdef", 7, 1, false}, + {"abcdef", 1, 100, "bcdef"}, + {"abcdef", -100, 3, "abc"}, + {"abcdef", -3, -1, "de"}, + {"abcdef", 2, nil, "cdef"}, + {"abcdef", int8(2), nil, "cdef"}, + {"abcdef", int16(2), nil, "cdef"}, + {"abcdef", int32(2), nil, "cdef"}, + {"abcdef", int64(2), nil, "cdef"}, + {"abcdef", 2, int8(3), "cde"}, + {"abcdef", 2, int16(3), "cde"}, + {"abcdef", 2, int32(3), "cde"}, + {"abcdef", 2, int64(3), "cde"}, + {123, 1, 3, "23"}, + {1.2e3, 0, 4, "1200"}, + {tstNoStringer{}, 0, 1, false}, + {"abcdef", 2.0, nil, "cdef"}, + {"abcdef", 2.0, 2, "cd"}, + {"abcdef", 2, 2.0, "cd"}, + {"ĀĀĀ", 1, 2, "ĀĀ"}, // # issue 1333 + {"abcdef", "doo", nil, false}, + {"abcdef", "doo", "doo", false}, + {"abcdef", 1, "doo", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + var result string + n = i + + if test.v3 == nil { + result, err = ns.Substr(test.v1, test.v2) + } else { + result, err = ns.Substr(test.v1, test.v2, test.v3) + } + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } + + n++ + _, err = ns.Substr("abcdef") + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", n) + } + + n++ + _, err = ns.Substr("abcdef", 1, 2, 3) + if err == nil { + t.Errorf("[%d] Substr didn't return an expected error", n) + } +} + +func TestTitle(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"test", "Test"}, + {template.HTML("hypertext"), "Hypertext"}, + {[]byte("bytes"), "Bytes"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Title(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestToLower(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"TEST", "test"}, + {template.HTML("LoWeR"), "lower"}, + {[]byte("BYTES"), "bytes"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ToLower(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestToUpper(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"test", "TEST"}, + {template.HTML("UpPeR"), "UPPER"}, + {[]byte("bytes"), "BYTES"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.ToUpper(test.s) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestTrim(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + cutset interface{} + expect interface{} + }{ + {"abba", "a", "bb"}, + {"abba", "ab", ""}, + {"", "<>", "tag"}, + {`"quote"`, `"`, "quote"}, + {1221, "1", "22"}, + {1221, "12", ""}, + {template.HTML(""), "<>", "tag"}, + {[]byte(""), "<>", "tag"}, + // errors + {"", tstNoStringer{}, false}, + {tstNoStringer{}, "", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.Trim(test.s, test.cutset) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestTrimPrefix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + prefix interface{} + expect interface{} + }{ + {"aabbaa", "a", "abbaa"}, + {"aabb", "b", "aabb"}, + {1234, "12", "34"}, + {1234, "34", "1234"}, + // errors + {"", tstNoStringer{}, false}, + {tstNoStringer{}, "", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.TrimPrefix(test.s, test.prefix) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} + +func TestTrimSuffix(t *testing.T) { + t.Parallel() + + for i, test := range []struct { + s interface{} + suffix interface{} + expect interface{} + }{ + {"aabbaa", "a", "aabba"}, + {"aabb", "b", "aab"}, + {1234, "12", "1234"}, + {1234, "34", "12"}, + // errors + {"", tstNoStringer{}, false}, + {tstNoStringer{}, "", false}, + } { + errMsg := fmt.Sprintf("[%d] %v", i, test) + + result, err := ns.TrimSuffix(test.s, test.suffix) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Equal(t, test.expect, result, errMsg) + } +} diff --git a/tpl/tplimpl/template_func_truncate.go b/tpl/strings/truncate.go similarity index 97% rename from tpl/tplimpl/template_func_truncate.go rename to tpl/strings/truncate.go index d4bb63272..923816e9f 100644 --- a/tpl/tplimpl/template_func_truncate.go +++ b/tpl/strings/truncate.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tplimpl +package strings import ( "errors" @@ -39,7 +39,7 @@ type htmlTag struct { openTag bool } -func truncate(a interface{}, options ...interface{}) (template.HTML, error) { +func (ns *Namespace) Truncate(a interface{}, options ...interface{}) (template.HTML, error) { length, err := cast.ToIntE(a) if err != nil { return "", err diff --git a/tpl/tplimpl/template_func_truncate_test.go b/tpl/strings/truncate_test.go similarity index 94% rename from tpl/tplimpl/template_func_truncate_test.go rename to tpl/strings/truncate_test.go index 9c4beecff..31c7028b5 100644 --- a/tpl/tplimpl/template_func_truncate_test.go +++ b/tpl/strings/truncate_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package tplimpl +package strings import ( "html/template" @@ -22,6 +22,7 @@ import ( func TestTruncate(t *testing.T) { t.Parallel() + var err error cases := []struct { v1 interface{} @@ -52,11 +53,11 @@ func TestTruncate(t *testing.T) { for i, c := range cases { var result template.HTML if c.v2 == nil { - result, err = truncate(c.v1) + result, err = ns.Truncate(c.v1) } else if c.v3 == nil { - result, err = truncate(c.v1, c.v2) + result, err = ns.Truncate(c.v1, c.v2) } else { - result, err = truncate(c.v1, c.v2, c.v3) + result, err = ns.Truncate(c.v1, c.v2, c.v3) } if c.isErr { @@ -75,7 +76,7 @@ func TestTruncate(t *testing.T) { } // Too many arguments - _, err = truncate(10, " ...", "I am a test sentence", "wrong") + _, err = ns.Truncate(10, " ...", "I am a test sentence", "wrong") if err == nil { t.Errorf("Should have errored") } diff --git a/tpl/time/time.go b/tpl/time/time.go new file mode 100644 index 000000000..fab2b9266 --- /dev/null +++ b/tpl/time/time.go @@ -0,0 +1,59 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package time + +import ( + _time "time" + + "github.com/spf13/cast" +) + +// New returns a new instance of the time-namespaced template functions. +func New() *Namespace { + return &Namespace{} +} + +// Namespace provides template functions for the "time" namespace. +type Namespace struct{} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { return ns } + +// AsTime converts the textual representation of the datetime string into +// a time.Time interface. +func (ns *Namespace) AsTime(v interface{}) (interface{}, error) { + t, err := cast.ToTimeE(v) + if err != nil { + return nil, err + } + + return t, nil +} + +// Format converts the textual representation of the datetime string into +// the other form or returns it of the time.Time value. These are formatted +// with the layout string +func (ns *Namespace) Format(layout string, v interface{}) (string, error) { + t, err := cast.ToTimeE(v) + if err != nil { + return "", err + } + + return t.Format(layout), nil +} + +// Now returns the current local time. +func (ns *Namespace) Now() _time.Time { + return _time.Now() +} diff --git a/tpl/time/time_test.go b/tpl/time/time_test.go new file mode 100644 index 000000000..c84d99235 --- /dev/null +++ b/tpl/time/time_test.go @@ -0,0 +1,67 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package time + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New() + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestFormat(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + layout string + value interface{} + expect interface{} + }{ + {"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"}, + {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"}, + {"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"}, + // The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone + {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, + {"Monday, Jan 2, 2006", 1421733600.123, false}, + {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"}, + {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"}, + {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"}, + {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"}, + } { + result, err := ns.Format(test.layout, test.value) + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] DateFormat didn't return an expected error, got %v", i, result) + } + } else { + if err != nil { + t.Errorf("[%d] DateFormat failed: %s", i, err) + continue + } + if result != test.expect { + t.Errorf("[%d] DateFormat got %v but expected %v", i, result, test.expect) + } + } + } +} diff --git a/tpl/tplimpl/reflect_helpers.go b/tpl/tplimpl/reflect_helpers.go deleted file mode 100644 index 7463683fc..000000000 --- a/tpl/tplimpl/reflect_helpers.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2017 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tplimpl - -import ( - "reflect" - "time" -) - -// toInt returns the int value if possible, -1 if not. -func toInt(v reflect.Value) int64 { - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() - case reflect.Interface: - return toInt(v.Elem()) - } - return -1 -} - -// toString returns the string value if possible, "" if not. -func toString(v reflect.Value) string { - switch v.Kind() { - case reflect.String: - return v.String() - case reflect.Interface: - return toString(v.Elem()) - } - return "" -} - -var ( - zero reflect.Value - errorType = reflect.TypeOf((*error)(nil)).Elem() - timeType = reflect.TypeOf((*time.Time)(nil)).Elem() -) - -func toTimeUnix(v reflect.Value) int64 { - if v.Kind() == reflect.Interface { - return toTimeUnix(v.Elem()) - } - if v.Type() != timeType { - panic("coding error: argument must be time.Time type reflect Value") - } - return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int() -} - -// indirect is taken from 'text/template/exec.go' -func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { - for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { - if v.IsNil() { - return v, true - } - if v.Kind() == reflect.Interface && v.NumMethod() > 0 { - break - } - } - return v, false -} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go index 52e968fdc..694b997f2 100644 --- a/tpl/tplimpl/templateFuncster.go +++ b/tpl/tplimpl/templateFuncster.go @@ -20,17 +20,43 @@ import ( texttemplate "text/template" bp "github.com/spf13/hugo/bufferpool" - - "image" - "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/tpl/collections" + "github.com/spf13/hugo/tpl/crypto" + "github.com/spf13/hugo/tpl/data" + "github.com/spf13/hugo/tpl/encoding" + "github.com/spf13/hugo/tpl/images" + "github.com/spf13/hugo/tpl/inflect" + "github.com/spf13/hugo/tpl/lang" + "github.com/spf13/hugo/tpl/math" + "github.com/spf13/hugo/tpl/os" + "github.com/spf13/hugo/tpl/safe" + hstrings "github.com/spf13/hugo/tpl/strings" + "github.com/spf13/hugo/tpl/time" + "github.com/spf13/hugo/tpl/transform" + "github.com/spf13/hugo/tpl/urls" ) // Some of the template funcs are'nt entirely stateless. type templateFuncster struct { funcMap template.FuncMap cachedPartials partialCache - image *imageHandler + + // Namespaces + collections *collections.Namespace + crypto *crypto.Namespace + data *data.Namespace + encoding *encoding.Namespace + images *images.Namespace + inflect *inflect.Namespace + lang *lang.Namespace + math *math.Namespace + os *os.Namespace + safe *safe.Namespace + strings *hstrings.Namespace + time *time.Namespace + transform *transform.Namespace + urls *urls.Namespace *deps.Deps } @@ -39,7 +65,22 @@ func newTemplateFuncster(deps *deps.Deps) *templateFuncster { return &templateFuncster{ Deps: deps, cachedPartials: partialCache{p: make(map[string]interface{})}, - image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}}, + + // Namespaces + collections: collections.New(deps), + crypto: crypto.New(), + data: data.New(deps), + encoding: encoding.New(), + images: images.New(deps), + inflect: inflect.New(), + lang: lang.New(deps), + math: math.New(), + os: os.New(deps), + safe: safe.New(), + strings: hstrings.New(deps), + time: time.New(), + transform: transform.New(deps), + urls: urls.New(deps), } } diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index 54cff81c6..8d86dfa54 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -16,1523 +16,14 @@ package tplimpl import ( - "bytes" - _md5 "crypto/md5" - _sha1 "crypto/sha1" - _sha256 "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" "fmt" - "html" "html/template" - "image" - "math/rand" - "net/url" - "os" - "reflect" - "regexp" - "sort" - "strconv" - "strings" "sync" - "time" - "unicode/utf8" - "github.com/spf13/hugo/hugofs" - - "github.com/bep/inflect" - "github.com/spf13/afero" "github.com/spf13/cast" - "github.com/spf13/hugo/helpers" - - // Importing image codecs for image.DecodeConfig - _ "image/gif" - _ "image/jpeg" - _ "image/png" + "github.com/spf13/hugo/tpl/compare" ) -// eq returns the boolean truth of arg1 == arg2. -func eq(x, y interface{}) bool { - normalize := func(v interface{}) interface{} { - vv := reflect.ValueOf(v) - switch vv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return vv.Int() - case reflect.Float32, reflect.Float64: - return vv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return vv.Uint() - default: - return v - } - } - x = normalize(x) - y = normalize(y) - return reflect.DeepEqual(x, y) -} - -// ne returns the boolean truth of arg1 != arg2. -func ne(x, y interface{}) bool { - return !eq(x, y) -} - -// ge returns the boolean truth of arg1 >= arg2. -func ge(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left >= right -} - -// gt returns the boolean truth of arg1 > arg2. -func gt(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left > right -} - -// le returns the boolean truth of arg1 <= arg2. -func le(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left <= right -} - -// lt returns the boolean truth of arg1 < arg2. -func lt(a, b interface{}) bool { - left, right := compareGetFloat(a, b) - return left < right -} - -// dictionary creates a map[string]interface{} from the given parameters by -// walking the parameters and treating them as key-value pairs. The number -// of parameters must be even. -func dictionary(values ...interface{}) (map[string]interface{}, error) { - if len(values)%2 != 0 { - return nil, errors.New("invalid dict call") - } - dict := make(map[string]interface{}, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].(string) - if !ok { - return nil, errors.New("dict keys must be strings") - } - dict[key] = values[i+1] - } - return dict, nil -} - -// slice returns a slice of all passed arguments -func slice(args ...interface{}) []interface{} { - return args -} - -func compareGetFloat(a interface{}, b interface{}) (float64, float64) { - var left, right float64 - var leftStr, rightStr *string - av := reflect.ValueOf(a) - - switch av.Kind() { - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: - left = float64(av.Len()) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - left = float64(av.Int()) - case reflect.Float32, reflect.Float64: - left = av.Float() - case reflect.String: - var err error - left, err = strconv.ParseFloat(av.String(), 64) - if err != nil { - str := av.String() - leftStr = &str - } - case reflect.Struct: - switch av.Type() { - case timeType: - left = float64(toTimeUnix(av)) - } - } - - bv := reflect.ValueOf(b) - - switch bv.Kind() { - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: - right = float64(bv.Len()) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - right = float64(bv.Int()) - case reflect.Float32, reflect.Float64: - right = bv.Float() - case reflect.String: - var err error - right, err = strconv.ParseFloat(bv.String(), 64) - if err != nil { - str := bv.String() - rightStr = &str - } - case reflect.Struct: - switch bv.Type() { - case timeType: - right = float64(toTimeUnix(bv)) - } - } - - switch { - case leftStr == nil || rightStr == nil: - case *leftStr < *rightStr: - return 0, 1 - case *leftStr > *rightStr: - return 1, 0 - default: - return 0, 0 - } - - return left, right -} - -// slicestr slices a string by specifying a half-open range with -// two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. -// The end index can be omitted, it defaults to the string's length. -func slicestr(a interface{}, startEnd ...interface{}) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - - var argStart, argEnd int - - argNum := len(startEnd) - - if argNum > 0 { - if argStart, err = cast.ToIntE(startEnd[0]); err != nil { - return "", errors.New("start argument must be integer") - } - } - if argNum > 1 { - if argEnd, err = cast.ToIntE(startEnd[1]); err != nil { - return "", errors.New("end argument must be integer") - } - } - - if argNum > 2 { - return "", errors.New("too many arguments") - } - - asRunes := []rune(aStr) - - if argNum > 0 && (argStart < 0 || argStart >= len(asRunes)) { - return "", errors.New("slice bounds out of range") - } - - if argNum == 2 { - if argEnd < 0 || argEnd > len(asRunes) { - return "", errors.New("slice bounds out of range") - } - return string(asRunes[argStart:argEnd]), nil - } else if argNum == 1 { - return string(asRunes[argStart:]), nil - } else { - return string(asRunes[:]), nil - } - -} - -// hasPrefix tests whether the input s begins with prefix. -func hasPrefix(s, prefix interface{}) (bool, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return false, err - } - - sp, err := cast.ToStringE(prefix) - if err != nil { - return false, err - } - - return strings.HasPrefix(ss, sp), nil -} - -// substr extracts parts of a string, beginning at the character at the specified -// position, and returns the specified number of characters. -// -// It normally takes two parameters: start and length. -// It can also take one parameter: start, i.e. length is omitted, in which case -// the substring starting from start until the end of the string will be returned. -// -// To extract characters from the end of the string, use a negative start number. -// -// In addition, borrowing from the extended behavior described at http://php.net/substr, -// if length is given and is negative, then that many characters will be omitted from -// the end of string. -func substr(a interface{}, nums ...interface{}) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - - var start, length int - - asRunes := []rune(aStr) - - switch len(nums) { - case 0: - return "", errors.New("too less arguments") - case 1: - if start, err = cast.ToIntE(nums[0]); err != nil { - return "", errors.New("start argument must be integer") - } - length = len(asRunes) - case 2: - if start, err = cast.ToIntE(nums[0]); err != nil { - return "", errors.New("start argument must be integer") - } - if length, err = cast.ToIntE(nums[1]); err != nil { - return "", errors.New("length argument must be integer") - } - default: - return "", errors.New("too many arguments") - } - - if start < -len(asRunes) { - start = 0 - } - if start > len(asRunes) { - return "", fmt.Errorf("start position out of bounds for %d-byte string", len(aStr)) - } - - var s, e int - if start >= 0 && length >= 0 { - s = start - e = start + length - } else if start < 0 && length >= 0 { - s = len(asRunes) + start - length + 1 - e = len(asRunes) + start + 1 - } else if start >= 0 && length < 0 { - s = start - e = len(asRunes) + length - } else { - s = len(asRunes) + start - e = len(asRunes) + length - } - - if s > e { - return "", fmt.Errorf("calculated start position greater than end position: %d > %d", s, e) - } - if e > len(asRunes) { - e = len(asRunes) - } - - return string(asRunes[s:e]), nil -} - -// split slices an input string into all substrings separated by delimiter. -func split(a interface{}, delimiter string) ([]string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return []string{}, err - } - return strings.Split(aStr, delimiter), nil -} - -// intersect returns the common elements in the given sets, l1 and l2. l1 and -// l2 must be of the same type and may be either arrays or slices. -func intersect(l1, l2 interface{}) (interface{}, error) { - if l1 == nil || l2 == nil { - return make([]interface{}, 0), nil - } - - l1v := reflect.ValueOf(l1) - l2v := reflect.ValueOf(l2) - - switch l1v.Kind() { - case reflect.Array, reflect.Slice: - switch l2v.Kind() { - case reflect.Array, reflect.Slice: - r := reflect.MakeSlice(l1v.Type(), 0, 0) - for i := 0; i < l1v.Len(); i++ { - l1vv := l1v.Index(i) - for j := 0; j < l2v.Len(); j++ { - l2vv := l2v.Index(j) - switch l1vv.Kind() { - case reflect.String: - if l1vv.Type() == l2vv.Type() && l1vv.String() == l2vv.String() && !in(r.Interface(), l2vv.Interface()) { - r = reflect.Append(r, l2vv) - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - switch l2vv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if l1vv.Int() == l2vv.Int() && !in(r.Interface(), l2vv.Interface()) { - r = reflect.Append(r, l2vv) - } - } - case reflect.Float32, reflect.Float64: - switch l2vv.Kind() { - case reflect.Float32, reflect.Float64: - if l1vv.Float() == l2vv.Float() && !in(r.Interface(), l2vv.Interface()) { - r = reflect.Append(r, l2vv) - } - } - } - } - } - return r.Interface(), nil - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) - } - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) - } -} - -// union returns the union of the given sets, l1 and l2. l1 and -// l2 must be of the same type and may be either arrays or slices. -// If l1 and l2 aren't of the same type then l1 will be returned. -// If either l1 or l2 is nil then the non-nil list will be returned. -func union(l1, l2 interface{}) (interface{}, error) { - if l1 == nil && l2 == nil { - return nil, errors.New("both arrays/slices have to be of the same type") - } else if l1 == nil && l2 != nil { - return l2, nil - } else if l1 != nil && l2 == nil { - return l1, nil - } - - l1v := reflect.ValueOf(l1) - l2v := reflect.ValueOf(l2) - - switch l1v.Kind() { - case reflect.Array, reflect.Slice: - switch l2v.Kind() { - case reflect.Array, reflect.Slice: - r := reflect.MakeSlice(l1v.Type(), 0, 0) - - if l1v.Type() != l2v.Type() { - return r.Interface(), nil - } - - for i := 0; i < l1v.Len(); i++ { - elem := l1v.Index(i) - if !in(r.Interface(), elem.Interface()) { - r = reflect.Append(r, elem) - } - } - - for j := 0; j < l2v.Len(); j++ { - elem := l2v.Index(j) - if !in(r.Interface(), elem.Interface()) { - r = reflect.Append(r, elem) - } - } - - return r.Interface(), nil - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(l2).Type().String()) - } - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(l1).Type().String()) - } -} - -type imageHandler struct { - imageConfigCache map[string]image.Config - sync.RWMutex - fs *hugofs.Fs -} - -// imageConfig returns the image.Config for the specified path relative to the -// working directory. -func (ic *imageHandler) config(path interface{}) (image.Config, error) { - filename, err := cast.ToStringE(path) - if err != nil { - return image.Config{}, err - } - - if filename == "" { - return image.Config{}, errors.New("config needs a filename") - } - - // Check cache for image config. - ic.RLock() - config, ok := ic.imageConfigCache[filename] - ic.RUnlock() - - if ok { - return config, nil - } - - f, err := ic.fs.WorkingDir.Open(filename) - if err != nil { - return image.Config{}, err - } - - config, _, err = image.DecodeConfig(f) - - ic.Lock() - ic.imageConfigCache[filename] = config - ic.Unlock() - - return config, err -} - -// in returns whether v is in the set l. l may be an array or slice. -func in(l interface{}, v interface{}) bool { - lv := reflect.ValueOf(l) - vv := reflect.ValueOf(v) - - switch lv.Kind() { - case reflect.Array, reflect.Slice: - for i := 0; i < lv.Len(); i++ { - lvv := lv.Index(i) - lvv, isNil := indirect(lvv) - if isNil { - continue - } - switch lvv.Kind() { - case reflect.String: - if vv.Type() == lvv.Type() && vv.String() == lvv.String() { - return true - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - switch vv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if vv.Int() == lvv.Int() { - return true - } - } - case reflect.Float32, reflect.Float64: - switch vv.Kind() { - case reflect.Float32, reflect.Float64: - if vv.Float() == lvv.Float() { - return true - } - } - } - } - case reflect.String: - if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) { - return true - } - } - return false -} - -// first returns the first N items in a rangeable list. -func first(limit interface{}, seq interface{}) (interface{}, error) { - if limit == nil || seq == nil { - return nil, errors.New("both limit and seq must be provided") - } - - limitv, err := cast.ToIntE(limit) - - if err != nil { - return nil, err - } - - if limitv < 1 { - return nil, errors.New("can't return negative/empty count of items from sequence") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - if limitv > seqv.Len() { - limitv = seqv.Len() - } - return seqv.Slice(0, limitv).Interface(), nil -} - -// findRE returns a list of strings that match the regular expression. By default all matches -// will be included. The number of matches can be limited with an optional third parameter. -func findRE(expr string, content interface{}, limit ...interface{}) ([]string, error) { - re, err := reCache.Get(expr) - if err != nil { - return nil, err - } - - conv, err := cast.ToStringE(content) - if err != nil { - return nil, err - } - - if len(limit) == 0 { - return re.FindAllString(conv, -1), nil - } - - lim, err := cast.ToIntE(limit[0]) - if err != nil { - return nil, err - } - - return re.FindAllString(conv, lim), nil -} - -// last returns the last N items in a rangeable list. -func last(limit interface{}, seq interface{}) (interface{}, error) { - if limit == nil || seq == nil { - return nil, errors.New("both limit and seq must be provided") - } - - limitv, err := cast.ToIntE(limit) - - if err != nil { - return nil, err - } - - if limitv < 1 { - return nil, errors.New("can't return negative/empty count of items from sequence") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - if limitv > seqv.Len() { - limitv = seqv.Len() - } - return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil -} - -// after returns all the items after the first N in a rangeable list. -func after(index interface{}, seq interface{}) (interface{}, error) { - if index == nil || seq == nil { - return nil, errors.New("both limit and seq must be provided") - } - - indexv, err := cast.ToIntE(index) - - if err != nil { - return nil, err - } - - if indexv < 1 { - return nil, errors.New("can't return negative/empty count of items from sequence") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - if indexv >= seqv.Len() { - return nil, errors.New("no items left") - } - return seqv.Slice(indexv, seqv.Len()).Interface(), nil -} - -// shuffle returns the given rangeable list in a randomised order. -func shuffle(seq interface{}) (interface{}, error) { - if seq == nil { - return nil, errors.New("both count and seq must be provided") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - // okay - default: - return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) - } - - shuffled := reflect.MakeSlice(reflect.TypeOf(seq), seqv.Len(), seqv.Len()) - - rand.Seed(time.Now().UTC().UnixNano()) - randomIndices := rand.Perm(seqv.Len()) - - for index, value := range randomIndices { - shuffled.Index(value).Set(seqv.Index(index)) - } - - return shuffled.Interface(), nil -} - -func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) { - if !obj.IsValid() { - return zero, errors.New("can't evaluate an invalid value") - } - typ := obj.Type() - obj, isNil := indirect(obj) - - // first, check whether obj has a method. In this case, obj is - // an interface, a struct or its pointer. If obj is a struct, - // to check all T and *T method, use obj pointer type Value - objPtr := obj - if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { - objPtr = objPtr.Addr() - } - mt, ok := objPtr.Type().MethodByName(elemName) - if ok { - if mt.PkgPath != "" { - return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ) - } - // struct pointer has one receiver argument and interface doesn't have an argument - if mt.Type.NumIn() > 1 || mt.Type.NumOut() == 0 || mt.Type.NumOut() > 2 { - return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) - } - if mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType) { - return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) - } - if mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType) { - return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) - } - res := objPtr.Method(mt.Index).Call([]reflect.Value{}) - if len(res) == 2 && !res[1].IsNil() { - return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error)) - } - return res[0], nil - } - - // elemName isn't a method so next start to check whether it is - // a struct field or a map value. In both cases, it mustn't be - // a nil value - if isNil { - return zero, fmt.Errorf("can't evaluate a nil pointer of type %s by a struct field or map key name %s", typ, elemName) - } - switch obj.Kind() { - case reflect.Struct: - ft, ok := obj.Type().FieldByName(elemName) - if ok { - if ft.PkgPath != "" && !ft.Anonymous { - return zero, fmt.Errorf("%s is an unexported field of struct type %s", elemName, typ) - } - return obj.FieldByIndex(ft.Index), nil - } - return zero, fmt.Errorf("%s isn't a field of struct type %s", elemName, typ) - case reflect.Map: - kv := reflect.ValueOf(elemName) - if kv.Type().AssignableTo(obj.Type().Key()) { - return obj.MapIndex(kv), nil - } - return zero, fmt.Errorf("%s isn't a key of map type %s", elemName, typ) - } - return zero, fmt.Errorf("%s is neither a struct field, a method nor a map element of type %s", elemName, typ) -} - -func checkCondition(v, mv reflect.Value, op string) (bool, error) { - v, vIsNil := indirect(v) - if !v.IsValid() { - vIsNil = true - } - mv, mvIsNil := indirect(mv) - if !mv.IsValid() { - mvIsNil = true - } - if vIsNil || mvIsNil { - switch op { - case "", "=", "==", "eq": - return vIsNil == mvIsNil, nil - case "!=", "<>", "ne": - return vIsNil != mvIsNil, nil - } - return false, nil - } - - if v.Kind() == reflect.Bool && mv.Kind() == reflect.Bool { - switch op { - case "", "=", "==", "eq": - return v.Bool() == mv.Bool(), nil - case "!=", "<>", "ne": - return v.Bool() != mv.Bool(), nil - } - return false, nil - } - - var ivp, imvp *int64 - var svp, smvp *string - var slv, slmv interface{} - var ima []int64 - var sma []string - if mv.Type() == v.Type() { - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - iv := v.Int() - ivp = &iv - imv := mv.Int() - imvp = &imv - case reflect.String: - sv := v.String() - svp = &sv - smv := mv.String() - smvp = &smv - case reflect.Struct: - switch v.Type() { - case timeType: - iv := toTimeUnix(v) - ivp = &iv - imv := toTimeUnix(mv) - imvp = &imv - } - case reflect.Array, reflect.Slice: - slv = v.Interface() - slmv = mv.Interface() - } - } else { - if mv.Kind() != reflect.Array && mv.Kind() != reflect.Slice { - return false, nil - } - - if mv.Len() == 0 { - return false, nil - } - - if v.Kind() != reflect.Interface && mv.Type().Elem().Kind() != reflect.Interface && mv.Type().Elem() != v.Type() { - return false, nil - } - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - iv := v.Int() - ivp = &iv - for i := 0; i < mv.Len(); i++ { - if anInt := toInt(mv.Index(i)); anInt != -1 { - ima = append(ima, anInt) - } - - } - case reflect.String: - sv := v.String() - svp = &sv - for i := 0; i < mv.Len(); i++ { - if aString := toString(mv.Index(i)); aString != "" { - sma = append(sma, aString) - } - } - case reflect.Struct: - switch v.Type() { - case timeType: - iv := toTimeUnix(v) - ivp = &iv - for i := 0; i < mv.Len(); i++ { - ima = append(ima, toTimeUnix(mv.Index(i))) - } - } - } - } - - switch op { - case "", "=", "==", "eq": - if ivp != nil && imvp != nil { - return *ivp == *imvp, nil - } else if svp != nil && smvp != nil { - return *svp == *smvp, nil - } - case "!=", "<>", "ne": - if ivp != nil && imvp != nil { - return *ivp != *imvp, nil - } else if svp != nil && smvp != nil { - return *svp != *smvp, nil - } - case ">=", "ge": - if ivp != nil && imvp != nil { - return *ivp >= *imvp, nil - } else if svp != nil && smvp != nil { - return *svp >= *smvp, nil - } - case ">", "gt": - if ivp != nil && imvp != nil { - return *ivp > *imvp, nil - } else if svp != nil && smvp != nil { - return *svp > *smvp, nil - } - case "<=", "le": - if ivp != nil && imvp != nil { - return *ivp <= *imvp, nil - } else if svp != nil && smvp != nil { - return *svp <= *smvp, nil - } - case "<", "lt": - if ivp != nil && imvp != nil { - return *ivp < *imvp, nil - } else if svp != nil && smvp != nil { - return *svp < *smvp, nil - } - case "in", "not in": - var r bool - if ivp != nil && len(ima) > 0 { - r = in(ima, *ivp) - } else if svp != nil { - if len(sma) > 0 { - r = in(sma, *svp) - } else if smvp != nil { - r = in(*smvp, *svp) - } - } else { - return false, nil - } - if op == "not in" { - return !r, nil - } - return r, nil - case "intersect": - r, err := intersect(slv, slmv) - if err != nil { - return false, err - } - - if reflect.TypeOf(r).Kind() == reflect.Slice { - s := reflect.ValueOf(r) - - if s.Len() > 0 { - return true, nil - } - return false, nil - } - return false, errors.New("invalid intersect values") - default: - return false, errors.New("no such operator") - } - return false, nil -} - -// parseWhereArgs parses the end arguments to the where function. Return a -// match value and an operator, if one is defined. -func parseWhereArgs(args ...interface{}) (mv reflect.Value, op string, err error) { - switch len(args) { - case 1: - mv = reflect.ValueOf(args[0]) - case 2: - var ok bool - if op, ok = args[0].(string); !ok { - err = errors.New("operator argument must be string type") - return - } - op = strings.TrimSpace(strings.ToLower(op)) - mv = reflect.ValueOf(args[1]) - default: - err = errors.New("can't evaluate the array by no match argument or more than or equal to two arguments") - } - return -} - -// checkWhereArray handles the where-matching logic when the seqv value is an -// Array or Slice. -func checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { - rv := reflect.MakeSlice(seqv.Type(), 0, 0) - for i := 0; i < seqv.Len(); i++ { - var vvv reflect.Value - rvv := seqv.Index(i) - if kv.Kind() == reflect.String { - vvv = rvv - for _, elemName := range path { - var err error - vvv, err = evaluateSubElem(vvv, elemName) - if err != nil { - return nil, err - } - } - } else { - vv, _ := indirect(rvv) - if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { - vvv = vv.MapIndex(kv) - } - } - - if ok, err := checkCondition(vvv, mv, op); ok { - rv = reflect.Append(rv, rvv) - } else if err != nil { - return nil, err - } - } - return rv.Interface(), nil -} - -// checkWhereMap handles the where-matching logic when the seqv value is a Map. -func checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { - rv := reflect.MakeMap(seqv.Type()) - keys := seqv.MapKeys() - for _, k := range keys { - elemv := seqv.MapIndex(k) - switch elemv.Kind() { - case reflect.Array, reflect.Slice: - r, err := checkWhereArray(elemv, kv, mv, path, op) - if err != nil { - return nil, err - } - - switch rr := reflect.ValueOf(r); rr.Kind() { - case reflect.Slice: - if rr.Len() > 0 { - rv.SetMapIndex(k, elemv) - } - } - case reflect.Interface: - elemvv, isNil := indirect(elemv) - if isNil { - continue - } - - switch elemvv.Kind() { - case reflect.Array, reflect.Slice: - r, err := checkWhereArray(elemvv, kv, mv, path, op) - if err != nil { - return nil, err - } - - switch rr := reflect.ValueOf(r); rr.Kind() { - case reflect.Slice: - if rr.Len() > 0 { - rv.SetMapIndex(k, elemv) - } - } - } - } - } - return rv.Interface(), nil -} - -// where returns a filtered subset of a given data type. -func where(seq, key interface{}, args ...interface{}) (interface{}, error) { - seqv, isNil := indirect(reflect.ValueOf(seq)) - if isNil { - return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) - } - - mv, op, err := parseWhereArgs(args...) - if err != nil { - return nil, err - } - - var path []string - kv := reflect.ValueOf(key) - if kv.Kind() == reflect.String { - path = strings.Split(strings.Trim(kv.String(), "."), ".") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice: - return checkWhereArray(seqv, kv, mv, path, op) - case reflect.Map: - return checkWhereMap(seqv, kv, mv, path, op) - default: - return nil, fmt.Errorf("can't iterate over %v", seq) - } -} - -// apply takes a map, array, or slice and returns a new slice with the function fname applied over it. -func (t *templateFuncster) apply(seq interface{}, fname string, args ...interface{}) (interface{}, error) { - if seq == nil { - return make([]interface{}, 0), nil - } - - if fname == "apply" { - return nil, errors.New("can't apply myself (no turtles allowed)") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - fn, found := t.funcMap[fname] - if !found { - return nil, errors.New("can't find function " + fname) - } - - fnv := reflect.ValueOf(fn) - - switch seqv.Kind() { - case reflect.Array, reflect.Slice: - r := make([]interface{}, seqv.Len()) - for i := 0; i < seqv.Len(); i++ { - vv := seqv.Index(i) - - vvv, err := applyFnToThis(fnv, vv, args...) - - if err != nil { - return nil, err - } - - r[i] = vvv.Interface() - } - - return r, nil - default: - return nil, fmt.Errorf("can't apply over %v", seq) - } -} - -func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, error) { - n := make([]reflect.Value, len(args)) - for i, arg := range args { - if arg == "." { - n[i] = this - } else { - n[i] = reflect.ValueOf(arg) - } - } - - num := fn.Type().NumIn() - - if fn.Type().IsVariadic() { - num-- - } - - // TODO(bep) see #1098 - also see template_tests.go - /*if len(args) < num { - return reflect.ValueOf(nil), errors.New("Too few arguments") - } else if len(args) > num { - return reflect.ValueOf(nil), errors.New("Too many arguments") - }*/ - - for i := 0; i < num; i++ { - if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) { - return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String()) - } - } - - res := fn.Call(n) - - if len(res) == 1 || res[1].IsNil() { - return res[0], nil - } - return reflect.ValueOf(nil), res[1].Interface().(error) -} - -// delimit takes a given sequence and returns a delimited HTML string. -// If last is passed to the function, it will be used as the final delimiter. -func delimit(seq, delimiter interface{}, last ...interface{}) (template.HTML, error) { - d, err := cast.ToStringE(delimiter) - if err != nil { - return "", err - } - - var dLast *string - if len(last) > 0 { - l := last[0] - dStr, err := cast.ToStringE(l) - if err != nil { - dLast = nil - } - dLast = &dStr - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return "", errors.New("can't iterate over a nil value") - } - - var str string - switch seqv.Kind() { - case reflect.Map: - sortSeq, err := sortSeq(seq) - if err != nil { - return "", err - } - seqv = reflect.ValueOf(sortSeq) - fallthrough - case reflect.Array, reflect.Slice, reflect.String: - for i := 0; i < seqv.Len(); i++ { - val := seqv.Index(i).Interface() - valStr, err := cast.ToStringE(val) - if err != nil { - continue - } - switch { - case i == seqv.Len()-2 && dLast != nil: - str += valStr + *dLast - case i == seqv.Len()-1: - str += valStr - default: - str += valStr + d - } - } - - default: - return "", fmt.Errorf("can't iterate over %v", seq) - } - - return template.HTML(str), nil -} - -// sortSeq returns a sorted sequence. -func sortSeq(seq interface{}, args ...interface{}) (interface{}, error) { - if seq == nil { - return nil, errors.New("sequence must be provided") - } - - seqv := reflect.ValueOf(seq) - seqv, isNil := indirect(seqv) - if isNil { - return nil, errors.New("can't iterate over a nil value") - } - - switch seqv.Kind() { - case reflect.Array, reflect.Slice, reflect.Map: - // ok - default: - return nil, errors.New("can't sort " + reflect.ValueOf(seq).Type().String()) - } - - // Create a list of pairs that will be used to do the sort - p := pairList{SortAsc: true, SliceType: reflect.SliceOf(seqv.Type().Elem())} - p.Pairs = make([]pair, seqv.Len()) - - var sortByField string - for i, l := range args { - dStr, err := cast.ToStringE(l) - switch { - case i == 0 && err != nil: - sortByField = "" - case i == 0 && err == nil: - sortByField = dStr - case i == 1 && err == nil && dStr == "desc": - p.SortAsc = false - case i == 1: - p.SortAsc = true - } - } - path := strings.Split(strings.Trim(sortByField, "."), ".") - - switch seqv.Kind() { - case reflect.Array, reflect.Slice: - for i := 0; i < seqv.Len(); i++ { - p.Pairs[i].Value = seqv.Index(i) - if sortByField == "" || sortByField == "value" { - p.Pairs[i].Key = p.Pairs[i].Value - } else { - v := p.Pairs[i].Value - var err error - for _, elemName := range path { - v, err = evaluateSubElem(v, elemName) - if err != nil { - return nil, err - } - } - p.Pairs[i].Key = v - } - } - - case reflect.Map: - keys := seqv.MapKeys() - for i := 0; i < seqv.Len(); i++ { - p.Pairs[i].Value = seqv.MapIndex(keys[i]) - if sortByField == "" { - p.Pairs[i].Key = keys[i] - } else if sortByField == "value" { - p.Pairs[i].Key = p.Pairs[i].Value - } else { - v := p.Pairs[i].Value - var err error - for _, elemName := range path { - v, err = evaluateSubElem(v, elemName) - if err != nil { - return nil, err - } - } - p.Pairs[i].Key = v - } - } - } - return p.sort(), nil -} - -// Credit for pair sorting method goes to Andrew Gerrand -// https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw -// A data structure to hold a key/value pair. -type pair struct { - Key reflect.Value - Value reflect.Value -} - -// A slice of pairs that implements sort.Interface to sort by Value. -type pairList struct { - Pairs []pair - SortAsc bool - SliceType reflect.Type -} - -func (p pairList) Swap(i, j int) { p.Pairs[i], p.Pairs[j] = p.Pairs[j], p.Pairs[i] } -func (p pairList) Len() int { return len(p.Pairs) } -func (p pairList) Less(i, j int) bool { - iv := p.Pairs[i].Key - jv := p.Pairs[j].Key - - if iv.IsValid() { - if jv.IsValid() { - // can only call Interface() on valid reflect Values - return lt(iv.Interface(), jv.Interface()) - } - // if j is invalid, test i against i's zero value - return lt(iv.Interface(), reflect.Zero(iv.Type())) - } - - if jv.IsValid() { - // if i is invalid, test j against j's zero value - return lt(reflect.Zero(jv.Type()), jv.Interface()) - } - - return false -} - -// sorts a pairList and returns a slice of sorted values -func (p pairList) sort() interface{} { - if p.SortAsc { - sort.Sort(p) - } else { - sort.Sort(sort.Reverse(p)) - } - sorted := reflect.MakeSlice(p.SliceType, len(p.Pairs), len(p.Pairs)) - for i, v := range p.Pairs { - sorted.Index(i).Set(v.Value) - } - - return sorted.Interface() -} - -// isSet returns whether a given array, channel, slice, or map has a key -// defined. -func isSet(a interface{}, key interface{}) bool { - av := reflect.ValueOf(a) - kv := reflect.ValueOf(key) - - switch av.Kind() { - case reflect.Array, reflect.Chan, reflect.Slice: - if int64(av.Len()) > kv.Int() { - return true - } - case reflect.Map: - if kv.Type() == av.Type().Key() { - return av.MapIndex(kv).IsValid() - } - } - - return false -} - -// returnWhenSet returns a given value if it set. Otherwise, it returns an -// empty string. -func returnWhenSet(a, k interface{}) interface{} { - av, isNil := indirect(reflect.ValueOf(a)) - if isNil { - return "" - } - - var avv reflect.Value - switch av.Kind() { - case reflect.Array, reflect.Slice: - index, ok := k.(int) - if ok && av.Len() > index { - avv = av.Index(index) - } - case reflect.Map: - kv := reflect.ValueOf(k) - if kv.Type().AssignableTo(av.Type().Key()) { - avv = av.MapIndex(kv) - } - } - - avv, isNil = indirect(avv) - - if isNil { - return "" - } - - if avv.IsValid() { - switch avv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return avv.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return avv.Uint() - case reflect.Float32, reflect.Float64: - return avv.Float() - case reflect.String: - return avv.String() - } - } - - return "" -} - -// highlight returns an HTML string with syntax highlighting applied. -func (t *templateFuncster) highlight(in interface{}, lang, opts string) (template.HTML, error) { - str, err := cast.ToStringE(in) - - if err != nil { - return "", err - } - - return template.HTML(helpers.Highlight(t.Cfg, html.UnescapeString(str), lang, opts)), nil -} - -var markdownTrimPrefix = []byte("

") -var markdownTrimSuffix = []byte("

\n") - -// markdownify renders a given string from Markdown to HTML. -func (t *templateFuncster) markdownify(in interface{}) (template.HTML, error) { - text, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - m := t.ContentSpec.RenderBytes(&helpers.RenderingContext{ - Cfg: t.Cfg, - Content: []byte(text), PageFmt: "markdown", - Config: t.ContentSpec.NewBlackfriday()}) - m = bytes.TrimPrefix(m, markdownTrimPrefix) - m = bytes.TrimSuffix(m, markdownTrimSuffix) - return template.HTML(m), nil -} - -// jsonify encodes a given object to JSON. -func jsonify(v interface{}) (template.HTML, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return template.HTML(b), nil -} - -// emojify "emojifies" the given string. -// -// See http://www.emoji-cheat-sheet.com/ -func emojify(in interface{}) (template.HTML, error) { - str, err := cast.ToStringE(in) - - if err != nil { - return "", err - } - - return template.HTML(helpers.Emojify([]byte(str))), nil -} - -// plainify strips any HTML and returns the plain text version. -func plainify(in interface{}) (string, error) { - s, err := cast.ToStringE(in) - - if err != nil { - return "", err - } - - return helpers.StripHTML(s), nil -} - -type reflinker interface { - Ref(refs ...string) (string, error) - RelRef(refs ...string) (string, error) -} - -// ref returns the absolute URL path to a given content item. -func ref(in interface{}, refs ...string) (template.HTML, error) { - p, ok := in.(reflinker) - if !ok { - return "", errors.New("invalid Page received in ref") - } - s, err := p.Ref(refs...) - return template.HTML(s), err -} - -// relRef returns the relative URL path to a given content item. -func relRef(in interface{}, refs ...string) (template.HTML, error) { - p, ok := in.(reflinker) - if !ok { - return "", errors.New("invalid Page received in relref") - } - - s, err := p.RelRef(refs...) - return template.HTML(s), err -} - -// chomp removes trailing newline characters from a string. -func chomp(text interface{}) (template.HTML, error) { - s, err := cast.ToStringE(text) - if err != nil { - return "", err - } - - return template.HTML(strings.TrimRight(s, "\r\n")), nil -} - -// lower returns a copy of the input s with all Unicode letters mapped to their -// lower case. -func lower(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return strings.ToLower(ss), nil -} - -// title returns a copy of the input s with all Unicode letters that begin words -// mapped to their title case. -func title(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return strings.Title(ss), nil -} - -// upper returns a copy of the input s with all Unicode letters mapped to their -// upper case. -func upper(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return strings.ToUpper(ss), nil -} - -// trim leading/trailing characters defined by b from a -func trim(a interface{}, b string) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - return strings.Trim(aStr, b), nil -} - -// replace all occurrences of b with c in a -func replace(a, b, c interface{}) (string, error) { - aStr, err := cast.ToStringE(a) - if err != nil { - return "", err - } - bStr, err := cast.ToStringE(b) - if err != nil { - return "", err - } - cStr, err := cast.ToStringE(c) - if err != nil { - return "", err - } - return strings.Replace(aStr, bStr, cStr, -1), nil -} - -// partialCache represents a cache of partials protected by a mutex. -type partialCache struct { - sync.RWMutex - p map[string]interface{} -} - // Get retrieves partial output from the cache based upon the partial name. // If the partial is not found in the cache, the partial is rendered and added // to the cache. @@ -1561,6 +52,12 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p interfa return } +// partialCache represents a cache of partials protected by a mutex. +type partialCache struct { + sync.RWMutex + p map[string]interface{} +} + // partialCached executes and caches partial templates. An optional variant // string parameter (a string slice actually, but be only use a variadic // argument to make it optional) can be passed so that a given partial can have @@ -1575,651 +72,120 @@ func (t *templateFuncster) partialCached(name string, context interface{}, varia return t.Get(key, name, context) } -// regexpCache represents a cache of regexp objects protected by a mutex. -type regexpCache struct { - mu sync.RWMutex - re map[string]*regexp.Regexp -} - -// Get retrieves a regexp object from the cache based upon the pattern. -// If the pattern is not found in the cache, create one -func (rc *regexpCache) Get(pattern string) (re *regexp.Regexp, err error) { - var ok bool - - if re, ok = rc.get(pattern); !ok { - re, err = regexp.Compile(pattern) - if err != nil { - return nil, err - } - rc.set(pattern, re) - } - - return re, nil -} - -func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) { - rc.mu.RLock() - re, ok = rc.re[key] - rc.mu.RUnlock() - return -} - -func (rc *regexpCache) set(key string, re *regexp.Regexp) { - rc.mu.Lock() - rc.re[key] = re - rc.mu.Unlock() -} - -var reCache = regexpCache{re: make(map[string]*regexp.Regexp)} - -// replaceRE exposes a regular expression replacement function to the templates. -func replaceRE(pattern, repl, src interface{}) (_ string, err error) { - patternStr, err := cast.ToStringE(pattern) - if err != nil { - return - } - - replStr, err := cast.ToStringE(repl) - if err != nil { - return - } - - srcStr, err := cast.ToStringE(src) - if err != nil { - return - } - - re, err := reCache.Get(patternStr) - if err != nil { - return "", err - } - return re.ReplaceAllString(srcStr, replStr), nil -} - -// asTime converts the textual representation of the datetime string into -// a time.Time interface. -func asTime(v interface{}) (interface{}, error) { - t, err := cast.ToTimeE(v) - if err != nil { - return nil, err - } - return t, nil -} - -// dateFormat converts the textual representation of the datetime string into -// the other form or returns it of the time.Time value. These are formatted -// with the layout string -func dateFormat(layout string, v interface{}) (string, error) { - t, err := cast.ToTimeE(v) - if err != nil { - return "", err - } - return t.Format(layout), nil -} - -// dfault checks whether a given value is set and returns a default value if it -// is not. "Set" in this context means non-zero for numeric types and times; -// non-zero length for strings, arrays, slices, and maps; -// any boolean or struct value; or non-nil for any other types. -func dfault(dflt interface{}, given ...interface{}) (interface{}, error) { - // given is variadic because the following construct will not pass a piped - // argument when the key is missing: {{ index . "key" | default "foo" }} - // The Go template will complain that we got 1 argument when we expectd 2. - - if len(given) == 0 { - return dflt, nil - } - if len(given) != 1 { - return nil, fmt.Errorf("wrong number of args for default: want 2 got %d", len(given)+1) - } - - g := reflect.ValueOf(given[0]) - if !g.IsValid() { - return dflt, nil - } - - set := false - - switch g.Kind() { - case reflect.Bool: - set = true - case reflect.String, reflect.Array, reflect.Slice, reflect.Map: - set = g.Len() != 0 - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - set = g.Int() != 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - set = g.Uint() != 0 - case reflect.Float32, reflect.Float64: - set = g.Float() != 0 - case reflect.Complex64, reflect.Complex128: - set = g.Complex() != 0 - case reflect.Struct: - switch actual := given[0].(type) { - case time.Time: - set = !actual.IsZero() - default: - set = true - } - default: - set = !g.IsNil() - } - - if set { - return given[0], nil - } - - return dflt, nil -} - -// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. -// -// Copied from Go stdlib src/text/template/exec.go. -func canBeNil(typ reflect.Type) bool { - switch typ.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return true - } - return false -} - -// prepareArg checks if value can be used as an argument of type argType, and -// converts an invalid value to appropriate zero if possible. -// -// Copied from Go stdlib src/text/template/funcs.go. -func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) { - if !value.IsValid() { - if !canBeNil(argType) { - return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType) - } - value = reflect.Zero(argType) - } - if !value.Type().AssignableTo(argType) { - return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType) - } - return value, nil -} - -// index returns the result of indexing its first argument by the following -// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each -// indexed item must be a map, slice, or array. -// -// Copied from Go stdlib src/text/template/funcs.go. -// Can hopefully be removed in Go 1.7, see https://github.com/golang/go/issues/14751 -func index(item interface{}, indices ...interface{}) (interface{}, error) { - v := reflect.ValueOf(item) - if !v.IsValid() { - return nil, errors.New("index of untyped nil") - } - for _, i := range indices { - index := reflect.ValueOf(i) - var isNil bool - if v, isNil = indirect(v); isNil { - return nil, errors.New("index of nil pointer") - } - switch v.Kind() { - case reflect.Array, reflect.Slice, reflect.String: - var x int64 - switch index.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - x = index.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - x = int64(index.Uint()) - case reflect.Invalid: - return nil, errors.New("cannot index slice/array with nil") - default: - return nil, fmt.Errorf("cannot index slice/array with type %s", index.Type()) - } - if x < 0 || x >= int64(v.Len()) { - // We deviate from stdlib here. Don't return an error if the - // index is out of range. - return nil, nil - } - v = v.Index(int(x)) - case reflect.Map: - index, err := prepareArg(index, v.Type().Key()) - if err != nil { - return nil, err - } - if x := v.MapIndex(index); x.IsValid() { - v = x - } else { - v = reflect.Zero(v.Type().Elem()) - } - case reflect.Invalid: - // the loop holds invariant: v.IsValid() - panic("unreachable") - default: - return nil, fmt.Errorf("can't index item of type %s", v.Type()) - } - } - return v.Interface(), nil -} - -// readFile reads the file named by filename relative to the given basepath -// and returns the contents as a string. -// There is a upper size limit set at 1 megabytes. -func readFile(fs *afero.BasePathFs, filename string) (string, error) { - if filename == "" { - return "", errors.New("readFile needs a filename") - } - - if info, err := fs.Stat(filename); err == nil { - if info.Size() > 1000000 { - return "", fmt.Errorf("File %q is too big", filename) - } - } else { - return "", err - } - b, err := afero.ReadFile(fs, filename) - - if err != nil { - return "", err - } - - return string(b), nil -} - -// readFileFromWorkingDir reads the file named by filename relative to the -// configured WorkingDir. -// It returns the contents as a string. -// There is a upper size limit set at 1 megabytes. -func (t *templateFuncster) readFileFromWorkingDir(i interface{}) (string, error) { - s, err := cast.ToStringE(i) - if err != nil { - return "", err - } - return readFile(t.Fs.WorkingDir, s) -} - -// readDirFromWorkingDir listst the directory content relative to the -// configured WorkingDir. -func (t *templateFuncster) readDirFromWorkingDir(i interface{}) ([]os.FileInfo, error) { - path, err := cast.ToStringE(i) - if err != nil { - return nil, err - } - - list, err := afero.ReadDir(t.Fs.WorkingDir, path) - - if err != nil { - return nil, fmt.Errorf("Failed to read Directory %s with error message %s", path, err) - } - - return list, nil -} - -// safeHTMLAttr returns a given string as html/template HTMLAttr content. -func safeHTMLAttr(a interface{}) (template.HTMLAttr, error) { - s, err := cast.ToStringE(a) - return template.HTMLAttr(s), err -} - -// safeCSS returns a given string as html/template CSS content. -func safeCSS(a interface{}) (template.CSS, error) { - s, err := cast.ToStringE(a) - return template.CSS(s), err -} - -// safeURL returns a given string as html/template URL content. -func safeURL(a interface{}) (template.URL, error) { - s, err := cast.ToStringE(a) - return template.URL(s), err -} - -// safeHTML returns a given string as html/template HTML content. -func safeHTML(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - return template.HTML(s), err -} - -// safeJS returns the given string as a html/template JS content. -func safeJS(a interface{}) (template.JS, error) { - s, err := cast.ToStringE(a) - return template.JS(s), err -} - -// mod returns a % b. -func mod(a, b interface{}) (int64, error) { - av := reflect.ValueOf(a) - bv := reflect.ValueOf(b) - var ai, bi int64 - - switch av.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - ai = av.Int() - default: - return 0, errors.New("Modulo operator can't be used with non integer value") - } - - switch bv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bi = bv.Int() - default: - return 0, errors.New("Modulo operator can't be used with non integer value") - } - - if bi == 0 { - return 0, errors.New("The number can't be divided by zero at modulo operation") - } - - return ai % bi, nil -} - -// modBool returns the boolean of a % b. If a % b == 0, return true. -func modBool(a, b interface{}) (bool, error) { - res, err := mod(a, b) - if err != nil { - return false, err - } - return res == int64(0), nil -} - -// base64Decode returns the base64 decoding of the given content. -func base64Decode(content interface{}) (string, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return "", err - } - - dec, err := base64.StdEncoding.DecodeString(conv) - - return string(dec), err -} - -// base64Encode returns the base64 encoding of the given content. -func base64Encode(content interface{}) (string, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return "", err - } - - return base64.StdEncoding.EncodeToString([]byte(conv)), nil -} - -// countWords returns the approximate word count of the given content. -func countWords(content interface{}) (int, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err.Error()) - } - - counter := 0 - for _, word := range strings.Fields(helpers.StripHTML(conv)) { - runeCount := utf8.RuneCountInString(word) - if len(word) == runeCount { - counter++ - } else { - counter += runeCount - } - } - - return counter, nil -} - -// countRunes returns the approximate rune count of the given content. -func countRunes(content interface{}) (int, error) { - conv, err := cast.ToStringE(content) - - if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err.Error()) - } - - counter := 0 - for _, r := range helpers.StripHTML(conv) { - if !helpers.IsWhitespace(r) { - counter++ - } - } - - return counter, nil -} - -// humanize returns the humanized form of a single parameter. -// If the parameter is either an integer or a string containing an integer -// value, the behavior is to add the appropriate ordinal. -// Example: "my-first-post" -> "My first post" -// Example: "103" -> "103rd" -// Example: 52 -> "52nd" -func humanize(in interface{}) (string, error) { - word, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - if word == "" { - return "", nil - } - - _, ok := in.(int) // original param was literal int value - _, err = strconv.Atoi(word) // original param was string containing an int value - if ok || err == nil { - return inflect.Ordinalize(word), nil - } - return inflect.Humanize(word), nil -} - -// pluralize returns the plural form of a single word. -func pluralize(in interface{}) (string, error) { - word, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return inflect.Pluralize(word), nil -} - -// singularize returns the singular form of a single word. -func singularize(in interface{}) (string, error) { - word, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return inflect.Singularize(word), nil -} - -// md5 hashes the given input and returns its MD5 checksum -func md5(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - hash := _md5.Sum([]byte(conv)) - return hex.EncodeToString(hash[:]), nil -} - -// sha1 hashes the given input and returns its SHA1 checksum -func sha1(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - hash := _sha1.Sum([]byte(conv)) - return hex.EncodeToString(hash[:]), nil -} - -// sha256 hashes the given input and returns its SHA256 checksum -func sha256(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - - hash := _sha256.Sum256([]byte(conv)) - return hex.EncodeToString(hash[:]), nil -} - -// querify encodes the given parameters “URL encoded” form ("bar=baz&foo=quux") sorted by key. -func querify(params ...interface{}) (string, error) { - qs := url.Values{} - vals, err := dictionary(params...) - if err != nil { - return "", errors.New("querify keys must be strings") - } - - for name, value := range vals { - qs.Add(name, fmt.Sprintf("%v", value)) - } - - return qs.Encode(), nil -} - -func htmlEscape(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return html.EscapeString(conv), nil -} - -func htmlUnescape(in interface{}) (string, error) { - conv, err := cast.ToStringE(in) - if err != nil { - return "", err - } - return html.UnescapeString(conv), nil -} - -func (t *templateFuncster) absURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - return template.HTML(t.PathSpec.AbsURL(s, false)), nil -} - -func (t *templateFuncster) relURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - return template.HTML(t.PathSpec.RelURL(s, false)), nil -} - -// getenv retrieves the value of the environment variable named by the key. -// It returns the value, which will be empty if the variable is not present. -func getenv(key interface{}) (string, error) { - skey, err := cast.ToStringE(key) - if err != nil { - return "", nil - } - - return os.Getenv(skey), nil -} - func (t *templateFuncster) initFuncMap() { funcMap := template.FuncMap{ - "absURL": t.absURL, - "absLangURL": func(i interface{}) (template.HTML, error) { - s, err := cast.ToStringE(i) - if err != nil { - return "", err - } - return template.HTML(t.PathSpec.AbsURL(s, true)), nil - }, - "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') }, - "after": after, - "apply": t.apply, - "base64Decode": base64Decode, - "base64Encode": base64Encode, - "chomp": chomp, - "countrunes": countRunes, - "countwords": countWords, - "default": dfault, - "dateFormat": dateFormat, - "delimit": delimit, - "dict": dictionary, - "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') }, - "echoParam": returnWhenSet, - "emojify": emojify, - "eq": eq, - "findRE": findRE, - "first": first, - "ge": ge, - "getCSV": t.getCSV, - "getJSON": t.getJSON, - "getenv": getenv, - "gt": gt, - "hasPrefix": hasPrefix, - "highlight": t.highlight, - "htmlEscape": htmlEscape, - "htmlUnescape": htmlUnescape, - "humanize": humanize, - "imageConfig": t.image.config, - "in": in, - "index": index, + // Namespaces + "collections": t.collections.Namespace, + "crypto": t.crypto.Namespace, + "encoding": t.encoding.Namespace, + "images": t.images.Namespace, + "inflect": t.inflect.Namespace, + "math": t.math.Namespace, + "os": t.os.Namespace, + "safe": t.safe.Namespace, + "strings": t.strings.Namespace, + //"time": t.time.Namespace, + "transform": t.transform.Namespace, + "urls": t.urls.Namespace, + + "absURL": t.urls.AbsURL, + "absLangURL": t.urls.AbsLangURL, + "add": t.math.Add, + "after": t.collections.After, + "apply": t.collections.Apply, + "base64Decode": t.encoding.Base64Decode, + "base64Encode": t.encoding.Base64Encode, + "chomp": t.strings.Chomp, + "countrunes": t.strings.CountRunes, + "countwords": t.strings.CountWords, + "default": compare.Default, + "dateFormat": t.time.Format, + "delimit": t.collections.Delimit, + "dict": t.collections.Dictionary, + "div": t.math.Div, + "echoParam": t.collections.EchoParam, + "emojify": t.transform.Emojify, + "eq": compare.Eq, + "findRE": t.strings.FindRE, + "first": t.collections.First, + "ge": compare.Ge, + "getCSV": t.data.GetCSV, + "getJSON": t.data.GetJSON, + "getenv": t.os.Getenv, + "gt": compare.Gt, + "hasPrefix": t.strings.HasPrefix, + "highlight": t.transform.Highlight, + "htmlEscape": t.transform.HTMLEscape, + "htmlUnescape": t.transform.HTMLUnescape, + "humanize": t.inflect.Humanize, + "imageConfig": t.images.Config, + "in": t.collections.In, + "index": t.collections.Index, "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, - "intersect": intersect, - "isSet": isSet, - "isset": isSet, - "jsonify": jsonify, - "last": last, - "le": le, - "lower": lower, - "lt": lt, - "markdownify": t.markdownify, - "md5": md5, - "mod": mod, - "modBool": modBool, - "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, - "ne": ne, - "now": func() time.Time { return time.Now() }, + "intersect": t.collections.Intersect, + "isSet": t.collections.IsSet, + "isset": t.collections.IsSet, + "jsonify": t.encoding.Jsonify, + "last": t.collections.Last, + "le": compare.Le, + "lower": t.strings.ToLower, + "lt": compare.Lt, + "markdownify": t.transform.Markdownify, + "md5": t.crypto.MD5, + "mod": t.math.Mod, + "modBool": t.math.ModBool, + "mul": t.math.Mul, + "ne": compare.Ne, + "now": t.time.Now, "partial": t.partial, "partialCached": t.partialCached, - "plainify": plainify, - "pluralize": pluralize, + "plainify": t.transform.Plainify, + "pluralize": t.inflect.Pluralize, "print": fmt.Sprint, "printf": fmt.Sprintf, "println": fmt.Sprintln, - "querify": querify, - "readDir": t.readDirFromWorkingDir, - "readFile": t.readFileFromWorkingDir, - "ref": ref, - "relURL": t.relURL, - "relLangURL": func(i interface{}) (template.HTML, error) { - s, err := cast.ToStringE(i) - if err != nil { - return "", err - } - return template.HTML(t.PathSpec.RelURL(s, true)), nil - }, - "relref": relRef, - "replace": replace, - "replaceRE": replaceRE, - "safeCSS": safeCSS, - "safeHTML": safeHTML, - "safeHTMLAttr": safeHTMLAttr, - "safeJS": safeJS, - "safeURL": safeURL, - "sanitizeURL": helpers.SanitizeURL, - "sanitizeurl": helpers.SanitizeURL, - "seq": helpers.Seq, - "sha1": sha1, - "sha256": sha256, - "shuffle": shuffle, - "singularize": singularize, - "slice": slice, - "slicestr": slicestr, - "sort": sortSeq, - "split": split, - "string": func(v interface{}) (string, error) { return cast.ToStringE(v) }, - "sub": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '-') }, - "substr": substr, - "title": title, - "time": asTime, - "trim": trim, - "truncate": truncate, - "union": union, - "upper": upper, - "urlize": t.PathSpec.URLize, - "where": where, - "i18n": t.Translate, - "T": t.Translate, + "querify": t.collections.Querify, + "readDir": t.os.ReadDir, + "readFile": t.os.ReadFile, + "ref": t.urls.Ref, + "relURL": t.urls.RelURL, + "relLangURL": t.urls.RelLangURL, + "relref": t.urls.RelRef, + "replace": t.strings.Replace, + "replaceRE": t.strings.ReplaceRE, + "safeCSS": t.safe.CSS, + "safeHTML": t.safe.HTML, + "safeHTMLAttr": t.safe.HTMLAttr, + "safeJS": t.safe.JS, + "safeJSStr": t.safe.JSStr, + "safeURL": t.safe.URL, + "sanitizeURL": t.safe.SanitizeURL, + "sanitizeurl": t.safe.SanitizeURL, + "seq": t.collections.Seq, + "sha1": t.crypto.SHA1, + "sha256": t.crypto.SHA256, + "shuffle": t.collections.Shuffle, + "singularize": t.inflect.Singularize, + "slice": t.collections.Slice, + "slicestr": t.strings.SliceString, + "sort": t.collections.Sort, + "split": t.strings.Split, + "string": func(v interface{}) (string, error) { return cast.ToStringE(v) }, + "sub": t.math.Sub, + "substr": t.strings.Substr, + "time": t.time.AsTime, + "title": t.strings.Title, + "trim": t.strings.Trim, + "truncate": t.strings.Truncate, + "union": t.collections.Union, + "upper": t.strings.ToUpper, + "urlize": t.PathSpec.URLize, + "where": t.collections.Where, + "i18n": t.lang.Translate, + "T": t.lang.T, } t.funcMap = funcMap t.Tmpl.(*templateHandler).setFuncs(funcMap) + t.collections.Funcs(funcMap) } diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 70a0ad5e5..af368ab5b 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -15,39 +15,25 @@ package tplimpl import ( "bytes" - "encoding/base64" - "errors" "fmt" - "html/template" - "image" - "image/color" - "image/png" - "math/rand" - "path" "path/filepath" "reflect" - "runtime" "strings" "testing" - "time" - - "github.com/spf13/hugo/tpl" - - "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/helpers" "io/ioutil" "log" "os" "github.com/spf13/afero" - "github.com/spf13/cast" "github.com/spf13/hugo/config" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/i18n" + "github.com/spf13/hugo/tpl" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -68,32 +54,6 @@ func newDepsConfig(cfg config.Provider) deps.DepsCfg { } } -type tstNoStringer struct { -} - -type tstCompareType int - -const ( - tstEq tstCompareType = iota - tstNe - tstGt - tstGe - tstLt - tstLe -) - -func tstIsEq(tp tstCompareType) bool { - return tp == tstEq || tp == tstGe || tp == tstLe -} - -func tstIsGt(tp tstCompareType) bool { - return tp == tstGt || tp == tstGe -} - -func tstIsLt(tp tstCompareType) bool { - return tp == tstLt || tp == tstLe -} - func TestFuncsInTemplate(t *testing.T) { t.Parallel() @@ -120,6 +80,7 @@ base64Decode 1: {{ "SGVsbG8gd29ybGQ=" | base64Decode }} base64Decode 2: {{ 42 | base64Encode | base64Decode }} base64Encode: {{ "Hello world" | base64Encode }} chomp: {{chomp "

Blockhead

\n" }} +crypto.MD5: {{ crypto.MD5 "Hello world, gophers!" }} dateFormat: {{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }} delimit: {{ delimit (slice "A" "B" "C") ", " " and " }} div: {{div 6 3}} @@ -175,6 +136,7 @@ singularize: {{ "cats" | singularize }} slicestr: {{slicestr "BatMan" 0 3}} slicestr: {{slicestr "BatMan" 3}} sort: {{ slice "B" "C" "A" | sort }} +strings.TrimPrefix: {{ strings.TrimPrefix "Goodbye,, world!" "Goodbye," }} sub: {{sub 3 2}} substr: {{substr "BatMan" 0 -3}} substr: {{substr "BatMan" 3 3}} @@ -197,6 +159,7 @@ base64Decode 1: Hello world base64Decode 2: 42 base64Encode: SGVsbG8gd29ybGQ= chomp:

Blockhead

+crypto.MD5: b3029f756f98f79e7f1b7f1d1f0dd53b dateFormat: Wednesday, Jan 21, 2015 delimit: A, B and C div: 2 @@ -252,6 +215,7 @@ singularize: cat slicestr: Bat slicestr: Man sort: [A B C] +strings.TrimPrefix: , world! sub: 1 substr: Bat substr: Man @@ -311,2155 +275,6 @@ urlize: bat-man } } -func TestCompare(t *testing.T) { - t.Parallel() - for _, this := range []struct { - tstCompareType - funcUnderTest func(a, b interface{}) bool - }{ - {tstGt, gt}, - {tstLt, lt}, - {tstGe, ge}, - {tstLe, le}, - {tstEq, eq}, - {tstNe, ne}, - } { - doTestCompare(t, this.tstCompareType, this.funcUnderTest) - } -} - -func doTestCompare(t *testing.T, tp tstCompareType, funcUnderTest func(a, b interface{}) bool) { - for i, this := range []struct { - left interface{} - right interface{} - expectIndicator int - }{ - {5, 8, -1}, - {8, 5, 1}, - {5, 5, 0}, - {int(5), int64(5), 0}, - {int32(5), int(5), 0}, - {int16(4), int(5), -1}, - {uint(15), uint64(15), 0}, - {-2, 1, -1}, - {2, -5, 1}, - {0.0, 1.23, -1}, - {1.1, 1.1, 0}, - {float32(1.0), float64(1.0), 0}, - {1.23, 0.0, 1}, - {"5", "5", 0}, - {"8", "5", 1}, - {"5", "0001", 1}, - {[]int{100, 99}, []int{1, 2, 3, 4}, -1}, - {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-20"), 0}, - {cast.ToTime("2015-11-19"), cast.ToTime("2015-11-20"), -1}, - {cast.ToTime("2015-11-20"), cast.ToTime("2015-11-19"), 1}, - } { - result := funcUnderTest(this.left, this.right) - success := false - - if this.expectIndicator == 0 { - if tstIsEq(tp) { - success = result - } else { - success = !result - } - } - - if this.expectIndicator < 0 { - success = result && (tstIsLt(tp) || tp == tstNe) - success = success || (!result && !tstIsLt(tp)) - } - - if this.expectIndicator > 0 { - success = result && (tstIsGt(tp) || tp == tstNe) - success = success || (!result && (!tstIsGt(tp) || tp != tstNe)) - } - - if !success { - t.Errorf("[%d][%s] %v compared to %v: %t", i, path.Base(runtime.FuncForPC(reflect.ValueOf(funcUnderTest).Pointer()).Name()), this.left, this.right, result) - } - } -} - -func TestMod(t *testing.T) { - t.Parallel() - for i, this := range []struct { - a interface{} - b interface{} - expect interface{} - }{ - {3, 2, int64(1)}, - {3, 1, int64(0)}, - {3, 0, false}, - {0, 3, int64(0)}, - {3.1, 2, false}, - {3, 2.1, false}, - {3.1, 2.1, false}, - {int8(3), int8(2), int64(1)}, - {int16(3), int16(2), int64(1)}, - {int32(3), int32(2), int64(1)}, - {int64(3), int64(2), int64(1)}, - } { - result, err := mod(this.a, this.b) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] modulo didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] modulo got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestModBool(t *testing.T) { - t.Parallel() - for i, this := range []struct { - a interface{} - b interface{} - expect interface{} - }{ - {3, 3, true}, - {3, 2, false}, - {3, 1, true}, - {3, 0, nil}, - {0, 3, true}, - {3.1, 2, nil}, - {3, 2.1, nil}, - {3.1, 2.1, nil}, - {int8(3), int8(3), true}, - {int8(3), int8(2), false}, - {int16(3), int16(3), true}, - {int16(3), int16(2), false}, - {int32(3), int32(3), true}, - {int32(3), int32(2), false}, - {int64(3), int64(3), true}, - {int64(3), int64(2), false}, - } { - result, err := modBool(this.a, this.b) - if this.expect == nil { - if err == nil { - t.Errorf("[%d] modulo didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] modulo got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestFirst(t *testing.T) { - t.Parallel() - for i, this := range []struct { - count interface{} - sequence interface{} - expect interface{} - }{ - {int(2), []string{"a", "b", "c"}, []string{"a", "b"}}, - {int32(3), []string{"a", "b"}, []string{"a", "b"}}, - {int64(2), []int{100, 200, 300}, []int{100, 200}}, - {100, []int{100, 200}, []int{100, 200}}, - {"1", []int{100, 200, 300}, []int{100}}, - {int64(-1), []int{100, 200, 300}, false}, - {"noint", []int{100, 200, 300}, false}, - {1, nil, false}, - {nil, []int{100}, false}, - {1, t, false}, - {1, (*string)(nil), false}, - } { - results, err := first(this.count, this.sequence) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) - } - } - } -} - -func TestLast(t *testing.T) { - t.Parallel() - for i, this := range []struct { - count interface{} - sequence interface{} - expect interface{} - }{ - {int(2), []string{"a", "b", "c"}, []string{"b", "c"}}, - {int32(3), []string{"a", "b"}, []string{"a", "b"}}, - {int64(2), []int{100, 200, 300}, []int{200, 300}}, - {100, []int{100, 200}, []int{100, 200}}, - {"1", []int{100, 200, 300}, []int{300}}, - {int64(-1), []int{100, 200, 300}, false}, - {"noint", []int{100, 200, 300}, false}, - {1, nil, false}, - {nil, []int{100}, false}, - {1, t, false}, - {1, (*string)(nil), false}, - } { - results, err := last(this.count, this.sequence) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) - } - } - } -} - -func TestAfter(t *testing.T) { - t.Parallel() - for i, this := range []struct { - count interface{} - sequence interface{} - expect interface{} - }{ - {int(2), []string{"a", "b", "c", "d"}, []string{"c", "d"}}, - {int32(3), []string{"a", "b"}, false}, - {int64(2), []int{100, 200, 300}, []int{300}}, - {100, []int{100, 200}, false}, - {"1", []int{100, 200, 300}, []int{200, 300}}, - {int64(-1), []int{100, 200, 300}, false}, - {"noint", []int{100, 200, 300}, false}, - {1, nil, false}, - {nil, []int{100}, false}, - {1, t, false}, - {1, (*string)(nil), false}, - } { - results, err := after(this.count, this.sequence) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect) - } - } - } -} - -func TestShuffleInputAndOutputFormat(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence interface{} - success bool - }{ - {[]string{"a", "b", "c", "d"}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100, 200}, true}, - {[]string{"a", "b"}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100, 200, 300}, true}, - {[]int{100}, true}, - {nil, false}, - {t, false}, - {(*string)(nil), false}, - } { - results, err := shuffle(this.sequence) - if !this.success { - if err == nil { - t.Errorf("[%d] First didn't return an expected error", i) - } - } else { - resultsv := reflect.ValueOf(results) - sequencev := reflect.ValueOf(this.sequence) - - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - - if resultsv.Len() != sequencev.Len() { - t.Errorf("Expected %d items, got %d items", sequencev.Len(), resultsv.Len()) - } - } - } -} - -func TestShuffleRandomising(t *testing.T) { - t.Parallel() - // Note that this test can fail with false negative result if the shuffle - // of the sequence happens to be the same as the original sequence. However - // the propability of the event is 10^-158 which is negligible. - sequenceLength := 100 - rand.Seed(time.Now().UTC().UnixNano()) - - for _, this := range []struct { - sequence []int - }{ - {rand.Perm(sequenceLength)}, - } { - results, _ := shuffle(this.sequence) - - resultsv := reflect.ValueOf(results) - - allSame := true - for index, value := range this.sequence { - allSame = allSame && (resultsv.Index(index).Interface() == value) - } - - if allSame { - t.Error("Expected sequence to be shuffled but was in the same order") - } - } -} - -func TestDictionary(t *testing.T) { - t.Parallel() - for i, this := range []struct { - v1 []interface{} - expecterr bool - expectedValue map[string]interface{} - }{ - {[]interface{}{"a", "b"}, false, map[string]interface{}{"a": "b"}}, - {[]interface{}{5, "b"}, true, nil}, - {[]interface{}{"a", 12, "b", []int{4}}, false, map[string]interface{}{"a": 12, "b": []int{4}}}, - {[]interface{}{"a", "b", "c"}, true, nil}, - } { - r, e := dictionary(this.v1...) - - if (this.expecterr && e == nil) || (!this.expecterr && e != nil) { - t.Errorf("[%d] got an unexpected error: %s", i, e) - } else if !this.expecterr { - if !reflect.DeepEqual(r, this.expectedValue) { - t.Errorf("[%d] got %v but expected %v", i, r, this.expectedValue) - } - } - } -} - -func blankImage(width, height int) []byte { - var buf bytes.Buffer - img := image.NewRGBA(image.Rect(0, 0, width, height)) - if err := png.Encode(&buf, img); err != nil { - panic(err) - } - return buf.Bytes() -} - -func TestImageConfig(t *testing.T) { - t.Parallel() - - workingDir := "/home/hugo" - - v := viper.New() - - v.Set("workingDir", workingDir) - - f := newTestFuncsterWithViper(v) - - for i, this := range []struct { - path string - input []byte - expected image.Config - }{ - { - path: "a.png", - input: blankImage(10, 10), - expected: image.Config{ - Width: 10, - Height: 10, - ColorModel: color.NRGBAModel, - }, - }, - { - path: "a.png", - input: blankImage(10, 10), - expected: image.Config{ - Width: 10, - Height: 10, - ColorModel: color.NRGBAModel, - }, - }, - { - path: "b.png", - input: blankImage(20, 15), - expected: image.Config{ - Width: 20, - Height: 15, - ColorModel: color.NRGBAModel, - }, - }, - { - path: "a.png", - input: blankImage(20, 15), - expected: image.Config{ - Width: 10, - Height: 10, - ColorModel: color.NRGBAModel, - }, - }, - } { - afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, this.path), this.input, 0755) - - result, err := f.image.config(this.path) - if err != nil { - t.Errorf("imageConfig returned error: %s", err) - } - - if !reflect.DeepEqual(result, this.expected) { - t.Errorf("[%d] imageConfig: expected '%v', got '%v'", i, this.expected, result) - } - - if len(f.image.imageConfigCache) == 0 { - t.Error("defaultImageConfigCache should have at least 1 item") - } - } - - if _, err := f.image.config(t); err == nil { - t.Error("Expected error from imageConfig when passed invalid path") - } - - if _, err := f.image.config("non-existent.png"); err == nil { - t.Error("Expected error from imageConfig when passed non-existent file") - } - - if _, err := f.image.config(""); err == nil { - t.Error("Expected error from imageConfig when passed empty path") - } - -} - -func TestIn(t *testing.T) { - t.Parallel() - for i, this := range []struct { - v1 interface{} - v2 interface{} - expect bool - }{ - {[]string{"a", "b", "c"}, "b", true}, - {[]interface{}{"a", "b", "c"}, "b", true}, - {[]interface{}{"a", "b", "c"}, "d", false}, - {[]string{"a", "b", "c"}, "d", false}, - {[]string{"a", "12", "c"}, 12, false}, - {[]int{1, 2, 4}, 2, true}, - {[]interface{}{1, 2, 4}, 2, true}, - {[]interface{}{1, 2, 4}, nil, false}, - {[]interface{}{nil}, nil, false}, - {[]int{1, 2, 4}, 3, false}, - {[]float64{1.23, 2.45, 4.67}, 1.23, true}, - {[]float64{1.234567, 2.45, 4.67}, 1.234568, false}, - {"this substring should be found", "substring", true}, - {"this substring should not be found", "subseastring", false}, - } { - result := in(this.v1, this.v2) - - if result != this.expect { - t.Errorf("[%d] got %v but expected %v", i, result, this.expect) - } - } -} - -func TestSlicestr(t *testing.T) { - t.Parallel() - var err error - for i, this := range []struct { - v1 interface{} - v2 interface{} - v3 interface{} - expect interface{} - }{ - {"abc", 1, 2, "b"}, - {"abc", 1, 3, "bc"}, - {"abcdef", 1, int8(3), "bc"}, - {"abcdef", 1, int16(3), "bc"}, - {"abcdef", 1, int32(3), "bc"}, - {"abcdef", 1, int64(3), "bc"}, - {"abc", 0, 1, "a"}, - {"abcdef", nil, nil, "abcdef"}, - {"abcdef", 0, 6, "abcdef"}, - {"abcdef", 0, 2, "ab"}, - {"abcdef", 2, nil, "cdef"}, - {"abcdef", int8(2), nil, "cdef"}, - {"abcdef", int16(2), nil, "cdef"}, - {"abcdef", int32(2), nil, "cdef"}, - {"abcdef", int64(2), nil, "cdef"}, - {123, 1, 3, "23"}, - {"abcdef", 6, nil, false}, - {"abcdef", 4, 7, false}, - {"abcdef", -1, nil, false}, - {"abcdef", -1, 7, false}, - {"abcdef", 1, -1, false}, - {tstNoStringer{}, 0, 1, false}, - {"ĀĀĀ", 0, 1, "Ā"}, // issue #1333 - {"a", t, nil, false}, - {"a", 1, t, false}, - } { - var result string - if this.v2 == nil { - result, err = slicestr(this.v1) - } else if this.v3 == nil { - result, err = slicestr(this.v1, this.v2) - } else { - result, err = slicestr(this.v1, this.v2, this.v3) - } - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Slice didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got %s but expected %s", i, result, this.expect) - } - } - } - - // Too many arguments - _, err = slicestr("a", 1, 2, 3) - if err == nil { - t.Errorf("Should have errored") - } -} - -func TestHasPrefix(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - prefix interface{} - want interface{} - isErr bool - }{ - {"abcd", "ab", true, false}, - {"abcd", "cd", false, false}, - {template.HTML("abcd"), "ab", true, false}, - {template.HTML("abcd"), "cd", false, false}, - {template.HTML("1234"), 12, true, false}, - {template.HTML("1234"), 34, false, false}, - {[]byte("abcd"), "ab", true, false}, - } - - for i, c := range cases { - res, err := hasPrefix(c.s, c.prefix) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.isErr, err != nil, err) - } - if res != c.want { - t.Errorf("[%d] want %v, got %v", i, c.want, res) - } - } -} - -func TestSubstr(t *testing.T) { - t.Parallel() - var err error - var n int - for i, this := range []struct { - v1 interface{} - v2 interface{} - v3 interface{} - expect interface{} - }{ - {"abc", 1, 2, "bc"}, - {"abc", 0, 1, "a"}, - {"abcdef", -1, 2, "ef"}, - {"abcdef", -3, 3, "bcd"}, - {"abcdef", 0, -1, "abcde"}, - {"abcdef", 2, -1, "cde"}, - {"abcdef", 4, -4, false}, - {"abcdef", 7, 1, false}, - {"abcdef", 1, 100, "bcdef"}, - {"abcdef", -100, 3, "abc"}, - {"abcdef", -3, -1, "de"}, - {"abcdef", 2, nil, "cdef"}, - {"abcdef", int8(2), nil, "cdef"}, - {"abcdef", int16(2), nil, "cdef"}, - {"abcdef", int32(2), nil, "cdef"}, - {"abcdef", int64(2), nil, "cdef"}, - {"abcdef", 2, int8(3), "cde"}, - {"abcdef", 2, int16(3), "cde"}, - {"abcdef", 2, int32(3), "cde"}, - {"abcdef", 2, int64(3), "cde"}, - {123, 1, 3, "23"}, - {1.2e3, 0, 4, "1200"}, - {tstNoStringer{}, 0, 1, false}, - {"abcdef", 2.0, nil, "cdef"}, - {"abcdef", 2.0, 2, "cd"}, - {"abcdef", 2, 2.0, "cd"}, - {"ĀĀĀ", 1, 2, "ĀĀ"}, // # issue 1333 - {"abcdef", "doo", nil, false}, - {"abcdef", "doo", "doo", false}, - {"abcdef", 1, "doo", false}, - } { - var result string - n = i - - if this.v3 == nil { - result, err = substr(this.v1, this.v2) - } else { - result, err = substr(this.v1, this.v2, this.v3) - } - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Substr didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got %s but expected %s", i, result, this.expect) - } - } - } - - n++ - _, err = substr("abcdef") - if err == nil { - t.Errorf("[%d] Substr didn't return an expected error", n) - } - - n++ - _, err = substr("abcdef", 1, 2, 3) - if err == nil { - t.Errorf("[%d] Substr didn't return an expected error", n) - } -} - -func TestSplit(t *testing.T) { - t.Parallel() - for i, this := range []struct { - v1 interface{} - v2 string - expect interface{} - }{ - {"a, b", ", ", []string{"a", "b"}}, - {"a & b & c", " & ", []string{"a", "b", "c"}}, - {"http://example.com", "http://", []string{"", "example.com"}}, - {123, "2", []string{"1", "3"}}, - {tstNoStringer{}, ",", false}, - } { - result, err := split(this.v1, this.v2) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Split didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got %s but expected %s", i, result, this.expect) - } - } - } -} - -func TestIntersect(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence1 interface{} - sequence2 interface{} - expect interface{} - }{ - {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b"}}, - {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b"}}, - {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{}}, - {[]string{}, []string{}, []string{}}, - {nil, nil, make([]interface{}, 0)}, - {[]string{"1", "2"}, []int{1, 2}, []string{}}, - {[]int{1, 2}, []string{"1", "2"}, []int{}}, - {[]int{1, 2, 4}, []int{2, 4}, []int{2, 4}}, - {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4}}, - {[]int{1, 2, 4}, []int{3, 6}, []int{}}, - {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4}}, - } { - results, err := intersect(this.sequence1, this.sequence2) - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] got %v but expected %v", i, results, this.expect) - } - } - - _, err1 := intersect("not an array or slice", []string{"a"}) - - if err1 == nil { - t.Error("Expected error for non array as first arg") - } - - _, err2 := intersect([]string{"a"}, "not an array or slice") - - if err2 == nil { - t.Error("Expected error for non array as second arg") - } -} - -func TestUnion(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence1 interface{} - sequence2 interface{} - expect interface{} - isErr bool - }{ - {[]string{"a", "b", "c", "c"}, []string{"a", "b", "b"}, []string{"a", "b", "c"}, false}, - {[]string{"a", "b"}, []string{"a", "b", "c"}, []string{"a", "b", "c"}, false}, - {[]string{"a", "b", "c"}, []string{"d", "e"}, []string{"a", "b", "c", "d", "e"}, false}, - {[]string{}, []string{}, []string{}, false}, - {[]string{"a", "b"}, nil, []string{"a", "b"}, false}, - {nil, []string{"a", "b"}, []string{"a", "b"}, false}, - {nil, nil, make([]interface{}, 0), true}, - {[]string{"1", "2"}, []int{1, 2}, make([]string, 0), false}, - {[]int{1, 2}, []string{"1", "2"}, make([]int, 0), false}, - {[]int{1, 2, 3}, []int{3, 4, 5}, []int{1, 2, 3, 4, 5}, false}, - {[]int{1, 2, 3}, []int{1, 2, 3}, []int{1, 2, 3}, false}, - {[]int{1, 2, 4}, []int{2, 4}, []int{1, 2, 4}, false}, - {[]int{2, 4}, []int{1, 2, 4}, []int{2, 4, 1}, false}, - {[]int{1, 2, 4}, []int{3, 6}, []int{1, 2, 4, 3, 6}, false}, - {[]float64{2.2, 4.4}, []float64{1.1, 2.2, 4.4}, []float64{2.2, 4.4, 1.1}, false}, - } { - results, err := union(this.sequence1, this.sequence2) - if err != nil && !this.isErr { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) && !this.isErr { - t.Errorf("[%d] got %v but expected %v", i, results, this.expect) - } - } - - _, err1 := union("not an array or slice", []string{"a"}) - - if err1 == nil { - t.Error("Expected error for non array as first arg") - } - - _, err2 := union([]string{"a"}, "not an array or slice") - - if err2 == nil { - t.Error("Expected error for non array as second arg") - } -} - -func TestIsSet(t *testing.T) { - t.Parallel() - aSlice := []interface{}{1, 2, 3, 5} - aMap := map[string]interface{}{"a": 1, "b": 2} - - assert.True(t, isSet(aSlice, 2)) - assert.True(t, isSet(aMap, "b")) - assert.False(t, isSet(aSlice, 22)) - assert.False(t, isSet(aMap, "bc")) -} - -func (x *TstX) TstRp() string { - return "r" + x.A -} - -func (x TstX) TstRv() string { - return "r" + x.B -} - -func (x TstX) unexportedMethod() string { - return x.unexported -} - -func (x TstX) MethodWithArg(s string) string { - return s -} - -func (x TstX) MethodReturnNothing() {} - -func (x TstX) MethodReturnErrorOnly() error { - return errors.New("some error occurred") -} - -func (x TstX) MethodReturnTwoValues() (string, string) { - return "foo", "bar" -} - -func (x TstX) MethodReturnValueWithError() (string, error) { - return "", errors.New("some error occurred") -} - -func (x TstX) String() string { - return fmt.Sprintf("A: %s, B: %s", x.A, x.B) -} - -type TstX struct { - A, B string - unexported string -} - -func TestTimeUnix(t *testing.T) { - t.Parallel() - var sec int64 = 1234567890 - tv := reflect.ValueOf(time.Unix(sec, 0)) - i := 1 - - res := toTimeUnix(tv) - if sec != res { - t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec) - } - - i++ - func(t *testing.T) { - defer func() { - if err := recover(); err == nil { - t.Errorf("[%d] timeUnix didn't return an expected error", i) - } - }() - iv := reflect.ValueOf(sec) - toTimeUnix(iv) - }(t) -} - -func TestEvaluateSubElem(t *testing.T) { - t.Parallel() - tstx := TstX{A: "foo", B: "bar"} - var inner struct { - S fmt.Stringer - } - inner.S = tstx - interfaceValue := reflect.ValueOf(&inner).Elem().Field(0) - - for i, this := range []struct { - value reflect.Value - key string - expect interface{} - }{ - {reflect.ValueOf(tstx), "A", "foo"}, - {reflect.ValueOf(&tstx), "TstRp", "rfoo"}, - {reflect.ValueOf(tstx), "TstRv", "rbar"}, - //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1, "foo"}, - {reflect.ValueOf(map[string]string{"key1": "foo", "key2": "bar"}), "key1", "foo"}, - {interfaceValue, "String", "A: foo, B: bar"}, - {reflect.Value{}, "foo", false}, - //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1.2, false}, - {reflect.ValueOf(tstx), "unexported", false}, - {reflect.ValueOf(tstx), "unexportedMethod", false}, - {reflect.ValueOf(tstx), "MethodWithArg", false}, - {reflect.ValueOf(tstx), "MethodReturnNothing", false}, - {reflect.ValueOf(tstx), "MethodReturnErrorOnly", false}, - {reflect.ValueOf(tstx), "MethodReturnTwoValues", false}, - {reflect.ValueOf(tstx), "MethodReturnValueWithError", false}, - {reflect.ValueOf((*TstX)(nil)), "A", false}, - {reflect.ValueOf(tstx), "C", false}, - {reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false}, - {reflect.ValueOf([]string{"foo", "bar"}), "1", false}, - } { - result, err := evaluateSubElem(this.value, this.key) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] evaluateSubElem didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if result.Kind() != reflect.String || result.String() != this.expect { - t.Errorf("[%d] evaluateSubElem with %v got %v but expected %v", i, this.key, result, this.expect) - } - } - } -} - -func TestCheckCondition(t *testing.T) { - t.Parallel() - type expect struct { - result bool - isError bool - } - - for i, this := range []struct { - value reflect.Value - match reflect.Value - op string - expect - }{ - {reflect.ValueOf(123), reflect.ValueOf(123), "", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("foo"), "", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - "", - expect{true, false}, - }, - {reflect.ValueOf(true), reflect.ValueOf(true), "", expect{true, false}}, - {reflect.ValueOf(nil), reflect.ValueOf(nil), "", expect{true, false}}, - {reflect.ValueOf(123), reflect.ValueOf(456), "!=", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar"), "!=", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - "!=", - expect{true, false}, - }, - {reflect.ValueOf(true), reflect.ValueOf(false), "!=", expect{true, false}}, - {reflect.ValueOf(123), reflect.ValueOf(nil), "!=", expect{true, false}}, - {reflect.ValueOf(456), reflect.ValueOf(123), ">=", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">=", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - ">=", - expect{true, false}, - }, - {reflect.ValueOf(456), reflect.ValueOf(123), ">", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - ">", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf(456), "<=", expect{true, false}}, - {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<=", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - "<=", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf(456), "<", expect{true, false}}, - {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - "<", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf([]int{123, 45, 678}), "in", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf([]string{"foo", "bar", "baz"}), "in", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf([]time.Time{ - time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.June, 26, 19, 18, 56, 12345, time.UTC), - }), - "in", - expect{true, false}, - }, - {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, - { - reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)), - reflect.ValueOf([]time.Time{ - time.Date(2015, time.February, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.March, 26, 19, 18, 56, 12345, time.UTC), - time.Date(2015, time.April, 26, 19, 18, 56, 12345, time.UTC), - }), - "not in", - expect{true, false}, - }, - {reflect.ValueOf("foo"), reflect.ValueOf("bar-foo-baz"), "in", expect{true, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf("bar--baz"), "not in", expect{true, false}}, - {reflect.Value{}, reflect.ValueOf("foo"), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.Value{}, "", expect{false, false}}, - {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf("foo"), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf((*TstX)(nil)), "", expect{false, false}}, - {reflect.ValueOf(true), reflect.ValueOf("foo"), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf(true), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf(map[int]string{}), "", expect{false, false}}, - {reflect.ValueOf("foo"), reflect.ValueOf([]int{1, 2}), "", expect{false, false}}, - {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf((*TstX)(nil)), ">", expect{false, false}}, - {reflect.ValueOf(true), reflect.ValueOf(false), ">", expect{false, false}}, - {reflect.ValueOf(123), reflect.ValueOf([]int{}), "in", expect{false, false}}, - {reflect.ValueOf(123), reflect.ValueOf(123), "op", expect{false, true}}, - } { - result, err := checkCondition(this.value, this.match, this.op) - if this.expect.isError { - if err == nil { - t.Errorf("[%d] checkCondition didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if result != this.expect.result { - t.Errorf("[%d] check condition %v %s %v, got %v but expected %v", i, this.value, this.op, this.match, result, this.expect.result) - } - } - } -} - -func TestWhere(t *testing.T) { - t.Parallel() - - type Mid struct { - Tst TstX - } - - d1 := time.Now() - d2 := d1.Add(1 * time.Hour) - d3 := d2.Add(1 * time.Hour) - d4 := d3.Add(1 * time.Hour) - d5 := d4.Add(1 * time.Hour) - d6 := d5.Add(1 * time.Hour) - - for i, this := range []struct { - sequence interface{} - key interface{} - op string - match interface{} - expect interface{} - }{ - { - sequence: []map[int]string{ - {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, - }, - key: 2, match: "m", - expect: []map[int]string{ - {1: "a", 2: "m"}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}, - }, - key: "b", match: 4, - expect: []map[string]int{ - {"a": 3, "b": 4}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", match: "f", - expect: []TstX{ - {A: "e", B: "f"}, - }, - }, - { - sequence: []*map[int]string{ - {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, - }, - key: 2, match: "m", - expect: []*map[int]string{ - {1: "a", 2: "m"}, - }, - }, - { - sequence: []*TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", match: "f", - expect: []*TstX{ - {A: "e", B: "f"}, - }, - }, - { - sequence: []*TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, - }, - key: "TstRp", match: "rc", - expect: []*TstX{ - {A: "c", B: "d"}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "c"}, - }, - key: "TstRv", match: "rc", - expect: []TstX{ - {A: "e", B: "c"}, - }, - }, - { - sequence: []map[string]TstX{ - {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, - }, - key: "foo.B", match: "d", - expect: []map[string]TstX{ - {"foo": TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]TstX{ - {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, - }, - key: ".foo.B", match: "d", - expect: []map[string]TstX{ - {"foo": TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]TstX{ - {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, - }, - key: "foo.TstRv", match: "rd", - expect: []map[string]TstX{ - {"foo": TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]*TstX{ - {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}, - }, - key: "foo.TstRp", match: "rc", - expect: []map[string]*TstX{ - {"foo": &TstX{A: "c", B: "d"}}, - }, - }, - { - sequence: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, - }, - key: "foo.Tst.B", match: "d", - expect: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, - }, - }, - { - sequence: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}, - }, - key: "foo.Tst.TstRv", match: "rd", - expect: []map[string]Mid{ - {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, - }, - }, - { - sequence: []map[string]*Mid{ - {"foo": &Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": &Mid{Tst: TstX{A: "e", B: "f"}}}, - }, - key: "foo.Tst.TstRp", match: "rc", - expect: []map[string]*Mid{ - {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - key: "b", op: ">", match: 3, - expect: []map[string]int{ - {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "!=", match: "f", - expect: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - key: "b", op: "in", match: []int{3, 4, 5}, - expect: []map[string]int{ - {"a": 3, "b": 4}, - }, - }, - { - sequence: []map[string][]string{ - {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"G", "H", "I"}, "b": []string{"J", "K", "L"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, - }, - key: "b", op: "intersect", match: []string{"D", "P", "Q"}, - expect: []map[string][]string{ - {"a": []string{"A", "B", "C"}, "b": []string{"D", "E", "F"}}, {"a": []string{"M", "N", "O"}, "b": []string{"P", "Q", "R"}}, - }, - }, - { - sequence: []map[string][]int{ - {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, {"a": []int{13, 14, 15}, "b": []int{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int{4, 10, 12}, - expect: []map[string][]int{ - {"a": []int{1, 2, 3}, "b": []int{4, 5, 6}}, {"a": []int{7, 8, 9}, "b": []int{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int8{ - {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, {"a": []int8{13, 14, 15}, "b": []int8{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int8{4, 10, 12}, - expect: []map[string][]int8{ - {"a": []int8{1, 2, 3}, "b": []int8{4, 5, 6}}, {"a": []int8{7, 8, 9}, "b": []int8{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int16{ - {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, {"a": []int16{13, 14, 15}, "b": []int16{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int16{4, 10, 12}, - expect: []map[string][]int16{ - {"a": []int16{1, 2, 3}, "b": []int16{4, 5, 6}}, {"a": []int16{7, 8, 9}, "b": []int16{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int32{ - {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, {"a": []int32{13, 14, 15}, "b": []int32{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int32{4, 10, 12}, - expect: []map[string][]int32{ - {"a": []int32{1, 2, 3}, "b": []int32{4, 5, 6}}, {"a": []int32{7, 8, 9}, "b": []int32{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]int64{ - {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, {"a": []int64{13, 14, 15}, "b": []int64{16, 17, 18}}, - }, - key: "b", op: "intersect", match: []int64{4, 10, 12}, - expect: []map[string][]int64{ - {"a": []int64{1, 2, 3}, "b": []int64{4, 5, 6}}, {"a": []int64{7, 8, 9}, "b": []int64{10, 11, 12}}, - }, - }, - { - sequence: []map[string][]float32{ - {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, {"a": []float32{13.0, 14.0, 15.0}, "b": []float32{16.0, 17.0, 18.0}}, - }, - key: "b", op: "intersect", match: []float32{4, 10, 12}, - expect: []map[string][]float32{ - {"a": []float32{1.0, 2.0, 3.0}, "b": []float32{4.0, 5.0, 6.0}}, {"a": []float32{7.0, 8.0, 9.0}, "b": []float32{10.0, 11.0, 12.0}}, - }, - }, - { - sequence: []map[string][]float64{ - {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, {"a": []float64{13.0, 14.0, 15.0}, "b": []float64{16.0, 17.0, 18.0}}, - }, - key: "b", op: "intersect", match: []float64{4, 10, 12}, - expect: []map[string][]float64{ - {"a": []float64{1.0, 2.0, 3.0}, "b": []float64{4.0, 5.0, 6.0}}, {"a": []float64{7.0, 8.0, 9.0}, "b": []float64{10.0, 11.0, 12.0}}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, - }, - key: "b", op: "in", match: slice(3, 4, 5), - expect: []map[string]int{ - {"a": 3, "b": 4}, - }, - }, - { - sequence: []map[string]time.Time{ - {"a": d1, "b": d2}, {"a": d3, "b": d4}, {"a": d5, "b": d6}, - }, - key: "b", op: "in", match: slice(d3, d4, d5), - expect: []map[string]time.Time{ - {"a": d3, "b": d4}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "not in", match: []string{"c", "d", "e"}, - expect: []TstX{ - {A: "a", B: "b"}, {A: "e", B: "f"}, - }, - }, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "not in", match: slice("c", t, "d", "e"), - expect: []TstX{ - {A: "a", B: "b"}, {A: "e", B: "f"}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, - }, - key: "b", op: "", match: nil, - expect: []map[string]int{ - {"a": 3}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, - }, - key: "b", op: "!=", match: nil, - expect: []map[string]int{ - {"a": 1, "b": 2}, {"a": 5, "b": 6}, - }, - }, - { - sequence: []map[string]int{ - {"a": 1, "b": 2}, {"a": 3}, {"a": 5, "b": 6}, - }, - key: "b", op: ">", match: nil, - expect: []map[string]int{}, - }, - { - sequence: []map[string]bool{ - {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, - }, - key: "b", op: "", match: true, - expect: []map[string]bool{ - {"c": true, "b": true}, - }, - }, - { - sequence: []map[string]bool{ - {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, - }, - key: "b", op: "!=", match: true, - expect: []map[string]bool{ - {"a": true, "b": false}, {"d": true, "b": false}, - }, - }, - { - sequence: []map[string]bool{ - {"a": true, "b": false}, {"c": true, "b": true}, {"d": true, "b": false}, - }, - key: "b", op: ">", match: false, - expect: []map[string]bool{}, - }, - {sequence: (*[]TstX)(nil), key: "A", match: "a", expect: false}, - {sequence: TstX{A: "a", B: "b"}, key: "A", match: "a", expect: false}, - {sequence: []map[string]*TstX{{"foo": nil}}, key: "foo.B", match: "d", expect: false}, - { - sequence: []TstX{ - {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, - }, - key: "B", op: "op", match: "f", - expect: false, - }, - { - sequence: map[string]interface{}{ - "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, - }, - key: "b", op: "in", match: slice(3, 4, 5), - expect: map[string]interface{}{ - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - }, - }, - { - sequence: map[string]interface{}{ - "foo": []interface{}{map[interface{}]interface{}{"a": 1, "b": 2}}, - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, - }, - key: "b", op: ">", match: 3, - expect: map[string]interface{}{ - "bar": []interface{}{map[interface{}]interface{}{"a": 3, "b": 4}}, - "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, - }, - }, - } { - var results interface{} - var err error - - if len(this.op) > 0 { - results, err = where(this.sequence, this.key, this.op, this.match) - } else { - results, err = where(this.sequence, this.key, this.match) - } - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Where didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, this.key, this.match, results, this.expect) - } - } - } - - var err error - _, err = where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1) - if err == nil { - t.Errorf("Where called with none string op value didn't return an expected error") - } - - _, err = where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2) - if err == nil { - t.Errorf("Where called with more than two variable arguments didn't return an expected error") - } - - _, err = where(map[string]int{"a": 1, "b": 2}, "a") - if err == nil { - t.Errorf("Where called with no variable arguments didn't return an expected error") - } -} - -func TestDelimit(t *testing.T) { - t.Parallel() - for i, this := range []struct { - sequence interface{} - delimiter interface{} - last interface{} - expect template.HTML - }{ - {[]string{"class1", "class2", "class3"}, " ", nil, "class1 class2 class3"}, - {[]int{1, 2, 3, 4, 5}, ",", nil, "1,2,3,4,5"}, - {[]int{1, 2, 3, 4, 5}, ", ", nil, "1, 2, 3, 4, 5"}, - {[]string{"class1", "class2", "class3"}, " ", " and ", "class1 class2 and class3"}, - {[]int{1, 2, 3, 4, 5}, ",", ",", "1,2,3,4,5"}, - {[]int{1, 2, 3, 4, 5}, ", ", ", and ", "1, 2, 3, 4, and 5"}, - // test maps with and without sorting required - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", nil, "10--20--30--40--50"}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", nil, "30--20--10--40--50"}, - {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", nil, "10--20--30--40--50"}, - {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", nil, "30--20--10--40--50"}, - {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", nil, "50--40--10--30--20"}, - {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", nil, "10--20--30--40--50"}, - {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", nil, "30--20--10--40--50"}, - {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, "--", nil, "30--20--10--40--50"}, - // test maps with a last delimiter - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "--", "--and--", "10--20--30--40--and--50"}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "--", "--and--", "30--20--10--40--and--50"}, - {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, "--", "--and--", "10--20--30--40--and--50"}, - {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, "--", "--and--", "30--20--10--40--and--50"}, - {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, "--", "--and--", "50--40--10--30--and--20"}, - {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, "--", "--and--", "10--20--30--40--and--50"}, - {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, - {map[float64]string{3.5: "10", 2.5: "20", 1.5: "30", 4.5: "40", 5.5: "50"}, "--", "--and--", "30--20--10--40--and--50"}, - } { - var result template.HTML - var err error - if this.last == nil { - result, err = delimit(this.sequence, this.delimiter) - } else { - result, err = delimit(this.sequence, this.delimiter, this.last) - } - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] Delimit called on sequence: %v | delimiter: `%v` | last: `%v`, got %v but expected %v", i, this.sequence, this.delimiter, this.last, result, this.expect) - } - } -} - -func TestSort(t *testing.T) { - t.Parallel() - type ts struct { - MyInt int - MyFloat float64 - MyString string - } - type mid struct { - Tst TstX - } - - for i, this := range []struct { - sequence interface{} - sortByField interface{} - sortAsc string - expect interface{} - }{ - {[]string{"class1", "class2", "class3"}, nil, "asc", []string{"class1", "class2", "class3"}}, - {[]string{"class3", "class1", "class2"}, nil, "asc", []string{"class1", "class2", "class3"}}, - {[]int{1, 2, 3, 4, 5}, nil, "asc", []int{1, 2, 3, 4, 5}}, - {[]int{5, 4, 3, 1, 2}, nil, "asc", []int{1, 2, 3, 4, 5}}, - // test sort key parameter is focibly set empty - {[]string{"class3", "class1", "class2"}, map[int]string{1: "a"}, "asc", []string{"class1", "class2", "class3"}}, - // test map sorting by keys - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, nil, "asc", []int{10, 20, 30, 40, 50}}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, nil, "asc", []int{30, 20, 10, 40, 50}}, - {map[string]string{"1": "10", "2": "20", "3": "30", "4": "40", "5": "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, - {map[string]string{"3": "10", "2": "20", "1": "30", "4": "40", "5": "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, - {map[string]string{"one": "10", "two": "20", "three": "30", "four": "40", "five": "50"}, nil, "asc", []string{"50", "40", "10", "30", "20"}}, - {map[int]string{1: "10", 2: "20", 3: "30", 4: "40", 5: "50"}, nil, "asc", []string{"10", "20", "30", "40", "50"}}, - {map[int]string{3: "10", 2: "20", 1: "30", 4: "40", 5: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, - {map[float64]string{3.3: "10", 2.3: "20", 1.3: "30", 4.3: "40", 5.3: "50"}, nil, "asc", []string{"30", "20", "10", "40", "50"}}, - // test map sorting by value - {map[string]int{"1": 10, "2": 20, "3": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, - {map[string]int{"3": 10, "2": 20, "1": 30, "4": 40, "5": 50}, "value", "asc", []int{10, 20, 30, 40, 50}}, - // test map sorting by field value - { - map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, - "MyInt", - "asc", - []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, - }, - { - map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, - "MyFloat", - "asc", - []ts{{10, 10.5, "ten"}, {20, 20.5, "twenty"}, {30, 30.5, "thirty"}, {40, 40.5, "forty"}, {50, 50.5, "fifty"}}, - }, - { - map[string]ts{"1": {10, 10.5, "ten"}, "2": {20, 20.5, "twenty"}, "3": {30, 30.5, "thirty"}, "4": {40, 40.5, "forty"}, "5": {50, 50.5, "fifty"}}, - "MyString", - "asc", - []ts{{50, 50.5, "fifty"}, {40, 40.5, "forty"}, {10, 10.5, "ten"}, {30, 30.5, "thirty"}, {20, 20.5, "twenty"}}, - }, - // test sort desc - {[]string{"class1", "class2", "class3"}, "value", "desc", []string{"class3", "class2", "class1"}}, - {[]string{"class3", "class1", "class2"}, "value", "desc", []string{"class3", "class2", "class1"}}, - // test sort by struct's method - { - []TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, - "TstRv", - "asc", - []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - { - []*TstX{{A: "i", B: "j"}, {A: "e", B: "f"}, {A: "c", B: "d"}, {A: "g", B: "h"}, {A: "a", B: "b"}}, - "TstRp", - "asc", - []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - // test map sorting by struct's method - { - map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, - "TstRv", - "asc", - []TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - { - map[string]*TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, - "TstRp", - "asc", - []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, - }, - // test sort by dot chaining key argument - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - "foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - ".foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - "foo.TstRv", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - []map[string]*TstX{{"foo": &TstX{A: "e", B: "f"}}, {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}}, - "foo.TstRp", - "asc", - []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, - }, - { - []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.A", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - { - []map[string]mid{{"foo": mid{Tst: TstX{A: "e", B: "f"}}}, {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.TstRv", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - // test map sorting by dot chaining key argument - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - "foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - ".foo.A", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - "foo.TstRv", - "asc", - []map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]*TstX{"1": {"foo": &TstX{A: "e", B: "f"}}, "2": {"foo": &TstX{A: "a", B: "b"}}, "3": {"foo": &TstX{A: "c", B: "d"}}}, - "foo.TstRp", - "asc", - []map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, - }, - { - map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.A", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - { - map[string]map[string]mid{"1": {"foo": mid{Tst: TstX{A: "e", B: "f"}}}, "2": {"foo": mid{Tst: TstX{A: "a", B: "b"}}}, "3": {"foo": mid{Tst: TstX{A: "c", B: "d"}}}}, - "foo.Tst.TstRv", - "asc", - []map[string]mid{{"foo": mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": mid{Tst: TstX{A: "e", B: "f"}}}}, - }, - // interface slice with missing elements - { - []interface{}{ - map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, - map[interface{}]interface{}{"Title": "Bar"}, - map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, - }, - "Weight", - "asc", - []interface{}{ - map[interface{}]interface{}{"Title": "Bar"}, - map[interface{}]interface{}{"Title": "Zap", "Weight": 5}, - map[interface{}]interface{}{"Title": "Foo", "Weight": 10}, - }, - }, - // test error cases - {(*[]TstX)(nil), nil, "asc", false}, - {TstX{A: "a", B: "b"}, nil, "asc", false}, - { - []map[string]TstX{{"foo": TstX{A: "e", B: "f"}}, {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}}, - "foo.NotAvailable", - "asc", - false, - }, - { - map[string]map[string]TstX{"1": {"foo": TstX{A: "e", B: "f"}}, "2": {"foo": TstX{A: "a", B: "b"}}, "3": {"foo": TstX{A: "c", B: "d"}}}, - "foo.NotAvailable", - "asc", - false, - }, - {nil, nil, "asc", false}, - } { - var result interface{} - var err error - if this.sortByField == nil { - result, err = sortSeq(this.sequence) - } else { - result, err = sortSeq(this.sequence, this.sortByField, this.sortAsc) - } - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Sort didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] Sort called on sequence: %v | sortByField: `%v` | got %v but expected %v", i, this.sequence, this.sortByField, result, this.expect) - } - } - } -} - -func TestReturnWhenSet(t *testing.T) { - t.Parallel() - for i, this := range []struct { - data interface{} - key interface{} - expect interface{} - }{ - {[]int{1, 2, 3}, 1, int64(2)}, - {[]uint{1, 2, 3}, 1, uint64(2)}, - {[]float64{1.1, 2.2, 3.3}, 1, float64(2.2)}, - {[]string{"foo", "bar", "baz"}, 1, "bar"}, - {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, 1, ""}, - {map[string]int{"foo": 1, "bar": 2, "baz": 3}, "bar", int64(2)}, - {map[string]uint{"foo": 1, "bar": 2, "baz": 3}, "bar", uint64(2)}, - {map[string]float64{"foo": 1.1, "bar": 2.2, "baz": 3.3}, "bar", float64(2.2)}, - {map[string]string{"foo": "FOO", "bar": "BAR", "baz": "BAZ"}, "bar", "BAR"}, - {map[string]TstX{"foo": {A: "a", B: "b"}, "bar": {A: "c", B: "d"}, "baz": {A: "e", B: "f"}}, "bar", ""}, - {(*[]string)(nil), "bar", ""}, - } { - result := returnWhenSet(this.data, this.key) - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] ReturnWhenSet got %v (type %v) but expected %v (type %v)", i, result, reflect.TypeOf(result), this.expect, reflect.TypeOf(this.expect)) - } - } -} - -func TestMarkdownify(t *testing.T) { - t.Parallel() - v := viper.New() - - f := newTestFuncsterWithViper(v) - - for i, this := range []struct { - in interface{} - expect interface{} - }{ - {"Hello **World!**", template.HTML("Hello World!")}, - {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes World!")}, - } { - result, err := f.markdownify(this.in) - if err != nil { - t.Fatalf("[%d] unexpected error in markdownify: %s", i, err) - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] markdownify got %v (type %v) but expected %v (type %v)", i, result, reflect.TypeOf(result), this.expect, reflect.TypeOf(this.expect)) - } - } - - if _, err := f.markdownify(t); err == nil { - t.Fatalf("markdownify should have errored") - } -} - -func TestApply(t *testing.T) { - t.Parallel() - - f := newTestFuncster() - - strings := []interface{}{"a\n", "b\n"} - noStringers := []interface{}{tstNoStringer{}, tstNoStringer{}} - - chomped, _ := f.apply(strings, "chomp", ".") - assert.Equal(t, []interface{}{template.HTML("a"), template.HTML("b")}, chomped) - - chomped, _ = f.apply(strings, "chomp", "c\n") - assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, chomped) - - chomped, _ = f.apply(nil, "chomp", ".") - assert.Equal(t, []interface{}{}, chomped) - - _, err := f.apply(strings, "apply", ".") - if err == nil { - t.Errorf("apply with apply should fail") - } - - var nilErr *error - _, err = f.apply(nilErr, "chomp", ".") - if err == nil { - t.Errorf("apply with nil in seq should fail") - } - - _, err = f.apply(strings, "dobedobedo", ".") - if err == nil { - t.Errorf("apply with unknown func should fail") - } - - _, err = f.apply(noStringers, "chomp", ".") - if err == nil { - t.Errorf("apply when func fails should fail") - } - - _, err = f.apply(tstNoStringer{}, "chomp", ".") - if err == nil { - t.Errorf("apply with non-sequence should fail") - } -} - -func TestChomp(t *testing.T) { - t.Parallel() - base := "\n This is\na story " - for i, item := range []string{ - "\n", "\n\n", - "\r", "\r\r", - "\r\n", "\r\n\r\n", - } { - c, _ := chomp(base + item) - chomped := string(c) - - if chomped != base { - t.Errorf("[%d] Chomp failed, got '%v'", i, chomped) - } - - _, err := chomp(tstNoStringer{}) - - if err == nil { - t.Errorf("Chomp should fail") - } - } -} - -func TestLower(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - want string - isErr bool - }{ - {"TEST", "test", false}, - {template.HTML("LoWeR"), "lower", false}, - {[]byte("BYTES"), "bytes", false}, - } - - for i, c := range cases { - res, err := lower(c.s) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) - } - - if res != c.want { - t.Errorf("[%d] lower failed: want %v, got %v", i, c.want, res) - } - } -} - -func TestTitle(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - want string - isErr bool - }{ - {"test", "Test", false}, - {template.HTML("hypertext"), "Hypertext", false}, - {[]byte("bytes"), "Bytes", false}, - } - - for i, c := range cases { - res, err := title(c.s) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) - } - - if res != c.want { - t.Errorf("[%d] title failed: want %v, got %v", i, c.want, res) - } - } -} - -func TestUpper(t *testing.T) { - t.Parallel() - cases := []struct { - s interface{} - want string - isErr bool - }{ - {"test", "TEST", false}, - {template.HTML("UpPeR"), "UPPER", false}, - {[]byte("bytes"), "BYTES", false}, - } - - for i, c := range cases { - res, err := upper(c.s) - if (err != nil) != c.isErr { - t.Fatalf("[%d] unexpected isErr state: want %v, got %v, err = %v", i, c.want, (err != nil), err) - } - - if res != c.want { - t.Errorf("[%d] upper failed: want %v, got %v", i, c.want, res) - } - } -} - -func TestHighlight(t *testing.T) { - t.Parallel() - code := "func boo() {}" - - f := newTestFuncster() - - highlighted, err := f.highlight(code, "go", "") - - if err != nil { - t.Fatal("Highlight returned error:", err) - } - - // this depends on a Pygments installation, but will always contain the function name. - if !strings.Contains(string(highlighted), "boo") { - t.Errorf("Highlight mismatch, got %v", highlighted) - } - - _, err = f.highlight(t, "go", "") - - if err == nil { - t.Error("Expected highlight error") - } -} - -func TestInflect(t *testing.T) { - t.Parallel() - for i, this := range []struct { - inflectFunc func(i interface{}) (string, error) - in interface{} - expected string - }{ - {humanize, "MyCamel", "My camel"}, - {humanize, "", ""}, - {humanize, "103", "103rd"}, - {humanize, "41", "41st"}, - {humanize, 103, "103rd"}, - {humanize, int64(92), "92nd"}, - {humanize, "5.5", "5.5"}, - {pluralize, "cat", "cats"}, - {pluralize, "", ""}, - {singularize, "cats", "cat"}, - {singularize, "", ""}, - } { - - result, err := this.inflectFunc(this.in) - - if err != nil { - t.Errorf("[%d] Unexpected Inflect error: %s", i, err) - } else if result != this.expected { - t.Errorf("[%d] Inflect method error, got %v expected %v", i, result, this.expected) - } - - _, err = this.inflectFunc(t) - if err == nil { - t.Errorf("[%d] Expected Inflect error", i) - } - } -} - -func TestCounterFuncs(t *testing.T) { - t.Parallel() - for i, this := range []struct { - countFunc func(i interface{}) (int, error) - in string - expected int - }{ - {countWords, "Do Be Do Be Do", 5}, - {countWords, "旁边", 2}, - {countRunes, "旁边", 2}, - } { - - result, err := this.countFunc(this.in) - - if err != nil { - t.Errorf("[%d] Unexpected counter error: %s", i, err) - } else if result != this.expected { - t.Errorf("[%d] Count method error, got %v expected %v", i, result, this.expected) - } - - _, err = this.countFunc(t) - if err == nil { - t.Errorf("[%d] Expected Count error", i) - } - } -} - -func TestReplace(t *testing.T) { - t.Parallel() - v, _ := replace("aab", "a", "b") - assert.Equal(t, "bbb", v) - v, _ = replace("11a11", 1, 2) - assert.Equal(t, "22a22", v) - v, _ = replace(12345, 1, 2) - assert.Equal(t, "22345", v) - _, e := replace(tstNoStringer{}, "a", "b") - assert.NotNil(t, e, "tstNoStringer isn't trimmable") - _, e = replace("a", tstNoStringer{}, "b") - assert.NotNil(t, e, "tstNoStringer cannot be converted to string") - _, e = replace("a", "b", tstNoStringer{}) - assert.NotNil(t, e, "tstNoStringer cannot be converted to string") -} - -func TestReplaceRE(t *testing.T) { - t.Parallel() - for i, val := range []struct { - pattern interface{} - repl interface{} - src interface{} - expect string - ok bool - }{ - {"^https?://([^/]+).*", "$1", "http://gohugo.io/docs", "gohugo.io", true}, - {"^https?://([^/]+).*", "$2", "http://gohugo.io/docs", "", true}, - {tstNoStringer{}, "$2", "http://gohugo.io/docs", "", false}, - {"^https?://([^/]+).*", tstNoStringer{}, "http://gohugo.io/docs", "", false}, - {"^https?://([^/]+).*", "$2", tstNoStringer{}, "", false}, - {"(ab)", "AB", "aabbaab", "aABbaAB", true}, - {"(ab", "AB", "aabb", "", false}, // invalid re - } { - v, err := replaceRE(val.pattern, val.repl, val.src) - if (err == nil) != val.ok { - t.Errorf("[%d] %s", i, err) - } - assert.Equal(t, val.expect, v) - } -} - -func TestFindRE(t *testing.T) { - t.Parallel() - for i, this := range []struct { - expr string - content interface{} - limit interface{} - expect []string - ok bool - }{ - {"[G|g]o", "Hugo is a static site generator written in Go.", 2, []string{"go", "Go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", -1, []string{"go", "Go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", 1, []string{"go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", "1", []string{"go"}, true}, - {"[G|g]o", "Hugo is a static site generator written in Go.", nil, []string(nil), true}, - {"[G|go", "Hugo is a static site generator written in Go.", nil, []string(nil), false}, - {"[G|g]o", t, nil, []string(nil), false}, - } { - var ( - res []string - err error - ) - - res, err = findRE(this.expr, this.content, this.limit) - if err != nil && this.ok { - t.Errorf("[%d] returned an unexpected error: %s", i, err) - } - - assert.Equal(t, this.expect, res) - } -} - -func TestTrim(t *testing.T) { - t.Parallel() - - for i, this := range []struct { - v1 interface{} - v2 string - expect interface{} - }{ - {"1234 my way 13", "123 ", "4 my way"}, - {" my way ", " ", "my way"}, - {1234, "14", "23"}, - {tstNoStringer{}, " ", false}, - } { - result, err := trim(this.v1, this.v2) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] trim didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(result, this.expect) { - t.Errorf("[%d] got '%s' but expected %s", i, result, this.expect) - } - } - } -} - -func TestDateFormat(t *testing.T) { - t.Parallel() - for i, this := range []struct { - layout string - value interface{} - expect interface{} - }{ - {"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"}, - {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"}, - {"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"}, - // The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone - {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, - {"Monday, Jan 2, 2006", 1421733600.123, false}, - {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"}, - {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"}, - {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"}, - {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"}, - } { - result, err := dateFormat(this.layout, this.value) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] DateFormat didn't return an expected error, got %v", i, result) - } - } else { - if err != nil { - t.Errorf("[%d] DateFormat failed: %s", i, err) - continue - } - if result != this.expect { - t.Errorf("[%d] DateFormat got %v but expected %v", i, result, this.expect) - } - } - } -} - -func TestDefaultFunc(t *testing.T) { - t.Parallel() - then := time.Now() - now := time.Now() - - for i, this := range []struct { - dflt interface{} - given interface{} - expected interface{} - }{ - {true, false, false}, - {"5", 0, "5"}, - - {"test1", "set", "set"}, - {"test2", "", "test2"}, - {"test3", nil, "test3"}, - - {[2]int{10, 20}, [2]int{1, 2}, [2]int{1, 2}}, - {[2]int{10, 20}, [0]int{}, [2]int{10, 20}}, - {[2]int{100, 200}, nil, [2]int{100, 200}}, - - {[]string{"one"}, []string{"uno"}, []string{"uno"}}, - {[]string{"two"}, []string{}, []string{"two"}}, - {[]string{"three"}, nil, []string{"three"}}, - - {map[string]int{"one": 1}, map[string]int{"uno": 1}, map[string]int{"uno": 1}}, - {map[string]int{"one": 1}, map[string]int{}, map[string]int{"one": 1}}, - {map[string]int{"two": 2}, nil, map[string]int{"two": 2}}, - - {10, 1, 1}, - {10, 0, 10}, - {20, nil, 20}, - - {float32(10), float32(1), float32(1)}, - {float32(10), 0, float32(10)}, - {float32(20), nil, float32(20)}, - - {complex(2, -2), complex(1, -1), complex(1, -1)}, - {complex(2, -2), complex(0, 0), complex(2, -2)}, - {complex(3, -3), nil, complex(3, -3)}, - - {struct{ f string }{f: "one"}, struct{ f string }{}, struct{ f string }{}}, - {struct{ f string }{f: "two"}, nil, struct{ f string }{f: "two"}}, - - {then, now, now}, - {then, time.Time{}, then}, - } { - res, err := dfault(this.dflt, this.given) - if err != nil { - t.Errorf("[%d] default returned an error: %s", i, err) - continue - } - if !reflect.DeepEqual(this.expected, res) { - t.Errorf("[%d] default returned %v, but expected %v", i, res, this.expected) - } - } -} - func TestDefault(t *testing.T) { t.Parallel() for i, this := range []struct { @@ -2490,509 +305,6 @@ func TestDefault(t *testing.T) { } } -func TestSafeHTML(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`
`, `{{ . }}`, `<div></div>`, `
`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeHTML(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeHTML: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeHTML returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeHTML, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -func TestSafeHTMLAttr(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`href="irc://irc.freenode.net/#golang"`, `irc`, `irc`, `irc`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeHTMLAttr(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeHTMLAttr: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeHTMLAttr returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeHTMLAttr, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -func TestSafeCSS(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`width: 60px;`, `
`, `
`, `
`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeCSS(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeCSS: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeCSS returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeCSS, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -// TODO(bep) what is this? Also look above. -func TestSafeJS(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`619c16f`, ``, ``, ``}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeJS(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeJS: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeJS returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeJS, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -// TODO(bep) what is this? -func TestSafeURL(t *testing.T) { - t.Parallel() - for i, this := range []struct { - str string - tmplStr string - expectWithoutEscape string - expectWithEscape string - }{ - {`irc://irc.freenode.net/#golang`, `IRC`, `IRC`, `IRC`}, - } { - tmpl, err := template.New("test").Parse(this.tmplStr) - if err != nil { - t.Errorf("[%d] unable to create new html template %q: %s", i, this.tmplStr, err) - continue - } - - buf := new(bytes.Buffer) - err = tmpl.Execute(buf, this.str) - if err != nil { - t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithoutEscape { - t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape) - } - - buf.Reset() - v, err := safeURL(this.str) - if err != nil { - t.Fatalf("[%d] unexpected error in safeURL: %s", i, err) - } - - err = tmpl.Execute(buf, v) - if err != nil { - t.Errorf("[%d] execute template with an escaped string value by safeURL returns unexpected error: %s", i, err) - } - if buf.String() != this.expectWithEscape { - t.Errorf("[%d] execute template with an escaped string value by safeURL, got %v but expected %v", i, buf.String(), this.expectWithEscape) - } - } -} - -func TestBase64Decode(t *testing.T) { - t.Parallel() - testStr := "abc123!?$*&()'-=@~" - enc := base64.StdEncoding.EncodeToString([]byte(testStr)) - result, err := base64Decode(enc) - - if err != nil { - t.Error("base64Decode returned error:", err) - } - - if result != testStr { - t.Errorf("base64Decode: got '%s', expected '%s'", result, testStr) - } - - _, err = base64Decode(t) - if err == nil { - t.Error("Expected error from base64Decode") - } -} - -func TestBase64Encode(t *testing.T) { - t.Parallel() - testStr := "YWJjMTIzIT8kKiYoKSctPUB+" - dec, err := base64.StdEncoding.DecodeString(testStr) - - if err != nil { - t.Error("base64Encode: the DecodeString function of the base64 package returned an error:", err) - } - - result, err := base64Encode(string(dec)) - - if err != nil { - t.Errorf("base64Encode: Can't cast arg '%s' into a string:", testStr) - } - - if result != testStr { - t.Errorf("base64Encode: got '%s', expected '%s'", result, testStr) - } - - _, err = base64Encode(t) - if err == nil { - t.Error("Expected error from base64Encode") - } -} - -func TestMD5(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input string - expectedHash string - }{ - {"Hello world, gophers!", "b3029f756f98f79e7f1b7f1d1f0dd53b"}, - {"Lorem ipsum dolor", "06ce65ac476fc656bea3fca5d02cfd81"}, - } { - result, err := md5(this.input) - if err != nil { - t.Errorf("md5 returned error: %s", err) - } - - if result != this.expectedHash { - t.Errorf("[%d] md5: expected '%s', got '%s'", i, this.expectedHash, result) - } - } - - _, err := md5(t) - if err == nil { - t.Error("Expected error from md5") - } -} - -func TestSHA1(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input string - expectedHash string - }{ - {"Hello world, gophers!", "c8b5b0e33d408246e30f53e32b8f7627a7a649d4"}, - {"Lorem ipsum dolor", "45f75b844be4d17b3394c6701768daf39419c99b"}, - } { - result, err := sha1(this.input) - if err != nil { - t.Errorf("sha1 returned error: %s", err) - } - - if result != this.expectedHash { - t.Errorf("[%d] sha1: expected '%s', got '%s'", i, this.expectedHash, result) - } - } - - _, err := sha1(t) - if err == nil { - t.Error("Expected error from sha1") - } -} - -func TestSHA256(t *testing.T) { - t.Parallel() - for i, this := range []struct { - input string - expectedHash string - }{ - {"Hello world, gophers!", "6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46"}, - {"Lorem ipsum dolor", "9b3e1beb7053e0f900a674dd1c99aca3355e1275e1b03d3cb1bc977f5154e196"}, - } { - result, err := sha256(this.input) - if err != nil { - t.Errorf("sha256 returned error: %s", err) - } - - if result != this.expectedHash { - t.Errorf("[%d] sha256: expected '%s', got '%s'", i, this.expectedHash, result) - } - } - - _, err := sha256(t) - if err == nil { - t.Error("Expected error from sha256") - } -} - -func TestReadFile(t *testing.T) { - t.Parallel() - - workingDir := "/home/hugo" - - v := viper.New() - - v.Set("workingDir", workingDir) - - f := newTestFuncsterWithViper(v) - - afero.WriteFile(f.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755) - afero.WriteFile(f.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755) - - for i, this := range []struct { - filename string - expect interface{} - }{ - {"", false}, - {"b", false}, - {filepath.FromSlash("/f/f1.txt"), "f1-content"}, - {filepath.FromSlash("f/f1.txt"), "f1-content"}, - {filepath.FromSlash("../f2.txt"), false}, - } { - result, err := f.readFileFromWorkingDir(this.filename) - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] readFile didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] readFile failed: %s", i, err) - continue - } - if result != this.expect { - t.Errorf("[%d] readFile got %q but expected %q", i, result, this.expect) - } - } - } -} - -func TestPartialHTMLAndText(t *testing.T) { - t.Parallel() - config := newDepsConfig(viper.New()) - - data := struct { - Name string - }{ - Name: "a+b+c", // This should get encoded in HTML. - } - - config.WithTemplate = func(templ tpl.TemplateHandler) error { - if err := templ.AddTemplate("htmlTemplate.html", `HTML Test|HTML:{{ partial "test.html" . -}}|Text:{{ partial "test.txt" . }} -CSS plain: -CSS safe: -`); err != nil { - return err - } - - if err := templ.AddTemplate("_text/textTemplate.txt", `Text Test|HTML:{{ partial "test.html" . -}}|Text:{{ partial "test.txt" . }} -CSS plain: `); err != nil { - return err - } - - if err := templ.AddTemplate("partials/test.html", "HTML Name: {{ .Name }}"); err != nil { - return err - } - if err := templ.AddTemplate("_text/partials/test.txt", "Text Name: {{ .Name }}"); err != nil { - return err - } - if err := templ.AddTemplate("_text/partials/mystyles.css", - `body { background-color: blue; } -`); err != nil { - return err - } - - return nil - } - - de, err := deps.New(config) - require.NoError(t, err) - require.NoError(t, de.LoadResources()) - - templ := de.Tmpl.Lookup("htmlTemplate.html") - require.NotNil(t, templ) - resultHTML, err := templ.ExecuteToString(data) - require.NoError(t, err) - - templ = de.Tmpl.Lookup("_text/textTemplate.txt") - require.NotNil(t, templ) - resultText, err := templ.ExecuteToString(data) - require.NoError(t, err) - - require.Contains(t, resultHTML, "HTML Test|HTML:HTML Name: a+b+c|Text:Text Name: a+b+c") - require.Contains(t, resultHTML, `CSS plain: `) - require.Contains(t, resultHTML, `CSS safe: