diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-01-02 12:33:26 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-03-23 18:51:22 +0100 |
commit | 597e418cb02883418f2cebb41400e8e61413f651 (patch) | |
tree | 177ad9c540b2583b6dab138c9f0490d28989c7f7 /hugolib/pagination.go | |
parent | 44f5c1c14cb1f42cc5f01739c289e9cfc83602af (diff) |
Make Page an interface
The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct.
This is all a preparation step for issue #5074, "pages from other data sources".
But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes.
Most notable changes:
* The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday.
This means that any markdown will partake in the global ToC and footnote context etc.
* The Custom Output formats are now "fully virtualized". This removes many of the current limitations.
* The taxonomy list type now has a reference to the `Page` object.
This improves the taxonomy template `.Title` situation and make common template constructs much simpler.
See #5074
Fixes #5763
Fixes #5758
Fixes #5090
Fixes #5204
Fixes #4695
Fixes #5607
Fixes #5707
Fixes #5719
Fixes #3113
Fixes #5706
Fixes #5767
Fixes #5723
Fixes #5769
Fixes #5770
Fixes #5771
Fixes #5759
Fixes #5776
Fixes #5777
Fixes #5778
Diffstat (limited to 'hugolib/pagination.go')
-rw-r--r-- | hugolib/pagination.go | 595 |
1 files changed, 0 insertions, 595 deletions
diff --git a/hugolib/pagination.go b/hugolib/pagination.go deleted file mode 100644 index 05846a6bb..000000000 --- a/hugolib/pagination.go +++ /dev/null @@ -1,595 +0,0 @@ -// 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 - - groups, err := toPagesGroup(seq) - if err != nil { - return nil, err - } - if groups != nil { - 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 toPagesGroup(seq interface{}) (PagesGroup, error) { - switch v := seq.(type) { - case nil: - return nil, nil - case PagesGroup: - return v, nil - case []PageGroup: - return PagesGroup(v), nil - case []interface{}: - l := len(v) - if l == 0 { - break - } - switch v[0].(type) { - case PageGroup: - pagesGroup := make(PagesGroup, l) - for i, ipg := range v { - if pg, ok := ipg.(PageGroup); ok { - pagesGroup[i] = pg - } else { - return nil, fmt.Errorf("unsupported type in paginate from slice, got %T instead of PageGroup", ipg) - } - } - return PagesGroup(pagesGroup), nil - } - } - - return nil, nil -} - -func toPages(seq interface{}) (Pages, error) { - if seq == nil { - return Pages{}, nil - } - - switch v := seq.(type) { - case Pages: - return v, nil - case *Pages: - return *(v), nil - case []*Page: - return Pages(v), nil - case WeightedPages: - return v.Pages(), nil - case PageGroup: - return v.Pages, nil - case []interface{}: - pages := make(Pages, len(v)) - success := true - for i, vv := range v { - p, ok := vv.(*Page) - if !ok { - success = false - break - } - pages[i] = p - } - if success { - return pages, nil - } - } - - return nil, fmt.Errorf("cannot convert type %T to Pages", 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, false) - // Note: The targetPath is massaged with MakePathSanitized - return d.PathSpec.URLizeFilename(link) - } -} |