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