Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helpers/general.go194
-rw-r--r--helpers/general_test.go131
-rw-r--r--hugolib/scratch.go5
-rw-r--r--tpl/collections/apply.go143
-rw-r--r--tpl/collections/apply_test.go92
-rw-r--r--tpl/collections/collections.go574
-rw-r--r--tpl/collections/collections_test.go610
-rw-r--r--tpl/collections/index.go107
-rw-r--r--tpl/collections/index_test.go60
-rw-r--r--tpl/collections/sort.go159
-rw-r--r--tpl/collections/sort_test.go237
-rw-r--r--tpl/collections/where.go421
-rw-r--r--tpl/collections/where_test.go606
-rw-r--r--tpl/compare/compare.go198
-rw-r--r--tpl/compare/compare_test.go197
-rw-r--r--tpl/crypto/crypto.go67
-rw-r--r--tpl/crypto/crypto_test.go111
-rw-r--r--tpl/data/data.go31
-rw-r--r--tpl/data/resources.go (renamed from tpl/tplimpl/template_resources.go)24
-rw-r--r--tpl/data/resources_test.go (renamed from tpl/tplimpl/template_resources_test.go)27
-rw-r--r--tpl/encoding/encoding.go64
-rw-r--r--tpl/encoding/encoding_test.go117
-rw-r--r--tpl/images/images.go82
-rw-r--r--tpl/images/images_test.go132
-rw-r--r--tpl/inflect/inflect.go79
-rw-r--r--tpl/inflect/inflect_test.go56
-rw-r--r--tpl/lang/lang.go49
-rw-r--r--tpl/math/math.go199
-rw-r--r--tpl/math/math_test.go228
-rw-r--r--tpl/os/os.go101
-rw-r--r--tpl/os/os_test.go65
-rw-r--r--tpl/safe/safe.go74
-rw-r--r--tpl/safe/safe_test.go222
-rw-r--r--tpl/strings/regexp.go109
-rw-r--r--tpl/strings/regexp_test.go86
-rw-r--r--tpl/strings/strings.go380
-rw-r--r--tpl/strings/strings_test.go639
-rw-r--r--tpl/strings/truncate.go (renamed from tpl/tplimpl/template_func_truncate.go)4
-rw-r--r--tpl/strings/truncate_test.go (renamed from tpl/tplimpl/template_func_truncate_test.go)11
-rw-r--r--tpl/time/time.go59
-rw-r--r--tpl/time/time_test.go67
-rw-r--r--tpl/tplimpl/reflect_helpers.go70
-rw-r--r--tpl/tplimpl/templateFuncster.go51
-rw-r--r--tpl/tplimpl/template_funcs.go2256
-rw-r--r--tpl/tplimpl/template_funcs_test.go2702
-rw-r--r--tpl/transform/transform.go119
-rw-r--r--tpl/transform/transform_test.go214
-rw-r--r--tpl/urls/urls.go108
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> &amp;tc!`, template.HTML(`Hello, <b>World</b> &amp;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>`, `{{ . }}`, `&lt;div&gt;&lt;/div&gt;`, `<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&#43;b&#43;c|Text:Text Name: a&#43;b&#43;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&#43;b&#43;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>`, `&#34;Foo &amp; Bar&#39;s Diner&#34; &lt;y@z&gt;`},
+ {"Hugo & Caddy > Wordpress & Apache", "Hugo &amp; Caddy &gt; Wordpress &amp; 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{}
+ }{
+ {`&quot;Foo &amp; Bar&#39;s Diner&quot; &lt;y@z&gt;`, `"Foo & Bar's Diner" <y@z>`},
+ {"Hugo &amp; Caddy &gt; Wordpress &amp; 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
+}