diff options
author | Cameron Moore <moorereason@gmail.com> | 2017-03-14 01:55:02 +0300 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-04-30 11:56:38 +0300 |
commit | de7c32a1a880820252e922e0c9fcf69e109c0d1b (patch) | |
tree | 07d813f2617dd4a889aaebb885a9c1281a229960 | |
parent | 154e18ddb9ad205055d5bd4827c87f3f0daf499f (diff) |
tpl: Add template function namespaces
This commit moves almost all of the template functions into separate
packages under tpl/ and adds a namespace framework. All changes should
be backward compatible for end users, as all existing function names in
the template funcMap are left intact.
Seq and DoArithmatic have been moved out of the helpers package and into
template namespaces.
Most of the tests involved have been refactored, and many new tests have
been written. There's still work to do, but this is a big improvement.
I got a little overzealous and added some new functions along the way:
- strings.Contains
- strings.ContainsAny
- strings.HasSuffix
- strings.TrimPrefix
- strings.TrimSuffix
Documentation is forthcoming.
Fixes #3042
48 files changed, 7069 insertions, 5268 deletions
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 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 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, <b>World</b> &tc!`, template.HTML(`Hello, <b>World</b> &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}, + {`<div class="test">旁边</div>`, 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}, + {`<div class="test">旁边</div>`, 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>", "<>", "tag"}, + {`"quote"`, `"`, "quote"}, + {1221, "1", "22"}, + {1221, "12", ""}, + {template.HTML("<tag>"), "<>", "tag"}, + {[]byte("<tag>"), "<>", "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 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 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("<p>") -var markdownTrimSuffix = []byte("</p>\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 "<p>Blockhead</p>\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: <p>Blockhead</p> +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 <strong>World!</strong>")}, - {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes <strong>World!</strong>")}, - } { - 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>`, `{{ . }}`, `<div></div>`, `<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"`, `<a {{ . }}>irc</a>`, `<a ZgotmplZ>irc</a>`, `<a href="irc://irc.freenode.net/#golang">irc</a>`}, - } { - 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;`, `<div style="{{ . }}"></div>`, `<div style="ZgotmplZ"></div>`, `<div style="width: 60px;"></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 := 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`, `<script>var x{{ . }};</script>`, `<script>var x"619c16f";</script>`, `<script>var x619c16f;</script>`}, - } { - 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`, `<a href="{{ . }}">IRC</a>`, `<a href="#ZgotmplZ">IRC</a>`, `<a href="irc://irc.freenode.net/#golang">IRC</a>`}, - } { - 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: <style type="text/css">{{ partial "mystyles.css" . -}}</style> -CSS safe: <style type="text/css">{{ partial "mystyles.css" . | safeCSS -}}</style> -`); err != nil { - return err - } - - if err := templ.AddTemplate("_text/textTemplate.txt", `Text Test|HTML:{{ partial "test.html" . -}}|Text:{{ partial "test.txt" . }} -CSS plain: <style type="text/css">{{ partial "mystyles.css" . -}}</style>`); 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: <style type="text/css">ZgotmplZ</style>`) - require.Contains(t, resultHTML, `CSS safe: <style type="text/css">body { background-color: blue; }`) - - require.Contains(t, resultText, "Text Test|HTML:HTML Name: a+b+c|Text:Text Name: a+b+c") - require.Contains(t, resultText, `CSS plain: <style type="text/css">body { background-color: blue; }`) - -} - -func TestPartialHTMLWithNoSuffix(t *testing.T) { - t.Parallel() - config := newDepsConfig(viper.New()) - - data := struct { - Name string - }{ - Name: "a", - } - - config.WithTemplate = func(templ tpl.TemplateHandler) error { - if err := templ.AddTemplate("htmlTemplate.html", `HTML Test Partial: {{ partial "test" . -}}`); err != nil { - return err - } - - if err := templ.AddTemplate("partials/test.html", "HTML Name: {{ .Name }}"); 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) - - require.Contains(t, resultHTML, "HTML Test Partial: HTML Name: a") - -} - -func TestPartialWithError(t *testing.T) { - t.Parallel() - config := newDepsConfig(viper.New()) - - data := struct { - Name string - }{ - Name: "bep", - } - - config.WithTemplate = func(templ tpl.TemplateHandler) error { - if err := templ.AddTemplate("container.html", `HTML Test Partial: {{ partial "fail.foo" . -}}`); err != nil { - return err - } - - if err := templ.AddTemplate("partials/fail.foo", "Template: {{ .DoesNotExist }}"); err != nil { - return err - } - - return nil - } - - de, err := deps.New(config) - require.NoError(t, err) - require.NoError(t, de.LoadResources()) - - templ := de.Tmpl.Lookup("container.html") - require.NotNil(t, templ) - result, err := templ.ExecuteToString(data) - require.Error(t, err) - - errStr := err.Error() - - require.Contains(t, errStr, `template: container.html:1:22: executing "container.html" at <partial "fail.foo" .>`) - require.Contains(t, errStr, `can't evaluate field DoesNotExist`) - - require.Empty(t, result) - -} - func TestPartialCached(t *testing.T) { t.Parallel() testCases := []struct { diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go new file mode 100644 index 000000000..7e3b81bed --- /dev/null +++ b/tpl/transform/transform.go @@ -0,0 +1,119 @@ +// 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 transform + +import ( + "bytes" + "html" + "html/template" + + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" +) + +// New returns a new instance of the transform-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "transform" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { + return ns +} + +// Emojify returns a copy of s with all emoji codes replaced with actual emojis. +// +// See http://www.emoji-cheat-sheet.com/ +func (ns *Namespace) Emojify(s interface{}) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return template.HTML(helpers.Emojify([]byte(ss))), nil +} + +// Highlight returns a copy of s as an HTML string with syntax +// highlighting applied. +func (ns *Namespace) Highlight(s interface{}, lang, opts string) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return template.HTML(helpers.Highlight(ns.deps.Cfg, html.UnescapeString(ss), lang, opts)), nil +} + +// HTMLEscape returns a copy of s with reserved HTML characters escaped. +func (ns *Namespace) HTMLEscape(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return html.EscapeString(ss), nil +} + +// HTMLUnescape returns a copy of with HTML escape requences converted to plain +// text. +func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return html.UnescapeString(ss), nil +} + +var markdownTrimPrefix = []byte("<p>") +var markdownTrimSuffix = []byte("</p>\n") + +// Markdownify renders a given input from Markdown to HTML. +func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + m := ns.deps.ContentSpec.RenderBytes( + &helpers.RenderingContext{ + Cfg: ns.deps.Cfg, + Content: []byte(ss), + PageFmt: "markdown", + Config: ns.deps.ContentSpec.NewBlackfriday(), + }, + ) + m = bytes.TrimPrefix(m, markdownTrimPrefix) + m = bytes.TrimSuffix(m, markdownTrimSuffix) + + return template.HTML(m), nil +} + +// Plainify returns a copy of s with all HTML tags removed. +func (ns *Namespace) Plainify(s interface{}) (string, error) { + ss, err := cast.ToStringE(s) + if err != nil { + return "", err + } + + return helpers.StripHTML(ss), nil +} diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go new file mode 100644 index 000000000..528697469 --- /dev/null +++ b/tpl/transform/transform_test.go @@ -0,0 +1,214 @@ +// 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 transform + +import ( + "fmt" + "html/template" + "testing" + + "github.com/spf13/hugo/config" + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type tstNoStringer struct{} + +func TestNamespace(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + assert.Equal(t, ns, ns.Namespace(), "object pointers should match") +} + +func TestEmojify(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {":notamoji:", template.HTML(":notamoji:")}, + {"I :heart: Hugo", template.HTML("I ❤️ Hugo")}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.Emojify(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 TestHighlight(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + lang string + opts string + expect interface{} + }{ + {"func boo() {}", "go", "", "boo"}, + {tstNoStringer{}, "go", "", false}, + } { + errMsg := fmt.Sprintf("[%d]", i) + + result, err := ns.Highlight(test.s, test.lang, test.opts) + + if b, ok := test.expect.(bool); ok && !b { + require.Error(t, err, errMsg) + continue + } + + require.NoError(t, err, errMsg) + assert.Contains(t, result, "boo", errMsg) + } +} + +func TestHTMLEscape(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {`"Foo & Bar's Diner" <y@z>`, `"Foo & Bar's Diner" <y@z>`}, + {"Hugo & Caddy > Wordpress & Apache", "Hugo & Caddy > Wordpress & Apache"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.HTMLEscape(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 TestHTMLUnescape(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {`"Foo & Bar's Diner" <y@z>`, `"Foo & Bar's Diner" <y@z>`}, + {"Hugo & Caddy > Wordpress & Apache", "Hugo & Caddy > Wordpress & Apache"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.HTMLUnescape(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 TestMarkdownify(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"Hello **World!**", template.HTML("Hello <strong>World!</strong>")}, + {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes <strong>World!</strong>")}, + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.Markdownify(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 TestPlainify(t *testing.T) { + t.Parallel() + + ns := New(newDeps(viper.New())) + + for i, test := range []struct { + s interface{} + expect interface{} + }{ + {"<em>Note:</em> blah <b>blah</b>", "Note: blah blah"}, + // errors + {tstNoStringer{}, false}, + } { + errMsg := fmt.Sprintf("[%d] %s", i, test.s) + + result, err := ns.Plainify(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 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/urls/urls.go b/tpl/urls/urls.go new file mode 100644 index 000000000..04ad1cb4d --- /dev/null +++ b/tpl/urls/urls.go @@ -0,0 +1,108 @@ +// 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 urls + +import ( + "errors" + "html/template" + + "github.com/spf13/cast" + "github.com/spf13/hugo/deps" +) + +// New returns a new instance of the urls-namespaced template functions. +func New(deps *deps.Deps) *Namespace { + return &Namespace{ + deps: deps, + } +} + +// Namespace provides template functions for the "urls" namespace. +type Namespace struct { + deps *deps.Deps +} + +// Namespace returns a pointer to the current namespace instance. +func (ns *Namespace) Namespace() *Namespace { + return ns +} + +// AbsURL takes a given string and converts it to an absolute URL. +func (ns *Namespace) AbsURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + + return template.HTML(ns.deps.PathSpec.AbsURL(s, false)), nil +} + +// RelURL takes a given string and prepends the relative path according to a +// page's position in the project directory structure. +func (ns *Namespace) RelURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", nil + } + + return template.HTML(ns.deps.PathSpec.RelURL(s, false)), 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 (ns *Namespace) 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 (ns *Namespace) 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 +} + +// RelLangURL takes a given string and prepends the relative path according to a +// page's position in the project directory structure and the current language. +func (ns *Namespace) RelLangURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + return template.HTML(ns.deps.PathSpec.RelURL(s, true)), nil +} + +// AbsLangURL takes a given string and converts it to an absolute URL according +// to a page's position in the project directory structure and the current +// language. +func (ns *Namespace) AbsLangURL(a interface{}) (template.HTML, error) { + s, err := cast.ToStringE(a) + if err != nil { + return "", err + } + + return template.HTML(ns.deps.PathSpec.AbsURL(s, true)), nil +} |