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 }