diff options
Diffstat (limited to 'hugolib/page__meta.go')
-rw-r--r-- | hugolib/page__meta.go | 652 |
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) +} |