Skip to content

Commit

Permalink
tpl: Add template function namespaces
Browse files Browse the repository at this point in the history
This commit moves almost all of the template functions into separate
packages under tpl/ and adds a namespace framework.  All changes should
be backward compatible for end users, as all existing function names in
the template funcMap are left intact.

Seq and DoArithmatic have been moved out of the helpers package and into
template namespaces.

Most of the tests involved have been refactored, and many new tests have
been written.  There's still work to do, but this is a big improvement.

I got a little overzealous and added some new functions along the way:

- strings.Contains
- strings.ContainsAny
- strings.HasSuffix
- strings.TrimPrefix
- strings.TrimSuffix

Documentation is forthcoming.

Fixes gohugoio#3042
  • Loading branch information
moorereason committed Apr 30, 2017
1 parent 656f818 commit 687fb3d
Show file tree
Hide file tree
Showing 48 changed files with 7,089 additions and 5,288 deletions.
194 changes: 0 additions & 194 deletions helpers/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ import (
"bytes"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"path/filepath"
"reflect"
"strings"
"sync"
"unicode"
Expand Down Expand Up @@ -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 {
Expand Down
131 changes: 0 additions & 131 deletions helpers/general_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions hugolib/scratch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 687fb3d

Please sign in to comment.