shithub: hugo

ref: a55640de8e3944d3b9f64b15155148a0e35cb31e
dir: /hugolib/page__meta.go/

View raw version
// 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().IsZero() {
		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().IsZero() {
		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().IsZero() {
			// Fall back to file extension
			p.markup = helpers.GuessType(p.File().Ext())
		}
		if p.markup == "" {
			p.markup = "unknown"
		}
	}

	if p.title == "" && p.f.IsZero() {
		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)
}