mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	This commit expands the Resource interface with 3 new methods: * Name * Title * Params All of these can be set in the Page front matter. `Name` will get its default value from the base filename, and is the value used in the ByPrefix and GetByPrefix lookup methods. Fixes #4244
		
			
				
	
	
		
			546 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			546 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 The Hugo Authors. All rights reserved.
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| // http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package hugolib
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"html/template"
 | |
| 	"math"
 | |
| 	"reflect"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/gohugoio/hugo/config"
 | |
| 
 | |
| 	"github.com/spf13/cast"
 | |
| )
 | |
| 
 | |
| // Pager represents one of the elements in a paginator.
 | |
| // The number, starting on 1, represents its place.
 | |
| type Pager struct {
 | |
| 	number int
 | |
| 	*paginator
 | |
| }
 | |
| 
 | |
| func (p Pager) String() string {
 | |
| 	return fmt.Sprintf("Pager %d", p.number)
 | |
| }
 | |
| 
 | |
| type paginatedElement interface {
 | |
| 	Len() int
 | |
| }
 | |
| 
 | |
| // Len returns the number of pages in the list.
 | |
| func (p Pages) Len() int {
 | |
| 	return len(p)
 | |
| }
 | |
| 
 | |
| // Len returns the number of pages in the page group.
 | |
| func (psg PagesGroup) Len() int {
 | |
| 	l := 0
 | |
| 	for _, pg := range psg {
 | |
| 		l += len(pg.Pages)
 | |
| 	}
 | |
| 	return l
 | |
| }
 | |
| 
 | |
| type pagers []*Pager
 | |
| 
 | |
| var (
 | |
| 	paginatorEmptyPages      Pages
 | |
| 	paginatorEmptyPageGroups PagesGroup
 | |
| )
 | |
| 
 | |
| type paginator struct {
 | |
| 	paginatedElements []paginatedElement
 | |
| 	pagers
 | |
| 	paginationURLFactory
 | |
| 	total   int
 | |
| 	size    int
 | |
| 	source  interface{}
 | |
| 	options []interface{}
 | |
| }
 | |
| 
 | |
| type paginationURLFactory func(int) string
 | |
| 
 | |
| // PageNumber returns the current page's number in the pager sequence.
 | |
| func (p *Pager) PageNumber() int {
 | |
| 	return p.number
 | |
| }
 | |
| 
 | |
| // URL returns the URL to the current page.
 | |
| func (p *Pager) URL() template.HTML {
 | |
| 	return template.HTML(p.paginationURLFactory(p.PageNumber()))
 | |
| }
 | |
| 
 | |
| // Pages returns the Pages on this page.
 | |
| // Note: If this return a non-empty result, then PageGroups() will return empty.
 | |
| func (p *Pager) Pages() Pages {
 | |
| 	if len(p.paginatedElements) == 0 {
 | |
| 		return paginatorEmptyPages
 | |
| 	}
 | |
| 
 | |
| 	if pages, ok := p.element().(Pages); ok {
 | |
| 		return pages
 | |
| 	}
 | |
| 
 | |
| 	return paginatorEmptyPages
 | |
| }
 | |
| 
 | |
| // PageGroups return Page groups for this page.
 | |
| // Note: If this return non-empty result, then Pages() will return empty.
 | |
| func (p *Pager) PageGroups() PagesGroup {
 | |
| 	if len(p.paginatedElements) == 0 {
 | |
| 		return paginatorEmptyPageGroups
 | |
| 	}
 | |
| 
 | |
| 	if groups, ok := p.element().(PagesGroup); ok {
 | |
| 		return groups
 | |
| 	}
 | |
| 
 | |
| 	return paginatorEmptyPageGroups
 | |
| }
 | |
| 
 | |
| func (p *Pager) element() paginatedElement {
 | |
| 	if len(p.paginatedElements) == 0 {
 | |
| 		return paginatorEmptyPages
 | |
| 	}
 | |
| 	return p.paginatedElements[p.PageNumber()-1]
 | |
| }
 | |
| 
 | |
| // page returns the Page with the given index
 | |
| func (p *Pager) page(index int) (*Page, error) {
 | |
| 
 | |
| 	if pages, ok := p.element().(Pages); ok {
 | |
| 		if pages != nil && len(pages) > index {
 | |
| 			return pages[index], nil
 | |
| 		}
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	// must be PagesGroup
 | |
| 	// this construction looks clumsy, but ...
 | |
| 	// ... it is the difference between 99.5% and 100% test coverage :-)
 | |
| 	groups := p.element().(PagesGroup)
 | |
| 
 | |
| 	i := 0
 | |
| 	for _, v := range groups {
 | |
| 		for _, page := range v.Pages {
 | |
| 			if i == index {
 | |
| 				return page, nil
 | |
| 			}
 | |
| 			i++
 | |
| 		}
 | |
| 	}
 | |
| 	return nil, nil
 | |
| }
 | |
| 
 | |
| // NumberOfElements gets the number of elements on this page.
 | |
| func (p *Pager) NumberOfElements() int {
 | |
| 	return p.element().Len()
 | |
| }
 | |
| 
 | |
| // HasPrev tests whether there are page(s) before the current.
 | |
| func (p *Pager) HasPrev() bool {
 | |
| 	return p.PageNumber() > 1
 | |
| }
 | |
| 
 | |
| // Prev returns the pager for the previous page.
 | |
| func (p *Pager) Prev() *Pager {
 | |
| 	if !p.HasPrev() {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return p.pagers[p.PageNumber()-2]
 | |
| }
 | |
| 
 | |
| // HasNext tests whether there are page(s) after the current.
 | |
| func (p *Pager) HasNext() bool {
 | |
| 	return p.PageNumber() < len(p.paginatedElements)
 | |
| }
 | |
| 
 | |
| // Next returns the pager for the next page.
 | |
| func (p *Pager) Next() *Pager {
 | |
| 	if !p.HasNext() {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return p.pagers[p.PageNumber()]
 | |
| }
 | |
| 
 | |
| // First returns the pager for the first page.
 | |
| func (p *Pager) First() *Pager {
 | |
| 	return p.pagers[0]
 | |
| }
 | |
| 
 | |
| // Last returns the pager for the last page.
 | |
| func (p *Pager) Last() *Pager {
 | |
| 	return p.pagers[len(p.pagers)-1]
 | |
| }
 | |
| 
 | |
| // Pagers returns a list of pagers that can be used to build a pagination menu.
 | |
| func (p *paginator) Pagers() pagers {
 | |
| 	return p.pagers
 | |
| }
 | |
| 
 | |
| // PageSize returns the size of each paginator page.
 | |
| func (p *paginator) PageSize() int {
 | |
| 	return p.size
 | |
| }
 | |
| 
 | |
| // TotalPages returns the number of pages in the paginator.
 | |
| func (p *paginator) TotalPages() int {
 | |
| 	return len(p.paginatedElements)
 | |
| }
 | |
| 
 | |
| // TotalNumberOfElements returns the number of elements on all pages in this paginator.
 | |
| func (p *paginator) TotalNumberOfElements() int {
 | |
| 	return p.total
 | |
| }
 | |
| 
 | |
| func splitPages(pages Pages, size int) []paginatedElement {
 | |
| 	var split []paginatedElement
 | |
| 	for low, j := 0, len(pages); low < j; low += size {
 | |
| 		high := int(math.Min(float64(low+size), float64(len(pages))))
 | |
| 		split = append(split, pages[low:high])
 | |
| 	}
 | |
| 
 | |
| 	return split
 | |
| }
 | |
| 
 | |
| func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement {
 | |
| 
 | |
| 	type keyPage struct {
 | |
| 		key  interface{}
 | |
| 		page *Page
 | |
| 	}
 | |
| 
 | |
| 	var (
 | |
| 		split     []paginatedElement
 | |
| 		flattened []keyPage
 | |
| 	)
 | |
| 
 | |
| 	for _, g := range pageGroups {
 | |
| 		for _, p := range g.Pages {
 | |
| 			flattened = append(flattened, keyPage{g.Key, p})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	numPages := len(flattened)
 | |
| 
 | |
| 	for low, j := 0, numPages; low < j; low += size {
 | |
| 		high := int(math.Min(float64(low+size), float64(numPages)))
 | |
| 
 | |
| 		var (
 | |
| 			pg         PagesGroup
 | |
| 			key        interface{}
 | |
| 			groupIndex = -1
 | |
| 		)
 | |
| 
 | |
| 		for k := low; k < high; k++ {
 | |
| 			kp := flattened[k]
 | |
| 			if key == nil || key != kp.key {
 | |
| 				key = kp.key
 | |
| 				pg = append(pg, PageGroup{Key: key})
 | |
| 				groupIndex++
 | |
| 			}
 | |
| 			pg[groupIndex].Pages = append(pg[groupIndex].Pages, kp.page)
 | |
| 		}
 | |
| 		split = append(split, pg)
 | |
| 	}
 | |
| 
 | |
| 	return split
 | |
| }
 | |
| 
 | |
| // Paginator get this Page's main output's paginator.
 | |
| func (p *Page) Paginator(options ...interface{}) (*Pager, error) {
 | |
| 	return p.mainPageOutput.Paginator(options...)
 | |
| }
 | |
| 
 | |
| // Paginator gets this PageOutput's paginator if it's already created.
 | |
| // If it's not, one will be created with all pages in Data["Pages"].
 | |
| func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) {
 | |
| 	if !p.IsNode() {
 | |
| 		return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.title)
 | |
| 	}
 | |
| 	pagerSize, err := resolvePagerSize(p.s.Cfg, options...)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var initError error
 | |
| 
 | |
| 	p.paginatorInit.Do(func() {
 | |
| 		if p.paginator != nil {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		pathDescriptor := p.targetPathDescriptor
 | |
| 		if p.s.owner.IsMultihost() {
 | |
| 			pathDescriptor.LangPrefix = ""
 | |
| 		}
 | |
| 		pagers, err := paginatePages(pathDescriptor, p.Data["Pages"], pagerSize)
 | |
| 
 | |
| 		if err != nil {
 | |
| 			initError = err
 | |
| 		}
 | |
| 
 | |
| 		if len(pagers) > 0 {
 | |
| 			// the rest of the nodes will be created later
 | |
| 			p.paginator = pagers[0]
 | |
| 			p.paginator.source = "paginator"
 | |
| 			p.paginator.options = options
 | |
| 		}
 | |
| 
 | |
| 	})
 | |
| 
 | |
| 	if initError != nil {
 | |
| 		return nil, initError
 | |
| 	}
 | |
| 
 | |
| 	return p.paginator, nil
 | |
| }
 | |
| 
 | |
| // Paginate invokes this Page's main output's Paginate method.
 | |
| func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
 | |
| 	return p.mainPageOutput.Paginate(seq, options...)
 | |
| }
 | |
| 
 | |
| // Paginate gets this PageOutput's paginator if it's already created.
 | |
| // If it's not, one will be created with the qiven sequence.
 | |
| // Note that repeated calls will return the same result, even if the sequence is different.
 | |
| func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
 | |
| 	if !p.IsNode() {
 | |
| 		return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.title)
 | |
| 	}
 | |
| 
 | |
| 	pagerSize, err := resolvePagerSize(p.s.Cfg, options...)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var initError error
 | |
| 
 | |
| 	p.paginatorInit.Do(func() {
 | |
| 		if p.paginator != nil {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		pathDescriptor := p.targetPathDescriptor
 | |
| 		if p.s.owner.IsMultihost() {
 | |
| 			pathDescriptor.LangPrefix = ""
 | |
| 		}
 | |
| 		pagers, err := paginatePages(pathDescriptor, seq, pagerSize)
 | |
| 
 | |
| 		if err != nil {
 | |
| 			initError = err
 | |
| 		}
 | |
| 
 | |
| 		if len(pagers) > 0 {
 | |
| 			// the rest of the nodes will be created later
 | |
| 			p.paginator = pagers[0]
 | |
| 			p.paginator.source = seq
 | |
| 			p.paginator.options = options
 | |
| 		}
 | |
| 
 | |
| 	})
 | |
| 
 | |
| 	if initError != nil {
 | |
| 		return nil, initError
 | |
| 	}
 | |
| 
 | |
| 	if p.paginator.source == "paginator" {
 | |
| 		return nil, errors.New("a Paginator was previously built for this Node without filters; look for earlier .Paginator usage")
 | |
| 	}
 | |
| 
 | |
| 	if !reflect.DeepEqual(options, p.paginator.options) || !probablyEqualPageLists(p.paginator.source, seq) {
 | |
| 		return nil, errors.New("invoked multiple times with different arguments")
 | |
| 	}
 | |
| 
 | |
| 	return p.paginator, nil
 | |
| }
 | |
| 
 | |
| func resolvePagerSize(cfg config.Provider, options ...interface{}) (int, error) {
 | |
| 	if len(options) == 0 {
 | |
| 		return cfg.GetInt("paginate"), nil
 | |
| 	}
 | |
| 
 | |
| 	if len(options) > 1 {
 | |
| 		return -1, errors.New("too many arguments, 'pager size' is currently the only option")
 | |
| 	}
 | |
| 
 | |
| 	pas, err := cast.ToIntE(options[0])
 | |
| 
 | |
| 	if err != nil || pas <= 0 {
 | |
| 		return -1, errors.New(("'pager size' must be a positive integer"))
 | |
| 	}
 | |
| 
 | |
| 	return pas, nil
 | |
| }
 | |
| 
 | |
| func paginatePages(td targetPathDescriptor, seq interface{}, pagerSize int) (pagers, error) {
 | |
| 
 | |
| 	if pagerSize <= 0 {
 | |
| 		return nil, errors.New("'paginate' configuration setting must be positive to paginate")
 | |
| 	}
 | |
| 
 | |
| 	urlFactory := newPaginationURLFactory(td)
 | |
| 
 | |
| 	var paginator *paginator
 | |
| 
 | |
| 	if groups, ok := seq.(PagesGroup); ok {
 | |
| 		paginator, _ = newPaginatorFromPageGroups(groups, pagerSize, urlFactory)
 | |
| 	} else {
 | |
| 		pages, err := toPages(seq)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		paginator, _ = newPaginatorFromPages(pages, pagerSize, urlFactory)
 | |
| 	}
 | |
| 
 | |
| 	pagers := paginator.Pagers()
 | |
| 
 | |
| 	return pagers, nil
 | |
| }
 | |
| 
 | |
| func toPages(seq interface{}) (Pages, error) {
 | |
| 	if seq == nil {
 | |
| 		return Pages{}, nil
 | |
| 	}
 | |
| 
 | |
| 	switch seq.(type) {
 | |
| 	case Pages:
 | |
| 		return seq.(Pages), nil
 | |
| 	case *Pages:
 | |
| 		return *(seq.(*Pages)), nil
 | |
| 	case WeightedPages:
 | |
| 		return (seq.(WeightedPages)).Pages(), nil
 | |
| 	case PageGroup:
 | |
| 		return (seq.(PageGroup)).Pages, nil
 | |
| 	default:
 | |
| 		return nil, fmt.Errorf("unsupported type in paginate, got %T", seq)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // probablyEqual checks page lists for probable equality.
 | |
| // It may return false positives.
 | |
| // The motivation behind this is to avoid potential costly reflect.DeepEqual
 | |
| // when "probably" is good enough.
 | |
| func probablyEqualPageLists(a1 interface{}, a2 interface{}) bool {
 | |
| 
 | |
| 	if a1 == nil || a2 == nil {
 | |
| 		return a1 == a2
 | |
| 	}
 | |
| 
 | |
| 	t1 := reflect.TypeOf(a1)
 | |
| 	t2 := reflect.TypeOf(a2)
 | |
| 
 | |
| 	if t1 != t2 {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	if g1, ok := a1.(PagesGroup); ok {
 | |
| 		g2 := a2.(PagesGroup)
 | |
| 		if len(g1) != len(g2) {
 | |
| 			return false
 | |
| 		}
 | |
| 		if len(g1) == 0 {
 | |
| 			return true
 | |
| 		}
 | |
| 		if g1.Len() != g2.Len() {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		return g1[0].Pages[0] == g2[0].Pages[0]
 | |
| 	}
 | |
| 
 | |
| 	p1, err1 := toPages(a1)
 | |
| 	p2, err2 := toPages(a2)
 | |
| 
 | |
| 	// probably the same wrong type
 | |
| 	if err1 != nil && err2 != nil {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	if len(p1) != len(p2) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	if len(p1) == 0 {
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	return p1[0] == p2[0]
 | |
| }
 | |
| 
 | |
| func newPaginatorFromPages(pages Pages, size int, urlFactory paginationURLFactory) (*paginator, error) {
 | |
| 
 | |
| 	if size <= 0 {
 | |
| 		return nil, errors.New("Paginator size must be positive")
 | |
| 	}
 | |
| 
 | |
| 	split := splitPages(pages, size)
 | |
| 
 | |
| 	return newPaginator(split, len(pages), size, urlFactory)
 | |
| }
 | |
| 
 | |
| func newPaginatorFromPageGroups(pageGroups PagesGroup, size int, urlFactory paginationURLFactory) (*paginator, error) {
 | |
| 
 | |
| 	if size <= 0 {
 | |
| 		return nil, errors.New("Paginator size must be positive")
 | |
| 	}
 | |
| 
 | |
| 	split := splitPageGroups(pageGroups, size)
 | |
| 
 | |
| 	return newPaginator(split, pageGroups.Len(), size, urlFactory)
 | |
| }
 | |
| 
 | |
| func newPaginator(elements []paginatedElement, total, size int, urlFactory paginationURLFactory) (*paginator, error) {
 | |
| 	p := &paginator{total: total, paginatedElements: elements, size: size, paginationURLFactory: urlFactory}
 | |
| 
 | |
| 	var ps pagers
 | |
| 
 | |
| 	if len(elements) > 0 {
 | |
| 		ps = make(pagers, len(elements))
 | |
| 		for i := range p.paginatedElements {
 | |
| 			ps[i] = &Pager{number: (i + 1), paginator: p}
 | |
| 		}
 | |
| 	} else {
 | |
| 		ps = make(pagers, 1)
 | |
| 		ps[0] = &Pager{number: 1, paginator: p}
 | |
| 	}
 | |
| 
 | |
| 	p.pagers = ps
 | |
| 
 | |
| 	return p, nil
 | |
| }
 | |
| 
 | |
| func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory {
 | |
| 
 | |
| 	return func(page int) string {
 | |
| 		pathDescriptor := d
 | |
| 		var rel string
 | |
| 		if page > 1 {
 | |
| 			rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath(), page)
 | |
| 			pathDescriptor.Addends = rel
 | |
| 		}
 | |
| 
 | |
| 		targetPath := createTargetPath(pathDescriptor)
 | |
| 		targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename())
 | |
| 		link := d.PathSpec.PrependBasePath(targetPath)
 | |
| 		// Note: The targetPath is massaged with MakePathSanitized
 | |
| 		return d.PathSpec.URLizeFilename(link)
 | |
| 	}
 | |
| }
 |