Files
hugo/tpl/collections/collections.go
T
Bjørn Erik Pedersen e3108225bf all: Run go fix ./...
2026-03-07 18:30:42 +01:00

764 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"context"
"errors"
"fmt"
"math/rand/v2"
"reflect"
"strings"
"time"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hreflect"
"github.com/gohugoio/hugo/common/hstore"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/tpl/compare"
"github.com/spf13/cast"
)
// New returns a new instance of the collections-namespaced template functions.
func New(deps *deps.Deps) *Namespace {
language := deps.Conf.Language().(*langs.Language)
if language == nil {
panic("language must be set")
}
loc := langs.GetLocation(language)
dCache := hmaps.NewCacheWithOptions[dKey, []int](hmaps.CacheOptions{Size: 100})
return &Namespace{
loc: loc,
sortComp: compare.New(loc, true),
dCache: dCache,
deps: deps,
}
}
// Namespace provides template functions for the "collections" namespace.
type Namespace struct {
loc *time.Location
sortComp *compare.Namespace
dCache *hmaps.Cache[dKey, []int]
deps *deps.Deps
}
// After returns all the items after the first n items in list l.
func (ns *Namespace) After(n any, l any) (any, error) {
if n == nil || l == nil {
return nil, errors.New("both limit and seq must be provided")
}
nv, err := cast.ToIntE(n)
if err != nil {
return nil, err
}
if nv < 0 {
return nil, errors.New("sequence bounds out of range [" + cast.ToString(nv) + ":]")
}
lv := reflect.ValueOf(l)
lv, isNil := hreflect.Indirect(lv)
if isNil {
return nil, errors.New("can't iterate over a nil value")
}
switch lv.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
// okay
default:
return nil, errors.New("can't iterate over " + reflect.ValueOf(l).Type().String())
}
if nv >= lv.Len() {
return lv.Slice(0, 0).Interface(), nil
}
return lv.Slice(nv, lv.Len()).Interface(), nil
}
// Delimit takes a given list l and returns a string delimited by sep.
// If last is passed to the function, it will be used as the final delimiter.
func (ns *Namespace) Delimit(ctx context.Context, l, sep any, last ...any) (string, error) {
d, err := cast.ToStringE(sep)
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
}
}
lv := reflect.ValueOf(l)
lv, isNil := hreflect.Indirect(lv)
if isNil {
return "", errors.New("can't iterate over a nil value")
}
var str strings.Builder
switch lv.Kind() {
case reflect.Map:
sortSeq, err := ns.Sort(ctx, l)
if err != nil {
return "", err
}
lv = reflect.ValueOf(sortSeq)
fallthrough
case reflect.Array, reflect.Slice, reflect.String:
for i := range lv.Len() {
val := lv.Index(i).Interface()
valStr, err := cast.ToStringE(val)
if err != nil {
continue
}
switch {
case i == lv.Len()-2 && dLast != nil:
str.WriteString(valStr + *dLast)
case i == lv.Len()-1:
str.WriteString(valStr)
default:
str.WriteString(valStr + d)
}
}
default:
return "", fmt.Errorf("can't iterate over %T", l)
}
return str.String(), nil
}
// Dictionary creates a new map from the given parameters by
// treating values as key-value pairs. The number of values must be even.
// The keys can be string slices, which will create the needed nested structure.
func (ns *Namespace) Dictionary(values ...any) (map[string]any, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dictionary call")
}
root := make(map[string]any)
for i := 0; i < len(values); i += 2 {
dict := root
var key string
switch v := values[i].(type) {
case string:
key = v
case []string:
for i := range len(v) - 1 {
key = v[i]
var m map[string]any
v, found := dict[key]
if found {
m = v.(map[string]any)
} else {
m = make(map[string]any)
dict[key] = m
}
dict = m
}
key = v[len(v)-1]
default:
return nil, errors.New("invalid dictionary key")
}
dict[key] = values[i+1]
}
return root, nil
}
// First returns the first limit items in list l.
func (ns *Namespace) First(limit any, l any) (any, error) {
if limit == nil || l == 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")
}
lv := reflect.ValueOf(l)
lv, isNil := hreflect.Indirect(lv)
if isNil {
return nil, errors.New("can't iterate over a nil value")
}
switch lv.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
// okay
default:
return nil, errors.New("can't iterate over " + reflect.ValueOf(l).Type().String())
}
if limitv > lv.Len() {
limitv = lv.Len()
}
return lv.Slice(0, limitv).Interface(), nil
}
// In returns whether v is in the list l. l may be an array or slice.
func (ns *Namespace) In(l any, v any) (bool, error) {
if l == nil || v == nil {
return false, nil
}
lv := reflect.ValueOf(l)
vv := reflect.ValueOf(v)
vvk := normalize(vv)
switch lv.Kind() {
case reflect.Array, reflect.Slice:
for i := range lv.Len() {
lvv, isNil := hreflect.Indirect(lv.Index(i))
if isNil {
continue
}
lvvk := normalize(lvv)
if lvvk == vvk {
return true, nil
}
}
}
ss, err := cast.ToStringE(l)
if err != nil {
return false, nil
}
su, err := cast.ToStringE(v)
if err != nil {
return false, nil
}
return strings.Contains(ss, su), 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 any) (any, error) {
if l1 == nil || l2 == nil {
return make([]any, 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[any]bool)}
switch l2v.Kind() {
case reflect.Array, reflect.Slice:
for i := range l1v.Len() {
l1vv := l1v.Index(i)
if !l1vv.Type().Comparable() {
return make([]any, 0), errors.New("intersect does not support slices or arrays of uncomparable types")
}
for j := range l2v.Len() {
l2vv := l2v.Index(j)
if !l2vv.Type().Comparable() {
return make([]any, 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 items by the given key.
// This is currently only supported for Pages.
func (ns *Namespace) Group(key any, items any) (any, 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 in c has the given key
// defined.
func (ns *Namespace) IsSet(c any, key any) (bool, error) {
av := reflect.ValueOf(c)
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:
ns.deps.Log.Warnf("calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), c)
}
return false, nil
}
// Last returns the last limit items in the list l.
func (ns *Namespace) Last(limit any, l any) (any, error) {
if limit == nil || l == 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(l)
seqv, isNil := hreflect.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(l).Type().String())
}
if limitv > seqv.Len() {
limitv = seqv.Len()
}
return seqv.Slice(seqv.Len()-limitv, seqv.Len()).Interface(), nil
}
// Reverse creates a copy of the list l and reverses it.
func (ns *Namespace) Reverse(l any) (any, error) {
if l == nil {
return nil, nil
}
v := reflect.ValueOf(l)
switch v.Kind() {
case reflect.Slice:
default:
return nil, errors.New("argument must be a slice")
}
sliceCopy := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
for i := v.Len() - 1; i >= 0; i-- {
element := sliceCopy.Index(i)
element.Set(v.Index(v.Len() - 1 - i))
}
return sliceCopy.Interface(), nil
}
// Sanity check for slices created by Seq and D.
const maxSeqSize = 1000000
var errSeqSizeExceedsLimit = errors.New("size of result exceeds limit")
// Seq creates a sequence of integers from args. 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 ...any) ([]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")
}
inc := 1
var last int
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 < -maxSeqSize {
return nil, errSeqSizeExceedsLimit
}
size := ((last - first) / inc) + 1
// sanity check
if size <= 0 || size > maxSeqSize {
return nil, errSeqSizeExceedsLimit
}
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 list l in a randomized order.
func (ns *Namespace) Shuffle(l any) (any, error) {
if l == nil {
return nil, errors.New("both count and seq must be provided")
}
lv := reflect.ValueOf(l)
lv, isNil := hreflect.Indirect(lv)
if isNil {
return nil, errors.New("can't iterate over a nil value")
}
switch lv.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
// okay
default:
return nil, errors.New("can't iterate over " + reflect.ValueOf(l).Type().String())
}
shuffled := reflect.MakeSlice(reflect.TypeOf(l), lv.Len(), lv.Len())
randomIndices := rand.Perm(lv.Len())
for index, value := range randomIndices {
shuffled.Index(value).Set(lv.Index(index))
}
return shuffled.Interface(), nil
}
// Slice returns a slice of all passed arguments.
func (ns *Namespace) Slice(args ...any) any {
if len(args) == 0 {
return args
}
return collections.Slice(args...)
}
type dKey struct {
seed uint64
n int
hi int
}
// D returns a sorted slice of unique random integers in the half-open interval
// [0, hi) using the provided seed value. The number of elements in the
// resulting slice is n or hi, whichever is less.
//
// If n <= hi, it returns a sorted random sample of size n using J. S. Vitters
// Method D for sequential random sampling.
//
// If n > hi, it returns the full, sorted range [0, hi) of size hi.
//
// If n == 0 or hi == 0, it returns an empty slice.
//
// Reference:
//
// J. S. Vitter, "An efficient algorithm for sequential random sampling," ACM Trans. Math. Softw., vol. 11, no. 1, pp. 3757, 1985.
// See also: https://getkerf.wordpress.com/2016/03/30/the-best-algorithm-no-one-knows-about/
func (ns *Namespace) D(seed, n, hi any) ([]int, error) {
seedInt, err := cast.ToInt64E(seed)
if err != nil || seedInt < 0 {
return nil, fmt.Errorf("the seed value (%v) must be a non-negative integer", seed)
}
nInt, err := cast.ToIntE(n)
if err != nil || nInt < 0 || nInt > maxSeqSize {
return nil, fmt.Errorf("the number of requested values (%v) must be a non-negative integer <= %d", n, maxSeqSize)
}
hiInt, err := cast.ToIntE(hi)
if err != nil || hiInt < 0 || hiInt > maxSeqSize {
return nil, fmt.Errorf("the maximum requested value (%v) must be a non-negative integer <= %d", hi, maxSeqSize)
}
if nInt == 0 || hiInt == 0 {
return []int{}, nil
}
key := dKey{seed: uint64(seedInt), n: nInt, hi: hiInt}
v, err := ns.dCache.GetOrCreate(key, func() ([]int, error) {
if key.n > key.hi {
result := make([]int, key.hi)
for i := 0; i < key.hi; i++ {
result[i] = i
}
return result, nil
}
prng := rand.New(rand.NewPCG(key.seed, 0))
result := make([]int, 0, key.n)
_d(prng, key.n, key.hi, func(i int) {
result = append(result, i)
})
return result, nil
})
return v, err
}
type intersector struct {
r reflect.Value
seen map[any]bool
}
func (i *intersector) appendIfNotSeen(v reflect.Value) {
k := normalize(v)
if !i.seen[k] {
i.r = reflect.Append(i.r, v)
i.seen[k] = true
}
}
func (i *intersector) handleValuePair(l1vv, l2vv reflect.Value) {
switch kind := l1vv.Kind(); {
case kind == reflect.String:
l2t, err := hreflect.ToStringE(l2vv)
if err == nil && l1vv.String() == l2t {
i.appendIfNotSeen(l1vv)
}
case hreflect.IsNumber(kind):
f1, err1 := hreflect.ToFloat64E(l1vv)
f2, err2 := hreflect.ToFloat64E(l2vv)
if err1 == nil && err2 == nil && f1 == f2 {
i.appendIfNotSeen(l1vv)
}
case kind == reflect.Pointer, kind == reflect.Struct:
if types.Unwrapv(l1vv.Interface()) == types.Unwrapv(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 any) (any, error) {
if l1 == nil && l2 == nil {
return []any{}, 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[any]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 := range l1v.Len() {
l1vv, isNil = hreflect.Indirect(l1v.Index(i))
if !l1vv.Type().Comparable() {
return []any{}, 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 := range l2v.Len() {
l2vv := l2v.Index(j)
typ := l1vv.Type()
switch kind := l1vv.Kind(); {
case kind == reflect.String:
l2t, err := hreflect.ToStringE(l2vv)
if err == nil {
ins.appendIfNotSeen(reflect.ValueOf(l2t))
}
case hreflect.IsNumber(kind):
var err error
l2vv, err = convertNumber(l2vv, typ)
if err == nil {
ins.appendIfNotSeen(l2vv)
}
case kind == reflect.Interface, kind == reflect.Struct, kind == reflect.Pointer:
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 returns a new list with duplicate elements in the list l removed.
func (ns *Namespace) Uniq(l any) (any, error) {
if l == nil {
return make([]any, 0), nil
}
v := reflect.ValueOf(l)
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, fmt.Errorf("type %T not supported", l)
}
seen := make(map[any]bool)
for i := range v.Len() {
ev, _ := hreflect.Indirect(v.Index(i))
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 any, values ...any) (types.KeyValues, error) {
return types.KeyValues{Key: key, Values: values}, nil
}
// NewScratch creates a new Scratch which can be used to store values in a
// thread safe way.
func (ns *Namespace) NewScratch() *hstore.Scratch {
return hstore.NewScratch()
}