diff options
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", ".") |