hugo/tpl/collections/collections.go
Baibhav Vatsa 65b7d4221b tpl: Modify error messages of after, first, and last
Modified the messages functions after, first, and last threw on being passed invalid parameters (index or limit) to be more standardised and resemble what Go compiler would throw.

Fixes #6415
2019-10-12 18:07:46 +02:00

701 lines
17 KiB
Go

// Copyright 2019 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 provides template functions for manipulating collections
// such as arrays, maps, and slices.
package collections
import (
"fmt"
"html/template"
"math/rand"
"net/url"
"reflect"
"strings"
"time"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
"github.com/pkg/errors"
"github.com/spf13/cast"
)
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
// 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 {
deps *deps.Deps
}
// 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 < 0 {
return nil, errors.New("sequence bounds out of range [" + cast.ToString(indexv) + ":]")
}
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 seqv.Slice(0, 0).Interface(), nil
}
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
} else {
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 < 0 {
return nil, errors.New("sequence length must be non-negative")
}
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, error) {
if l == nil || v == nil {
return false, nil
}
lv := reflect.ValueOf(l)
vv := reflect.ValueOf(v)
if !vv.Type().Comparable() {
return false, errors.Errorf("value to check must be comparable: %T", v)
}
// Normalize numeric types to float64 etc.
vvk := normalize(vv)
switch lv.Kind() {
case reflect.Array, reflect.Slice:
for i := 0; i < lv.Len(); i++ {
lvv, isNil := indirectInterface(lv.Index(i))
if isNil || !lvv.Type().Comparable() {
continue
}
lvvk := normalize(lvv)
if lvvk == vvk {
return true, nil
}
}
case reflect.String:
if vv.Type() == lv.Type() && strings.Contains(lv.String(), vv.String()) {
return true, nil
}
}
return false, 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 (ns *Namespace) Intersect(l1, l2 interface{}) (interface{}, error) {
if l1 == nil || l2 == nil {
return make([]interface{}, 0), nil
}
var ins *intersector
l1v := reflect.ValueOf(l1)
l2v := reflect.ValueOf(l2)
switch l1v.Kind() {
case reflect.Array, reflect.Slice:
ins = &intersector{r: reflect.MakeSlice(l1v.Type(), 0, 0), seen: make(map[interface{}]bool)}
switch l2v.Kind() {
case reflect.Array, reflect.Slice:
for i := 0; i < l1v.Len(); i++ {
l1vv := l1v.Index(i)
if !l1vv.Type().Comparable() {
return make([]interface{}, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
}
for j := 0; j < l2v.Len(); j++ {
l2vv := l2v.Index(j)
if !l2vv.Type().Comparable() {
return make([]interface{}, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
}
ins.handleValuePair(l1vv, l2vv)
}
}
return ins.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())
}
}
// Group groups a set of elements by the given key.
// This is currently only supported for Pages.
func (ns *Namespace) Group(key interface{}, items interface{}) (interface{}, error) {
if key == nil {
return nil, errors.New("nil is not a valid key to group by")
}
if g, ok := items.(collections.Grouper); ok {
return g.Group(key, items)
}
in := newSliceElement(items)
if g, ok := in.(collections.Grouper); ok {
return g.Group(key, items)
}
return nil, fmt.Errorf("grouping not supported for type %T %T", items, in)
}
// 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:
k, err := cast.ToIntE(key)
if err != nil {
return false, fmt.Errorf("isset unable to use key of type %T as index", key)
}
if av.Len() > k {
return true, nil
}
case reflect.Map:
if kv.Type() == av.Type().Key() {
return av.MapIndex(kv).IsValid(), nil
}
default:
helpers.DistinctFeedbackLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), a)
}
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 < 0 {
return nil, errors.New("sequence length must be non-negative")
}
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())
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{} {
if len(args) == 0 {
return args
}
return collections.Slice(args...)
}
type intersector struct {
r reflect.Value
seen map[interface{}]bool
}
func (i *intersector) appendIfNotSeen(v reflect.Value) {
vi := v.Interface()
if !i.seen[vi] {
i.r = reflect.Append(i.r, v)
i.seen[vi] = true
}
}
func (i *intersector) handleValuePair(l1vv, l2vv reflect.Value) {
switch kind := l1vv.Kind(); {
case kind == reflect.String:
l2t, err := toString(l2vv)
if err == nil && l1vv.String() == l2t {
i.appendIfNotSeen(l1vv)
}
case isNumber(kind):
f1, err1 := numberToFloat(l1vv)
f2, err2 := numberToFloat(l2vv)
if err1 == nil && err2 == nil && f1 == f2 {
i.appendIfNotSeen(l1vv)
}
case kind == reflect.Ptr, kind == reflect.Struct:
if l1vv.Interface() == l2vv.Interface() {
i.appendIfNotSeen(l1vv)
}
case kind == reflect.Interface:
i.handleValuePair(reflect.ValueOf(l1vv.Interface()), l2vv)
}
}
// 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 []interface{}{}, nil
} 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)
var ins *intersector
switch l1v.Kind() {
case reflect.Array, reflect.Slice:
switch l2v.Kind() {
case reflect.Array, reflect.Slice:
ins = &intersector{r: reflect.MakeSlice(l1v.Type(), 0, 0), seen: make(map[interface{}]bool)}
if l1v.Type() != l2v.Type() &&
l1v.Type().Elem().Kind() != reflect.Interface &&
l2v.Type().Elem().Kind() != reflect.Interface {
return ins.r.Interface(), nil
}
var (
l1vv reflect.Value
isNil bool
)
for i := 0; i < l1v.Len(); i++ {
l1vv, isNil = indirectInterface(l1v.Index(i))
if !l1vv.Type().Comparable() {
return []interface{}{}, errors.New("union does not support slices or arrays of uncomparable types")
}
if !isNil {
ins.appendIfNotSeen(l1vv)
}
}
if !l1vv.IsValid() {
// The first slice may be empty. Pick the first value of the second
// to use as a prototype.
if l2v.Len() > 0 {
l1vv = l2v.Index(0)
}
}
for j := 0; j < l2v.Len(); j++ {
l2vv := l2v.Index(j)
switch kind := l1vv.Kind(); {
case kind == reflect.String:
l2t, err := toString(l2vv)
if err == nil {
ins.appendIfNotSeen(reflect.ValueOf(l2t))
}
case isNumber(kind):
var err error
l2vv, err = convertNumber(l2vv, kind)
if err == nil {
ins.appendIfNotSeen(l2vv)
}
case kind == reflect.Interface, kind == reflect.Struct, kind == reflect.Ptr:
ins.appendIfNotSeen(l2vv)
}
}
return ins.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())
}
}
// Uniq takes in a slice or array and returns a slice with subsequent
// duplicate elements removed.
func (ns *Namespace) Uniq(seq interface{}) (interface{}, error) {
if seq == nil {
return make([]interface{}, 0), nil
}
v := reflect.ValueOf(seq)
var slice reflect.Value
switch v.Kind() {
case reflect.Slice:
slice = reflect.MakeSlice(v.Type(), 0, 0)
case reflect.Array:
slice = reflect.MakeSlice(reflect.SliceOf(v.Type().Elem()), 0, 0)
default:
return nil, errors.Errorf("type %T not supported", seq)
}
seen := make(map[interface{}]bool)
for i := 0; i < v.Len(); i++ {
ev, _ := indirectInterface(v.Index(i))
if !ev.Type().Comparable() {
return nil, errors.New("elements must be comparable")
}
key := normalize(ev)
if _, found := seen[key]; !found {
slice = reflect.Append(slice, ev)
seen[key] = true
}
}
return slice.Interface(), nil
}
// KeyVals creates a key and values wrapper.
func (ns *Namespace) KeyVals(key interface{}, vals ...interface{}) (types.KeyValues, error) {
return types.KeyValues{Key: key, Values: vals}, nil
}
// NewScratch creates a new Scratch which can be used to store values in a
// thread safe way.
func (ns *Namespace) NewScratch() *maps.Scratch {
return maps.NewScratch()
}