summaryrefslogtreecommitdiffstats
path: root/hugolib/page__meta.go
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib/page__meta.go')
-rw-r--r--hugolib/page__meta.go652
1 files changed, 652 insertions, 0 deletions
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
new file mode 100644
index 000000000..8532f5016
--- /dev/null
+++ b/hugolib/page__meta.go
@@ -0,0 +1,652 @@
+// 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 hugolib
+
+import (
+ "fmt"
+ "path"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/related"
+
+ "github.com/gohugoio/hugo/source"
+ "github.com/markbates/inflect"
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/gohugoio/hugo/output"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/spf13/cast"
+)
+
+var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)
+
+type pageMeta struct {
+ // kind is the discriminator that identifies the different page types
+ // in the different page collections. This can, as an example, be used
+ // to to filter regular pages, find sections etc.
+ // Kind will, for the pages available to the templates, be one of:
+ // page, home, section, taxonomy and taxonomyTerm.
+ // It is of string type to make it easy to reason about in
+ // the templates.
+ kind string
+
+ // This is a standalone page not part of any page collection. These
+ // include sitemap, robotsTXT and similar. It will have no pageOutputs, but
+ // a fixed pageOutput.
+ standalone bool
+
+ bundleType string
+
+ // Params contains configuration defined in the params section of page frontmatter.
+ params map[string]interface{}
+
+ title string
+ linkTitle string
+
+ resourcePath string
+
+ weight int
+
+ markup string
+ contentType string
+
+ // whether the content is in a CJK language.
+ isCJKLanguage bool
+
+ layout string
+
+ aliases []string
+
+ draft bool
+
+ description string
+ keywords []string
+
+ urlPaths pagemeta.URLPath
+
+ resource.Dates
+
+ // This is enabled if it is a leaf bundle (the "index.md" type) and it is marked as headless in front matter.
+ // Being headless means that
+ // 1. The page itself is not rendered to disk
+ // 2. It is not available in .Site.Pages etc.
+ // 3. But you can get it via .Site.GetPage
+ headless bool
+
+ // A key that maps to translation(s) of this page. This value is fetched
+ // from the page front matter.
+ translationKey string
+
+ // From front matter.
+ configuredOutputFormats output.Formats
+
+ // This is the raw front matter metadata that is going to be assigned to
+ // the Resources above.
+ resourcesMetadata []map[string]interface{}
+
+ f source.File
+
+ sections []string
+
+ // Sitemap overrides from front matter.
+ sitemap config.Sitemap
+
+ s *Site
+
+ renderingConfig *helpers.BlackFriday
+}
+
+func (p *pageMeta) Aliases() []string {
+ return p.aliases
+}
+
+func (p *pageMeta) Author() page.Author {
+ authors := p.Authors()
+
+ for _, author := range authors {
+ return author
+ }
+ return page.Author{}
+}
+
+func (p *pageMeta) Authors() page.AuthorList {
+ authorKeys, ok := p.params["authors"]
+ if !ok {
+ return page.AuthorList{}
+ }
+ authors := authorKeys.([]string)
+ if len(authors) < 1 || len(p.s.Info.Authors) < 1 {
+ return page.AuthorList{}
+ }
+
+ al := make(page.AuthorList)
+ for _, author := range authors {
+ a, ok := p.s.Info.Authors[author]
+ if ok {
+ al[author] = a
+ }
+ }
+ return al
+}
+
+func (p *pageMeta) BundleType() string {
+ return p.bundleType
+}
+
+func (p *pageMeta) Description() string {
+ return p.description
+}
+
+func (p *pageMeta) Lang() string {
+ return p.s.Lang()
+}
+
+func (p *pageMeta) Draft() bool {
+ return p.draft
+}
+
+func (p *pageMeta) File() source.File {
+ return p.f
+}
+
+func (p *pageMeta) IsHome() bool {
+ return p.Kind() == page.KindHome
+}
+
+func (p *pageMeta) Keywords() []string {
+ return p.keywords
+}
+
+func (p *pageMeta) Kind() string {
+ return p.kind
+}
+
+func (p *pageMeta) Layout() string {
+ return p.layout
+}
+
+func (p *pageMeta) LinkTitle() string {
+ if p.linkTitle != "" {
+ return p.linkTitle
+ }
+
+ return p.Title()
+}
+
+func (p *pageMeta) Name() string {
+ if p.resourcePath != "" {
+ return p.resourcePath
+ }
+ return p.Title()
+}
+
+func (p *pageMeta) IsNode() bool {
+ return !p.IsPage()
+}
+
+func (p *pageMeta) IsPage() bool {
+ return p.Kind() == page.KindPage
+}
+
+// Param is a convenience method to do lookups in Page's and Site's Params map,
+// in that order.
+//
+// This method is also implemented on SiteInfo.
+// TODO(bep) interface
+func (p *pageMeta) Param(key interface{}) (interface{}, error) {
+ return resource.Param(p, p.s.Info.Params(), key)
+}
+
+func (p *pageMeta) Params() map[string]interface{} {
+ return p.params
+}
+
+func (p *pageMeta) Path() string {
+ if p.File() != nil {
+ return p.File().Path()
+ }
+ return p.SectionsPath()
+}
+
+// RelatedKeywords implements the related.Document interface needed for fast page searches.
+func (p *pageMeta) 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 *pageMeta) IsSection() bool {
+ return p.Kind() == page.KindSection
+}
+
+func (p *pageMeta) Section() string {
+ if p.IsHome() {
+ return ""
+ }
+
+ if p.IsNode() {
+ if len(p.sections) == 0 {
+ // May be a sitemap or similar.
+ return ""
+ }
+ return p.sections[0]
+ }
+
+ if p.File() != nil {
+ return p.File().Section()
+ }
+
+ panic("invalid page state")
+
+}
+
+func (p *pageMeta) SectionsEntries() []string {
+ return p.sections
+}
+
+func (p *pageMeta) SectionsPath() string {
+ return path.Join(p.SectionsEntries()...)
+}
+
+func (p *pageMeta) Sitemap() config.Sitemap {
+ return p.sitemap
+}
+
+func (p *pageMeta) Title() string {
+ return p.title
+}
+
+func (p *pageMeta) Type() string {
+ if p.contentType != "" {
+ return p.contentType
+ }
+
+ if x := p.Section(); x != "" {
+ return x
+ }
+
+ return "page"
+}
+
+func (p *pageMeta) Weight() int {
+ return p.weight
+}
+
+func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}) error {
+ if frontmatter == nil {
+ return errors.New("missing frontmatter data")
+ }
+
+ pm.params = make(map[string]interface{})
+
+ // Needed for case insensitive fetching of params values
+ maps.ToLower(frontmatter)
+
+ var mtime time.Time
+ if p.File().FileInfo() != nil {
+ mtime = p.File().FileInfo().ModTime()
+ }
+
+ var gitAuthorDate time.Time
+ if p.gitInfo != nil {
+ gitAuthorDate = p.gitInfo.AuthorDate
+ }
+
+ descriptor := &pagemeta.FrontMatterDescriptor{
+ Frontmatter: frontmatter,
+ Params: pm.params,
+ Dates: &pm.Dates,
+ PageURLs: &pm.urlPaths,
+ BaseFilename: p.File().ContentBaseName(),
+ ModTime: mtime,
+ GitAuthorDate: gitAuthorDate,
+ }
+
+ // Handle the date separately
+ // TODO(bep) we need to "do more" in this area so this can be split up and
+ // more easily tested without the Page, but the coupling is strong.
+ err := pm.s.frontmatterHandler.HandleDates(descriptor)
+ if err != nil {
+ p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
+ }
+
+ var sitemapSet bool
+
+ var draft, published, isCJKLanguage *bool
+ for k, v := range frontmatter {
+ loki := strings.ToLower(k)
+
+ if loki == "published" { // Intentionally undocumented
+ vv, err := cast.ToBoolE(v)
+ if err == nil {
+ published = &vv
+ }
+ // published may also be a date
+ continue
+ }
+
+ if pm.s.frontmatterHandler.IsDateKey(loki) {
+ continue
+ }
+
+ switch loki {
+ case "title":
+ pm.title = cast.ToString(v)
+ pm.params[loki] = pm.title
+ case "linktitle":
+ pm.linkTitle = cast.ToString(v)
+ pm.params[loki] = pm.linkTitle
+ case "description":
+ pm.description = cast.ToString(v)
+ pm.params[loki] = pm.description
+ case "slug":
+ // Don't start or end with a -
+ pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-")
+ pm.params[loki] = pm.Slug()
+ case "url":
+ if url := cast.ToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
+ return fmt.Errorf("only relative URLs are supported, %v provided", url)
+ }
+ pm.urlPaths.URL = cast.ToString(v)
+ pm.params[loki] = pm.urlPaths.URL
+ case "type":
+ pm.contentType = cast.ToString(v)
+ pm.params[loki] = pm.contentType
+ case "keywords":
+ pm.keywords = cast.ToStringSlice(v)
+ pm.params[loki] = pm.keywords
+ case "headless":
+ // For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output).
+ // We may expand on this in the future, but that gets more complex pretty fast.
+ if p.File().TranslationBaseName() == "index" {
+ pm.headless = cast.ToBool(v)
+ }
+ pm.params[loki] = pm.headless
+ case "outputs":
+ o := cast.ToStringSlice(v)
+ if len(o) > 0 {
+ // Output formats are exlicitly set in front matter, use those.
+ outFormats, err := p.s.outputFormatsConfig.GetByNames(o...)
+
+ if err != nil {
+ p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)
+ } else {
+ pm.configuredOutputFormats = outFormats
+ pm.params[loki] = outFormats
+ }
+
+ }
+ case "draft":
+ draft = new(bool)
+ *draft = cast.ToBool(v)
+ case "layout":
+ pm.layout = cast.ToString(v)
+ pm.params[loki] = pm.layout
+ case "markup":
+ pm.markup = cast.ToString(v)
+ pm.params[loki] = pm.markup
+ case "weight":
+ pm.weight = cast.ToInt(v)
+ pm.params[loki] = pm.weight
+ case "aliases":
+ pm.aliases = cast.ToStringSlice(v)
+ for _, alias := range pm.aliases {
+ if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
+ return fmt.Errorf("only relative aliases are supported, %v provided", alias)
+ }
+ }
+ pm.params[loki] = pm.aliases
+ case "sitemap":
+ p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, cast.ToStringMap(v))
+ pm.params[loki] = p.m.sitemap
+ sitemapSet = true
+ case "iscjklanguage":
+ isCJKLanguage = new(bool)
+ *isCJKLanguage = cast.ToBool(v)
+ case "translationkey":
+ pm.translationKey = cast.ToString(v)
+ pm.params[loki] = pm.translationKey
+ case "resources":
+ var resources []map[string]interface{}
+ handled := true
+
+ switch vv := v.(type) {
+ case []map[interface{}]interface{}:
+ for _, vvv := range vv {
+ resources = append(resources, cast.ToStringMap(vvv))
+ }
+ case []map[string]interface{}:
+ resources = append(resources, vv...)
+ case []interface{}:
+ for _, vvv := range vv {
+ switch vvvv := vvv.(type) {
+ case map[interface{}]interface{}:
+ resources = append(resources, cast.ToStringMap(vvvv))
+ case map[string]interface{}:
+ resources = append(resources, vvvv)
+ }
+ }
+ default:
+ handled = false
+ }
+
+ if handled {
+ pm.params[loki] = resources
+ pm.resourcesMetadata = resources
+ break
+ }
+ fallthrough
+
+ default:
+ // If not one of the explicit values, store in Params
+ switch vv := v.(type) {
+ case bool:
+ pm.params[loki] = vv
+ case string:
+ pm.params[loki] = vv
+ case int64, int32, int16, int8, int:
+ pm.params[loki] = vv
+ case float64, float32:
+ pm.params[loki] = vv
+ case time.Time:
+ pm.params[loki] = vv
+ default: // handle array of strings as well
+ switch vvv := vv.(type) {
+ case []interface{}:
+ if len(vvv) > 0 {
+ switch vvv[0].(type) {
+ case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter
+ pm.params[loki] = vvv
+ case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter
+ pm.params[loki] = vvv
+ case []interface{}:
+ pm.params[loki] = vvv
+ default:
+ a := make([]string, len(vvv))
+ for i, u := range vvv {
+ a[i] = cast.ToString(u)
+ }
+
+ pm.params[loki] = a
+ }
+ } else {
+ pm.params[loki] = []string{}
+ }
+ default:
+ pm.params[loki] = vv
+ }
+ }
+ }
+ }
+
+ if !sitemapSet {
+ pm.sitemap = p.s.siteCfg.sitemap
+ }
+
+ pm.markup = helpers.GuessType(pm.markup)
+
+ if draft != nil && published != nil {
+ pm.draft = *draft
+ p.m.s.Log.WARN.Printf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
+ } else if draft != nil {
+ pm.draft = *draft
+ } else if published != nil {
+ pm.draft = !*published
+ }
+ pm.params["draft"] = pm.draft
+
+ if isCJKLanguage != nil {
+ pm.isCJKLanguage = *isCJKLanguage
+ } else if p.s.siteCfg.hasCJKLanguage {
+ if cjkRe.Match(p.source.parsed.Input()) {
+ pm.isCJKLanguage = true
+ } else {
+ pm.isCJKLanguage = false
+ }
+ }
+
+ pm.params["iscjklanguage"] = p.m.isCJKLanguage
+
+ return nil
+}
+
+func (p *pageMeta) applyDefaultValues() error {
+ if p.markup == "" {
+ if p.File() != nil {
+ // Fall back to {file extension
+ p.markup = helpers.GuessType(p.File().Ext())
+ }
+ if p.markup == "" {
+ p.markup = "unknown"
+ }
+ }
+
+ if p.title == "" {
+ switch p.Kind() {
+ case page.KindHome:
+ p.title = p.s.Info.title
+ case page.KindSection:
+ sectionName := helpers.FirstUpper(p.sections[0])
+ if p.s.Cfg.GetBool("pluralizeListTitles") {
+ p.title = inflect.Pluralize(sectionName)
+ } else {
+ p.title = sectionName
+ }
+ case page.KindTaxonomy:
+ key := p.sections[len(p.sections)-1]
+ p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1)
+ case page.KindTaxonomyTerm:
+ p.title = p.s.titleFunc(p.sections[0])
+ case kind404:
+ p.title = "404 Page not found"
+
+ }
+ }
+
+ if p.IsNode() {
+ p.bundleType = "branch"
+ } else {
+ source := p.File()
+ if fi, ok := source.(*fileInfo); ok {
+ switch fi.bundleTp {
+ case bundleBranch:
+ p.bundleType = "branch"
+ case bundleLeaf:
+ p.bundleType = "leaf"
+ }
+ }
+ }
+
+ bfParam := getParamToLower(p, "blackfriday")
+ if bfParam != nil {
+ p.renderingConfig = p.s.ContentSpec.BlackFriday
+
+ // Create a copy so we can modify it.
+ bf := *p.s.ContentSpec.BlackFriday
+ p.renderingConfig = &bf
+ pageParam := cast.ToStringMap(bfParam)
+ if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil {
+ return errors.WithMessage(err, "failed to decode rendering config")
+ }
+ }
+
+ return nil
+
+}
+
+// The output formats this page will be rendered to.
+func (m *pageMeta) outputFormats() output.Formats {
+ if len(m.configuredOutputFormats) > 0 {
+ return m.configuredOutputFormats
+ }
+
+ return m.s.outputFormats[m.Kind()]
+}
+
+func (p *pageMeta) Slug() string {
+ return p.urlPaths.Slug
+}
+
+func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) interface{} {
+ v := m.Params()[strings.ToLower(key)]
+
+ if v == nil {
+ return nil
+ }
+
+ switch val := v.(type) {
+ case bool:
+ return val
+ case string:
+ if stringToLower {
+ return strings.ToLower(val)
+ }
+ return val
+ case int64, int32, int16, int8, int:
+ return cast.ToInt(v)
+ case float64, float32:
+ return cast.ToFloat64(v)
+ case time.Time:
+ return val
+ case []string:
+ if stringToLower {
+ return helpers.SliceToLower(val)
+ }
+ return v
+ case map[string]interface{}: // JSON and TOML
+ return v
+ case map[interface{}]interface{}: // YAML
+ return v
+ }
+
+ //p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v))
+ return nil
+}
+
+func getParamToLower(m resource.ResourceParamsProvider, key string) interface{} {
+ return getParam(m, key, true)
+}