mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-29 22:29:56 +02:00
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
This commit is contained in:
365
resources/page/page.go
Normal file
365
resources/page/page.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// Copyright 2019 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 page contains the core interfaces and types for the Page resource,
|
||||
// a core component in Hugo.
|
||||
package page
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"github.com/bep/gitmap"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
|
||||
"github.com/gohugoio/hugo/navigation"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
)
|
||||
|
||||
// Clear clears any global package state.
|
||||
func Clear() error {
|
||||
spc.clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AlternativeOutputFormatsProvider provides alternative output formats for a
|
||||
// Page.
|
||||
type AlternativeOutputFormatsProvider interface {
|
||||
// AlternativeOutputFormats gives the alternative output formats for the
|
||||
// current output.
|
||||
// Note that we use the term "alternative" and not "alternate" here, as it
|
||||
// does not necessarily replace the other format, it is an alternative representation.
|
||||
AlternativeOutputFormats() OutputFormats
|
||||
}
|
||||
|
||||
// AuthorProvider provides author information.
|
||||
type AuthorProvider interface {
|
||||
Author() Author
|
||||
Authors() AuthorList
|
||||
}
|
||||
|
||||
// ChildCareProvider provides accessors to child resources.
|
||||
type ChildCareProvider interface {
|
||||
Pages() Pages
|
||||
Resources() resource.Resources
|
||||
}
|
||||
|
||||
// ContentProvider provides the content related values for a Page.
|
||||
type ContentProvider interface {
|
||||
Content() (interface{}, error)
|
||||
Plain() string
|
||||
PlainWords() []string
|
||||
Summary() template.HTML
|
||||
Truncated() bool
|
||||
FuzzyWordCount() int
|
||||
WordCount() int
|
||||
ReadingTime() int
|
||||
Len() int
|
||||
}
|
||||
|
||||
// FileProvider provides the source file.
|
||||
type FileProvider interface {
|
||||
File() source.File
|
||||
}
|
||||
|
||||
// GetPageProvider provides the GetPage method.
|
||||
type GetPageProvider interface {
|
||||
// GetPage looks up a page for the given ref.
|
||||
// {{ with .GetPage "blog" }}{{ .Title }}{{ end }}
|
||||
//
|
||||
// This will return nil when no page could be found, and will return
|
||||
// an error if the ref is ambiguous.
|
||||
GetPage(ref string) (Page, error)
|
||||
}
|
||||
|
||||
// GitInfoProvider provides Git info.
|
||||
type GitInfoProvider interface {
|
||||
GitInfo() *gitmap.GitInfo
|
||||
}
|
||||
|
||||
// InSectionPositioner provides section navigation.
|
||||
type InSectionPositioner interface {
|
||||
NextInSection() Page
|
||||
PrevInSection() Page
|
||||
}
|
||||
|
||||
// InternalDependencies is considered an internal interface.
|
||||
type InternalDependencies interface {
|
||||
GetRelatedDocsHandler() *RelatedDocsHandler
|
||||
}
|
||||
|
||||
// OutputFormatsProvider provides the OutputFormats of a Page.
|
||||
type OutputFormatsProvider interface {
|
||||
OutputFormats() OutputFormats
|
||||
}
|
||||
|
||||
// Page is the core interface in Hugo.
|
||||
type Page interface {
|
||||
ContentProvider
|
||||
TableOfContentsProvider
|
||||
PageWithoutContent
|
||||
}
|
||||
|
||||
// PageMetaProvider provides page metadata, typically provided via front matter.
|
||||
type PageMetaProvider interface {
|
||||
// The 4 page dates
|
||||
resource.Dated
|
||||
|
||||
// Aliases forms the base for redirects generation.
|
||||
Aliases() []string
|
||||
|
||||
// BundleType returns the bundle type: "leaf", "branch" or an empty string if it is none.
|
||||
// See https://gohugo.io/content-management/page-bundles/
|
||||
BundleType() string
|
||||
|
||||
// A configured description.
|
||||
Description() string
|
||||
|
||||
// Whether this is a draft. Will only be true if run with the --buildDrafts (-D) flag.
|
||||
Draft() bool
|
||||
|
||||
// IsHome returns whether this is the home page.
|
||||
IsHome() bool
|
||||
|
||||
// Configured keywords.
|
||||
Keywords() []string
|
||||
|
||||
// The Page Kind. One of page, home, section, taxonomy, taxonomyTerm.
|
||||
Kind() string
|
||||
|
||||
// The configured layout to use to render this page. Typically set in front matter.
|
||||
Layout() string
|
||||
|
||||
// The title used for links.
|
||||
LinkTitle() string
|
||||
|
||||
// IsNode returns whether this is an item of one of the list types in Hugo,
|
||||
// i.e. not a regular content
|
||||
IsNode() bool
|
||||
|
||||
// IsPage returns whether this is a regular content
|
||||
IsPage() bool
|
||||
|
||||
// Param looks for a param in Page and then in Site config.
|
||||
Param(key interface{}) (interface{}, error)
|
||||
|
||||
// Path gets the relative path, including file name and extension if relevant,
|
||||
// to the source of this Page. It will be relative to any content root.
|
||||
Path() string
|
||||
|
||||
// The slug, typically defined in front matter.
|
||||
Slug() string
|
||||
|
||||
// This page's language code. Will be the same as the site's.
|
||||
Lang() string
|
||||
|
||||
// IsSection returns whether this is a section
|
||||
IsSection() bool
|
||||
|
||||
// Section returns the first path element below the content root.
|
||||
Section() string
|
||||
|
||||
// Returns a slice of sections (directories if it's a file) to this
|
||||
// Page.
|
||||
SectionsEntries() []string
|
||||
|
||||
// SectionsPath is SectionsEntries joined with a /.
|
||||
SectionsPath() string
|
||||
|
||||
// Sitemap returns the sitemap configuration for this page.
|
||||
Sitemap() config.Sitemap
|
||||
|
||||
// Type is a discriminator used to select layouts etc. It is typically set
|
||||
// in front matter, but will fall back to the root section.
|
||||
Type() string
|
||||
|
||||
// The configured weight, used as the first sort value in the default
|
||||
// page sort if non-zero.
|
||||
Weight() int
|
||||
}
|
||||
|
||||
// PageRenderProvider provides a way for a Page to render itself.
|
||||
type PageRenderProvider interface {
|
||||
Render(layout ...string) template.HTML
|
||||
}
|
||||
|
||||
// PageWithoutContent is the Page without any of the content methods.
|
||||
type PageWithoutContent interface {
|
||||
RawContentProvider
|
||||
resource.Resource
|
||||
PageMetaProvider
|
||||
resource.LanguageProvider
|
||||
|
||||
// For pages backed by a file.
|
||||
FileProvider
|
||||
|
||||
// Output formats
|
||||
OutputFormatsProvider
|
||||
AlternativeOutputFormatsProvider
|
||||
|
||||
// Tree navigation
|
||||
ChildCareProvider
|
||||
TreeProvider
|
||||
|
||||
// Horisontal navigation
|
||||
InSectionPositioner
|
||||
PageRenderProvider
|
||||
PaginatorProvider
|
||||
Positioner
|
||||
navigation.PageMenusProvider
|
||||
|
||||
// TODO(bep)
|
||||
AuthorProvider
|
||||
|
||||
// Page lookups/refs
|
||||
GetPageProvider
|
||||
RefProvider
|
||||
|
||||
resource.TranslationKeyProvider
|
||||
TranslationsProvider
|
||||
|
||||
SitesProvider
|
||||
|
||||
// Helper methods
|
||||
ShortcodeInfoProvider
|
||||
compare.Eqer
|
||||
maps.Scratcher
|
||||
RelatedKeywordsProvider
|
||||
|
||||
DeprecatedWarningPageMethods
|
||||
}
|
||||
|
||||
// Positioner provides next/prev navigation.
|
||||
type Positioner interface {
|
||||
Next() Page
|
||||
Prev() Page
|
||||
|
||||
// Deprecated: Use Prev. Will be removed in Hugo 0.57
|
||||
PrevPage() Page
|
||||
|
||||
// Deprecated: Use Next. Will be removed in Hugo 0.57
|
||||
NextPage() Page
|
||||
}
|
||||
|
||||
// RawContentProvider provides the raw, unprocessed content of the page.
|
||||
type RawContentProvider interface {
|
||||
RawContent() string
|
||||
}
|
||||
|
||||
// RefProvider provides the methods needed to create reflinks to pages.
|
||||
type RefProvider interface {
|
||||
Ref(argsm map[string]interface{}) (string, error)
|
||||
RefFrom(argsm map[string]interface{}, source interface{}) (string, error)
|
||||
RelRef(argsm map[string]interface{}) (string, error)
|
||||
RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error)
|
||||
}
|
||||
|
||||
// RelatedKeywordsProvider allows a Page to be indexed.
|
||||
type RelatedKeywordsProvider interface {
|
||||
// Make it indexable as a related.Document
|
||||
RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error)
|
||||
}
|
||||
|
||||
// ShortcodeInfoProvider provides info about the shortcodes in a Page.
|
||||
type ShortcodeInfoProvider interface {
|
||||
// HasShortcode return whether the page has a shortcode with the given name.
|
||||
// This method is mainly motivated with the Hugo Docs site's need for a list
|
||||
// of pages with the `todo` shortcode in it.
|
||||
HasShortcode(name string) bool
|
||||
}
|
||||
|
||||
// SitesProvider provide accessors to get sites.
|
||||
type SitesProvider interface {
|
||||
Site() Site
|
||||
Sites() Sites
|
||||
}
|
||||
|
||||
// TableOfContentsProvider provides the table of contents for a Page.
|
||||
type TableOfContentsProvider interface {
|
||||
TableOfContents() template.HTML
|
||||
}
|
||||
|
||||
// TranslationsProvider provides access to any translations.
|
||||
type TranslationsProvider interface {
|
||||
|
||||
// IsTranslated returns whether this content file is translated to
|
||||
// other language(s).
|
||||
IsTranslated() bool
|
||||
|
||||
// AllTranslations returns all translations, including the current Page.
|
||||
AllTranslations() Pages
|
||||
|
||||
// Translations returns the translations excluding the current Page.
|
||||
Translations() Pages
|
||||
}
|
||||
|
||||
// TreeProvider provides section tree navigation.
|
||||
type TreeProvider interface {
|
||||
|
||||
// IsAncestor returns whether the current page is an ancestor of the given
|
||||
// Note that this method is not relevant for taxonomy lists and taxonomy terms pages.
|
||||
IsAncestor(other interface{}) (bool, error)
|
||||
|
||||
// CurrentSection returns the page's current section or the page itself if home or a section.
|
||||
// Note that this will return nil for pages that is not regular, home or section pages.
|
||||
CurrentSection() Page
|
||||
|
||||
// IsDescendant returns whether the current page is a descendant of the given
|
||||
// Note that this method is not relevant for taxonomy lists and taxonomy terms pages.
|
||||
IsDescendant(other interface{}) (bool, error)
|
||||
|
||||
// FirstSection returns the section on level 1 below home, e.g. "/docs".
|
||||
// For the home page, this will return itself.
|
||||
FirstSection() Page
|
||||
|
||||
// InSection returns whether the given page is in the current section.
|
||||
// Note that this will always return false for pages that are
|
||||
// not either regular, home or section pages.
|
||||
InSection(other interface{}) (bool, error)
|
||||
|
||||
// Parent returns a section's parent section or a page's section.
|
||||
// To get a section's subsections, see Page's Sections method.
|
||||
Parent() Page
|
||||
|
||||
// Sections returns this section's subsections, if any.
|
||||
// Note that for non-sections, this method will always return an empty list.
|
||||
Sections() Pages
|
||||
}
|
||||
|
||||
// DeprecatedWarningPageMethods lists deprecated Page methods that will trigger
|
||||
// a WARNING if invoked.
|
||||
// This was added in Hugo 0.55.
|
||||
type DeprecatedWarningPageMethods interface {
|
||||
source.FileWithoutOverlap
|
||||
DeprecatedWarningPageMethods1
|
||||
}
|
||||
|
||||
type DeprecatedWarningPageMethods1 interface {
|
||||
IsDraft() bool
|
||||
Hugo() hugo.Info
|
||||
LanguagePrefix() string
|
||||
GetParam(key string) interface{}
|
||||
RSSLink() template.URL
|
||||
URL() string
|
||||
}
|
||||
|
||||
// Move here to trigger ERROR instead of WARNING.
|
||||
// TODO(bep) create wrappers and put into the Page once it has some methods.
|
||||
type DeprecatedErrorPageMethods interface {
|
||||
}
|
45
resources/page/page_author.go
Normal file
45
resources/page/page_author.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
// AuthorList is a list of all authors and their metadata.
|
||||
type AuthorList map[string]Author
|
||||
|
||||
// Author contains details about the author of a page.
|
||||
type Author struct {
|
||||
GivenName string
|
||||
FamilyName string
|
||||
DisplayName string
|
||||
Thumbnail string
|
||||
Image string
|
||||
ShortBio string
|
||||
LongBio string
|
||||
Email string
|
||||
Social AuthorSocial
|
||||
}
|
||||
|
||||
// AuthorSocial is a place to put social details per author. These are the
|
||||
// standard keys that themes will expect to have available, but can be
|
||||
// expanded to any others on a per site basis
|
||||
// - website
|
||||
// - github
|
||||
// - facebook
|
||||
// - twitter
|
||||
// - googleplus
|
||||
// - pinterest
|
||||
// - instagram
|
||||
// - youtube
|
||||
// - linkedin
|
||||
// - skype
|
||||
type AuthorSocial map[string]string
|
42
resources/page/page_data.go
Normal file
42
resources/page/page_data.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 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 page contains the core interfaces and types for the Page resource,
|
||||
// a core component in Hugo.
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Data represents the .Data element in a Page in Hugo. We make this
|
||||
// a type so we can do lazy loading of .Data.Pages
|
||||
type Data map[string]interface{}
|
||||
|
||||
// Pages returns the pages stored with key "pages". If this is a func,
|
||||
// it will be invoked.
|
||||
func (d Data) Pages() Pages {
|
||||
v, found := d["pages"]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case Pages:
|
||||
return vv
|
||||
case func() Pages:
|
||||
return vv()
|
||||
default:
|
||||
panic(fmt.Sprintf("%T is not Pages", v))
|
||||
}
|
||||
}
|
57
resources/page/page_data_test.go
Normal file
57
resources/page/page_data_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"text/template"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPageData(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
data := make(Data)
|
||||
|
||||
assert.Nil(data.Pages())
|
||||
|
||||
pages := Pages{
|
||||
&testPage{title: "a1"},
|
||||
&testPage{title: "a2"},
|
||||
}
|
||||
|
||||
data["pages"] = pages
|
||||
|
||||
assert.Equal(pages, data.Pages())
|
||||
|
||||
data["pages"] = func() Pages {
|
||||
return pages
|
||||
}
|
||||
|
||||
assert.Equal(pages, data.Pages())
|
||||
|
||||
templ, err := template.New("").Parse(`Pages: {{ .Pages }}`)
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
var buff bytes.Buffer
|
||||
|
||||
assert.NoError(templ.Execute(&buff, data))
|
||||
|
||||
assert.Contains(buff.String(), "Pages(2)")
|
||||
|
||||
}
|
1
resources/page/page_generate/.gitignore
vendored
Normal file
1
resources/page/page_generate/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
generate
|
212
resources/page/page_generate/generate_page_wrappers.go
Normal file
212
resources/page/page_generate/generate_page_wrappers.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright 2019 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 page_generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/codegen"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
)
|
||||
|
||||
const header = `// Copyright 2019 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.
|
||||
|
||||
// This file is autogenerated.
|
||||
`
|
||||
|
||||
var (
|
||||
fileInterfaceDeprecated = reflect.TypeOf((*source.FileWithoutOverlap)(nil)).Elem()
|
||||
pageInterfaceDeprecated = reflect.TypeOf((*page.DeprecatedWarningPageMethods)(nil)).Elem()
|
||||
pageInterface = reflect.TypeOf((*page.Page)(nil)).Elem()
|
||||
|
||||
packageDir = filepath.FromSlash("resources/page")
|
||||
)
|
||||
|
||||
func Generate(c *codegen.Inspector) error {
|
||||
if err := generateMarshalJSON(c); err != nil {
|
||||
return errors.Wrap(err, "failed to generate JSON marshaler")
|
||||
|
||||
}
|
||||
|
||||
if err := generateDeprecatedWrappers(c); err != nil {
|
||||
return errors.Wrap(err, "failed to generate deprecate wrappers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateMarshalJSON(c *codegen.Inspector) error {
|
||||
filename := filepath.Join(c.ProjectRootDir, packageDir, "page_marshaljson.autogen.go")
|
||||
f, err := os.Create(filename)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
includes := []reflect.Type{pageInterface}
|
||||
|
||||
// Exclude these methods
|
||||
excludes := []reflect.Type{
|
||||
// We need to eveluate the deprecated vs JSON in the future,
|
||||
// but leave them out for now.
|
||||
pageInterfaceDeprecated,
|
||||
|
||||
// Leave this out for now. We need to revisit the author issue.
|
||||
reflect.TypeOf((*page.AuthorProvider)(nil)).Elem(),
|
||||
|
||||
// navigation.PageMenus
|
||||
|
||||
// Prevent loops.
|
||||
reflect.TypeOf((*page.SitesProvider)(nil)).Elem(),
|
||||
reflect.TypeOf((*page.Positioner)(nil)).Elem(),
|
||||
|
||||
reflect.TypeOf((*page.ChildCareProvider)(nil)).Elem(),
|
||||
reflect.TypeOf((*page.TreeProvider)(nil)).Elem(),
|
||||
reflect.TypeOf((*page.InSectionPositioner)(nil)).Elem(),
|
||||
reflect.TypeOf((*page.PaginatorProvider)(nil)).Elem(),
|
||||
reflect.TypeOf((*maps.Scratcher)(nil)).Elem(),
|
||||
}
|
||||
|
||||
methods := c.MethodsFromTypes(
|
||||
includes,
|
||||
excludes)
|
||||
|
||||
if len(methods) == 0 {
|
||||
return errors.New("no methods found")
|
||||
}
|
||||
|
||||
marshalJSON, pkgImports := methods.ToMarshalJSON("Page", "github.com/gohugoio/hugo/resources/page")
|
||||
|
||||
fmt.Fprintf(f, `%s
|
||||
|
||||
package page
|
||||
|
||||
%s
|
||||
|
||||
|
||||
%s
|
||||
|
||||
|
||||
`, header, importsString(pkgImports), marshalJSON)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateDeprecatedWrappers(c *codegen.Inspector) error {
|
||||
filename := filepath.Join(c.ProjectRootDir, packageDir, "page_wrappers.autogen.go")
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Generate a wrapper for deprecated page methods
|
||||
|
||||
reasons := map[string]string{
|
||||
"IsDraft": "Use .Draft.",
|
||||
"Hugo": "Use the global hugo function.",
|
||||
"LanguagePrefix": "Use .Site.LanguagePrefix.",
|
||||
"GetParam": "Use .Param or .Params.myParam.",
|
||||
"RSSLink": `Use the Output Format's link, e.g. something like:
|
||||
{{ with .OutputFormats.Get "RSS" }}{{ . RelPermalink }}{{ end }}`,
|
||||
"URL": "Use .Permalink or .RelPermalink. If what you want is the front matter URL value, use .Params.url",
|
||||
}
|
||||
|
||||
deprecated := func(name string, tp reflect.Type) string {
|
||||
var alternative string
|
||||
if tp == fileInterfaceDeprecated {
|
||||
alternative = "Use .File." + name
|
||||
} else {
|
||||
var found bool
|
||||
alternative, found = reasons[name]
|
||||
if !found {
|
||||
panic(fmt.Sprintf("no deprecated reason found for %q", name))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("helpers.Deprecated(%q, %q, %q, false)", "Page", "."+name, alternative)
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
|
||||
methods := c.MethodsFromTypes([]reflect.Type{fileInterfaceDeprecated, pageInterfaceDeprecated}, nil)
|
||||
|
||||
for _, m := range methods {
|
||||
fmt.Fprint(&buff, m.Declaration("*pageDeprecated"))
|
||||
fmt.Fprintln(&buff, " {")
|
||||
fmt.Fprintf(&buff, "\t%s\n", deprecated(m.Name, m.Owner))
|
||||
fmt.Fprintf(&buff, "\t%s\n}\n", m.Delegate("p", "p"))
|
||||
|
||||
}
|
||||
|
||||
pkgImports := append(methods.Imports(), "github.com/gohugoio/hugo/helpers")
|
||||
|
||||
fmt.Fprintf(f, `%s
|
||||
|
||||
package page
|
||||
|
||||
%s
|
||||
// NewDeprecatedWarningPage adds deprecation warnings to the given implementation.
|
||||
func NewDeprecatedWarningPage(p DeprecatedWarningPageMethods) DeprecatedWarningPageMethods {
|
||||
return &pageDeprecated{p: p}
|
||||
}
|
||||
|
||||
type pageDeprecated struct {
|
||||
p DeprecatedWarningPageMethods
|
||||
}
|
||||
|
||||
%s
|
||||
|
||||
`, header, importsString(pkgImports), buff.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importsString(imps []string) string {
|
||||
if len(imps) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(imps) == 1 {
|
||||
return fmt.Sprintf("import %q", imps[0])
|
||||
}
|
||||
|
||||
impsStr := "import (\n"
|
||||
for _, imp := range imps {
|
||||
impsStr += fmt.Sprintf("%q\n", imp)
|
||||
}
|
||||
|
||||
return impsStr + ")"
|
||||
}
|
25
resources/page/page_kinds.go
Normal file
25
resources/page/page_kinds.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
const (
|
||||
KindPage = "page"
|
||||
|
||||
// The rest are node types; home page, sections etc.
|
||||
|
||||
KindHome = "home"
|
||||
KindSection = "section"
|
||||
KindTaxonomy = "taxonomy"
|
||||
KindTaxonomyTerm = "taxonomyTerm"
|
||||
)
|
31
resources/page/page_kinds_test.go
Normal file
31
resources/page/page_kinds_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKind(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Add tests for these constants to make sure they don't change
|
||||
require.Equal(t, "page", KindPage)
|
||||
require.Equal(t, "home", KindHome)
|
||||
require.Equal(t, "section", KindSection)
|
||||
require.Equal(t, "taxonomy", KindTaxonomy)
|
||||
require.Equal(t, "taxonomyTerm", KindTaxonomyTerm)
|
||||
|
||||
}
|
198
resources/page/page_marshaljson.autogen.go
Normal file
198
resources/page/page_marshaljson.autogen.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// This file is autogenerated.
|
||||
|
||||
package page
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/navigation"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
"html/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MarshalPageToJSON(p Page) ([]byte, error) {
|
||||
content, err := p.Content()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plain := p.Plain()
|
||||
plainWords := p.PlainWords()
|
||||
summary := p.Summary()
|
||||
truncated := p.Truncated()
|
||||
fuzzyWordCount := p.FuzzyWordCount()
|
||||
wordCount := p.WordCount()
|
||||
readingTime := p.ReadingTime()
|
||||
length := p.Len()
|
||||
tableOfContents := p.TableOfContents()
|
||||
rawContent := p.RawContent()
|
||||
mediaType := p.MediaType()
|
||||
resourceType := p.ResourceType()
|
||||
permalink := p.Permalink()
|
||||
relPermalink := p.RelPermalink()
|
||||
name := p.Name()
|
||||
title := p.Title()
|
||||
params := p.Params()
|
||||
data := p.Data()
|
||||
date := p.Date()
|
||||
lastmod := p.Lastmod()
|
||||
publishDate := p.PublishDate()
|
||||
expiryDate := p.ExpiryDate()
|
||||
aliases := p.Aliases()
|
||||
bundleType := p.BundleType()
|
||||
description := p.Description()
|
||||
draft := p.Draft()
|
||||
isHome := p.IsHome()
|
||||
keywords := p.Keywords()
|
||||
kind := p.Kind()
|
||||
layout := p.Layout()
|
||||
linkTitle := p.LinkTitle()
|
||||
isNode := p.IsNode()
|
||||
isPage := p.IsPage()
|
||||
path := p.Path()
|
||||
slug := p.Slug()
|
||||
lang := p.Lang()
|
||||
isSection := p.IsSection()
|
||||
section := p.Section()
|
||||
sectionsEntries := p.SectionsEntries()
|
||||
sectionsPath := p.SectionsPath()
|
||||
sitemap := p.Sitemap()
|
||||
typ := p.Type()
|
||||
weight := p.Weight()
|
||||
language := p.Language()
|
||||
file := p.File()
|
||||
outputFormats := p.OutputFormats()
|
||||
alternativeOutputFormats := p.AlternativeOutputFormats()
|
||||
menus := p.Menus()
|
||||
translationKey := p.TranslationKey()
|
||||
isTranslated := p.IsTranslated()
|
||||
allTranslations := p.AllTranslations()
|
||||
translations := p.Translations()
|
||||
|
||||
s := struct {
|
||||
Content interface{}
|
||||
Plain string
|
||||
PlainWords []string
|
||||
Summary template.HTML
|
||||
Truncated bool
|
||||
FuzzyWordCount int
|
||||
WordCount int
|
||||
ReadingTime int
|
||||
Len int
|
||||
TableOfContents template.HTML
|
||||
RawContent string
|
||||
MediaType media.Type
|
||||
ResourceType string
|
||||
Permalink string
|
||||
RelPermalink string
|
||||
Name string
|
||||
Title string
|
||||
Params map[string]interface{}
|
||||
Data interface{}
|
||||
Date time.Time
|
||||
Lastmod time.Time
|
||||
PublishDate time.Time
|
||||
ExpiryDate time.Time
|
||||
Aliases []string
|
||||
BundleType string
|
||||
Description string
|
||||
Draft bool
|
||||
IsHome bool
|
||||
Keywords []string
|
||||
Kind string
|
||||
Layout string
|
||||
LinkTitle string
|
||||
IsNode bool
|
||||
IsPage bool
|
||||
Path string
|
||||
Slug string
|
||||
Lang string
|
||||
IsSection bool
|
||||
Section string
|
||||
SectionsEntries []string
|
||||
SectionsPath string
|
||||
Sitemap config.Sitemap
|
||||
Type string
|
||||
Weight int
|
||||
Language *langs.Language
|
||||
File source.File
|
||||
OutputFormats OutputFormats
|
||||
AlternativeOutputFormats OutputFormats
|
||||
Menus navigation.PageMenus
|
||||
TranslationKey string
|
||||
IsTranslated bool
|
||||
AllTranslations Pages
|
||||
Translations Pages
|
||||
}{
|
||||
Content: content,
|
||||
Plain: plain,
|
||||
PlainWords: plainWords,
|
||||
Summary: summary,
|
||||
Truncated: truncated,
|
||||
FuzzyWordCount: fuzzyWordCount,
|
||||
WordCount: wordCount,
|
||||
ReadingTime: readingTime,
|
||||
Len: length,
|
||||
TableOfContents: tableOfContents,
|
||||
RawContent: rawContent,
|
||||
MediaType: mediaType,
|
||||
ResourceType: resourceType,
|
||||
Permalink: permalink,
|
||||
RelPermalink: relPermalink,
|
||||
Name: name,
|
||||
Title: title,
|
||||
Params: params,
|
||||
Data: data,
|
||||
Date: date,
|
||||
Lastmod: lastmod,
|
||||
PublishDate: publishDate,
|
||||
ExpiryDate: expiryDate,
|
||||
Aliases: aliases,
|
||||
BundleType: bundleType,
|
||||
Description: description,
|
||||
Draft: draft,
|
||||
IsHome: isHome,
|
||||
Keywords: keywords,
|
||||
Kind: kind,
|
||||
Layout: layout,
|
||||
LinkTitle: linkTitle,
|
||||
IsNode: isNode,
|
||||
IsPage: isPage,
|
||||
Path: path,
|
||||
Slug: slug,
|
||||
Lang: lang,
|
||||
IsSection: isSection,
|
||||
Section: section,
|
||||
SectionsEntries: sectionsEntries,
|
||||
SectionsPath: sectionsPath,
|
||||
Sitemap: sitemap,
|
||||
Type: typ,
|
||||
Weight: weight,
|
||||
Language: language,
|
||||
File: file,
|
||||
OutputFormats: outputFormats,
|
||||
AlternativeOutputFormats: alternativeOutputFormats,
|
||||
Menus: menus,
|
||||
TranslationKey: translationKey,
|
||||
IsTranslated: isTranslated,
|
||||
AllTranslations: allTranslations,
|
||||
Translations: translations,
|
||||
}
|
||||
|
||||
return json.Marshal(&s)
|
||||
}
|
463
resources/page/page_nop.go
Normal file
463
resources/page/page_nop.go
Normal file
@@ -0,0 +1,463 @@
|
||||
// Copyright 2019 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 page contains the core interfaces and types for the Page resource,
|
||||
// a core component in Hugo.
|
||||
package page
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/bep/gitmap"
|
||||
"github.com/gohugoio/hugo/navigation"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
NopPage Page = new(nopPage)
|
||||
NilPage *nopPage
|
||||
)
|
||||
|
||||
// PageNop implements Page, but does nothing.
|
||||
type nopPage int
|
||||
|
||||
func (p *nopPage) Aliases() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Sitemap() config.Sitemap {
|
||||
return config.Sitemap{}
|
||||
}
|
||||
|
||||
func (p *nopPage) Layout() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) RSSLink() template.URL {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Author() Author {
|
||||
return Author{}
|
||||
|
||||
}
|
||||
func (p *nopPage) Authors() AuthorList {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) AllTranslations() Pages {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) LanguagePrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) AlternativeOutputFormats() OutputFormats {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) BaseFileName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) BundleType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Content() (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *nopPage) ContentBaseName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) CurrentSection() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Data() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Date() (t time.Time) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) Description() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) RefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (p *nopPage) RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Dir() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Draft() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) Eq(other interface{}) bool {
|
||||
return p == other
|
||||
}
|
||||
|
||||
func (p *nopPage) ExpiryDate() (t time.Time) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) Ext() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Extension() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
var nilFile *source.FileInfo
|
||||
|
||||
func (p *nopPage) File() source.File {
|
||||
return nilFile
|
||||
}
|
||||
|
||||
func (p *nopPage) FileInfo() os.FileInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Filename() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) FirstSection() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) FuzzyWordCount() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *nopPage) GetPage(ref string) (Page, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) GetParam(key string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) GitInfo() *gitmap.GitInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) HasShortcode(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) Hugo() (h hugo.Info) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) InSection(other interface{}) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) IsAncestor(other interface{}) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) IsDescendant(other interface{}) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) IsDraft() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) IsHome() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) IsNode() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) IsPage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) IsSection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) IsTranslated() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) Keywords() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Kind() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Lang() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Language() *langs.Language {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Lastmod() (t time.Time) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) Len() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *nopPage) LinkTitle() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) LogicalName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) MediaType() (m media.Type) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) Menus() (m navigation.PageMenus) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) Name() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Next() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) OutputFormats() OutputFormats {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Pages() Pages {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Paginator(options ...interface{}) (*Pager, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Param(key interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Params() map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Parent() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Path() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Permalink() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Plain() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) PlainWords() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Prev() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) PublishDate() (t time.Time) {
|
||||
return
|
||||
}
|
||||
|
||||
func (p *nopPage) PrevInSection() Page {
|
||||
return nil
|
||||
}
|
||||
func (p *nopPage) NextInSection() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) PrevPage() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) NextPage() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) RawContent() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) ReadingTime() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *nopPage) Ref(argsm map[string]interface{}) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *nopPage) RelPermalink() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) RelRef(argsm map[string]interface{}) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Render(layout ...string) template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) ResourceType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Resources() resource.Resources {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Scratch() *maps.Scratch {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Section() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Sections() Pages {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) SectionsEntries() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) SectionsPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Site() Site {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Sites() Sites {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Slug() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) String() string {
|
||||
return "nopPage"
|
||||
}
|
||||
|
||||
func (p *nopPage) Summary() template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) TableOfContents() template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Title() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) TranslationBaseName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) TranslationKey() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Translations() Pages {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *nopPage) Truncated() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *nopPage) Type() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) URL() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) UniqueID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Weight() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *nopPage) WordCount() int {
|
||||
return 0
|
||||
}
|
85
resources/page/page_outputformat.go
Normal file
85
resources/page/page_outputformat.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019 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 page contains the core interfaces and types for the Page resource,
|
||||
// a core component in Hugo.
|
||||
package page
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
)
|
||||
|
||||
// OutputFormats holds a list of the relevant output formats for a given page.
|
||||
type OutputFormats []OutputFormat
|
||||
|
||||
// OutputFormat links to a representation of a resource.
|
||||
type OutputFormat struct {
|
||||
// Rel constains a value that can be used to construct a rel link.
|
||||
// This is value is fetched from the output format definition.
|
||||
// Note that for pages with only one output format,
|
||||
// this method will always return "canonical".
|
||||
// As an example, the AMP output format will, by default, return "amphtml".
|
||||
//
|
||||
// See:
|
||||
// https://www.ampproject.org/docs/guides/deploy/discovery
|
||||
//
|
||||
// Most other output formats will have "alternate" as value for this.
|
||||
Rel string
|
||||
|
||||
Format output.Format
|
||||
|
||||
relPermalink string
|
||||
permalink string
|
||||
}
|
||||
|
||||
// Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc.
|
||||
func (o OutputFormat) Name() string {
|
||||
return o.Format.Name
|
||||
}
|
||||
|
||||
// MediaType returns this OutputFormat's MediaType (MIME type).
|
||||
func (o OutputFormat) MediaType() media.Type {
|
||||
return o.Format.MediaType
|
||||
}
|
||||
|
||||
// Permalink returns the absolute permalink to this output format.
|
||||
func (o OutputFormat) Permalink() string {
|
||||
return o.permalink
|
||||
}
|
||||
|
||||
// RelPermalink returns the relative permalink to this output format.
|
||||
func (o OutputFormat) RelPermalink() string {
|
||||
return o.relPermalink
|
||||
}
|
||||
|
||||
func NewOutputFormat(relPermalink, permalink string, isCanonical bool, f output.Format) OutputFormat {
|
||||
rel := f.Rel
|
||||
if isCanonical {
|
||||
rel = "canonical"
|
||||
}
|
||||
return OutputFormat{Rel: rel, Format: f, relPermalink: relPermalink, permalink: permalink}
|
||||
}
|
||||
|
||||
// Get gets a OutputFormat given its name, i.e. json, html etc.
|
||||
// It returns nil if none found.
|
||||
func (o OutputFormats) Get(name string) *OutputFormat {
|
||||
for _, f := range o {
|
||||
if strings.EqualFold(f.Format.Name, name) {
|
||||
return &f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
334
resources/page/page_paths.go
Normal file
334
resources/page/page_paths.go
Normal file
@@ -0,0 +1,334 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
)
|
||||
|
||||
const slash = "/"
|
||||
|
||||
// TargetPathDescriptor describes how a file path for a given resource
|
||||
// should look like on the file system. The same descriptor is then later used to
|
||||
// create both the permalinks and the relative links, paginator URLs etc.
|
||||
//
|
||||
// The big motivating behind this is to have only one source of truth for URLs,
|
||||
// and by that also get rid of most of the fragile string parsing/encoding etc.
|
||||
//
|
||||
//
|
||||
type TargetPathDescriptor struct {
|
||||
PathSpec *helpers.PathSpec
|
||||
|
||||
Type output.Format
|
||||
Kind string
|
||||
|
||||
Sections []string
|
||||
|
||||
// For regular content pages this is either
|
||||
// 1) the Slug, if set,
|
||||
// 2) the file base name (TranslationBaseName).
|
||||
BaseName string
|
||||
|
||||
// Source directory.
|
||||
Dir string
|
||||
|
||||
// Typically a language prefix added to file paths.
|
||||
PrefixFilePath string
|
||||
|
||||
// Typically a language prefix added to links.
|
||||
PrefixLink string
|
||||
|
||||
// If in multihost mode etc., every link/path needs to be prefixed, even
|
||||
// if set in URL.
|
||||
ForcePrefix bool
|
||||
|
||||
// URL from front matter if set. Will override any Slug etc.
|
||||
URL string
|
||||
|
||||
// Used to create paginator links.
|
||||
Addends string
|
||||
|
||||
// The expanded permalink if defined for the section, ready to use.
|
||||
ExpandedPermalink string
|
||||
|
||||
// Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
|
||||
UglyURLs bool
|
||||
}
|
||||
|
||||
// TODO(bep) move this type.
|
||||
type TargetPaths struct {
|
||||
|
||||
// Where to store the file on disk relative to the publish dir. OS slashes.
|
||||
TargetFilename string
|
||||
|
||||
// The directory to write sub-resources of the above.
|
||||
SubResourceBaseTarget string
|
||||
|
||||
// The base for creating links to sub-resources of the above.
|
||||
SubResourceBaseLink string
|
||||
|
||||
// The relative permalink to this resources. Unix slashes.
|
||||
Link string
|
||||
}
|
||||
|
||||
func (p TargetPaths) RelPermalink(s *helpers.PathSpec) string {
|
||||
return s.PrependBasePath(p.Link, false)
|
||||
}
|
||||
|
||||
func (p TargetPaths) PermalinkForOutputFormat(s *helpers.PathSpec, f output.Format) string {
|
||||
var baseURL string
|
||||
var err error
|
||||
if f.Protocol != "" {
|
||||
baseURL, err = s.BaseURL.WithProtocol(f.Protocol)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
baseURL = s.BaseURL.String()
|
||||
}
|
||||
|
||||
return s.PermalinkForBaseURL(p.Link, baseURL)
|
||||
}
|
||||
|
||||
func isHtmlIndex(s string) bool {
|
||||
return strings.HasSuffix(s, "/index.html")
|
||||
}
|
||||
|
||||
func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
|
||||
|
||||
if d.Type.Name == "" {
|
||||
panic("CreateTargetPath: missing type")
|
||||
}
|
||||
|
||||
// Normalize all file Windows paths to simplify what's next.
|
||||
if helpers.FilePathSeparator != slash {
|
||||
d.Dir = filepath.ToSlash(d.Dir)
|
||||
d.PrefixFilePath = filepath.ToSlash(d.PrefixFilePath)
|
||||
|
||||
}
|
||||
|
||||
pagePath := slash
|
||||
|
||||
var (
|
||||
pagePathDir string
|
||||
link string
|
||||
linkDir string
|
||||
)
|
||||
|
||||
// The top level index files, i.e. the home page etc., needs
|
||||
// the index base even when uglyURLs is enabled.
|
||||
needsBase := true
|
||||
|
||||
isUgly := d.UglyURLs && !d.Type.NoUgly
|
||||
baseNameSameAsType := d.BaseName != "" && d.BaseName == d.Type.BaseName
|
||||
|
||||
if d.ExpandedPermalink == "" && baseNameSameAsType {
|
||||
isUgly = true
|
||||
}
|
||||
|
||||
if d.Kind != KindPage && d.URL == "" && len(d.Sections) > 0 {
|
||||
if d.ExpandedPermalink != "" {
|
||||
pagePath = pjoin(pagePath, d.ExpandedPermalink)
|
||||
} else {
|
||||
pagePath = pjoin(d.Sections...)
|
||||
}
|
||||
needsBase = false
|
||||
}
|
||||
|
||||
if d.Type.Path != "" {
|
||||
pagePath = pjoin(pagePath, d.Type.Path)
|
||||
}
|
||||
|
||||
if d.Kind != KindHome && d.URL != "" {
|
||||
pagePath = pjoin(pagePath, d.URL)
|
||||
|
||||
if d.Addends != "" {
|
||||
pagePath = pjoin(pagePath, d.Addends)
|
||||
}
|
||||
|
||||
pagePathDir = pagePath
|
||||
link = pagePath
|
||||
hasDot := strings.Contains(d.URL, ".")
|
||||
hasSlash := strings.HasSuffix(d.URL, slash)
|
||||
|
||||
if hasSlash || !hasDot {
|
||||
pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
|
||||
} else if hasDot {
|
||||
pagePathDir = path.Dir(pagePathDir)
|
||||
}
|
||||
|
||||
if !isHtmlIndex(pagePath) {
|
||||
link = pagePath
|
||||
} else if !hasSlash {
|
||||
link += slash
|
||||
}
|
||||
|
||||
linkDir = pagePathDir
|
||||
|
||||
if d.ForcePrefix {
|
||||
|
||||
// Prepend language prefix if not already set in URL
|
||||
if d.PrefixFilePath != "" && !strings.HasPrefix(d.URL, slash+d.PrefixFilePath) {
|
||||
pagePath = pjoin(d.PrefixFilePath, pagePath)
|
||||
pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
|
||||
}
|
||||
|
||||
if d.PrefixLink != "" && !strings.HasPrefix(d.URL, slash+d.PrefixLink) {
|
||||
link = pjoin(d.PrefixLink, link)
|
||||
linkDir = pjoin(d.PrefixLink, linkDir)
|
||||
}
|
||||
}
|
||||
|
||||
} else if d.Kind == KindPage {
|
||||
|
||||
if d.ExpandedPermalink != "" {
|
||||
pagePath = pjoin(pagePath, d.ExpandedPermalink)
|
||||
|
||||
} else {
|
||||
if d.Dir != "" {
|
||||
pagePath = pjoin(pagePath, d.Dir)
|
||||
}
|
||||
if d.BaseName != "" {
|
||||
pagePath = pjoin(pagePath, d.BaseName)
|
||||
}
|
||||
}
|
||||
|
||||
if d.Addends != "" {
|
||||
pagePath = pjoin(pagePath, d.Addends)
|
||||
}
|
||||
|
||||
link = pagePath
|
||||
|
||||
if baseNameSameAsType {
|
||||
link = strings.TrimSuffix(link, d.BaseName)
|
||||
}
|
||||
|
||||
pagePathDir = link
|
||||
link = link + slash
|
||||
linkDir = pagePathDir
|
||||
|
||||
if isUgly {
|
||||
pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix())
|
||||
} else {
|
||||
pagePath = pjoin(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
|
||||
}
|
||||
|
||||
if isUgly && !isHtmlIndex(pagePath) {
|
||||
link = pagePath
|
||||
}
|
||||
|
||||
if d.PrefixFilePath != "" {
|
||||
pagePath = pjoin(d.PrefixFilePath, pagePath)
|
||||
pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
|
||||
}
|
||||
|
||||
if d.PrefixLink != "" {
|
||||
link = pjoin(d.PrefixLink, link)
|
||||
linkDir = pjoin(d.PrefixLink, linkDir)
|
||||
}
|
||||
|
||||
} else {
|
||||
if d.Addends != "" {
|
||||
pagePath = pjoin(pagePath, d.Addends)
|
||||
}
|
||||
|
||||
needsBase = needsBase && d.Addends == ""
|
||||
|
||||
// No permalink expansion etc. for node type pages (for now)
|
||||
base := ""
|
||||
|
||||
if needsBase || !isUgly {
|
||||
base = d.Type.BaseName
|
||||
}
|
||||
|
||||
pagePathDir = pagePath
|
||||
link = pagePath
|
||||
linkDir = pagePathDir
|
||||
|
||||
if base != "" {
|
||||
pagePath = path.Join(pagePath, addSuffix(base, d.Type.MediaType.FullSuffix()))
|
||||
} else {
|
||||
pagePath = addSuffix(pagePath, d.Type.MediaType.FullSuffix())
|
||||
|
||||
}
|
||||
|
||||
if !isHtmlIndex(pagePath) {
|
||||
link = pagePath
|
||||
} else {
|
||||
link += slash
|
||||
}
|
||||
|
||||
if d.PrefixFilePath != "" {
|
||||
pagePath = pjoin(d.PrefixFilePath, pagePath)
|
||||
pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
|
||||
}
|
||||
|
||||
if d.PrefixLink != "" {
|
||||
link = pjoin(d.PrefixLink, link)
|
||||
linkDir = pjoin(d.PrefixLink, linkDir)
|
||||
}
|
||||
}
|
||||
|
||||
pagePath = pjoin(slash, pagePath)
|
||||
pagePathDir = strings.TrimSuffix(path.Join(slash, pagePathDir), slash)
|
||||
|
||||
hadSlash := strings.HasSuffix(link, slash)
|
||||
link = strings.Trim(link, slash)
|
||||
if hadSlash {
|
||||
link += slash
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(link, slash) {
|
||||
link = slash + link
|
||||
}
|
||||
|
||||
linkDir = strings.TrimSuffix(path.Join(slash, linkDir), slash)
|
||||
|
||||
// Note: MakePathSanitized will lower case the path if
|
||||
// disablePathToLower isn't set.
|
||||
pagePath = d.PathSpec.MakePathSanitized(pagePath)
|
||||
pagePathDir = d.PathSpec.MakePathSanitized(pagePathDir)
|
||||
link = d.PathSpec.MakePathSanitized(link)
|
||||
linkDir = d.PathSpec.MakePathSanitized(linkDir)
|
||||
|
||||
tp.TargetFilename = filepath.FromSlash(pagePath)
|
||||
tp.SubResourceBaseTarget = filepath.FromSlash(pagePathDir)
|
||||
tp.SubResourceBaseLink = linkDir
|
||||
tp.Link = d.PathSpec.URLizeFilename(link)
|
||||
if tp.Link == "" {
|
||||
tp.Link = slash
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func addSuffix(s, suffix string) string {
|
||||
return strings.Trim(s, slash) + suffix
|
||||
}
|
||||
|
||||
// Like path.Join, but preserves one trailing slash if present.
|
||||
func pjoin(elem ...string) string {
|
||||
hadSlash := strings.HasSuffix(elem[len(elem)-1], slash)
|
||||
joined := path.Join(elem...)
|
||||
if hadSlash && !strings.HasSuffix(joined, slash) {
|
||||
return joined + slash
|
||||
}
|
||||
return joined
|
||||
}
|
258
resources/page/page_paths_test.go
Normal file
258
resources/page/page_paths_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/media"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/gohugoio/hugo/output"
|
||||
)
|
||||
|
||||
func TestPageTargetPath(t *testing.T) {
|
||||
|
||||
pathSpec := newTestPathSpec()
|
||||
|
||||
noExtNoDelimMediaType := media.TextType
|
||||
noExtNoDelimMediaType.Suffixes = []string{}
|
||||
noExtNoDelimMediaType.Delimiter = ""
|
||||
|
||||
// Netlify style _redirects
|
||||
noExtDelimFormat := output.Format{
|
||||
Name: "NER",
|
||||
MediaType: noExtNoDelimMediaType,
|
||||
BaseName: "_redirects",
|
||||
}
|
||||
|
||||
for _, langPrefixPath := range []string{"", "no"} {
|
||||
for _, langPrefixLink := range []string{"", "no"} {
|
||||
for _, uglyURLs := range []bool{false, true} {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
d TargetPathDescriptor
|
||||
expected TargetPaths
|
||||
}{
|
||||
{"JSON home", TargetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, TargetPaths{TargetFilename: "/index.json", SubResourceBaseTarget: "", Link: "/index.json"}},
|
||||
{"AMP home", TargetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, TargetPaths{TargetFilename: "/amp/index.html", SubResourceBaseTarget: "/amp", Link: "/amp/"}},
|
||||
{"HTML home", TargetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/index.html", SubResourceBaseTarget: "", Link: "/"}},
|
||||
{"Netlify redirects", TargetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, TargetPaths{TargetFilename: "/_redirects", SubResourceBaseTarget: "", Link: "/_redirects"}},
|
||||
{"HTML section list", TargetPathDescriptor{
|
||||
Kind: KindSection,
|
||||
Sections: []string{"sect1"},
|
||||
BaseName: "_index",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/sect1/index.html", SubResourceBaseTarget: "/sect1", Link: "/sect1/"}},
|
||||
{"HTML taxonomy list", TargetPathDescriptor{
|
||||
Kind: KindTaxonomy,
|
||||
Sections: []string{"tags", "hugo"},
|
||||
BaseName: "_index",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/tags/hugo/index.html", SubResourceBaseTarget: "/tags/hugo", Link: "/tags/hugo/"}},
|
||||
{"HTML taxonomy term", TargetPathDescriptor{
|
||||
Kind: KindTaxonomy,
|
||||
Sections: []string{"tags"},
|
||||
BaseName: "_index",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/tags/index.html", SubResourceBaseTarget: "/tags", Link: "/tags/"}},
|
||||
{
|
||||
"HTML page", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/a/b",
|
||||
BaseName: "mypage",
|
||||
Sections: []string{"a"},
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/mypage/index.html", SubResourceBaseTarget: "/a/b/mypage", Link: "/a/b/mypage/"}},
|
||||
|
||||
{
|
||||
"HTML page with index as base", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/a/b",
|
||||
BaseName: "index",
|
||||
Sections: []string{"a"},
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/index.html", SubResourceBaseTarget: "/a/b", Link: "/a/b/"}},
|
||||
|
||||
{
|
||||
"HTML page with special chars", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/a/b",
|
||||
BaseName: "My Page!",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/my-page/index.html", SubResourceBaseTarget: "/a/b/my-page", Link: "/a/b/my-page/"}},
|
||||
{"RSS home", TargetPathDescriptor{Kind: "rss", Type: output.RSSFormat}, TargetPaths{TargetFilename: "/index.xml", SubResourceBaseTarget: "", Link: "/index.xml"}},
|
||||
{"RSS section list", TargetPathDescriptor{
|
||||
Kind: "rss",
|
||||
Sections: []string{"sect1"},
|
||||
Type: output.RSSFormat}, TargetPaths{TargetFilename: "/sect1/index.xml", SubResourceBaseTarget: "/sect1", Link: "/sect1/index.xml"}},
|
||||
{
|
||||
"AMP page", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/a/b/c",
|
||||
BaseName: "myamp",
|
||||
Type: output.AMPFormat}, TargetPaths{TargetFilename: "/amp/a/b/c/myamp/index.html", SubResourceBaseTarget: "/amp/a/b/c/myamp", Link: "/amp/a/b/c/myamp/"}},
|
||||
{
|
||||
"AMP page with URL with suffix", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/sect/",
|
||||
BaseName: "mypage",
|
||||
URL: "/some/other/url.xhtml",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/some/other/url.xhtml", SubResourceBaseTarget: "/some/other", Link: "/some/other/url.xhtml"}},
|
||||
{
|
||||
"JSON page with URL without suffix", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/sect/",
|
||||
BaseName: "mypage",
|
||||
URL: "/some/other/path/",
|
||||
Type: output.JSONFormat}, TargetPaths{TargetFilename: "/some/other/path/index.json", SubResourceBaseTarget: "/some/other/path", Link: "/some/other/path/index.json"}},
|
||||
{
|
||||
"JSON page with URL without suffix and no trailing slash", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/sect/",
|
||||
BaseName: "mypage",
|
||||
URL: "/some/other/path",
|
||||
Type: output.JSONFormat}, TargetPaths{TargetFilename: "/some/other/path/index.json", SubResourceBaseTarget: "/some/other/path", Link: "/some/other/path/index.json"}},
|
||||
{
|
||||
"HTML page with URL without suffix and no trailing slash", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/sect/",
|
||||
BaseName: "mypage",
|
||||
URL: "/some/other/path",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/some/other/path/index.html", SubResourceBaseTarget: "/some/other/path", Link: "/some/other/path/"}},
|
||||
{
|
||||
"HTML page with expanded permalink", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/a/b",
|
||||
BaseName: "mypage",
|
||||
ExpandedPermalink: "/2017/10/my-title/",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/2017/10/my-title/index.html", SubResourceBaseTarget: "/2017/10/my-title", Link: "/2017/10/my-title/"}},
|
||||
{
|
||||
"Paginated HTML home", TargetPathDescriptor{
|
||||
Kind: KindHome,
|
||||
BaseName: "_index",
|
||||
Type: output.HTMLFormat,
|
||||
Addends: "page/3"}, TargetPaths{TargetFilename: "/page/3/index.html", SubResourceBaseTarget: "/page/3", Link: "/page/3/"}},
|
||||
{
|
||||
"Paginated Taxonomy list", TargetPathDescriptor{
|
||||
Kind: KindTaxonomy,
|
||||
BaseName: "_index",
|
||||
Sections: []string{"tags", "hugo"},
|
||||
Type: output.HTMLFormat,
|
||||
Addends: "page/3"}, TargetPaths{TargetFilename: "/tags/hugo/page/3/index.html", SubResourceBaseTarget: "/tags/hugo/page/3", Link: "/tags/hugo/page/3/"}},
|
||||
{
|
||||
"Regular page with addend", TargetPathDescriptor{
|
||||
Kind: KindPage,
|
||||
Dir: "/a/b",
|
||||
BaseName: "mypage",
|
||||
Addends: "c/d/e",
|
||||
Type: output.HTMLFormat}, TargetPaths{TargetFilename: "/a/b/mypage/c/d/e/index.html", SubResourceBaseTarget: "/a/b/mypage/c/d/e", Link: "/a/b/mypage/c/d/e/"}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("langPrefixPath=%s,langPrefixLink=%s,uglyURLs=%t,name=%s", langPrefixPath, langPrefixLink, uglyURLs, test.name),
|
||||
func(t *testing.T) {
|
||||
|
||||
test.d.ForcePrefix = true
|
||||
test.d.PathSpec = pathSpec
|
||||
test.d.UglyURLs = uglyURLs
|
||||
test.d.PrefixFilePath = langPrefixPath
|
||||
test.d.PrefixLink = langPrefixLink
|
||||
test.d.Dir = filepath.FromSlash(test.d.Dir)
|
||||
isUgly := uglyURLs && !test.d.Type.NoUgly
|
||||
|
||||
expected := test.expected
|
||||
|
||||
// TODO(bep) simplify
|
||||
if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName {
|
||||
} else if test.d.Kind == KindHome && test.d.Type.Path != "" {
|
||||
} else if test.d.Type.MediaType.Suffix() != "" && (!strings.HasPrefix(expected.TargetFilename, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly {
|
||||
expected.TargetFilename = strings.Replace(expected.TargetFilename,
|
||||
"/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix(),
|
||||
"."+test.d.Type.MediaType.Suffix(), 1)
|
||||
expected.Link = strings.TrimSuffix(expected.Link, "/") + "." + test.d.Type.MediaType.Suffix()
|
||||
|
||||
}
|
||||
|
||||
if test.d.PrefixFilePath != "" && !strings.HasPrefix(test.d.URL, "/"+test.d.PrefixFilePath) {
|
||||
expected.TargetFilename = "/" + test.d.PrefixFilePath + expected.TargetFilename
|
||||
expected.SubResourceBaseTarget = "/" + test.d.PrefixFilePath + expected.SubResourceBaseTarget
|
||||
}
|
||||
|
||||
if test.d.PrefixLink != "" && !strings.HasPrefix(test.d.URL, "/"+test.d.PrefixLink) {
|
||||
expected.Link = "/" + test.d.PrefixLink + expected.Link
|
||||
}
|
||||
|
||||
expected.TargetFilename = filepath.FromSlash(expected.TargetFilename)
|
||||
expected.SubResourceBaseTarget = filepath.FromSlash(expected.SubResourceBaseTarget)
|
||||
|
||||
pagePath := CreateTargetPaths(test.d)
|
||||
|
||||
if !eqTargetPaths(pagePath, expected) {
|
||||
t.Fatalf("[%d] [%s] targetPath expected\n%#v, got:\n%#v", i, test.name, expected, pagePath)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPageTargetPathPrefix(t *testing.T) {
|
||||
pathSpec := newTestPathSpec()
|
||||
tests := []struct {
|
||||
name string
|
||||
d TargetPathDescriptor
|
||||
expected TargetPaths
|
||||
}{
|
||||
{"URL set, prefix both, no force", TargetPathDescriptor{Kind: KindPage, Type: output.JSONFormat, URL: "/mydir/my.json", ForcePrefix: false, PrefixFilePath: "pf", PrefixLink: "pl"},
|
||||
TargetPaths{TargetFilename: "/mydir/my.json", SubResourceBaseTarget: "/mydir", SubResourceBaseLink: "/mydir", Link: "/mydir/my.json"}},
|
||||
{"URL set, prefix both, force", TargetPathDescriptor{Kind: KindPage, Type: output.JSONFormat, URL: "/mydir/my.json", ForcePrefix: true, PrefixFilePath: "pf", PrefixLink: "pl"},
|
||||
TargetPaths{TargetFilename: "/pf/mydir/my.json", SubResourceBaseTarget: "/pf/mydir", SubResourceBaseLink: "/pl/mydir", Link: "/pl/mydir/my.json"}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf(test.name),
|
||||
func(t *testing.T) {
|
||||
test.d.PathSpec = pathSpec
|
||||
expected := test.expected
|
||||
expected.TargetFilename = filepath.FromSlash(expected.TargetFilename)
|
||||
expected.SubResourceBaseTarget = filepath.FromSlash(expected.SubResourceBaseTarget)
|
||||
|
||||
pagePath := CreateTargetPaths(test.d)
|
||||
|
||||
if pagePath != expected {
|
||||
t.Fatalf("[%d] [%s] targetPath expected\n%#v, got:\n%#v", i, test.name, expected, pagePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func eqTargetPaths(p1, p2 TargetPaths) bool {
|
||||
|
||||
if p1.Link != p2.Link {
|
||||
return false
|
||||
}
|
||||
|
||||
if p1.SubResourceBaseTarget != p2.SubResourceBaseTarget {
|
||||
return false
|
||||
}
|
||||
|
||||
if p1.TargetFilename != p2.TargetFilename {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
97
resources/page/page_wrappers.autogen.go
Normal file
97
resources/page/page_wrappers.autogen.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// This file is autogenerated.
|
||||
|
||||
package page
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
// NewDeprecatedWarningPage adds deprecation warnings to the given implementation.
|
||||
func NewDeprecatedWarningPage(p DeprecatedWarningPageMethods) DeprecatedWarningPageMethods {
|
||||
return &pageDeprecated{p: p}
|
||||
}
|
||||
|
||||
type pageDeprecated struct {
|
||||
p DeprecatedWarningPageMethods
|
||||
}
|
||||
|
||||
func (p *pageDeprecated) Filename() string {
|
||||
helpers.Deprecated("Page", ".Filename", "Use .File.Filename", false)
|
||||
return p.p.Filename()
|
||||
}
|
||||
func (p *pageDeprecated) Dir() string {
|
||||
helpers.Deprecated("Page", ".Dir", "Use .File.Dir", false)
|
||||
return p.p.Dir()
|
||||
}
|
||||
func (p *pageDeprecated) IsDraft() bool {
|
||||
helpers.Deprecated("Page", ".IsDraft", "Use .Draft.", false)
|
||||
return p.p.IsDraft()
|
||||
}
|
||||
func (p *pageDeprecated) Extension() string {
|
||||
helpers.Deprecated("Page", ".Extension", "Use .File.Extension", false)
|
||||
return p.p.Extension()
|
||||
}
|
||||
func (p *pageDeprecated) Hugo() hugo.Info {
|
||||
helpers.Deprecated("Page", ".Hugo", "Use the global hugo function.", false)
|
||||
return p.p.Hugo()
|
||||
}
|
||||
func (p *pageDeprecated) Ext() string {
|
||||
helpers.Deprecated("Page", ".Ext", "Use .File.Ext", false)
|
||||
return p.p.Ext()
|
||||
}
|
||||
func (p *pageDeprecated) LanguagePrefix() string {
|
||||
helpers.Deprecated("Page", ".LanguagePrefix", "Use .Site.LanguagePrefix.", false)
|
||||
return p.p.LanguagePrefix()
|
||||
}
|
||||
func (p *pageDeprecated) GetParam(arg0 string) interface{} {
|
||||
helpers.Deprecated("Page", ".GetParam", "Use .Param or .Params.myParam.", false)
|
||||
return p.p.GetParam(arg0)
|
||||
}
|
||||
func (p *pageDeprecated) LogicalName() string {
|
||||
helpers.Deprecated("Page", ".LogicalName", "Use .File.LogicalName", false)
|
||||
return p.p.LogicalName()
|
||||
}
|
||||
func (p *pageDeprecated) BaseFileName() string {
|
||||
helpers.Deprecated("Page", ".BaseFileName", "Use .File.BaseFileName", false)
|
||||
return p.p.BaseFileName()
|
||||
}
|
||||
func (p *pageDeprecated) RSSLink() template.URL {
|
||||
helpers.Deprecated("Page", ".RSSLink", "Use the Output Format's link, e.g. something like: \n {{ with .OutputFormats.Get \"RSS\" }}{{ . RelPermalink }}{{ end }}", false)
|
||||
return p.p.RSSLink()
|
||||
}
|
||||
func (p *pageDeprecated) TranslationBaseName() string {
|
||||
helpers.Deprecated("Page", ".TranslationBaseName", "Use .File.TranslationBaseName", false)
|
||||
return p.p.TranslationBaseName()
|
||||
}
|
||||
func (p *pageDeprecated) URL() string {
|
||||
helpers.Deprecated("Page", ".URL", "Use .Permalink or .RelPermalink. If what you want is the front matter URL value, use .Params.url", false)
|
||||
return p.p.URL()
|
||||
}
|
||||
func (p *pageDeprecated) ContentBaseName() string {
|
||||
helpers.Deprecated("Page", ".ContentBaseName", "Use .File.ContentBaseName", false)
|
||||
return p.p.ContentBaseName()
|
||||
}
|
||||
func (p *pageDeprecated) UniqueID() string {
|
||||
helpers.Deprecated("Page", ".UniqueID", "Use .File.UniqueID", false)
|
||||
return p.p.UniqueID()
|
||||
}
|
||||
func (p *pageDeprecated) FileInfo() os.FileInfo {
|
||||
helpers.Deprecated("Page", ".FileInfo", "Use .File.FileInfo", false)
|
||||
return p.p.FileInfo()
|
||||
}
|
369
resources/page/pagegroup.go
Normal file
369
resources/page/pagegroup.go
Normal file
@@ -0,0 +1,369 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/collections"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
_ collections.Slicer = PageGroup{}
|
||||
)
|
||||
|
||||
// PageGroup represents a group of pages, grouped by the key.
|
||||
// The key is typically a year or similar.
|
||||
type PageGroup struct {
|
||||
Key interface{}
|
||||
Pages
|
||||
}
|
||||
|
||||
type mapKeyValues []reflect.Value
|
||||
|
||||
func (v mapKeyValues) Len() int { return len(v) }
|
||||
func (v mapKeyValues) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
|
||||
|
||||
type mapKeyByInt struct{ mapKeyValues }
|
||||
|
||||
func (s mapKeyByInt) Less(i, j int) bool { return s.mapKeyValues[i].Int() < s.mapKeyValues[j].Int() }
|
||||
|
||||
type mapKeyByStr struct{ mapKeyValues }
|
||||
|
||||
func (s mapKeyByStr) Less(i, j int) bool {
|
||||
return s.mapKeyValues[i].String() < s.mapKeyValues[j].String()
|
||||
}
|
||||
|
||||
func sortKeys(v []reflect.Value, order string) []reflect.Value {
|
||||
if len(v) <= 1 {
|
||||
return v
|
||||
}
|
||||
|
||||
switch v[0].Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if order == "desc" {
|
||||
sort.Sort(sort.Reverse(mapKeyByInt{v}))
|
||||
} else {
|
||||
sort.Sort(mapKeyByInt{v})
|
||||
}
|
||||
case reflect.String:
|
||||
if order == "desc" {
|
||||
sort.Sort(sort.Reverse(mapKeyByStr{v}))
|
||||
} else {
|
||||
sort.Sort(mapKeyByStr{v})
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// PagesGroup represents a list of page groups.
|
||||
// This is what you get when doing page grouping in the templates.
|
||||
type PagesGroup []PageGroup
|
||||
|
||||
// Reverse reverses the order of this list of page groups.
|
||||
func (p PagesGroup) Reverse() PagesGroup {
|
||||
for i, j := 0, len(p)-1; i < j; i, j = i+1, j-1 {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
pagePtrType = reflect.TypeOf((*Page)(nil)).Elem()
|
||||
pagesType = reflect.TypeOf(Pages{})
|
||||
)
|
||||
|
||||
// GroupBy groups by the value in the given field or method name and with the given order.
|
||||
// Valid values for order is asc, desc, rev and reverse.
|
||||
func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
|
||||
if len(p) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
direction := "asc"
|
||||
|
||||
if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") {
|
||||
direction = "desc"
|
||||
}
|
||||
|
||||
var ft interface{}
|
||||
m, ok := pagePtrType.MethodByName(key)
|
||||
if ok {
|
||||
if m.Type.NumOut() == 0 || m.Type.NumOut() > 2 {
|
||||
return nil, errors.New(key + " is a Page method but you can't use it with GroupBy")
|
||||
}
|
||||
if m.Type.NumOut() == 1 && m.Type.Out(0).Implements(errorType) {
|
||||
return nil, errors.New(key + " is a Page method but you can't use it with GroupBy")
|
||||
}
|
||||
if m.Type.NumOut() == 2 && !m.Type.Out(1).Implements(errorType) {
|
||||
return nil, errors.New(key + " is a Page method but you can't use it with GroupBy")
|
||||
}
|
||||
ft = m
|
||||
} else {
|
||||
ft, ok = pagePtrType.Elem().FieldByName(key)
|
||||
if !ok {
|
||||
return nil, errors.New(key + " is neither a field nor a method of Page")
|
||||
}
|
||||
}
|
||||
|
||||
var tmp reflect.Value
|
||||
switch e := ft.(type) {
|
||||
case reflect.StructField:
|
||||
tmp = reflect.MakeMap(reflect.MapOf(e.Type, pagesType))
|
||||
case reflect.Method:
|
||||
tmp = reflect.MakeMap(reflect.MapOf(e.Type.Out(0), pagesType))
|
||||
}
|
||||
|
||||
for _, e := range p {
|
||||
ppv := reflect.ValueOf(e)
|
||||
var fv reflect.Value
|
||||
switch ft.(type) {
|
||||
case reflect.StructField:
|
||||
fv = ppv.Elem().FieldByName(key)
|
||||
case reflect.Method:
|
||||
fv = ppv.MethodByName(key).Call([]reflect.Value{})[0]
|
||||
}
|
||||
if !fv.IsValid() {
|
||||
continue
|
||||
}
|
||||
if !tmp.MapIndex(fv).IsValid() {
|
||||
tmp.SetMapIndex(fv, reflect.MakeSlice(pagesType, 0, 0))
|
||||
}
|
||||
tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv))
|
||||
}
|
||||
|
||||
sortedKeys := sortKeys(tmp.MapKeys(), direction)
|
||||
r := make([]PageGroup, len(sortedKeys))
|
||||
for i, k := range sortedKeys {
|
||||
r[i] = PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GroupByParam groups by the given page parameter key's value and with the given order.
|
||||
// Valid values for order is asc, desc, rev and reverse.
|
||||
func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) {
|
||||
if len(p) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
direction := "asc"
|
||||
|
||||
if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") {
|
||||
direction = "desc"
|
||||
}
|
||||
|
||||
var tmp reflect.Value
|
||||
var keyt reflect.Type
|
||||
for _, e := range p {
|
||||
param := resource.GetParamToLower(e, key)
|
||||
if param != nil {
|
||||
if _, ok := param.([]string); !ok {
|
||||
keyt = reflect.TypeOf(param)
|
||||
tmp = reflect.MakeMap(reflect.MapOf(keyt, pagesType))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !tmp.IsValid() {
|
||||
return nil, errors.New("there is no such a param")
|
||||
}
|
||||
|
||||
for _, e := range p {
|
||||
param := resource.GetParam(e, key)
|
||||
|
||||
if param == nil || reflect.TypeOf(param) != keyt {
|
||||
continue
|
||||
}
|
||||
v := reflect.ValueOf(param)
|
||||
if !tmp.MapIndex(v).IsValid() {
|
||||
tmp.SetMapIndex(v, reflect.MakeSlice(pagesType, 0, 0))
|
||||
}
|
||||
tmp.SetMapIndex(v, reflect.Append(tmp.MapIndex(v), reflect.ValueOf(e)))
|
||||
}
|
||||
|
||||
var r []PageGroup
|
||||
for _, k := range sortKeys(tmp.MapKeys(), direction) {
|
||||
r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)})
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (p Pages) groupByDateField(sorter func(p Pages) Pages, formatter func(p Page) string, order ...string) (PagesGroup, error) {
|
||||
if len(p) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sp := sorter(p)
|
||||
|
||||
if !(len(order) > 0 && (strings.ToLower(order[0]) == "asc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse")) {
|
||||
sp = sp.Reverse()
|
||||
}
|
||||
|
||||
date := formatter(sp[0].(Page))
|
||||
var r []PageGroup
|
||||
r = append(r, PageGroup{Key: date, Pages: make(Pages, 0)})
|
||||
r[0].Pages = append(r[0].Pages, sp[0])
|
||||
|
||||
i := 0
|
||||
for _, e := range sp[1:] {
|
||||
date = formatter(e.(Page))
|
||||
if r[i].Key.(string) != date {
|
||||
r = append(r, PageGroup{Key: date})
|
||||
i++
|
||||
}
|
||||
r[i].Pages = append(r[i].Pages, e)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GroupByDate groups by the given page's Date value in
|
||||
// the given format and with the given order.
|
||||
// Valid values for order is asc, desc, rev and reverse.
|
||||
// For valid format strings, see https://golang.org/pkg/time/#Time.Format
|
||||
func (p Pages) GroupByDate(format string, order ...string) (PagesGroup, error) {
|
||||
sorter := func(p Pages) Pages {
|
||||
return p.ByDate()
|
||||
}
|
||||
formatter := func(p Page) string {
|
||||
return p.Date().Format(format)
|
||||
}
|
||||
return p.groupByDateField(sorter, formatter, order...)
|
||||
}
|
||||
|
||||
// GroupByPublishDate groups by the given page's PublishDate value in
|
||||
// the given format and with the given order.
|
||||
// Valid values for order is asc, desc, rev and reverse.
|
||||
// For valid format strings, see https://golang.org/pkg/time/#Time.Format
|
||||
func (p Pages) GroupByPublishDate(format string, order ...string) (PagesGroup, error) {
|
||||
sorter := func(p Pages) Pages {
|
||||
return p.ByPublishDate()
|
||||
}
|
||||
formatter := func(p Page) string {
|
||||
return p.PublishDate().Format(format)
|
||||
}
|
||||
return p.groupByDateField(sorter, formatter, order...)
|
||||
}
|
||||
|
||||
// GroupByExpiryDate groups by the given page's ExpireDate value in
|
||||
// the given format and with the given order.
|
||||
// Valid values for order is asc, desc, rev and reverse.
|
||||
// For valid format strings, see https://golang.org/pkg/time/#Time.Format
|
||||
func (p Pages) GroupByExpiryDate(format string, order ...string) (PagesGroup, error) {
|
||||
sorter := func(p Pages) Pages {
|
||||
return p.ByExpiryDate()
|
||||
}
|
||||
formatter := func(p Page) string {
|
||||
return p.ExpiryDate().Format(format)
|
||||
}
|
||||
return p.groupByDateField(sorter, formatter, order...)
|
||||
}
|
||||
|
||||
// GroupByParamDate groups by a date set as a param on the page in
|
||||
// the given format and with the given order.
|
||||
// Valid values for order is asc, desc, rev and reverse.
|
||||
// For valid format strings, see https://golang.org/pkg/time/#Time.Format
|
||||
func (p Pages) GroupByParamDate(key string, format string, order ...string) (PagesGroup, error) {
|
||||
sorter := func(p Pages) Pages {
|
||||
var r Pages
|
||||
for _, e := range p {
|
||||
param := resource.GetParamToLower(e, key)
|
||||
if _, ok := param.(time.Time); ok {
|
||||
r = append(r, e)
|
||||
}
|
||||
}
|
||||
pdate := func(p1, p2 Page) bool {
|
||||
p1p, p2p := p1.(Page), p2.(Page)
|
||||
return resource.GetParamToLower(p1p, key).(time.Time).Unix() < resource.GetParamToLower(p2p, key).(time.Time).Unix()
|
||||
}
|
||||
pageBy(pdate).Sort(r)
|
||||
return r
|
||||
}
|
||||
formatter := func(p Page) string {
|
||||
return resource.GetParamToLower(p, key).(time.Time).Format(format)
|
||||
}
|
||||
return p.groupByDateField(sorter, formatter, order...)
|
||||
}
|
||||
|
||||
// Slice is not meant to be used externally. It's a bridge function
|
||||
// for the template functions. See collections.Slice.
|
||||
func (p PageGroup) Slice(in interface{}) (interface{}, error) {
|
||||
switch items := in.(type) {
|
||||
case PageGroup:
|
||||
return items, nil
|
||||
case []interface{}:
|
||||
groups := make(PagesGroup, len(items))
|
||||
for i, v := range items {
|
||||
g, ok := v.(PageGroup)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type %T is not a PageGroup", v)
|
||||
}
|
||||
groups[i] = g
|
||||
}
|
||||
return groups, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid slice type %T", items)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ToPagesGroup tries to convert seq into a PagesGroup.
|
||||
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, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
409
resources/page/pagegroup_test.go
Normal file
409
resources/page/pagegroup_test.go
Normal file
@@ -0,0 +1,409 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type pageGroupTestObject struct {
|
||||
path string
|
||||
weight int
|
||||
date string
|
||||
param string
|
||||
}
|
||||
|
||||
var pageGroupTestSources = []pageGroupTestObject{
|
||||
{"/section1/testpage1.md", 3, "2012-04-06", "foo"},
|
||||
{"/section1/testpage2.md", 3, "2012-01-01", "bar"},
|
||||
{"/section1/testpage3.md", 2, "2012-04-06", "foo"},
|
||||
{"/section2/testpage4.md", 1, "2012-03-02", "bar"},
|
||||
{"/section2/testpage5.md", 1, "2012-04-06", "baz"},
|
||||
}
|
||||
|
||||
func preparePageGroupTestPages(t *testing.T) Pages {
|
||||
var pages Pages
|
||||
for _, src := range pageGroupTestSources {
|
||||
p := newTestPage()
|
||||
p.path = src.path
|
||||
if p.path != "" {
|
||||
p.section = strings.Split(strings.TrimPrefix(p.path, "/"), "/")[0]
|
||||
}
|
||||
p.weight = src.weight
|
||||
p.date = cast.ToTime(src.date)
|
||||
p.pubDate = cast.ToTime(src.date)
|
||||
p.expiryDate = cast.ToTime(src.date)
|
||||
p.params["custom_param"] = src.param
|
||||
p.params["custom_date"] = cast.ToTime(src.date)
|
||||
pages = append(pages, p)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
func TestGroupByWithFieldNameArg(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: 1, Pages: Pages{pages[3], pages[4]}},
|
||||
{Key: 2, Pages: Pages{pages[2]}},
|
||||
{Key: 3, Pages: Pages{pages[0], pages[1]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupBy("Weight")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByWithMethodNameArg(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "section1", Pages: Pages{pages[0], pages[1], pages[2]}},
|
||||
{Key: "section2", Pages: Pages{pages[3], pages[4]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupBy("Type")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByWithSectionArg(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "section1", Pages: Pages{pages[0], pages[1], pages[2]}},
|
||||
{Key: "section2", Pages: Pages{pages[3], pages[4]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupBy("Section")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be\n%#v, got\n%#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByInReverseOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: 3, Pages: Pages{pages[0], pages[1]}},
|
||||
{Key: 2, Pages: Pages{pages[2]}},
|
||||
{Key: 1, Pages: Pages{pages[3], pages[4]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupBy("Weight", "desc")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByCalledWithEmptyPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pages Pages
|
||||
groups, err := pages.GroupBy("Weight")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if groups != nil {
|
||||
t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamCalledWithUnavailableKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
_, err := pages.GroupByParam("UnavailableKey")
|
||||
if err == nil {
|
||||
t.Errorf("GroupByParam should return an error but didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
|
||||
groups1, err := pages.GroupBy("Weight", "desc")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
|
||||
groups2, err := pages.GroupBy("Weight")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
groups2 = groups2.Reverse()
|
||||
|
||||
if !reflect.DeepEqual(groups2, groups1) {
|
||||
t.Errorf("PagesGroup is sorted in unexpected order. It should be %#v, got %#v", groups2, groups1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "bar", Pages: Pages{pages[1], pages[3]}},
|
||||
{Key: "baz", Pages: Pages{pages[4]}},
|
||||
{Key: "foo", Pages: Pages{pages[0], pages[2]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByParam("custom_param")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamInReverseOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "foo", Pages: Pages{pages[0], pages[2]}},
|
||||
{Key: "baz", Pages: Pages{pages[4]}},
|
||||
{Key: "bar", Pages: Pages{pages[1], pages[3]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByParam("custom_param", "desc")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
testStr := "TestString"
|
||||
p := newTestPage()
|
||||
p.params["custom_param"] = testStr
|
||||
pages := Pages{p}
|
||||
|
||||
groups, err := pages.GroupByParam("custom_param")
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(testStr, groups[0].Key)
|
||||
|
||||
}
|
||||
|
||||
func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
delete(pages[1].Params(), "custom_param")
|
||||
delete(pages[3].Params(), "custom_param")
|
||||
delete(pages[4].Params(), "custom_param")
|
||||
|
||||
expect := PagesGroup{
|
||||
{Key: "foo", Pages: Pages{pages[0], pages[2]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByParam("custom_param")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamCalledWithEmptyPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pages Pages
|
||||
groups, err := pages.GroupByParam("custom_param")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if groups != nil {
|
||||
t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamCalledWithUnavailableParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
_, err := pages.GroupByParam("unavailable_param")
|
||||
if err == nil {
|
||||
t.Errorf("GroupByParam should return an error but didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByDate("2006-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByDateInReverseOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByDate("2006-01", "asc")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByPublishDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByPublishDate("2006-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByPublishDateInReverseOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByDate("2006-01", "asc")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByPublishDateWithEmptyPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pages Pages
|
||||
groups, err := pages.GroupByPublishDate("2006-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if groups != nil {
|
||||
t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByExpiryDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByExpiryDate("2006-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByParamDate("custom_date", "2006-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamDateInReverseOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
expect := PagesGroup{
|
||||
{Key: "2012-01", Pages: Pages{pages[1]}},
|
||||
{Key: "2012-03", Pages: Pages{pages[3]}},
|
||||
{Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}},
|
||||
}
|
||||
|
||||
groups, err := pages.GroupByParamDate("custom_date", "2006-01", "asc")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(groups, expect) {
|
||||
t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupByParamDateWithEmptyPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
var pages Pages
|
||||
groups, err := pages.GroupByParamDate("custom_date", "2006-01")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to make PagesGroup array: %s", err)
|
||||
}
|
||||
if groups != nil {
|
||||
t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups)
|
||||
}
|
||||
}
|
427
resources/page/pagemeta/page_frontmatter.go
Normal file
427
resources/page/pagemeta/page_frontmatter.go
Normal file
@@ -0,0 +1,427 @@
|
||||
// Copyright 2019 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 pagemeta
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// FrontMatterHandler maps front matter into Page fields and .Params.
|
||||
// Note that we currently have only extracted the date logic.
|
||||
type FrontMatterHandler struct {
|
||||
fmConfig frontmatterConfig
|
||||
|
||||
dateHandler frontMatterFieldHandler
|
||||
lastModHandler frontMatterFieldHandler
|
||||
publishDateHandler frontMatterFieldHandler
|
||||
expiryDateHandler frontMatterFieldHandler
|
||||
|
||||
// A map of all date keys configured, including any custom.
|
||||
allDateKeys map[string]bool
|
||||
|
||||
logger *loggers.Logger
|
||||
}
|
||||
|
||||
// FrontMatterDescriptor describes how to handle front matter for a given Page.
|
||||
// It has pointers to values in the receiving page which gets updated.
|
||||
type FrontMatterDescriptor struct {
|
||||
|
||||
// This the Page's front matter.
|
||||
Frontmatter map[string]interface{}
|
||||
|
||||
// This is the Page's base filename (BaseFilename), e.g. page.md., or
|
||||
// if page is a leaf bundle, the bundle folder name (ContentBaseName).
|
||||
BaseFilename string
|
||||
|
||||
// The content file's mod time.
|
||||
ModTime time.Time
|
||||
|
||||
// May be set from the author date in Git.
|
||||
GitAuthorDate time.Time
|
||||
|
||||
// The below are pointers to values on Page and will be modified.
|
||||
|
||||
// This is the Page's params.
|
||||
Params map[string]interface{}
|
||||
|
||||
// This is the Page's dates.
|
||||
Dates *resource.Dates
|
||||
|
||||
// This is the Page's Slug etc.
|
||||
PageURLs *URLPath
|
||||
}
|
||||
|
||||
var (
|
||||
dateFieldAliases = map[string][]string{
|
||||
fmDate: {},
|
||||
fmLastmod: {"modified"},
|
||||
fmPubDate: {"pubdate", "published"},
|
||||
fmExpiryDate: {"unpublishdate"},
|
||||
}
|
||||
)
|
||||
|
||||
// HandleDates updates all the dates given the current configuration and the
|
||||
// supplied front matter params. Note that this requires all lower-case keys
|
||||
// in the params map.
|
||||
func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error {
|
||||
if d.Dates == nil {
|
||||
panic("missing dates")
|
||||
}
|
||||
|
||||
if f.dateHandler == nil {
|
||||
panic("missing date handler")
|
||||
}
|
||||
|
||||
if _, err := f.dateHandler(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.lastModHandler(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.publishDateHandler(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.expiryDateHandler(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsDateKey returns whether the given front matter key is considered a date by the current
|
||||
// configuration.
|
||||
func (f FrontMatterHandler) IsDateKey(key string) bool {
|
||||
return f.allDateKeys[key]
|
||||
}
|
||||
|
||||
// A Zero date is a signal that the name can not be parsed.
|
||||
// This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/:
|
||||
// "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers"
|
||||
func dateAndSlugFromBaseFilename(name string) (time.Time, string) {
|
||||
withoutExt, _ := helpers.FileAndExt(name)
|
||||
|
||||
if len(withoutExt) < 10 {
|
||||
// This can not be a date.
|
||||
return time.Time{}, ""
|
||||
}
|
||||
|
||||
// Note: Hugo currently have no custom timezone support.
|
||||
// We will have to revisit this when that is in place.
|
||||
d, err := time.Parse("2006-01-02", withoutExt[:10])
|
||||
if err != nil {
|
||||
return time.Time{}, ""
|
||||
}
|
||||
|
||||
// Be a little lenient with the format here.
|
||||
slug := strings.Trim(withoutExt[10:], " -_")
|
||||
|
||||
return d, slug
|
||||
}
|
||||
|
||||
type frontMatterFieldHandler func(d *FrontMatterDescriptor) (bool, error)
|
||||
|
||||
func (f FrontMatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler {
|
||||
return func(d *FrontMatterDescriptor) (bool, error) {
|
||||
for _, h := range handlers {
|
||||
// First successful handler wins.
|
||||
success, err := h(d)
|
||||
if err != nil {
|
||||
f.logger.ERROR.Println(err)
|
||||
} else if success {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
type frontmatterConfig struct {
|
||||
date []string
|
||||
lastmod []string
|
||||
publishDate []string
|
||||
expiryDate []string
|
||||
}
|
||||
|
||||
const (
|
||||
// These are all the date handler identifiers
|
||||
// All identifiers not starting with a ":" maps to a front matter parameter.
|
||||
fmDate = "date"
|
||||
fmPubDate = "publishdate"
|
||||
fmLastmod = "lastmod"
|
||||
fmExpiryDate = "expirydate"
|
||||
|
||||
// Gets date from filename, e.g 218-02-22-mypage.md
|
||||
fmFilename = ":filename"
|
||||
|
||||
// Gets date from file OS mod time.
|
||||
fmModTime = ":filemodtime"
|
||||
|
||||
// Gets date from Git
|
||||
fmGitAuthorDate = ":git"
|
||||
)
|
||||
|
||||
// This is the config you get when doing nothing.
|
||||
func newDefaultFrontmatterConfig() frontmatterConfig {
|
||||
return frontmatterConfig{
|
||||
date: []string{fmDate, fmPubDate, fmLastmod},
|
||||
lastmod: []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate},
|
||||
publishDate: []string{fmPubDate, fmDate},
|
||||
expiryDate: []string{fmExpiryDate},
|
||||
}
|
||||
}
|
||||
|
||||
func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) {
|
||||
c := newDefaultFrontmatterConfig()
|
||||
defaultConfig := c
|
||||
|
||||
if cfg.IsSet("frontmatter") {
|
||||
fm := cfg.GetStringMap("frontmatter")
|
||||
for k, v := range fm {
|
||||
loki := strings.ToLower(k)
|
||||
switch loki {
|
||||
case fmDate:
|
||||
c.date = toLowerSlice(v)
|
||||
case fmPubDate:
|
||||
c.publishDate = toLowerSlice(v)
|
||||
case fmLastmod:
|
||||
c.lastmod = toLowerSlice(v)
|
||||
case fmExpiryDate:
|
||||
c.expiryDate = toLowerSlice(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expander := func(c, d []string) []string {
|
||||
out := expandDefaultValues(c, d)
|
||||
out = addDateFieldAliases(out)
|
||||
return out
|
||||
}
|
||||
|
||||
c.date = expander(c.date, defaultConfig.date)
|
||||
c.publishDate = expander(c.publishDate, defaultConfig.publishDate)
|
||||
c.lastmod = expander(c.lastmod, defaultConfig.lastmod)
|
||||
c.expiryDate = expander(c.expiryDate, defaultConfig.expiryDate)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func addDateFieldAliases(values []string) []string {
|
||||
var complete []string
|
||||
|
||||
for _, v := range values {
|
||||
complete = append(complete, v)
|
||||
if aliases, found := dateFieldAliases[v]; found {
|
||||
complete = append(complete, aliases...)
|
||||
}
|
||||
}
|
||||
return helpers.UniqueStrings(complete)
|
||||
}
|
||||
|
||||
func expandDefaultValues(values []string, defaults []string) []string {
|
||||
var out []string
|
||||
for _, v := range values {
|
||||
if v == ":default" {
|
||||
out = append(out, defaults...)
|
||||
} else {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toLowerSlice(in interface{}) []string {
|
||||
out := cast.ToStringSlice(in)
|
||||
for i := 0; i < len(out); i++ {
|
||||
out[i] = strings.ToLower(out[i])
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration.
|
||||
// If no logger is provided, one will be created.
|
||||
func NewFrontmatterHandler(logger *loggers.Logger, cfg config.Provider) (FrontMatterHandler, error) {
|
||||
|
||||
if logger == nil {
|
||||
logger = loggers.NewErrorLogger()
|
||||
}
|
||||
|
||||
frontMatterConfig, err := newFrontmatterConfig(cfg)
|
||||
if err != nil {
|
||||
return FrontMatterHandler{}, err
|
||||
}
|
||||
|
||||
allDateKeys := make(map[string]bool)
|
||||
addKeys := func(vals []string) {
|
||||
for _, k := range vals {
|
||||
if !strings.HasPrefix(k, ":") {
|
||||
allDateKeys[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeys(frontMatterConfig.date)
|
||||
addKeys(frontMatterConfig.expiryDate)
|
||||
addKeys(frontMatterConfig.lastmod)
|
||||
addKeys(frontMatterConfig.publishDate)
|
||||
|
||||
f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys}
|
||||
|
||||
if err := f.createHandlers(); err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *FrontMatterHandler) createHandlers() error {
|
||||
var err error
|
||||
|
||||
if f.dateHandler, err = f.createDateHandler(f.fmConfig.date,
|
||||
func(d *FrontMatterDescriptor, t time.Time) {
|
||||
d.Dates.FDate = t
|
||||
setParamIfNotSet(fmDate, t, d)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.lastModHandler, err = f.createDateHandler(f.fmConfig.lastmod,
|
||||
func(d *FrontMatterDescriptor, t time.Time) {
|
||||
setParamIfNotSet(fmLastmod, t, d)
|
||||
d.Dates.FLastmod = t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.publishDate,
|
||||
func(d *FrontMatterDescriptor, t time.Time) {
|
||||
setParamIfNotSet(fmPubDate, t, d)
|
||||
d.Dates.FPublishDate = t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.expiryDate,
|
||||
func(d *FrontMatterDescriptor, t time.Time) {
|
||||
setParamIfNotSet(fmExpiryDate, t, d)
|
||||
d.Dates.FExpiryDate = t
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setParamIfNotSet(key string, value interface{}, d *FrontMatterDescriptor) {
|
||||
if _, found := d.Params[key]; found {
|
||||
return
|
||||
}
|
||||
d.Params[key] = value
|
||||
}
|
||||
|
||||
func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) {
|
||||
var h *frontmatterFieldHandlers
|
||||
var handlers []frontMatterFieldHandler
|
||||
|
||||
for _, identifier := range identifiers {
|
||||
switch identifier {
|
||||
case fmFilename:
|
||||
handlers = append(handlers, h.newDateFilenameHandler(setter))
|
||||
case fmModTime:
|
||||
handlers = append(handlers, h.newDateModTimeHandler(setter))
|
||||
case fmGitAuthorDate:
|
||||
handlers = append(handlers, h.newDateGitAuthorDateHandler(setter))
|
||||
default:
|
||||
handlers = append(handlers, h.newDateFieldHandler(identifier, setter))
|
||||
}
|
||||
}
|
||||
|
||||
return f.newChainedFrontMatterFieldHandler(handlers...), nil
|
||||
|
||||
}
|
||||
|
||||
type frontmatterFieldHandlers int
|
||||
|
||||
func (f *frontmatterFieldHandlers) newDateFieldHandler(key string, setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
|
||||
return func(d *FrontMatterDescriptor) (bool, error) {
|
||||
v, found := d.Frontmatter[key]
|
||||
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
date, err := cast.ToTimeE(v)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// We map several date keys to one, so, for example,
|
||||
// "expirydate", "unpublishdate" will all set .ExpiryDate (first found).
|
||||
setter(d, date)
|
||||
|
||||
// This is the params key as set in front matter.
|
||||
d.Params[key] = date
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontmatterFieldHandlers) newDateFilenameHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
|
||||
return func(d *FrontMatterDescriptor) (bool, error) {
|
||||
date, slug := dateAndSlugFromBaseFilename(d.BaseFilename)
|
||||
if date.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
setter(d, date)
|
||||
|
||||
if _, found := d.Frontmatter["slug"]; !found {
|
||||
// Use slug from filename
|
||||
d.PageURLs.Slug = slug
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontmatterFieldHandlers) newDateModTimeHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
|
||||
return func(d *FrontMatterDescriptor) (bool, error) {
|
||||
if d.ModTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
setter(d, d.ModTime)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frontmatterFieldHandlers) newDateGitAuthorDateHandler(setter func(d *FrontMatterDescriptor, t time.Time)) frontMatterFieldHandler {
|
||||
return func(d *FrontMatterDescriptor) (bool, error) {
|
||||
if d.GitAuthorDate.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
setter(d, d.GitAuthorDate)
|
||||
return true, nil
|
||||
}
|
||||
}
|
262
resources/page/pagemeta/page_frontmatter_test.go
Normal file
262
resources/page/pagemeta/page_frontmatter_test.go
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright 2019 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 pagemeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDateAndSlugFromBaseFilename(t *testing.T) {
|
||||
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
date string
|
||||
slug string
|
||||
}{
|
||||
{"page.md", "0001-01-01", ""},
|
||||
{"2012-09-12-page.md", "2012-09-12", "page"},
|
||||
{"2018-02-28-page.md", "2018-02-28", "page"},
|
||||
{"2018-02-28_page.md", "2018-02-28", "page"},
|
||||
{"2018-02-28 page.md", "2018-02-28", "page"},
|
||||
{"2018-02-28page.md", "2018-02-28", "page"},
|
||||
{"2018-02-28-.md", "2018-02-28", ""},
|
||||
{"2018-02-28-.md", "2018-02-28", ""},
|
||||
{"2018-02-28.md", "2018-02-28", ""},
|
||||
{"2018-02-28-page", "2018-02-28", "page"},
|
||||
{"2012-9-12-page.md", "0001-01-01", ""},
|
||||
{"asdfasdf.md", "0001-01-01", ""},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
expecteFDate, err := time.Parse("2006-01-02", test.date)
|
||||
assert.NoError(err)
|
||||
|
||||
errMsg := fmt.Sprintf("Test %d", i)
|
||||
gotDate, gotSlug := dateAndSlugFromBaseFilename(test.name)
|
||||
|
||||
assert.Equal(expecteFDate, gotDate, errMsg)
|
||||
assert.Equal(test.slug, gotSlug, errMsg)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func newTestFd() *FrontMatterDescriptor {
|
||||
return &FrontMatterDescriptor{
|
||||
Frontmatter: make(map[string]interface{}),
|
||||
Params: make(map[string]interface{}),
|
||||
Dates: &resource.Dates{},
|
||||
PageURLs: &URLPath{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontMatterNewConfig(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
cfg := viper.New()
|
||||
|
||||
cfg.Set("frontmatter", map[string]interface{}{
|
||||
"date": []string{"publishDate", "LastMod"},
|
||||
"Lastmod": []string{"publishDate"},
|
||||
"expiryDate": []string{"lastMod"},
|
||||
"publishDate": []string{"date"},
|
||||
})
|
||||
|
||||
fc, err := newFrontmatterConfig(cfg)
|
||||
assert.NoError(err)
|
||||
assert.Equal([]string{"publishdate", "pubdate", "published", "lastmod", "modified"}, fc.date)
|
||||
assert.Equal([]string{"publishdate", "pubdate", "published"}, fc.lastmod)
|
||||
assert.Equal([]string{"lastmod", "modified"}, fc.expiryDate)
|
||||
assert.Equal([]string{"date"}, fc.publishDate)
|
||||
|
||||
// Default
|
||||
cfg = viper.New()
|
||||
fc, err = newFrontmatterConfig(cfg)
|
||||
assert.NoError(err)
|
||||
assert.Equal([]string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}, fc.date)
|
||||
assert.Equal([]string{":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}, fc.lastmod)
|
||||
assert.Equal([]string{"expirydate", "unpublishdate"}, fc.expiryDate)
|
||||
assert.Equal([]string{"publishdate", "pubdate", "published", "date"}, fc.publishDate)
|
||||
|
||||
// :default keyword
|
||||
cfg.Set("frontmatter", map[string]interface{}{
|
||||
"date": []string{"d1", ":default"},
|
||||
"lastmod": []string{"d2", ":default"},
|
||||
"expiryDate": []string{"d3", ":default"},
|
||||
"publishDate": []string{"d4", ":default"},
|
||||
})
|
||||
fc, err = newFrontmatterConfig(cfg)
|
||||
assert.NoError(err)
|
||||
assert.Equal([]string{"d1", "date", "publishdate", "pubdate", "published", "lastmod", "modified"}, fc.date)
|
||||
assert.Equal([]string{"d2", ":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}, fc.lastmod)
|
||||
assert.Equal([]string{"d3", "expirydate", "unpublishdate"}, fc.expiryDate)
|
||||
assert.Equal([]string{"d4", "publishdate", "pubdate", "published", "date"}, fc.publishDate)
|
||||
|
||||
}
|
||||
|
||||
func TestFrontMatterDatesHandlers(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
for _, handlerID := range []string{":filename", ":fileModTime", ":git"} {
|
||||
|
||||
cfg := viper.New()
|
||||
|
||||
cfg.Set("frontmatter", map[string]interface{}{
|
||||
"date": []string{handlerID, "date"},
|
||||
})
|
||||
|
||||
handler, err := NewFrontmatterHandler(nil, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
d1, _ := time.Parse("2006-01-02", "2018-02-01")
|
||||
d2, _ := time.Parse("2006-01-02", "2018-02-02")
|
||||
|
||||
d := newTestFd()
|
||||
switch strings.ToLower(handlerID) {
|
||||
case ":filename":
|
||||
d.BaseFilename = "2018-02-01-page.md"
|
||||
case ":filemodtime":
|
||||
d.ModTime = d1
|
||||
case ":git":
|
||||
d.GitAuthorDate = d1
|
||||
}
|
||||
d.Frontmatter["date"] = d2
|
||||
assert.NoError(handler.HandleDates(d))
|
||||
assert.Equal(d1, d.Dates.FDate)
|
||||
assert.Equal(d2, d.Params["date"])
|
||||
|
||||
d = newTestFd()
|
||||
d.Frontmatter["date"] = d2
|
||||
assert.NoError(handler.HandleDates(d))
|
||||
assert.Equal(d2, d.Dates.FDate)
|
||||
assert.Equal(d2, d.Params["date"])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontMatterDatesCustomConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
cfg := viper.New()
|
||||
cfg.Set("frontmatter", map[string]interface{}{
|
||||
"date": []string{"mydate"},
|
||||
"lastmod": []string{"publishdate"},
|
||||
"publishdate": []string{"publishdate"},
|
||||
})
|
||||
|
||||
handler, err := NewFrontmatterHandler(nil, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
testDate, err := time.Parse("2006-01-02", "2018-02-01")
|
||||
assert.NoError(err)
|
||||
|
||||
d := newTestFd()
|
||||
d.Frontmatter["mydate"] = testDate
|
||||
testDate = testDate.Add(24 * time.Hour)
|
||||
d.Frontmatter["date"] = testDate
|
||||
testDate = testDate.Add(24 * time.Hour)
|
||||
d.Frontmatter["lastmod"] = testDate
|
||||
testDate = testDate.Add(24 * time.Hour)
|
||||
d.Frontmatter["publishdate"] = testDate
|
||||
testDate = testDate.Add(24 * time.Hour)
|
||||
d.Frontmatter["expirydate"] = testDate
|
||||
|
||||
assert.NoError(handler.HandleDates(d))
|
||||
|
||||
assert.Equal(1, d.Dates.FDate.Day())
|
||||
assert.Equal(4, d.Dates.FLastmod.Day())
|
||||
assert.Equal(4, d.Dates.FPublishDate.Day())
|
||||
assert.Equal(5, d.Dates.FExpiryDate.Day())
|
||||
|
||||
assert.Equal(d.Dates.FDate, d.Params["date"])
|
||||
assert.Equal(d.Dates.FDate, d.Params["mydate"])
|
||||
assert.Equal(d.Dates.FPublishDate, d.Params["publishdate"])
|
||||
assert.Equal(d.Dates.FExpiryDate, d.Params["expirydate"])
|
||||
|
||||
assert.False(handler.IsDateKey("date")) // This looks odd, but is configured like this.
|
||||
assert.True(handler.IsDateKey("mydate"))
|
||||
assert.True(handler.IsDateKey("publishdate"))
|
||||
assert.True(handler.IsDateKey("pubdate"))
|
||||
|
||||
}
|
||||
|
||||
func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
cfg := viper.New()
|
||||
|
||||
cfg.Set("frontmatter", map[string]interface{}{
|
||||
"date": []string{"mydate", ":default"},
|
||||
"publishdate": []string{":default", "mypubdate"},
|
||||
})
|
||||
|
||||
handler, err := NewFrontmatterHandler(nil, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
testDate, _ := time.Parse("2006-01-02", "2018-02-01")
|
||||
d := newTestFd()
|
||||
d.Frontmatter["mydate"] = testDate
|
||||
d.Frontmatter["date"] = testDate.Add(1 * 24 * time.Hour)
|
||||
d.Frontmatter["mypubdate"] = testDate.Add(2 * 24 * time.Hour)
|
||||
d.Frontmatter["publishdate"] = testDate.Add(3 * 24 * time.Hour)
|
||||
|
||||
assert.NoError(handler.HandleDates(d))
|
||||
|
||||
assert.Equal(1, d.Dates.FDate.Day())
|
||||
assert.Equal(2, d.Dates.FLastmod.Day())
|
||||
assert.Equal(4, d.Dates.FPublishDate.Day())
|
||||
assert.True(d.Dates.FExpiryDate.IsZero())
|
||||
|
||||
}
|
||||
|
||||
func TestExpandDefaultValues(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
assert.Equal([]string{"a", "b", "c", "d"}, expandDefaultValues([]string{"a", ":default", "d"}, []string{"b", "c"}))
|
||||
assert.Equal([]string{"a", "b", "c"}, expandDefaultValues([]string{"a", "b", "c"}, []string{"a", "b", "c"}))
|
||||
assert.Equal([]string{"b", "c", "a", "b", "c", "d"}, expandDefaultValues([]string{":default", "a", ":default", "d"}, []string{"b", "c"}))
|
||||
|
||||
}
|
||||
|
||||
func TestFrontMatterDateFieldHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
handlers := new(frontmatterFieldHandlers)
|
||||
|
||||
fd := newTestFd()
|
||||
d, _ := time.Parse("2006-01-02", "2018-02-01")
|
||||
fd.Frontmatter["date"] = d
|
||||
h := handlers.newDateFieldHandler("date", func(d *FrontMatterDescriptor, t time.Time) { d.Dates.FDate = t })
|
||||
|
||||
handled, err := h(fd)
|
||||
assert.True(handled)
|
||||
assert.NoError(err)
|
||||
assert.Equal(d, fd.Dates.FDate)
|
||||
}
|
21
resources/page/pagemeta/pagemeta.go
Normal file
21
resources/page/pagemeta/pagemeta.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019 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 pagemeta
|
||||
|
||||
type URLPath struct {
|
||||
URL string
|
||||
Permalink string
|
||||
Slug string
|
||||
Section string
|
||||
}
|
115
resources/page/pages.go
Normal file
115
resources/page/pages.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.ResourcesConverter = Pages{}
|
||||
)
|
||||
|
||||
// Pages is a slice of pages. This is the most common list type in Hugo.
|
||||
type Pages []Page
|
||||
|
||||
func (ps Pages) String() string {
|
||||
return fmt.Sprintf("Pages(%d)", len(ps))
|
||||
}
|
||||
|
||||
// Used in tests.
|
||||
func (ps Pages) shuffle() {
|
||||
for i := range ps {
|
||||
j := rand.Intn(i + 1)
|
||||
ps[i], ps[j] = ps[j], ps[i]
|
||||
}
|
||||
}
|
||||
|
||||
// ToResources wraps resource.ResourcesConverter
|
||||
func (pages Pages) ToResources() resource.Resources {
|
||||
r := make(resource.Resources, len(pages))
|
||||
for i, p := range pages {
|
||||
r[i] = p
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ToPages tries to convert seq into Pages.
|
||||
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 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)
|
||||
}
|
||||
|
||||
func (p Pages) Group(key interface{}, in interface{}) (interface{}, error) {
|
||||
pages, err := ToPages(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return PageGroup{Key: key, Pages: pages}, nil
|
||||
}
|
||||
|
||||
// Len returns the number of pages in the list.
|
||||
func (p Pages) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
func (ps Pages) removeFirstIfFound(p Page) Pages {
|
||||
ii := -1
|
||||
for i, pp := range ps {
|
||||
if p.Eq(pp) {
|
||||
ii = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ii != -1 {
|
||||
ps = append(ps[:ii], ps[ii+1:]...)
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
// PagesFactory somehow creates some Pages.
|
||||
// We do a lot of lazy Pages initialization in Hugo, so we need a type.
|
||||
type PagesFactory func() Pages
|
136
resources/page/pages_cache.go
Normal file
136
resources/page/pages_cache.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type pageCacheEntry struct {
|
||||
in []Pages
|
||||
out Pages
|
||||
}
|
||||
|
||||
func (entry pageCacheEntry) matches(pageLists []Pages) bool {
|
||||
if len(entry.in) != len(pageLists) {
|
||||
return false
|
||||
}
|
||||
for i, p := range pageLists {
|
||||
if !pagesEqual(p, entry.in[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type pageCache struct {
|
||||
sync.RWMutex
|
||||
m map[string][]pageCacheEntry
|
||||
}
|
||||
|
||||
func newPageCache() *pageCache {
|
||||
return &pageCache{m: make(map[string][]pageCacheEntry)}
|
||||
}
|
||||
|
||||
func (c *pageCache) clear() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.m = make(map[string][]pageCacheEntry)
|
||||
}
|
||||
|
||||
// get/getP gets a Pages slice from the cache matching the given key and
|
||||
// all the provided Pages slices.
|
||||
// If none found in cache, a copy of the first slice is created.
|
||||
//
|
||||
// If an apply func is provided, that func is applied to the newly created copy.
|
||||
//
|
||||
// The getP variant' apply func takes a pointer to Pages.
|
||||
//
|
||||
// The cache and the execution of the apply func is protected by a RWMutex.
|
||||
func (c *pageCache) get(key string, apply func(p Pages), pageLists ...Pages) (Pages, bool) {
|
||||
return c.getP(key, func(p *Pages) {
|
||||
if apply != nil {
|
||||
apply(*p)
|
||||
}
|
||||
}, pageLists...)
|
||||
}
|
||||
|
||||
func (c *pageCache) getP(key string, apply func(p *Pages), pageLists ...Pages) (Pages, bool) {
|
||||
c.RLock()
|
||||
if cached, ok := c.m[key]; ok {
|
||||
for _, entry := range cached {
|
||||
if entry.matches(pageLists) {
|
||||
c.RUnlock()
|
||||
return entry.out, true
|
||||
}
|
||||
}
|
||||
}
|
||||
c.RUnlock()
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
// double-check
|
||||
if cached, ok := c.m[key]; ok {
|
||||
for _, entry := range cached {
|
||||
if entry.matches(pageLists) {
|
||||
return entry.out, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p := pageLists[0]
|
||||
pagesCopy := append(Pages(nil), p...)
|
||||
|
||||
if apply != nil {
|
||||
apply(&pagesCopy)
|
||||
}
|
||||
|
||||
entry := pageCacheEntry{in: pageLists, out: pagesCopy}
|
||||
if v, ok := c.m[key]; ok {
|
||||
c.m[key] = append(v, entry)
|
||||
} else {
|
||||
c.m[key] = []pageCacheEntry{entry}
|
||||
}
|
||||
|
||||
return pagesCopy, false
|
||||
|
||||
}
|
||||
|
||||
// pagesEqual returns whether p1 and p2 are equal.
|
||||
func pagesEqual(p1, p2 Pages) bool {
|
||||
if p1 == nil && p2 == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if p1 == nil || p2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if p1.Len() != p2.Len() {
|
||||
return false
|
||||
}
|
||||
|
||||
if p1.Len() == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := 0; i < len(p1); i++ {
|
||||
if p1[i] != p2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
86
resources/page/pages_cache_test.go
Normal file
86
resources/page/pages_cache_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPageCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c1 := newPageCache()
|
||||
|
||||
changeFirst := func(p Pages) {
|
||||
p[0].(*testPage).description = "changed"
|
||||
}
|
||||
|
||||
var o1 uint64
|
||||
var o2 uint64
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var l1 sync.Mutex
|
||||
var l2 sync.Mutex
|
||||
|
||||
var testPageSets []Pages
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
testPageSets = append(testPageSets, createSortTestPages(i+1))
|
||||
}
|
||||
|
||||
for j := 0; j < 100; j++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for k, pages := range testPageSets {
|
||||
l1.Lock()
|
||||
p, c := c1.get("k1", nil, pages)
|
||||
assert.Equal(t, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)), c)
|
||||
l1.Unlock()
|
||||
p2, c2 := c1.get("k1", nil, p)
|
||||
assert.True(t, c2)
|
||||
assert.True(t, pagesEqual(p, p2))
|
||||
assert.True(t, pagesEqual(p, pages))
|
||||
assert.NotNil(t, p)
|
||||
|
||||
l2.Lock()
|
||||
p3, c3 := c1.get("k2", changeFirst, pages)
|
||||
assert.Equal(t, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)), c3)
|
||||
l2.Unlock()
|
||||
assert.NotNil(t, p3)
|
||||
assert.Equal(t, p3[0].(*testPage).description, "changed")
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkPageCache(b *testing.B) {
|
||||
cache := newPageCache()
|
||||
pages := make(Pages, 30)
|
||||
for i := 0; i < 30; i++ {
|
||||
pages[i] = &testPage{title: "p" + strconv.Itoa(i)}
|
||||
}
|
||||
key := "key"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.getP(key, nil, pages)
|
||||
}
|
||||
}
|
64
resources/page/pages_language_merge.go
Normal file
64
resources/page/pages_language_merge.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
_ pagesLanguageMerger = (*Pages)(nil)
|
||||
)
|
||||
|
||||
type pagesLanguageMerger interface {
|
||||
MergeByLanguage(other Pages) Pages
|
||||
// Needed for integration with the tpl package.
|
||||
MergeByLanguageInterface(other interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
// MergeByLanguage supplies missing translations in p1 with values from p2.
|
||||
// The result is sorted by the default sort order for pages.
|
||||
func (p1 Pages) MergeByLanguage(p2 Pages) Pages {
|
||||
merge := func(pages *Pages) {
|
||||
m := make(map[string]bool)
|
||||
for _, p := range *pages {
|
||||
m[p.TranslationKey()] = true
|
||||
}
|
||||
|
||||
for _, p := range p2 {
|
||||
if _, found := m[p.TranslationKey()]; !found {
|
||||
*pages = append(*pages, p)
|
||||
}
|
||||
}
|
||||
|
||||
SortByDefault(*pages)
|
||||
}
|
||||
|
||||
out, _ := spc.getP("pages.MergeByLanguage", merge, p1, p2)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// MergeByLanguageInterface is the generic version of MergeByLanguage. It
|
||||
// is here just so it can be called from the tpl package.
|
||||
func (p1 Pages) MergeByLanguageInterface(in interface{}) (interface{}, error) {
|
||||
if in == nil {
|
||||
return p1, nil
|
||||
}
|
||||
p2, ok := in.(Pages)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T cannot be merged by language", in)
|
||||
}
|
||||
return p1.MergeByLanguage(p2), nil
|
||||
}
|
42
resources/page/pages_prev_next.go
Normal file
42
resources/page/pages_prev_next.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
// Prev returns the previous page reletive to the given
|
||||
func (p Pages) Prev(cur Page) Page {
|
||||
for x, c := range p {
|
||||
if c.Eq(cur) {
|
||||
if x == 0 {
|
||||
// TODO(bep) consider return nil here to get it line with the other Prevs
|
||||
return p[len(p)-1]
|
||||
}
|
||||
return p[x-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next returns the next page reletive to the given
|
||||
func (p Pages) Next(cur Page) Page {
|
||||
for x, c := range p {
|
||||
if c.Eq(cur) {
|
||||
if x < len(p)-1 {
|
||||
return p[x+1]
|
||||
}
|
||||
// TODO(bep) consider return nil here to get it line with the other Nexts
|
||||
return p[0]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
83
resources/page/pages_prev_next_test.go
Normal file
83
resources/page/pages_prev_next_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type pagePNTestObject struct {
|
||||
path string
|
||||
weight int
|
||||
date string
|
||||
}
|
||||
|
||||
var pagePNTestSources = []pagePNTestObject{
|
||||
{"/section1/testpage1.md", 5, "2012-04-06"},
|
||||
{"/section1/testpage2.md", 4, "2012-01-01"},
|
||||
{"/section1/testpage3.md", 3, "2012-04-06"},
|
||||
{"/section2/testpage4.md", 2, "2012-03-02"},
|
||||
{"/section2/testpage5.md", 1, "2012-04-06"},
|
||||
}
|
||||
|
||||
func TestPrev(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
assert.Equal(t, pages.Prev(pages[0]), pages[4])
|
||||
assert.Equal(t, pages.Prev(pages[1]), pages[0])
|
||||
assert.Equal(t, pages.Prev(pages[4]), pages[3])
|
||||
}
|
||||
|
||||
func TestNext(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := preparePageGroupTestPages(t)
|
||||
assert.Equal(t, pages.Next(pages[0]), pages[1])
|
||||
assert.Equal(t, pages.Next(pages[1]), pages[2])
|
||||
assert.Equal(t, pages.Next(pages[4]), pages[0])
|
||||
}
|
||||
|
||||
func prepareWeightedPagesPrevNext(t *testing.T) WeightedPages {
|
||||
w := WeightedPages{}
|
||||
|
||||
for _, src := range pagePNTestSources {
|
||||
p := newTestPage()
|
||||
p.path = src.path
|
||||
p.weight = src.weight
|
||||
p.date = cast.ToTime(src.date)
|
||||
p.pubDate = cast.ToTime(src.date)
|
||||
w = append(w, WeightedPage{Weight: p.weight, Page: p})
|
||||
}
|
||||
|
||||
w.Sort()
|
||||
return w
|
||||
}
|
||||
|
||||
func TestWeightedPagesPrev(t *testing.T) {
|
||||
t.Parallel()
|
||||
w := prepareWeightedPagesPrevNext(t)
|
||||
assert.Equal(t, w.Prev(w[0].Page), w[4].Page)
|
||||
assert.Equal(t, w.Prev(w[1].Page), w[0].Page)
|
||||
assert.Equal(t, w.Prev(w[4].Page), w[3].Page)
|
||||
}
|
||||
|
||||
func TestWeightedPagesNext(t *testing.T) {
|
||||
t.Parallel()
|
||||
w := prepareWeightedPagesPrevNext(t)
|
||||
assert.Equal(t, w.Next(w[0].Page), w[1].Page)
|
||||
assert.Equal(t, w.Next(w[1].Page), w[2].Page)
|
||||
assert.Equal(t, w.Next(w[4].Page), w[0].Page)
|
||||
}
|
199
resources/page/pages_related.go
Normal file
199
resources/page/pages_related.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
var (
|
||||
// Assert that Pages and PageGroup implements the PageGenealogist interface.
|
||||
_ PageGenealogist = (Pages)(nil)
|
||||
_ PageGenealogist = PageGroup{}
|
||||
)
|
||||
|
||||
// A PageGenealogist finds related pages in a page collection. This interface is implemented
|
||||
// by Pages and PageGroup, which makes it available as `{{ .RegularRelated . }}` etc.
|
||||
type PageGenealogist interface {
|
||||
|
||||
// Template example:
|
||||
// {{ $related := .RegularPages.Related . }}
|
||||
Related(doc related.Document) (Pages, error)
|
||||
|
||||
// Template example:
|
||||
// {{ $related := .RegularPages.RelatedIndices . "tags" "date" }}
|
||||
RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error)
|
||||
|
||||
// Template example:
|
||||
// {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks") ( keyVals "date" .Date ) }}
|
||||
RelatedTo(args ...types.KeyValues) (Pages, error)
|
||||
}
|
||||
|
||||
// Related searches all the configured indices with the search keywords from the
|
||||
// supplied document.
|
||||
func (p Pages) Related(doc related.Document) (Pages, error) {
|
||||
result, err := p.searchDoc(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if page, ok := doc.(Page); ok {
|
||||
return result.removeFirstIfFound(page), nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
// RelatedIndices searches the given indices with the search keywords from the
|
||||
// supplied document.
|
||||
func (p Pages) RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) {
|
||||
indicesStr, err := cast.ToStringSliceE(indices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := p.searchDoc(doc, indicesStr...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if page, ok := doc.(Page); ok {
|
||||
return result.removeFirstIfFound(page), nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
// RelatedTo searches the given indices with the corresponding values.
|
||||
func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) {
|
||||
if len(p) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return p.search(args...)
|
||||
|
||||
}
|
||||
|
||||
func (p Pages) search(args ...types.KeyValues) (Pages, error) {
|
||||
return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
|
||||
return idx.SearchKeyValues(args...)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) {
|
||||
return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
|
||||
return idx.SearchDoc(doc, indices...)
|
||||
})
|
||||
}
|
||||
|
||||
func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) {
|
||||
if len(p) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
d, ok := p[0].(InternalDependencies)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid type %T in related serch", p[0])
|
||||
}
|
||||
|
||||
cache := d.GetRelatedDocsHandler()
|
||||
|
||||
searchIndex, err := cache.getOrCreateIndex(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := search(searchIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result) > 0 {
|
||||
mp := make(Pages, len(result))
|
||||
for i, match := range result {
|
||||
mp[i] = match.(Page)
|
||||
}
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type cachedPostingList struct {
|
||||
p Pages
|
||||
|
||||
postingList *related.InvertedIndex
|
||||
}
|
||||
|
||||
type RelatedDocsHandler struct {
|
||||
cfg related.Config
|
||||
|
||||
postingLists []*cachedPostingList
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRelatedDocsHandler(cfg related.Config) *RelatedDocsHandler {
|
||||
return &RelatedDocsHandler{cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *RelatedDocsHandler) Clone() *RelatedDocsHandler {
|
||||
return NewRelatedDocsHandler(s.cfg)
|
||||
}
|
||||
|
||||
// This assumes that a lock has been acquired.
|
||||
func (s *RelatedDocsHandler) getIndex(p Pages) *related.InvertedIndex {
|
||||
for _, ci := range s.postingLists {
|
||||
if pagesEqual(p, ci.p) {
|
||||
return ci.postingList
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) {
|
||||
s.mu.RLock()
|
||||
cachedIndex := s.getIndex(p)
|
||||
if cachedIndex != nil {
|
||||
s.mu.RUnlock()
|
||||
return cachedIndex, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if cachedIndex := s.getIndex(p); cachedIndex != nil {
|
||||
return cachedIndex, nil
|
||||
}
|
||||
|
||||
searchIndex := related.NewInvertedIndex(s.cfg)
|
||||
|
||||
for _, page := range p {
|
||||
if err := searchIndex.Add(page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.postingLists = append(s.postingLists, &cachedPostingList{p: p, postingList: searchIndex})
|
||||
|
||||
return searchIndex, nil
|
||||
}
|
86
resources/page/pages_related_test.go
Normal file
86
resources/page/pages_related_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRelated(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
t.Parallel()
|
||||
|
||||
pages := Pages{
|
||||
&testPage{
|
||||
title: "Page 1",
|
||||
pubDate: mustParseDate("2017-01-03"),
|
||||
params: map[string]interface{}{
|
||||
"keywords": []string{"hugo", "says"},
|
||||
},
|
||||
},
|
||||
&testPage{
|
||||
title: "Page 2",
|
||||
pubDate: mustParseDate("2017-01-02"),
|
||||
params: map[string]interface{}{
|
||||
"keywords": []string{"hugo", "rocks"},
|
||||
},
|
||||
},
|
||||
&testPage{
|
||||
title: "Page 3",
|
||||
pubDate: mustParseDate("2017-01-01"),
|
||||
params: map[string]interface{}{
|
||||
"keywords": []string{"bep", "says"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := pages.RelatedTo(types.NewKeyValuesStrings("keywords", "hugo", "rocks"))
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Len(result, 2)
|
||||
assert.Equal("Page 2", result[0].Title())
|
||||
assert.Equal("Page 1", result[1].Title())
|
||||
|
||||
result, err = pages.Related(pages[0])
|
||||
assert.NoError(err)
|
||||
assert.Len(result, 2)
|
||||
assert.Equal("Page 2", result[0].Title())
|
||||
assert.Equal("Page 3", result[1].Title())
|
||||
|
||||
result, err = pages.RelatedIndices(pages[0], "keywords")
|
||||
assert.NoError(err)
|
||||
assert.Len(result, 2)
|
||||
assert.Equal("Page 2", result[0].Title())
|
||||
assert.Equal("Page 3", result[1].Title())
|
||||
|
||||
result, err = pages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks"))
|
||||
assert.NoError(err)
|
||||
assert.Len(result, 2)
|
||||
assert.Equal("Page 2", result[0].Title())
|
||||
assert.Equal("Page 3", result[1].Title())
|
||||
}
|
||||
|
||||
func mustParseDate(s string) time.Time {
|
||||
d, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return d
|
||||
}
|
348
resources/page/pages_sort.go
Normal file
348
resources/page/pages_sort.go
Normal file
@@ -0,0 +1,348 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
var spc = newPageCache()
|
||||
|
||||
/*
|
||||
* Implementation of a custom sorter for Pages
|
||||
*/
|
||||
|
||||
// A pageSorter implements the sort interface for Pages
|
||||
type pageSorter struct {
|
||||
pages Pages
|
||||
by pageBy
|
||||
}
|
||||
|
||||
// pageBy is a closure used in the Sort.Less method.
|
||||
type pageBy func(p1, p2 Page) bool
|
||||
|
||||
// Sort stable sorts the pages given the receiver's sort order.
|
||||
func (by pageBy) Sort(pages Pages) {
|
||||
ps := &pageSorter{
|
||||
pages: pages,
|
||||
by: by, // The Sort method's receiver is the function (closure) that defines the sort order.
|
||||
}
|
||||
sort.Stable(ps)
|
||||
}
|
||||
|
||||
// DefaultPageSort is the default sort func for pages in Hugo:
|
||||
// Order by Weight, Date, LinkTitle and then full file path.
|
||||
var DefaultPageSort = func(p1, p2 Page) bool {
|
||||
if p1.Weight() == p2.Weight() {
|
||||
if p1.Date().Unix() == p2.Date().Unix() {
|
||||
if p1.LinkTitle() == p2.LinkTitle() {
|
||||
if p1.File() == nil || p2.File() == nil {
|
||||
return p1.File() == nil
|
||||
}
|
||||
return p1.File().Filename() < p2.File().Filename()
|
||||
}
|
||||
return (p1.LinkTitle() < p2.LinkTitle())
|
||||
}
|
||||
return p1.Date().Unix() > p2.Date().Unix()
|
||||
}
|
||||
|
||||
if p2.Weight() == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if p1.Weight() == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return p1.Weight() < p2.Weight()
|
||||
}
|
||||
|
||||
var languagePageSort = func(p1, p2 Page) bool {
|
||||
|
||||
if p1.Language().Weight == p2.Language().Weight {
|
||||
if p1.Date().Unix() == p2.Date().Unix() {
|
||||
if p1.LinkTitle() == p2.LinkTitle() {
|
||||
if p1.File() != nil && p2.File() != nil {
|
||||
return p1.File().Filename() < p2.File().Filename()
|
||||
}
|
||||
}
|
||||
return (p1.LinkTitle() < p2.LinkTitle())
|
||||
}
|
||||
return p1.Date().Unix() > p2.Date().Unix()
|
||||
}
|
||||
|
||||
if p2.Language().Weight == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if p1.Language().Weight == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return p1.Language().Weight < p2.Language().Weight
|
||||
}
|
||||
|
||||
func (ps *pageSorter) Len() int { return len(ps.pages) }
|
||||
func (ps *pageSorter) Swap(i, j int) { ps.pages[i], ps.pages[j] = ps.pages[j], ps.pages[i] }
|
||||
|
||||
// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter.
|
||||
func (ps *pageSorter) Less(i, j int) bool { return ps.by(ps.pages[i], ps.pages[j]) }
|
||||
|
||||
// Limit limits the number of pages returned to n.
|
||||
func (p Pages) Limit(n int) Pages {
|
||||
if len(p) > n {
|
||||
return p[0:n]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ByWeight sorts the Pages by weight and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByWeight() Pages {
|
||||
const key = "pageSort.ByWeight"
|
||||
pages, _ := spc.get(key, pageBy(DefaultPageSort).Sort, p)
|
||||
return pages
|
||||
}
|
||||
|
||||
// SortByDefault sorts pages by the default sort.
|
||||
func SortByDefault(pages Pages) {
|
||||
pageBy(DefaultPageSort).Sort(pages)
|
||||
}
|
||||
|
||||
// ByTitle sorts the Pages by title and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByTitle() Pages {
|
||||
|
||||
const key = "pageSort.ByTitle"
|
||||
|
||||
title := func(p1, p2 Page) bool {
|
||||
return p1.Title() < p2.Title()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(title).Sort, p)
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByLinkTitle sorts the Pages by link title and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByLinkTitle() Pages {
|
||||
|
||||
const key = "pageSort.ByLinkTitle"
|
||||
|
||||
linkTitle := func(p1, p2 Page) bool {
|
||||
return p1.LinkTitle() < p2.LinkTitle()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(linkTitle).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByDate sorts the Pages by date and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByDate() Pages {
|
||||
|
||||
const key = "pageSort.ByDate"
|
||||
|
||||
date := func(p1, p2 Page) bool {
|
||||
return p1.Date().Unix() < p2.Date().Unix()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(date).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByPublishDate sorts the Pages by publish date and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByPublishDate() Pages {
|
||||
|
||||
const key = "pageSort.ByPublishDate"
|
||||
|
||||
pubDate := func(p1, p2 Page) bool {
|
||||
return p1.PublishDate().Unix() < p2.PublishDate().Unix()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(pubDate).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByExpiryDate sorts the Pages by publish date and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByExpiryDate() Pages {
|
||||
|
||||
const key = "pageSort.ByExpiryDate"
|
||||
|
||||
expDate := func(p1, p2 Page) bool {
|
||||
return p1.ExpiryDate().Unix() < p2.ExpiryDate().Unix()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(expDate).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByLastmod sorts the Pages by the last modification date and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByLastmod() Pages {
|
||||
|
||||
const key = "pageSort.ByLastmod"
|
||||
|
||||
date := func(p1, p2 Page) bool {
|
||||
return p1.Lastmod().Unix() < p2.Lastmod().Unix()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(date).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByLength sorts the Pages by length and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByLength() Pages {
|
||||
|
||||
const key = "pageSort.ByLength"
|
||||
|
||||
length := func(p1, p2 Page) bool {
|
||||
|
||||
p1l, ok1 := p1.(resource.LengthProvider)
|
||||
p2l, ok2 := p2.(resource.LengthProvider)
|
||||
|
||||
if !ok1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !ok2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return p1l.Len() < p2l.Len()
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(length).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByLanguage sorts the Pages by the language's Weight.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByLanguage() Pages {
|
||||
|
||||
const key = "pageSort.ByLanguage"
|
||||
|
||||
pages, _ := spc.get(key, pageBy(languagePageSort).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// SortByLanguage sorts the pages by language.
|
||||
func SortByLanguage(pages Pages) {
|
||||
pageBy(languagePageSort).Sort(pages)
|
||||
}
|
||||
|
||||
// Reverse reverses the order in Pages and returns a copy.
|
||||
//
|
||||
// Adjacent invocations on the same receiver will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) Reverse() Pages {
|
||||
const key = "pageSort.Reverse"
|
||||
|
||||
reverseFunc := func(pages Pages) {
|
||||
for i, j := 0, len(pages)-1; i < j; i, j = i+1, j-1 {
|
||||
pages[i], pages[j] = pages[j], pages[i]
|
||||
}
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, reverseFunc, p)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
// ByParam sorts the pages according to the given page Params key.
|
||||
//
|
||||
// Adjacent invocations on the same receiver with the same paramsKey will return a cached result.
|
||||
//
|
||||
// This may safely be executed in parallel.
|
||||
func (p Pages) ByParam(paramsKey interface{}) Pages {
|
||||
paramsKeyStr := cast.ToString(paramsKey)
|
||||
key := "pageSort.ByParam." + paramsKeyStr
|
||||
|
||||
paramsKeyComparator := func(p1, p2 Page) bool {
|
||||
v1, _ := p1.Param(paramsKeyStr)
|
||||
v2, _ := p2.Param(paramsKeyStr)
|
||||
|
||||
if v1 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if v2 == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
isNumeric := func(v interface{}) bool {
|
||||
switch v.(type) {
|
||||
case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if isNumeric(v1) && isNumeric(v2) {
|
||||
return cast.ToFloat64(v1) < cast.ToFloat64(v2)
|
||||
}
|
||||
|
||||
s1 := cast.ToString(v1)
|
||||
s2 := cast.ToString(v2)
|
||||
|
||||
return s1 < s2
|
||||
}
|
||||
|
||||
pages, _ := spc.get(key, pageBy(paramsKeyComparator).Sort, p)
|
||||
|
||||
return pages
|
||||
}
|
279
resources/page/pages_sort_test.go
Normal file
279
resources/page/pages_sort_test.go
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultSort(t *testing.T) {
|
||||
t.Parallel()
|
||||
d1 := time.Now()
|
||||
d2 := d1.Add(-1 * time.Hour)
|
||||
d3 := d1.Add(-2 * time.Hour)
|
||||
d4 := d1.Add(-3 * time.Hour)
|
||||
|
||||
p := createSortTestPages(4)
|
||||
|
||||
// first by weight
|
||||
setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "a", "c", "d"}, [4]int{4, 3, 2, 1}, p)
|
||||
SortByDefault(p)
|
||||
|
||||
assert.Equal(t, 1, p[0].Weight())
|
||||
|
||||
// Consider zero weight, issue #2673
|
||||
setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "a", "d", "c"}, [4]int{0, 0, 0, 1}, p)
|
||||
SortByDefault(p)
|
||||
|
||||
assert.Equal(t, 1, p[0].Weight())
|
||||
|
||||
// next by date
|
||||
setSortVals([4]time.Time{d3, d4, d1, d2}, [4]string{"a", "b", "c", "d"}, [4]int{1, 1, 1, 1}, p)
|
||||
SortByDefault(p)
|
||||
assert.Equal(t, d1, p[0].Date())
|
||||
|
||||
// finally by link title
|
||||
setSortVals([4]time.Time{d3, d3, d3, d3}, [4]string{"b", "c", "a", "d"}, [4]int{1, 1, 1, 1}, p)
|
||||
SortByDefault(p)
|
||||
assert.Equal(t, "al", p[0].LinkTitle())
|
||||
assert.Equal(t, "bl", p[1].LinkTitle())
|
||||
assert.Equal(t, "cl", p[2].LinkTitle())
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/4953
|
||||
func TestSortByLinkTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
pages := createSortTestPages(6)
|
||||
|
||||
for i, p := range pages {
|
||||
pp := p.(*testPage)
|
||||
if i < 5 {
|
||||
pp.title = fmt.Sprintf("title%d", i)
|
||||
}
|
||||
|
||||
if i > 2 {
|
||||
pp.linkTitle = fmt.Sprintf("linkTitle%d", i)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pages.shuffle()
|
||||
|
||||
bylt := pages.ByLinkTitle()
|
||||
|
||||
for i, p := range bylt {
|
||||
msg := fmt.Sprintf("test: %d", i)
|
||||
if i < 3 {
|
||||
assert.Equal(fmt.Sprintf("linkTitle%d", i+3), p.LinkTitle(), msg)
|
||||
} else {
|
||||
assert.Equal(fmt.Sprintf("title%d", i-3), p.LinkTitle(), msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortByN(t *testing.T) {
|
||||
t.Parallel()
|
||||
d1 := time.Now()
|
||||
d2 := d1.Add(-2 * time.Hour)
|
||||
d3 := d1.Add(-10 * time.Hour)
|
||||
d4 := d1.Add(-20 * time.Hour)
|
||||
|
||||
p := createSortTestPages(4)
|
||||
|
||||
for i, this := range []struct {
|
||||
sortFunc func(p Pages) Pages
|
||||
assertFunc func(p Pages) bool
|
||||
}{
|
||||
{(Pages).ByWeight, func(p Pages) bool { return p[0].Weight() == 1 }},
|
||||
{(Pages).ByTitle, func(p Pages) bool { return p[0].Title() == "ab" }},
|
||||
{(Pages).ByLinkTitle, func(p Pages) bool { return p[0].LinkTitle() == "abl" }},
|
||||
{(Pages).ByDate, func(p Pages) bool { return p[0].Date() == d4 }},
|
||||
{(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate() == d4 }},
|
||||
{(Pages).ByExpiryDate, func(p Pages) bool { return p[0].ExpiryDate() == d4 }},
|
||||
{(Pages).ByLastmod, func(p Pages) bool { return p[1].Lastmod() == d3 }},
|
||||
{(Pages).ByLength, func(p Pages) bool { return p[0].(resource.LengthProvider).Len() == len(p[0].(*testPage).content) }},
|
||||
} {
|
||||
setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "ab", "cde", "fg"}, [4]int{0, 3, 2, 1}, p)
|
||||
|
||||
sorted := this.sortFunc(p)
|
||||
if !this.assertFunc(sorted) {
|
||||
t.Errorf("[%d] sort error", i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := createSortTestPages(10)
|
||||
firstFive := p.Limit(5)
|
||||
assert.Equal(t, 5, len(firstFive))
|
||||
for i := 0; i < 5; i++ {
|
||||
assert.Equal(t, p[i], firstFive[i])
|
||||
}
|
||||
assert.Equal(t, p, p.Limit(10))
|
||||
assert.Equal(t, p, p.Limit(11))
|
||||
}
|
||||
|
||||
func TestPageSortReverse(t *testing.T) {
|
||||
t.Parallel()
|
||||
p1 := createSortTestPages(10)
|
||||
assert.Equal(t, 0, p1[0].(*testPage).fuzzyWordCount)
|
||||
assert.Equal(t, 9, p1[9].(*testPage).fuzzyWordCount)
|
||||
p2 := p1.Reverse()
|
||||
assert.Equal(t, 9, p2[0].(*testPage).fuzzyWordCount)
|
||||
assert.Equal(t, 0, p2[9].(*testPage).fuzzyWordCount)
|
||||
// cached
|
||||
assert.True(t, pagesEqual(p2, p1.Reverse()))
|
||||
}
|
||||
|
||||
func TestPageSortByParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
var k interface{} = "arbitrarily.nested"
|
||||
|
||||
unsorted := createSortTestPages(10)
|
||||
delete(unsorted[9].Params(), "arbitrarily")
|
||||
|
||||
firstSetValue, _ := unsorted[0].Param(k)
|
||||
secondSetValue, _ := unsorted[1].Param(k)
|
||||
lastSetValue, _ := unsorted[8].Param(k)
|
||||
unsetValue, _ := unsorted[9].Param(k)
|
||||
|
||||
assert.Equal(t, "xyz100", firstSetValue)
|
||||
assert.Equal(t, "xyz99", secondSetValue)
|
||||
assert.Equal(t, "xyz92", lastSetValue)
|
||||
assert.Equal(t, nil, unsetValue)
|
||||
|
||||
sorted := unsorted.ByParam("arbitrarily.nested")
|
||||
firstSetSortedValue, _ := sorted[0].Param(k)
|
||||
secondSetSortedValue, _ := sorted[1].Param(k)
|
||||
lastSetSortedValue, _ := sorted[8].Param(k)
|
||||
unsetSortedValue, _ := sorted[9].Param(k)
|
||||
|
||||
assert.Equal(t, firstSetValue, firstSetSortedValue)
|
||||
assert.Equal(t, secondSetValue, lastSetSortedValue)
|
||||
assert.Equal(t, lastSetValue, secondSetSortedValue)
|
||||
assert.Equal(t, unsetValue, unsetSortedValue)
|
||||
}
|
||||
|
||||
func TestPageSortByParamNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
var k interface{} = "arbitrarily.nested"
|
||||
|
||||
n := 10
|
||||
unsorted := createSortTestPages(n)
|
||||
for i := 0; i < n; i++ {
|
||||
v := 100 - i
|
||||
if i%2 == 0 {
|
||||
v = 100.0 - i
|
||||
}
|
||||
|
||||
unsorted[i].(*testPage).params = map[string]interface{}{
|
||||
"arbitrarily": map[string]interface{}{
|
||||
"nested": v,
|
||||
},
|
||||
}
|
||||
}
|
||||
delete(unsorted[9].Params(), "arbitrarily")
|
||||
|
||||
firstSetValue, _ := unsorted[0].Param(k)
|
||||
secondSetValue, _ := unsorted[1].Param(k)
|
||||
lastSetValue, _ := unsorted[8].Param(k)
|
||||
unsetValue, _ := unsorted[9].Param(k)
|
||||
|
||||
assert.Equal(t, 100, firstSetValue)
|
||||
assert.Equal(t, 99, secondSetValue)
|
||||
assert.Equal(t, 92, lastSetValue)
|
||||
assert.Equal(t, nil, unsetValue)
|
||||
|
||||
sorted := unsorted.ByParam("arbitrarily.nested")
|
||||
firstSetSortedValue, _ := sorted[0].Param(k)
|
||||
secondSetSortedValue, _ := sorted[1].Param(k)
|
||||
lastSetSortedValue, _ := sorted[8].Param(k)
|
||||
unsetSortedValue, _ := sorted[9].Param(k)
|
||||
|
||||
assert.Equal(t, 92, firstSetSortedValue)
|
||||
assert.Equal(t, 93, secondSetSortedValue)
|
||||
assert.Equal(t, 100, lastSetSortedValue)
|
||||
assert.Equal(t, unsetValue, unsetSortedValue)
|
||||
}
|
||||
|
||||
func BenchmarkSortByWeightAndReverse(b *testing.B) {
|
||||
p := createSortTestPages(300)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p = p.ByWeight().Reverse()
|
||||
}
|
||||
}
|
||||
|
||||
func setSortVals(dates [4]time.Time, titles [4]string, weights [4]int, pages Pages) {
|
||||
for i := range dates {
|
||||
this := pages[i].(*testPage)
|
||||
other := pages[len(dates)-1-i].(*testPage)
|
||||
|
||||
this.date = dates[i]
|
||||
this.lastMod = dates[i]
|
||||
this.weight = weights[i]
|
||||
this.title = titles[i]
|
||||
// make sure we compare apples and ... apples ...
|
||||
other.linkTitle = this.Title() + "l"
|
||||
other.pubDate = dates[i]
|
||||
other.expiryDate = dates[i]
|
||||
other.content = titles[i] + "_content"
|
||||
}
|
||||
lastLastMod := pages[2].Lastmod()
|
||||
pages[2].(*testPage).lastMod = pages[1].Lastmod()
|
||||
pages[1].(*testPage).lastMod = lastLastMod
|
||||
|
||||
for _, p := range pages {
|
||||
p.(*testPage).content = ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createSortTestPages(num int) Pages {
|
||||
pages := make(Pages, num)
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
p := newTestPage()
|
||||
p.path = fmt.Sprintf("/x/y/p%d.md", i)
|
||||
p.params = map[string]interface{}{
|
||||
"arbitrarily": map[string]interface{}{
|
||||
"nested": ("xyz" + fmt.Sprintf("%v", 100-i)),
|
||||
},
|
||||
}
|
||||
|
||||
w := 5
|
||||
|
||||
if i%2 == 0 {
|
||||
w = 10
|
||||
}
|
||||
p.fuzzyWordCount = i
|
||||
p.weight = w
|
||||
p.description = "initial"
|
||||
|
||||
pages[i] = p
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
404
resources/page/pagination.go
Normal file
404
resources/page/pagination.go
Normal file
@@ -0,0 +1,404 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// PaginatorProvider provides two ways to create a page paginator.
|
||||
type PaginatorProvider interface {
|
||||
Paginator(options ...interface{}) (*Pager, error)
|
||||
Paginate(seq interface{}, options ...interface{}) (*Pager, error)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type pagers []*Pager
|
||||
|
||||
var (
|
||||
paginatorEmptyPages Pages
|
||||
paginatorEmptyPageGroups PagesGroup
|
||||
)
|
||||
|
||||
type Paginator struct {
|
||||
paginatedElements []paginatedElement
|
||||
pagers
|
||||
paginationURLFactory
|
||||
total int
|
||||
size int
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 Paginate(td TargetPathDescriptor, seq interface{}, pagerSize int) (*Paginator, 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)
|
||||
}
|
||||
|
||||
return paginator, nil
|
||||
}
|
||||
|
||||
// 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(pageNumber int) string {
|
||||
pathDescriptor := d
|
||||
var rel string
|
||||
if pageNumber > 1 {
|
||||
rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath, pageNumber)
|
||||
pathDescriptor.Addends = rel
|
||||
}
|
||||
|
||||
return CreateTargetPaths(pathDescriptor).RelPermalink(d.PathSpec)
|
||||
|
||||
}
|
||||
}
|
307
resources/page/pagination_test.go
Normal file
307
resources/page/pagination_test.go
Normal file
@@ -0,0 +1,307 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSplitPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pages := createTestPages(21)
|
||||
chunks := splitPages(pages, 5)
|
||||
require.Equal(t, 5, len(chunks))
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
require.Equal(t, 5, chunks[i].Len())
|
||||
}
|
||||
|
||||
lastChunk := chunks[4]
|
||||
require.Equal(t, 1, lastChunk.Len())
|
||||
|
||||
}
|
||||
|
||||
func TestSplitPageGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := createTestPages(21)
|
||||
groups, _ := pages.GroupBy("Weight", "desc")
|
||||
chunks := splitPageGroups(groups, 5)
|
||||
require.Equal(t, 5, len(chunks))
|
||||
|
||||
firstChunk := chunks[0]
|
||||
|
||||
// alternate weight 5 and 10
|
||||
if groups, ok := firstChunk.(PagesGroup); ok {
|
||||
require.Equal(t, 5, groups.Len())
|
||||
for _, pg := range groups {
|
||||
// first group 10 in weight
|
||||
require.Equal(t, 10, pg.Key)
|
||||
for _, p := range pg.Pages {
|
||||
require.True(t, p.FuzzyWordCount()%2 == 0) // magic test
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Excepted PageGroup")
|
||||
}
|
||||
|
||||
lastChunk := chunks[4]
|
||||
|
||||
if groups, ok := lastChunk.(PagesGroup); ok {
|
||||
require.Equal(t, 1, groups.Len())
|
||||
for _, pg := range groups {
|
||||
// last should have 5 in weight
|
||||
require.Equal(t, 5, pg.Key)
|
||||
for _, p := range pg.Pages {
|
||||
require.True(t, p.FuzzyWordCount()%2 != 0) // magic test
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Excepted PageGroup")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPager(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := createTestPages(21)
|
||||
groups, _ := pages.GroupBy("Weight", "desc")
|
||||
|
||||
urlFactory := func(page int) string {
|
||||
return fmt.Sprintf("page/%d/", page)
|
||||
}
|
||||
|
||||
_, err := newPaginatorFromPages(pages, -1, urlFactory)
|
||||
require.NotNil(t, err)
|
||||
|
||||
_, err = newPaginatorFromPageGroups(groups, -1, urlFactory)
|
||||
require.NotNil(t, err)
|
||||
|
||||
pag, err := newPaginatorFromPages(pages, 5, urlFactory)
|
||||
require.Nil(t, err)
|
||||
doTestPages(t, pag)
|
||||
first := pag.Pagers()[0].First()
|
||||
require.Equal(t, "Pager 1", first.String())
|
||||
require.NotEmpty(t, first.Pages())
|
||||
require.Empty(t, first.PageGroups())
|
||||
|
||||
pag, err = newPaginatorFromPageGroups(groups, 5, urlFactory)
|
||||
require.Nil(t, err)
|
||||
doTestPages(t, pag)
|
||||
first = pag.Pagers()[0].First()
|
||||
require.NotEmpty(t, first.PageGroups())
|
||||
require.Empty(t, first.Pages())
|
||||
|
||||
}
|
||||
|
||||
func doTestPages(t *testing.T, paginator *Paginator) {
|
||||
|
||||
paginatorPages := paginator.Pagers()
|
||||
|
||||
require.Equal(t, 5, len(paginatorPages))
|
||||
require.Equal(t, 21, paginator.TotalNumberOfElements())
|
||||
require.Equal(t, 5, paginator.PageSize())
|
||||
require.Equal(t, 5, paginator.TotalPages())
|
||||
|
||||
first := paginatorPages[0]
|
||||
require.Equal(t, template.HTML("page/1/"), first.URL())
|
||||
require.Equal(t, first, first.First())
|
||||
require.True(t, first.HasNext())
|
||||
require.Equal(t, paginatorPages[1], first.Next())
|
||||
require.False(t, first.HasPrev())
|
||||
require.Nil(t, first.Prev())
|
||||
require.Equal(t, 5, first.NumberOfElements())
|
||||
require.Equal(t, 1, first.PageNumber())
|
||||
|
||||
third := paginatorPages[2]
|
||||
require.True(t, third.HasNext())
|
||||
require.True(t, third.HasPrev())
|
||||
require.Equal(t, paginatorPages[1], third.Prev())
|
||||
|
||||
last := paginatorPages[4]
|
||||
require.Equal(t, template.HTML("page/5/"), last.URL())
|
||||
require.Equal(t, last, last.Last())
|
||||
require.False(t, last.HasNext())
|
||||
require.Nil(t, last.Next())
|
||||
require.True(t, last.HasPrev())
|
||||
require.Equal(t, 1, last.NumberOfElements())
|
||||
require.Equal(t, 5, last.PageNumber())
|
||||
}
|
||||
|
||||
func TestPagerNoPages(t *testing.T) {
|
||||
t.Parallel()
|
||||
pages := createTestPages(0)
|
||||
groups, _ := pages.GroupBy("Weight", "desc")
|
||||
|
||||
urlFactory := func(page int) string {
|
||||
return fmt.Sprintf("page/%d/", page)
|
||||
}
|
||||
|
||||
paginator, _ := newPaginatorFromPages(pages, 5, urlFactory)
|
||||
doTestPagerNoPages(t, paginator)
|
||||
|
||||
first := paginator.Pagers()[0].First()
|
||||
require.Empty(t, first.PageGroups())
|
||||
require.Empty(t, first.Pages())
|
||||
|
||||
paginator, _ = newPaginatorFromPageGroups(groups, 5, urlFactory)
|
||||
doTestPagerNoPages(t, paginator)
|
||||
|
||||
first = paginator.Pagers()[0].First()
|
||||
require.Empty(t, first.PageGroups())
|
||||
require.Empty(t, first.Pages())
|
||||
|
||||
}
|
||||
|
||||
func doTestPagerNoPages(t *testing.T, paginator *Paginator) {
|
||||
paginatorPages := paginator.Pagers()
|
||||
|
||||
require.Equal(t, 1, len(paginatorPages))
|
||||
require.Equal(t, 0, paginator.TotalNumberOfElements())
|
||||
require.Equal(t, 5, paginator.PageSize())
|
||||
require.Equal(t, 0, paginator.TotalPages())
|
||||
|
||||
// pageOne should be nothing but the first
|
||||
pageOne := paginatorPages[0]
|
||||
require.NotNil(t, pageOne.First())
|
||||
require.False(t, pageOne.HasNext())
|
||||
require.False(t, pageOne.HasPrev())
|
||||
require.Nil(t, pageOne.Next())
|
||||
require.Equal(t, 1, len(pageOne.Pagers()))
|
||||
require.Equal(t, 0, pageOne.Pages().Len())
|
||||
require.Equal(t, 0, pageOne.NumberOfElements())
|
||||
require.Equal(t, 0, pageOne.TotalNumberOfElements())
|
||||
require.Equal(t, 0, pageOne.TotalPages())
|
||||
require.Equal(t, 1, pageOne.PageNumber())
|
||||
require.Equal(t, 5, pageOne.PageSize())
|
||||
|
||||
}
|
||||
|
||||
func TestPaginationURLFactory(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := viper.New()
|
||||
cfg.Set("paginatePath", "zoo")
|
||||
|
||||
for _, uglyURLs := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
d TargetPathDescriptor
|
||||
baseURL string
|
||||
page int
|
||||
expected string
|
||||
expectedUgly string
|
||||
}{
|
||||
{"HTML home page 32",
|
||||
TargetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/", "/zoo/32.html"},
|
||||
{"JSON home page 42",
|
||||
TargetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/index.json", "/zoo/42.json"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
d := test.d
|
||||
cfg.Set("baseURL", test.baseURL)
|
||||
cfg.Set("uglyURLs", uglyURLs)
|
||||
d.UglyURLs = uglyURLs
|
||||
|
||||
pathSpec := newTestPathSpecFor(cfg)
|
||||
d.PathSpec = pathSpec
|
||||
|
||||
factory := newPaginationURLFactory(d)
|
||||
|
||||
got := factory(test.page)
|
||||
|
||||
if uglyURLs {
|
||||
require.Equal(t, test.expectedUgly, got)
|
||||
} else {
|
||||
require.Equal(t, test.expected, got)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbablyEqualPageLists(t *testing.T) {
|
||||
t.Parallel()
|
||||
fivePages := createTestPages(5)
|
||||
zeroPages := createTestPages(0)
|
||||
zeroPagesByWeight, _ := createTestPages(0).GroupBy("Weight", "asc")
|
||||
fivePagesByWeight, _ := createTestPages(5).GroupBy("Weight", "asc")
|
||||
ninePagesByWeight, _ := createTestPages(9).GroupBy("Weight", "asc")
|
||||
|
||||
for i, this := range []struct {
|
||||
v1 interface{}
|
||||
v2 interface{}
|
||||
expect bool
|
||||
}{
|
||||
{nil, nil, true},
|
||||
{"a", "b", true},
|
||||
{"a", fivePages, false},
|
||||
{fivePages, "a", false},
|
||||
{fivePages, createTestPages(2), false},
|
||||
{fivePages, fivePages, true},
|
||||
{zeroPages, zeroPages, true},
|
||||
{fivePagesByWeight, fivePagesByWeight, true},
|
||||
{zeroPagesByWeight, fivePagesByWeight, false},
|
||||
{zeroPagesByWeight, zeroPagesByWeight, true},
|
||||
{fivePagesByWeight, fivePages, false},
|
||||
{fivePagesByWeight, ninePagesByWeight, false},
|
||||
} {
|
||||
result := probablyEqualPageLists(this.v1, this.v2)
|
||||
|
||||
if result != this.expect {
|
||||
t.Errorf("[%d] got %t but expected %t", i, result, this.expect)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginationPage(t *testing.T) {
|
||||
t.Parallel()
|
||||
urlFactory := func(page int) string {
|
||||
return fmt.Sprintf("page/%d/", page)
|
||||
}
|
||||
|
||||
fivePages := createTestPages(7)
|
||||
fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy("FuzzyWordCount", "asc")
|
||||
|
||||
p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory)
|
||||
p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory)
|
||||
|
||||
f1 := p1.pagers[0].First()
|
||||
f2 := p2.pagers[0].First()
|
||||
|
||||
page11, _ := f1.page(1)
|
||||
page1Nil, _ := f1.page(3)
|
||||
|
||||
page21, _ := f2.page(1)
|
||||
page2Nil, _ := f2.page(3)
|
||||
|
||||
require.Equal(t, 3, page11.FuzzyWordCount())
|
||||
require.Nil(t, page1Nil)
|
||||
|
||||
require.NotNil(t, page21)
|
||||
require.Equal(t, 3, page21.FuzzyWordCount())
|
||||
require.Nil(t, page2Nil)
|
||||
}
|
248
resources/page/permalinks.go
Normal file
248
resources/page/permalinks.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
)
|
||||
|
||||
// PermalinkExpander holds permalin mappings per section.
|
||||
type PermalinkExpander struct {
|
||||
// knownPermalinkAttributes maps :tags in a permalink specification to a
|
||||
// function which, given a page and the tag, returns the resulting string
|
||||
// to be used to replace that tag.
|
||||
knownPermalinkAttributes map[string]pageToPermaAttribute
|
||||
|
||||
expanders map[string]func(Page) (string, error)
|
||||
|
||||
ps *helpers.PathSpec
|
||||
}
|
||||
|
||||
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
|
||||
// PathSpec.
|
||||
func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) {
|
||||
|
||||
p := PermalinkExpander{ps: ps}
|
||||
|
||||
p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
|
||||
"year": p.pageToPermalinkDate,
|
||||
"month": p.pageToPermalinkDate,
|
||||
"monthname": p.pageToPermalinkDate,
|
||||
"day": p.pageToPermalinkDate,
|
||||
"weekday": p.pageToPermalinkDate,
|
||||
"weekdayname": p.pageToPermalinkDate,
|
||||
"yearday": p.pageToPermalinkDate,
|
||||
"section": p.pageToPermalinkSection,
|
||||
"sections": p.pageToPermalinkSections,
|
||||
"title": p.pageToPermalinkTitle,
|
||||
"slug": p.pageToPermalinkSlugElseTitle,
|
||||
"filename": p.pageToPermalinkFilename,
|
||||
}
|
||||
|
||||
patterns := ps.Cfg.GetStringMapString("permalinks")
|
||||
if patterns == nil {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
e, err := p.parse(patterns)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
p.expanders = e
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Expand expands the path in p according to the rules defined for the given key.
|
||||
// If no rules are found for the given key, an empty string is returned.
|
||||
func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
|
||||
expand, found := l.expanders[key]
|
||||
|
||||
if !found {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return expand(p)
|
||||
|
||||
}
|
||||
|
||||
func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
|
||||
|
||||
expanders := make(map[string]func(Page) (string, error))
|
||||
|
||||
for k, pattern := range patterns {
|
||||
if !l.validate(pattern) {
|
||||
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
|
||||
}
|
||||
|
||||
pattern := pattern
|
||||
matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
|
||||
|
||||
callbacks := make([]pageToPermaAttribute, len(matches))
|
||||
replacements := make([]string, len(matches))
|
||||
for i, m := range matches {
|
||||
replacement := m[0]
|
||||
attr := replacement[1:]
|
||||
replacements[i] = replacement
|
||||
callback, ok := l.knownPermalinkAttributes[attr]
|
||||
|
||||
if !ok {
|
||||
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkAttributeUnknown}
|
||||
}
|
||||
|
||||
callbacks[i] = callback
|
||||
}
|
||||
|
||||
expanders[k] = func(p Page) (string, error) {
|
||||
|
||||
if matches == nil {
|
||||
return pattern, nil
|
||||
}
|
||||
|
||||
newField := pattern
|
||||
|
||||
for i, replacement := range replacements {
|
||||
attr := replacement[1:]
|
||||
callback := callbacks[i]
|
||||
newAttr, err := callback(p, attr)
|
||||
|
||||
if err != nil {
|
||||
return "", &permalinkExpandError{pattern: pattern, err: err}
|
||||
}
|
||||
|
||||
newField = strings.Replace(newField, replacement, newAttr, 1)
|
||||
|
||||
}
|
||||
|
||||
return newField, nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return expanders, nil
|
||||
}
|
||||
|
||||
// pageToPermaAttribute is the type of a function which, given a page and a tag
|
||||
// can return a string to go in that position in the page (or an error)
|
||||
type pageToPermaAttribute func(Page, string) (string, error)
|
||||
|
||||
var attributeRegexp = regexp.MustCompile(`:\w+`)
|
||||
|
||||
// validate determines if a PathPattern is well-formed
|
||||
func (l PermalinkExpander) validate(pp string) bool {
|
||||
fragments := strings.Split(pp[1:], "/")
|
||||
var bail = false
|
||||
for i := range fragments {
|
||||
if bail {
|
||||
return false
|
||||
}
|
||||
if len(fragments[i]) == 0 {
|
||||
bail = true
|
||||
continue
|
||||
}
|
||||
|
||||
matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1)
|
||||
if matches == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
k := strings.ToLower(match[0][1:])
|
||||
if _, ok := l.knownPermalinkAttributes[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type permalinkExpandError struct {
|
||||
pattern string
|
||||
err error
|
||||
}
|
||||
|
||||
func (pee *permalinkExpandError) Error() string {
|
||||
return fmt.Sprintf("error expanding %q: %s", string(pee.pattern), pee.err)
|
||||
}
|
||||
|
||||
var (
|
||||
errPermalinkIllFormed = errors.New("permalink ill-formed")
|
||||
errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
|
||||
)
|
||||
|
||||
func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string, error) {
|
||||
// a Page contains a Node which provides a field Date, time.Time
|
||||
switch dateField {
|
||||
case "year":
|
||||
return strconv.Itoa(p.Date().Year()), nil
|
||||
case "month":
|
||||
return fmt.Sprintf("%02d", int(p.Date().Month())), nil
|
||||
case "monthname":
|
||||
return p.Date().Month().String(), nil
|
||||
case "day":
|
||||
return fmt.Sprintf("%02d", p.Date().Day()), nil
|
||||
case "weekday":
|
||||
return strconv.Itoa(int(p.Date().Weekday())), nil
|
||||
case "weekdayname":
|
||||
return p.Date().Weekday().String(), nil
|
||||
case "yearday":
|
||||
return strconv.Itoa(p.Date().YearDay()), nil
|
||||
}
|
||||
//TODO: support classic strftime escapes too
|
||||
// (and pass those through despite not being in the map)
|
||||
panic("coding error: should not be here")
|
||||
}
|
||||
|
||||
// pageToPermalinkTitle returns the URL-safe form of the title
|
||||
func (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error) {
|
||||
return l.ps.URLize(p.Title()), nil
|
||||
}
|
||||
|
||||
// pageToPermalinkFilename returns the URL-safe form of the filename
|
||||
func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, error) {
|
||||
name := p.File().TranslationBaseName()
|
||||
if name == "index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
}
|
||||
|
||||
return l.ps.URLize(name), nil
|
||||
}
|
||||
|
||||
// if the page has a slug, return the slug, else return the title
|
||||
func (l PermalinkExpander) pageToPermalinkSlugElseTitle(p Page, a string) (string, error) {
|
||||
if p.Slug() != "" {
|
||||
return l.ps.URLize(p.Slug()), nil
|
||||
}
|
||||
return l.pageToPermalinkTitle(p, a)
|
||||
}
|
||||
|
||||
func (l PermalinkExpander) pageToPermalinkSection(p Page, _ string) (string, error) {
|
||||
return p.Section(), nil
|
||||
}
|
||||
|
||||
func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) {
|
||||
return p.CurrentSection().SectionsPath(), nil
|
||||
}
|
180
resources/page/permalinks_test.go
Normal file
180
resources/page/permalinks_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testdataPermalinks is used by a couple of tests; the expandsTo content is
|
||||
// subject to the data in simplePageJSON.
|
||||
var testdataPermalinks = []struct {
|
||||
spec string
|
||||
valid bool
|
||||
expandsTo string
|
||||
}{
|
||||
{":title", true, "spf13-vim-3.0-release-and-new-website"},
|
||||
{"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"},
|
||||
{"/:year/:yearday/:month/:monthname/:day/:weekday/:weekdayname/", true, "/2012/97/04/April/06/5/Friday/"}, // Dates
|
||||
{"/:section/", true, "/blue/"}, // Section
|
||||
{"/:title/", true, "/spf13-vim-3.0-release-and-new-website/"}, // Title
|
||||
{"/:slug/", true, "/the-slug/"}, // Slug
|
||||
{"/:filename/", true, "/test-page/"}, // Filename
|
||||
// TODO(moorereason): need test scaffolding for this.
|
||||
//{"/:sections/", false, "/blue/"}, // Sections
|
||||
|
||||
// Failures
|
||||
{"/blog/:fred", false, ""},
|
||||
{"/:year//:title", false, ""},
|
||||
}
|
||||
|
||||
func TestPermalinkExpansion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
page := newTestPageWithFile("/test-page/index.md")
|
||||
page.title = "Spf13 Vim 3.0 Release and new website"
|
||||
d, _ := time.Parse("2006-01-02", "2012-04-06")
|
||||
page.date = d
|
||||
page.section = "blue"
|
||||
page.slug = "The Slug"
|
||||
|
||||
for i, item := range testdataPermalinks {
|
||||
|
||||
msg := fmt.Sprintf("Test %d", i)
|
||||
|
||||
if !item.valid {
|
||||
continue
|
||||
}
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": item.spec,
|
||||
}
|
||||
|
||||
ps := newTestPathSpec()
|
||||
ps.Cfg.Set("permalinks", permalinksConfig)
|
||||
|
||||
expander, err := NewPermalinkExpander(ps)
|
||||
assert.NoError(err)
|
||||
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
assert.NoError(err)
|
||||
assert.Equal(item.expandsTo, expanded, msg)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermalinkExpansionMultiSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
page := newTestPage()
|
||||
page.title = "Page Title"
|
||||
d, _ := time.Parse("2006-01-02", "2012-04-06")
|
||||
page.date = d
|
||||
page.section = "blue"
|
||||
page.slug = "The Slug"
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": "/:slug",
|
||||
"blog": "/:section/:year",
|
||||
}
|
||||
|
||||
ps := newTestPathSpec()
|
||||
ps.Cfg.Set("permalinks", permalinksConfig)
|
||||
|
||||
expander, err := NewPermalinkExpander(ps)
|
||||
assert.NoError(err)
|
||||
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
assert.NoError(err)
|
||||
assert.Equal("/the-slug", expanded)
|
||||
|
||||
expanded, err = expander.Expand("blog", page)
|
||||
assert.NoError(err)
|
||||
assert.Equal("/blue/2012", expanded)
|
||||
|
||||
}
|
||||
|
||||
func TestPermalinkExpansionConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": "/:slug/",
|
||||
}
|
||||
|
||||
ps := newTestPathSpec()
|
||||
ps.Cfg.Set("permalinks", permalinksConfig)
|
||||
|
||||
expander, err := NewPermalinkExpander(ps)
|
||||
assert.NoError(err)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 1; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
page := newTestPage()
|
||||
for j := 1; j < 20; j++ {
|
||||
page.slug = fmt.Sprintf("slug%d", i+j)
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
assert.NoError(err)
|
||||
assert.Equal(fmt.Sprintf("/%s/", page.slug), expanded)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkPermalinkExpand(b *testing.B) {
|
||||
page := newTestPage()
|
||||
page.title = "Hugo Rocks"
|
||||
d, _ := time.Parse("2006-01-02", "2019-02-28")
|
||||
page.date = d
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": "/:year-:month-:title",
|
||||
}
|
||||
|
||||
ps := newTestPathSpec()
|
||||
ps.Cfg.Set("permalinks", permalinksConfig)
|
||||
|
||||
expander, err := NewPermalinkExpander(ps)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
s, err := expander.Expand("posts", page)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if s != "/2019-02-hugo-rocks" {
|
||||
b.Fatal(s)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
53
resources/page/site.go
Normal file
53
resources/page/site.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/navigation"
|
||||
)
|
||||
|
||||
// Site represents a site in the build. This is currently a very narrow interface,
|
||||
// but the actual implementation will be richer, see hugolib.SiteInfo.
|
||||
type Site interface {
|
||||
Language() *langs.Language
|
||||
RegularPages() Pages
|
||||
Pages() Pages
|
||||
IsServer() bool
|
||||
ServerPort() int
|
||||
Title() string
|
||||
Sites() Sites
|
||||
Hugo() hugo.Info
|
||||
BaseURL() template.URL
|
||||
Taxonomies() interface{}
|
||||
LastChange() time.Time
|
||||
Menus() navigation.Menus
|
||||
Params() map[string]interface{}
|
||||
Data() map[string]interface{}
|
||||
}
|
||||
|
||||
// Sites represents an ordered list of sites (languages).
|
||||
type Sites []Site
|
||||
|
||||
// First is a convenience method to get the first Site, i.e. the main language.
|
||||
func (s Sites) First() Site {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s[0]
|
||||
}
|
554
resources/page/testhelpers_test.go
Normal file
554
resources/page/testhelpers_test.go
Normal file
@@ -0,0 +1,554 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/bep/gitmap"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/gohugoio/hugo/navigation"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
|
||||
"github.com/gohugoio/hugo/source"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.LengthProvider = (*testPage)(nil)
|
||||
_ Page = (*testPage)(nil)
|
||||
)
|
||||
|
||||
var relatedDocsHandler = NewRelatedDocsHandler(related.DefaultConfig)
|
||||
|
||||
func newTestPage() *testPage {
|
||||
return newTestPageWithFile("/a/b/c.md")
|
||||
}
|
||||
|
||||
func newTestPageWithFile(filename string) *testPage {
|
||||
filename = filepath.FromSlash(filename)
|
||||
file := source.NewTestFile(filename)
|
||||
return &testPage{
|
||||
params: make(map[string]interface{}),
|
||||
data: make(map[string]interface{}),
|
||||
file: file,
|
||||
}
|
||||
}
|
||||
|
||||
func newTestPathSpec() *helpers.PathSpec {
|
||||
return newTestPathSpecFor(viper.New())
|
||||
}
|
||||
|
||||
func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec {
|
||||
config.SetBaseTestDefaults(cfg)
|
||||
fs := hugofs.NewMem(cfg)
|
||||
s, err := helpers.NewPathSpec(fs, cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type testPage struct {
|
||||
description string
|
||||
title string
|
||||
linkTitle string
|
||||
|
||||
section string
|
||||
|
||||
content string
|
||||
|
||||
fuzzyWordCount int
|
||||
|
||||
path string
|
||||
|
||||
slug string
|
||||
|
||||
// Dates
|
||||
date time.Time
|
||||
lastMod time.Time
|
||||
expiryDate time.Time
|
||||
pubDate time.Time
|
||||
|
||||
weight int
|
||||
|
||||
params map[string]interface{}
|
||||
data map[string]interface{}
|
||||
|
||||
file source.File
|
||||
}
|
||||
|
||||
func (p *testPage) Aliases() []string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) AllTranslations() Pages {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) AlternativeOutputFormats() OutputFormats {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Author() Author {
|
||||
return Author{}
|
||||
|
||||
}
|
||||
func (p *testPage) Authors() AuthorList {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) BaseFileName() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) BundleType() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Content() (interface{}, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) ContentBaseName() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) CurrentSection() Page {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Data() interface{} {
|
||||
return p.data
|
||||
}
|
||||
|
||||
func (p *testPage) Sitemap() config.Sitemap {
|
||||
return config.Sitemap{}
|
||||
}
|
||||
|
||||
func (p *testPage) Layout() string {
|
||||
return ""
|
||||
}
|
||||
func (p *testPage) Date() time.Time {
|
||||
return p.date
|
||||
}
|
||||
|
||||
func (p *testPage) Description() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *testPage) Dir() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Draft() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Eq(other interface{}) bool {
|
||||
return p == other
|
||||
}
|
||||
|
||||
func (p *testPage) ExpiryDate() time.Time {
|
||||
return p.expiryDate
|
||||
}
|
||||
|
||||
func (p *testPage) Ext() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Extension() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) File() source.File {
|
||||
return p.file
|
||||
}
|
||||
|
||||
func (p *testPage) FileInfo() os.FileInfo {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Filename() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) FirstSection() Page {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) FuzzyWordCount() int {
|
||||
return p.fuzzyWordCount
|
||||
}
|
||||
|
||||
func (p *testPage) GetPage(ref string) (Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) GetParam(key string) interface{} {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) GetRelatedDocsHandler() *RelatedDocsHandler {
|
||||
return relatedDocsHandler
|
||||
}
|
||||
|
||||
func (p *testPage) GitInfo() *gitmap.GitInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) HasShortcode(name string) bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Hugo() hugo.Info {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) InSection(other interface{}) (bool, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsAncestor(other interface{}) (bool, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsDescendant(other interface{}) (bool, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsDraft() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *testPage) IsHome() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsNode() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsPage() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsSection() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) IsTranslated() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Keywords() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) Kind() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Lang() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Language() *langs.Language {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) LanguagePrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *testPage) Lastmod() time.Time {
|
||||
return p.lastMod
|
||||
}
|
||||
|
||||
func (p *testPage) Len() int {
|
||||
return len(p.content)
|
||||
}
|
||||
|
||||
func (p *testPage) LinkTitle() string {
|
||||
if p.linkTitle == "" {
|
||||
return p.title
|
||||
}
|
||||
return p.linkTitle
|
||||
}
|
||||
|
||||
func (p *testPage) LogicalName() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) MediaType() media.Type {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Menus() navigation.PageMenus {
|
||||
return navigation.PageMenus{}
|
||||
}
|
||||
|
||||
func (p *testPage) Name() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Next() Page {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) NextInSection() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) NextPage() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) OutputFormats() OutputFormats {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Pages() Pages {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Paginate(seq interface{}, options ...interface{}) (*Pager, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *testPage) Paginator(options ...interface{}) (*Pager, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *testPage) Param(key interface{}) (interface{}, error) {
|
||||
return resource.Param(p, nil, key)
|
||||
}
|
||||
|
||||
func (p *testPage) Params() map[string]interface{} {
|
||||
return p.params
|
||||
}
|
||||
|
||||
func (p *testPage) Parent() Page {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Path() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
func (p *testPage) Permalink() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Plain() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) PlainWords() []string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Prev() Page {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) PrevInSection() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) PrevPage() Page {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPage) PublishDate() time.Time {
|
||||
return p.pubDate
|
||||
}
|
||||
|
||||
func (p *testPage) RSSLink() template.URL {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *testPage) RawContent() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) ReadingTime() int {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Ref(argsm map[string]interface{}) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) RefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *testPage) RelPermalink() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) RelRef(argsm map[string]interface{}) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *testPage) Render(layout ...string) template.HTML {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) ResourceType() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Resources() resource.Resources {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Scratch() *maps.Scratch {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
|
||||
v, err := p.Param(cfg.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg.ToKeywords(v)
|
||||
}
|
||||
|
||||
func (p *testPage) Section() string {
|
||||
return p.section
|
||||
}
|
||||
|
||||
func (p *testPage) Sections() Pages {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) SectionsEntries() []string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) SectionsPath() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Site() Site {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Sites() Sites {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Slug() string {
|
||||
return p.slug
|
||||
}
|
||||
|
||||
func (p *testPage) String() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
func (p *testPage) Summary() template.HTML {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) TableOfContents() template.HTML {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Title() string {
|
||||
return p.title
|
||||
}
|
||||
|
||||
func (p *testPage) TranslationBaseName() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) TranslationKey() string {
|
||||
return p.path
|
||||
}
|
||||
|
||||
func (p *testPage) Translations() Pages {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Truncated() bool {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Type() string {
|
||||
return p.section
|
||||
}
|
||||
|
||||
func (p *testPage) URL() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *testPage) UniqueID() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Weight() int {
|
||||
return p.weight
|
||||
}
|
||||
|
||||
func (p *testPage) WordCount() int {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func createTestPages(num int) Pages {
|
||||
pages := make(Pages, num)
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
m := &testPage{
|
||||
path: fmt.Sprintf("/x/y/z/p%d.md", i),
|
||||
weight: 5,
|
||||
fuzzyWordCount: i + 2, // magic
|
||||
}
|
||||
|
||||
if i%2 == 0 {
|
||||
m.weight = 10
|
||||
}
|
||||
pages[i] = m
|
||||
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
140
resources/page/weighted.go
Normal file
140
resources/page/weighted.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2019 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 page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/gohugoio/hugo/common/collections"
|
||||
)
|
||||
|
||||
var (
|
||||
_ collections.Slicer = WeightedPage{}
|
||||
)
|
||||
|
||||
// WeightedPages is a list of Pages with their corresponding (and relative) weight
|
||||
// [{Weight: 30, Page: *1}, {Weight: 40, Page: *2}]
|
||||
type WeightedPages []WeightedPage
|
||||
|
||||
// Page will return the Page (of Kind taxonomyList) that represents this set
|
||||
// of pages. This method will panic if p is empty, as that should never happen.
|
||||
func (p WeightedPages) Page() Page {
|
||||
if len(p) == 0 {
|
||||
panic("WeightedPages is empty")
|
||||
}
|
||||
|
||||
first := p[0]
|
||||
|
||||
// TODO(bep) fix tests
|
||||
if first.getOwner == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return first.getOwner()
|
||||
}
|
||||
|
||||
// A WeightedPage is a Page with a weight.
|
||||
type WeightedPage struct {
|
||||
Weight int
|
||||
Page
|
||||
|
||||
// A callback used to fetch the owning Page. This avoids having to do
|
||||
// manual .Site.GetPage lookups. It is implemented in this roundabout way
|
||||
// because we cannot add additional state to the WeightedPages slice
|
||||
// without breaking lots of templates in the wild.
|
||||
getOwner func() Page
|
||||
}
|
||||
|
||||
func NewWeightedPage(weight int, p Page, getOwner func() Page) WeightedPage {
|
||||
return WeightedPage{Weight: weight, Page: p, getOwner: getOwner}
|
||||
}
|
||||
|
||||
func (w WeightedPage) String() string {
|
||||
return fmt.Sprintf("WeightedPage(%d,%q)", w.Weight, w.Page.Title())
|
||||
}
|
||||
|
||||
// Slice is not meant to be used externally. It's a bridge function
|
||||
// for the template functions. See collections.Slice.
|
||||
func (p WeightedPage) Slice(in interface{}) (interface{}, error) {
|
||||
switch items := in.(type) {
|
||||
case WeightedPages:
|
||||
return items, nil
|
||||
case []interface{}:
|
||||
weighted := make(WeightedPages, len(items))
|
||||
for i, v := range items {
|
||||
g, ok := v.(WeightedPage)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type %T is not a WeightedPage", v)
|
||||
}
|
||||
weighted[i] = g
|
||||
}
|
||||
return weighted, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid slice type %T", items)
|
||||
}
|
||||
}
|
||||
|
||||
// Pages returns the Pages in this weighted page set.
|
||||
func (wp WeightedPages) Pages() Pages {
|
||||
pages := make(Pages, len(wp))
|
||||
for i := range wp {
|
||||
pages[i] = wp[i].Page
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
// Prev returns the previous Page relative to the given Page in
|
||||
// this weighted page set.
|
||||
func (wp WeightedPages) Prev(cur Page) Page {
|
||||
for x, c := range wp {
|
||||
if c.Page == cur {
|
||||
if x == 0 {
|
||||
return wp[len(wp)-1].Page
|
||||
}
|
||||
return wp[x-1].Page
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next returns the next Page relative to the given Page in
|
||||
// this weighted page set.
|
||||
func (wp WeightedPages) Next(cur Page) Page {
|
||||
for x, c := range wp {
|
||||
if c.Page == cur {
|
||||
if x < len(wp)-1 {
|
||||
return wp[x+1].Page
|
||||
}
|
||||
return wp[0].Page
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wp WeightedPages) Len() int { return len(wp) }
|
||||
func (wp WeightedPages) Swap(i, j int) { wp[i], wp[j] = wp[j], wp[i] }
|
||||
|
||||
// Sort stable sorts this weighted page set.
|
||||
func (wp WeightedPages) Sort() { sort.Stable(wp) }
|
||||
|
||||
// Count returns the number of pages in this weighted page set.
|
||||
func (wp WeightedPages) Count() int { return len(wp) }
|
||||
|
||||
func (wp WeightedPages) Less(i, j int) bool {
|
||||
if wp[i].Weight == wp[j].Weight {
|
||||
return DefaultPageSort(wp[i].Page, wp[j].Page)
|
||||
}
|
||||
return wp[i].Weight < wp[j].Weight
|
||||
}
|
Reference in New Issue
Block a user