mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
92baa14fd3
Fixes #5757
654 lines
15 KiB
Go
654 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"
|
|
"path/filepath"
|
|
"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 i, alias := range pm.aliases {
|
|
if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
|
|
return fmt.Errorf("http* aliases not supported: %q", alias)
|
|
}
|
|
pm.aliases[i] = filepath.ToSlash(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)
|
|
}
|