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