hugo/hugolib/page__meta.go

653 lines
15 KiB
Go

// 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)
}