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 }