github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/page/pagegroup.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package page
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"reflect"
    20  	"sort"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/spf13/cast"
    25  
    26  	"github.com/gohugoio/hugo/common/collections"
    27  	"github.com/gohugoio/hugo/compare"
    28  
    29  	"github.com/gohugoio/hugo/resources/resource"
    30  )
    31  
    32  var (
    33  	_ collections.Slicer   = PageGroup{}
    34  	_ compare.ProbablyEqer = PageGroup{}
    35  	_ compare.ProbablyEqer = PagesGroup{}
    36  )
    37  
    38  // PageGroup represents a group of pages, grouped by the key.
    39  // The key is typically a year or similar.
    40  type PageGroup struct {
    41  	Key interface{}
    42  	Pages
    43  }
    44  
    45  type mapKeyValues []reflect.Value
    46  
    47  func (v mapKeyValues) Len() int      { return len(v) }
    48  func (v mapKeyValues) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
    49  
    50  type mapKeyByInt struct{ mapKeyValues }
    51  
    52  func (s mapKeyByInt) Less(i, j int) bool { return s.mapKeyValues[i].Int() < s.mapKeyValues[j].Int() }
    53  
    54  type mapKeyByStr struct{ mapKeyValues }
    55  
    56  func (s mapKeyByStr) Less(i, j int) bool {
    57  	return compare.LessStrings(s.mapKeyValues[i].String(), s.mapKeyValues[j].String())
    58  }
    59  
    60  func sortKeys(v []reflect.Value, order string) []reflect.Value {
    61  	if len(v) <= 1 {
    62  		return v
    63  	}
    64  
    65  	switch v[0].Kind() {
    66  	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    67  		if order == "desc" {
    68  			sort.Sort(sort.Reverse(mapKeyByInt{v}))
    69  		} else {
    70  			sort.Sort(mapKeyByInt{v})
    71  		}
    72  	case reflect.String:
    73  		if order == "desc" {
    74  			sort.Sort(sort.Reverse(mapKeyByStr{v}))
    75  		} else {
    76  			sort.Sort(mapKeyByStr{v})
    77  		}
    78  	}
    79  	return v
    80  }
    81  
    82  // PagesGroup represents a list of page groups.
    83  // This is what you get when doing page grouping in the templates.
    84  type PagesGroup []PageGroup
    85  
    86  // Reverse reverses the order of this list of page groups.
    87  func (p PagesGroup) Reverse() PagesGroup {
    88  	for i, j := 0, len(p)-1; i < j; i, j = i+1, j-1 {
    89  		p[i], p[j] = p[j], p[i]
    90  	}
    91  
    92  	return p
    93  }
    94  
    95  var (
    96  	errorType   = reflect.TypeOf((*error)(nil)).Elem()
    97  	pagePtrType = reflect.TypeOf((*Page)(nil)).Elem()
    98  	pagesType   = reflect.TypeOf(Pages{})
    99  )
   100  
   101  // GroupBy groups by the value in the given field or method name and with the given order.
   102  // Valid values for order is asc, desc, rev and reverse.
   103  func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
   104  	if len(p) < 1 {
   105  		return nil, nil
   106  	}
   107  
   108  	direction := "asc"
   109  
   110  	if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") {
   111  		direction = "desc"
   112  	}
   113  
   114  	var ft interface{}
   115  	m, ok := pagePtrType.MethodByName(key)
   116  	if ok {
   117  		if m.Type.NumOut() == 0 || m.Type.NumOut() > 2 {
   118  			return nil, errors.New(key + " is a Page method but you can't use it with GroupBy")
   119  		}
   120  		if m.Type.NumOut() == 1 && m.Type.Out(0).Implements(errorType) {
   121  			return nil, errors.New(key + " is a Page method but you can't use it with GroupBy")
   122  		}
   123  		if m.Type.NumOut() == 2 && !m.Type.Out(1).Implements(errorType) {
   124  			return nil, errors.New(key + " is a Page method but you can't use it with GroupBy")
   125  		}
   126  		ft = m
   127  	} else {
   128  		ft, ok = pagePtrType.Elem().FieldByName(key)
   129  		if !ok {
   130  			return nil, errors.New(key + " is neither a field nor a method of Page")
   131  		}
   132  	}
   133  
   134  	var tmp reflect.Value
   135  	switch e := ft.(type) {
   136  	case reflect.StructField:
   137  		tmp = reflect.MakeMap(reflect.MapOf(e.Type, pagesType))
   138  	case reflect.Method:
   139  		tmp = reflect.MakeMap(reflect.MapOf(e.Type.Out(0), pagesType))
   140  	}
   141  
   142  	for _, e := range p {
   143  		ppv := reflect.ValueOf(e)
   144  		var fv reflect.Value
   145  		switch ft.(type) {
   146  		case reflect.StructField:
   147  			fv = ppv.Elem().FieldByName(key)
   148  		case reflect.Method:
   149  			fv = ppv.MethodByName(key).Call([]reflect.Value{})[0]
   150  		}
   151  		if !fv.IsValid() {
   152  			continue
   153  		}
   154  		if !tmp.MapIndex(fv).IsValid() {
   155  			tmp.SetMapIndex(fv, reflect.MakeSlice(pagesType, 0, 0))
   156  		}
   157  		tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv))
   158  	}
   159  
   160  	sortedKeys := sortKeys(tmp.MapKeys(), direction)
   161  	r := make([]PageGroup, len(sortedKeys))
   162  	for i, k := range sortedKeys {
   163  		r[i] = PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)}
   164  	}
   165  
   166  	return r, nil
   167  }
   168  
   169  // GroupByParam groups by the given page parameter key's value and with the given order.
   170  // Valid values for order is asc, desc, rev and reverse.
   171  func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) {
   172  	if len(p) < 1 {
   173  		return nil, nil
   174  	}
   175  
   176  	direction := "asc"
   177  
   178  	if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") {
   179  		direction = "desc"
   180  	}
   181  
   182  	var tmp reflect.Value
   183  	var keyt reflect.Type
   184  	for _, e := range p {
   185  		param := resource.GetParamToLower(e, key)
   186  		if param != nil {
   187  			if _, ok := param.([]string); !ok {
   188  				keyt = reflect.TypeOf(param)
   189  				tmp = reflect.MakeMap(reflect.MapOf(keyt, pagesType))
   190  				break
   191  			}
   192  		}
   193  	}
   194  	if !tmp.IsValid() {
   195  		return nil, errors.New("there is no such a param")
   196  	}
   197  
   198  	for _, e := range p {
   199  		param := resource.GetParam(e, key)
   200  
   201  		if param == nil || reflect.TypeOf(param) != keyt {
   202  			continue
   203  		}
   204  		v := reflect.ValueOf(param)
   205  		if !tmp.MapIndex(v).IsValid() {
   206  			tmp.SetMapIndex(v, reflect.MakeSlice(pagesType, 0, 0))
   207  		}
   208  		tmp.SetMapIndex(v, reflect.Append(tmp.MapIndex(v), reflect.ValueOf(e)))
   209  	}
   210  
   211  	var r []PageGroup
   212  	for _, k := range sortKeys(tmp.MapKeys(), direction) {
   213  		r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)})
   214  	}
   215  
   216  	return r, nil
   217  }
   218  
   219  func (p Pages) groupByDateField(sorter func(p Pages) Pages, formatter func(p Page) string, order ...string) (PagesGroup, error) {
   220  	if len(p) < 1 {
   221  		return nil, nil
   222  	}
   223  
   224  	sp := sorter(p)
   225  
   226  	if !(len(order) > 0 && (strings.ToLower(order[0]) == "asc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse")) {
   227  		sp = sp.Reverse()
   228  	}
   229  
   230  	if sp == nil {
   231  		return nil, nil
   232  	}
   233  
   234  	date := formatter(sp[0].(Page))
   235  	var r []PageGroup
   236  	r = append(r, PageGroup{Key: date, Pages: make(Pages, 0)})
   237  	r[0].Pages = append(r[0].Pages, sp[0])
   238  
   239  	i := 0
   240  	for _, e := range sp[1:] {
   241  		date = formatter(e.(Page))
   242  		if r[i].Key.(string) != date {
   243  			r = append(r, PageGroup{Key: date})
   244  			i++
   245  		}
   246  		r[i].Pages = append(r[i].Pages, e)
   247  	}
   248  	return r, nil
   249  }
   250  
   251  // GroupByDate groups by the given page's Date value in
   252  // the given format and with the given order.
   253  // Valid values for order is asc, desc, rev and reverse.
   254  // For valid format strings, see https://golang.org/pkg/time/#Time.Format
   255  func (p Pages) GroupByDate(format string, order ...string) (PagesGroup, error) {
   256  	sorter := func(p Pages) Pages {
   257  		return p.ByDate()
   258  	}
   259  	formatter := func(p Page) string {
   260  		return p.Date().Format(format)
   261  	}
   262  	return p.groupByDateField(sorter, formatter, order...)
   263  }
   264  
   265  // GroupByPublishDate groups by the given page's PublishDate value in
   266  // the given format and with the given order.
   267  // Valid values for order is asc, desc, rev and reverse.
   268  // For valid format strings, see https://golang.org/pkg/time/#Time.Format
   269  func (p Pages) GroupByPublishDate(format string, order ...string) (PagesGroup, error) {
   270  	sorter := func(p Pages) Pages {
   271  		return p.ByPublishDate()
   272  	}
   273  	formatter := func(p Page) string {
   274  		return p.PublishDate().Format(format)
   275  	}
   276  	return p.groupByDateField(sorter, formatter, order...)
   277  }
   278  
   279  // GroupByExpiryDate groups by the given page's ExpireDate value in
   280  // the given format and with the given order.
   281  // Valid values for order is asc, desc, rev and reverse.
   282  // For valid format strings, see https://golang.org/pkg/time/#Time.Format
   283  func (p Pages) GroupByExpiryDate(format string, order ...string) (PagesGroup, error) {
   284  	sorter := func(p Pages) Pages {
   285  		return p.ByExpiryDate()
   286  	}
   287  	formatter := func(p Page) string {
   288  		return p.ExpiryDate().Format(format)
   289  	}
   290  	return p.groupByDateField(sorter, formatter, order...)
   291  }
   292  
   293  // GroupByLastmod groups by the given page's Lastmod value in
   294  // the given format and with the given order.
   295  // Valid values for order is asc, desc, rev and reverse.
   296  // For valid format strings, see https://golang.org/pkg/time/#Time.Format
   297  func (p Pages) GroupByLastmod(format string, order ...string) (PagesGroup, error) {
   298  	sorter := func(p Pages) Pages {
   299  		return p.ByLastmod()
   300  	}
   301  	formatter := func(p Page) string {
   302  		return p.Lastmod().Format(format)
   303  	}
   304  	return p.groupByDateField(sorter, formatter, order...)
   305  }
   306  
   307  // GroupByParamDate groups by a date set as a param on the page in
   308  // the given format and with the given order.
   309  // Valid values for order is asc, desc, rev and reverse.
   310  // For valid format strings, see https://golang.org/pkg/time/#Time.Format
   311  func (p Pages) GroupByParamDate(key string, format string, order ...string) (PagesGroup, error) {
   312  	// Cache the dates.
   313  	dates := make(map[Page]time.Time)
   314  
   315  	sorter := func(pages Pages) Pages {
   316  		var r Pages
   317  
   318  		for _, p := range pages {
   319  			param := resource.GetParam(p, key)
   320  			var t time.Time
   321  
   322  			if param != nil {
   323  				var ok bool
   324  				if t, ok = param.(time.Time); !ok {
   325  					// Probably a string. Try to convert it to time.Time.
   326  					t = cast.ToTime(param)
   327  				}
   328  			}
   329  
   330  			dates[p] = t
   331  			r = append(r, p)
   332  		}
   333  
   334  		pdate := func(p1, p2 Page) bool {
   335  			return dates[p1].Unix() < dates[p2].Unix()
   336  		}
   337  		pageBy(pdate).Sort(r)
   338  		return r
   339  	}
   340  	formatter := func(p Page) string {
   341  		return dates[p].Format(format)
   342  	}
   343  	return p.groupByDateField(sorter, formatter, order...)
   344  }
   345  
   346  // ProbablyEq wraps compare.ProbablyEqer
   347  func (p PageGroup) ProbablyEq(other interface{}) bool {
   348  	otherP, ok := other.(PageGroup)
   349  	if !ok {
   350  		return false
   351  	}
   352  
   353  	if p.Key != otherP.Key {
   354  		return false
   355  	}
   356  
   357  	return p.Pages.ProbablyEq(otherP.Pages)
   358  }
   359  
   360  // Slice is not meant to be used externally. It's a bridge function
   361  // for the template functions. See collections.Slice.
   362  func (p PageGroup) Slice(in interface{}) (interface{}, error) {
   363  	switch items := in.(type) {
   364  	case PageGroup:
   365  		return items, nil
   366  	case []interface{}:
   367  		groups := make(PagesGroup, len(items))
   368  		for i, v := range items {
   369  			g, ok := v.(PageGroup)
   370  			if !ok {
   371  				return nil, fmt.Errorf("type %T is not a PageGroup", v)
   372  			}
   373  			groups[i] = g
   374  		}
   375  		return groups, nil
   376  	default:
   377  		return nil, fmt.Errorf("invalid slice type %T", items)
   378  	}
   379  }
   380  
   381  // Len returns the number of pages in the page group.
   382  func (psg PagesGroup) Len() int {
   383  	l := 0
   384  	for _, pg := range psg {
   385  		l += len(pg.Pages)
   386  	}
   387  	return l
   388  }
   389  
   390  // ProbablyEq wraps compare.ProbablyEqer
   391  func (psg PagesGroup) ProbablyEq(other interface{}) bool {
   392  	otherPsg, ok := other.(PagesGroup)
   393  	if !ok {
   394  		return false
   395  	}
   396  
   397  	if len(psg) != len(otherPsg) {
   398  		return false
   399  	}
   400  
   401  	for i := range psg {
   402  		if !psg[i].ProbablyEq(otherPsg[i]) {
   403  			return false
   404  		}
   405  	}
   406  
   407  	return true
   408  }
   409  
   410  // ToPagesGroup tries to convert seq into a PagesGroup.
   411  func ToPagesGroup(seq interface{}) (PagesGroup, error) {
   412  	switch v := seq.(type) {
   413  	case nil:
   414  		return nil, nil
   415  	case PagesGroup:
   416  		return v, nil
   417  	case []PageGroup:
   418  		return PagesGroup(v), nil
   419  	case []interface{}:
   420  		l := len(v)
   421  		if l == 0 {
   422  			break
   423  		}
   424  		switch v[0].(type) {
   425  		case PageGroup:
   426  			pagesGroup := make(PagesGroup, l)
   427  			for i, ipg := range v {
   428  				if pg, ok := ipg.(PageGroup); ok {
   429  					pagesGroup[i] = pg
   430  				} else {
   431  					return nil, fmt.Errorf("unsupported type in paginate from slice, got %T instead of PageGroup", ipg)
   432  				}
   433  			}
   434  			return pagesGroup, nil
   435  		}
   436  	}
   437  
   438  	return nil, nil
   439  }