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