From 597e418cb02883418f2cebb41400e8e61413f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 2 Jan 2019 12:33:26 +0100 Subject: 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 --- resources/page/page.go | 365 ++++++++++++++ resources/page/page_author.go | 45 ++ resources/page/page_data.go | 42 ++ resources/page/page_data_test.go | 57 +++ resources/page/page_generate/.gitignore | 1 + .../page/page_generate/generate_page_wrappers.go | 212 ++++++++ resources/page/page_kinds.go | 25 + resources/page/page_kinds_test.go | 31 ++ resources/page/page_marshaljson.autogen.go | 198 ++++++++ resources/page/page_nop.go | 463 +++++++++++++++++ resources/page/page_outputformat.go | 85 ++++ resources/page/page_paths.go | 334 +++++++++++++ resources/page/page_paths_test.go | 258 ++++++++++ resources/page/page_wrappers.autogen.go | 97 ++++ resources/page/pagegroup.go | 369 ++++++++++++++ resources/page/pagegroup_test.go | 409 +++++++++++++++ resources/page/pagemeta/page_frontmatter.go | 427 ++++++++++++++++ resources/page/pagemeta/page_frontmatter_test.go | 262 ++++++++++ resources/page/pagemeta/pagemeta.go | 21 + resources/page/pages.go | 115 +++++ resources/page/pages_cache.go | 136 +++++ resources/page/pages_cache_test.go | 86 ++++ resources/page/pages_language_merge.go | 64 +++ resources/page/pages_prev_next.go | 42 ++ resources/page/pages_prev_next_test.go | 83 +++ resources/page/pages_related.go | 199 ++++++++ resources/page/pages_related_test.go | 86 ++++ resources/page/pages_sort.go | 348 +++++++++++++ resources/page/pages_sort_test.go | 279 +++++++++++ resources/page/pagination.go | 404 +++++++++++++++ resources/page/pagination_test.go | 307 ++++++++++++ resources/page/permalinks.go | 248 +++++++++ resources/page/permalinks_test.go | 180 +++++++ resources/page/site.go | 53 ++ resources/page/testhelpers_test.go | 554 +++++++++++++++++++++ resources/page/weighted.go | 140 ++++++ 36 files changed, 7025 insertions(+) create mode 100644 resources/page/page.go create mode 100644 resources/page/page_author.go create mode 100644 resources/page/page_data.go create mode 100644 resources/page/page_data_test.go create mode 100644 resources/page/page_generate/.gitignore create mode 100644 resources/page/page_generate/generate_page_wrappers.go create mode 100644 resources/page/page_kinds.go create mode 100644 resources/page/page_kinds_test.go create mode 100644 resources/page/page_marshaljson.autogen.go create mode 100644 resources/page/page_nop.go create mode 100644 resources/page/page_outputformat.go create mode 100644 resources/page/page_paths.go create mode 100644 resources/page/page_paths_test.go create mode 100644 resources/page/page_wrappers.autogen.go create mode 100644 resources/page/pagegroup.go create mode 100644 resources/page/pagegroup_test.go create mode 100644 resources/page/pagemeta/page_frontmatter.go create mode 100644 resources/page/pagemeta/page_frontmatter_test.go create mode 100644 resources/page/pagemeta/pagemeta.go create mode 100644 resources/page/pages.go create mode 100644 resources/page/pages_cache.go create mode 100644 resources/page/pages_cache_test.go create mode 100644 resources/page/pages_language_merge.go create mode 100644 resources/page/pages_prev_next.go create mode 100644 resources/page/pages_prev_next_test.go create mode 100644 resources/page/pages_related.go create mode 100644 resources/page/pages_related_test.go create mode 100644 resources/page/pages_sort.go create mode 100644 resources/page/pages_sort_test.go create mode 100644 resources/page/pagination.go create mode 100644 resources/page/pagination_test.go create mode 100644 resources/page/permalinks.go create mode 100644 resources/page/permalinks_test.go create mode 100644 resources/page/site.go create mode 100644 resources/page/testhelpers_test.go create mode 100644 resources/page/weighted.go (limited to 'resources/page') diff --git a/resources/page/page.go b/resources/page/page.go new file mode 100644 index 000000000..efbefb456 --- /dev/null +++ b/resources/page/page.go @@ -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 { +} diff --git a/resources/page/page_author.go b/resources/page/page_author.go new file mode 100644 index 000000000..9e8a95182 --- /dev/null +++ b/resources/page/page_author.go @@ -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 diff --git a/resources/page/page_data.go b/resources/page/page_data.go new file mode 100644 index 000000000..3345a44da --- /dev/null +++ b/resources/page/page_data.go @@ -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)) + } +} diff --git a/resources/page/page_data_test.go b/resources/page/page_data_test.go new file mode 100644 index 000000000..b6641bcd7 --- /dev/null +++ b/resources/page/page_data_test.go @@ -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)") + +} diff --git a/resources/page/page_generate/.gitignore b/resources/page/page_generate/.gitignore new file mode 100644 index 000000000..84fd70a9f --- /dev/null +++ b/resources/page/page_generate/.gitignore @@ -0,0 +1 @@ +generate \ No newline at end of file diff --git a/resources/page/page_generate/generate_page_wrappers.go b/resources/page/page_generate/generate_page_wrappers.go new file mode 100644 index 000000000..af85cb429 --- /dev/null +++ b/resources/page/page_generate/generate_page_wrappers.go @@ -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 + ")" +} diff --git a/resources/page/page_kinds.go b/resources/page/page_kinds.go new file mode 100644 index 000000000..a2e59438e --- /dev/null +++ b/resources/page/page_kinds.go @@ -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" +) diff --git a/resources/page/page_kinds_test.go b/resources/page/page_kinds_test.go new file mode 100644 index 000000000..8ad7343dc --- /dev/null +++ b/resources/page/page_kinds_test.go @@ -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) + +} diff --git a/resources/page/page_marshaljson.autogen.go b/resources/page/page_marshaljson.autogen.go new file mode 100644 index 000000000..5f4c9d32f --- /dev/null +++ b/resources/page/page_marshaljson.autogen.go @@ -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) +} diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go new file mode 100644 index 000000000..7afbee216 --- /dev/null +++ b/resources/page/page_nop.go @@ -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 +} diff --git a/resources/page/page_outputformat.go b/resources/page/page_outputformat.go new file mode 100644 index 000000000..ff4213cc4 --- /dev/null +++ b/resources/page/page_outputformat.go @@ -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 +} diff --git a/resources/page/page_paths.go b/resources/page/page_paths.go new file mode 100644 index 000000000..160c225b1 --- /dev/null +++ b/resources/page/page_paths.go @@ -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 +} diff --git a/resources/page/page_paths_test.go b/resources/page/page_paths_test.go new file mode 100644 index 000000000..4aaa41e8a --- /dev/null +++ b/resources/page/page_paths_test.go @@ -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 != "") &&