github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/hugolib/page__meta.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package hugolib
    15  
    16  import (
    17  	"fmt"
    18  	"path"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/gohugoio/hugo/langs"
    26  
    27  	"github.com/gobuffalo/flect"
    28  	"github.com/gohugoio/hugo/markup/converter"
    29  
    30  	"github.com/gohugoio/hugo/hugofs/files"
    31  
    32  	"github.com/gohugoio/hugo/common/hugo"
    33  
    34  	"github.com/gohugoio/hugo/related"
    35  
    36  	"github.com/gohugoio/hugo/source"
    37  	"github.com/pkg/errors"
    38  
    39  	"github.com/gohugoio/hugo/common/maps"
    40  	"github.com/gohugoio/hugo/config"
    41  	"github.com/gohugoio/hugo/helpers"
    42  
    43  	"github.com/gohugoio/hugo/output"
    44  	"github.com/gohugoio/hugo/resources/page"
    45  	"github.com/gohugoio/hugo/resources/page/pagemeta"
    46  	"github.com/gohugoio/hugo/resources/resource"
    47  	"github.com/spf13/cast"
    48  )
    49  
    50  var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)
    51  
    52  type pageMeta struct {
    53  	// kind is the discriminator that identifies the different page types
    54  	// in the different page collections. This can, as an example, be used
    55  	// to to filter regular pages, find sections etc.
    56  	// Kind will, for the pages available to the templates, be one of:
    57  	// page, home, section, taxonomy and term.
    58  	// It is of string type to make it easy to reason about in
    59  	// the templates.
    60  	kind string
    61  
    62  	// This is a standalone page not part of any page collection. These
    63  	// include sitemap, robotsTXT and similar. It will have no pageOutputs, but
    64  	// a fixed pageOutput.
    65  	standalone bool
    66  
    67  	draft       bool // Only published when running with -D flag
    68  	buildConfig pagemeta.BuildConfig
    69  
    70  	bundleType files.ContentClass
    71  
    72  	// Params contains configuration defined in the params section of page frontmatter.
    73  	params map[string]interface{}
    74  
    75  	title     string
    76  	linkTitle string
    77  
    78  	summary string
    79  
    80  	resourcePath string
    81  
    82  	weight int
    83  
    84  	markup      string
    85  	contentType string
    86  
    87  	// whether the content is in a CJK language.
    88  	isCJKLanguage bool
    89  
    90  	layout string
    91  
    92  	aliases []string
    93  
    94  	description string
    95  	keywords    []string
    96  
    97  	urlPaths pagemeta.URLPath
    98  
    99  	resource.Dates
   100  
   101  	// Set if this page is bundled inside another.
   102  	bundled bool
   103  
   104  	// A key that maps to translation(s) of this page. This value is fetched
   105  	// from the page front matter.
   106  	translationKey string
   107  
   108  	// From front matter.
   109  	configuredOutputFormats output.Formats
   110  
   111  	// This is the raw front matter metadata that is going to be assigned to
   112  	// the Resources above.
   113  	resourcesMetadata []map[string]interface{}
   114  
   115  	f source.File
   116  
   117  	sections []string
   118  
   119  	// Sitemap overrides from front matter.
   120  	sitemap config.Sitemap
   121  
   122  	s *Site
   123  
   124  	renderingConfigOverrides map[string]interface{}
   125  	contentConverterInit     sync.Once
   126  	contentConverter         converter.Converter
   127  }
   128  
   129  func (p *pageMeta) Aliases() []string {
   130  	return p.aliases
   131  }
   132  
   133  func (p *pageMeta) Author() page.Author {
   134  	authors := p.Authors()
   135  
   136  	for _, author := range authors {
   137  		return author
   138  	}
   139  	return page.Author{}
   140  }
   141  
   142  func (p *pageMeta) Authors() page.AuthorList {
   143  	authorKeys, ok := p.params["authors"]
   144  	if !ok {
   145  		return page.AuthorList{}
   146  	}
   147  	authors := authorKeys.([]string)
   148  	if len(authors) < 1 || len(p.s.Info.Authors) < 1 {
   149  		return page.AuthorList{}
   150  	}
   151  
   152  	al := make(page.AuthorList)
   153  	for _, author := range authors {
   154  		a, ok := p.s.Info.Authors[author]
   155  		if ok {
   156  			al[author] = a
   157  		}
   158  	}
   159  	return al
   160  }
   161  
   162  func (p *pageMeta) BundleType() files.ContentClass {
   163  	return p.bundleType
   164  }
   165  
   166  func (p *pageMeta) Description() string {
   167  	return p.description
   168  }
   169  
   170  func (p *pageMeta) Lang() string {
   171  	return p.s.Lang()
   172  }
   173  
   174  func (p *pageMeta) Draft() bool {
   175  	return p.draft
   176  }
   177  
   178  func (p *pageMeta) File() source.File {
   179  	return p.f
   180  }
   181  
   182  func (p *pageMeta) IsHome() bool {
   183  	return p.Kind() == page.KindHome
   184  }
   185  
   186  func (p *pageMeta) Keywords() []string {
   187  	return p.keywords
   188  }
   189  
   190  func (p *pageMeta) Kind() string {
   191  	return p.kind
   192  }
   193  
   194  func (p *pageMeta) Layout() string {
   195  	return p.layout
   196  }
   197  
   198  func (p *pageMeta) LinkTitle() string {
   199  	if p.linkTitle != "" {
   200  		return p.linkTitle
   201  	}
   202  
   203  	return p.Title()
   204  }
   205  
   206  func (p *pageMeta) Name() string {
   207  	if p.resourcePath != "" {
   208  		return p.resourcePath
   209  	}
   210  	return p.Title()
   211  }
   212  
   213  func (p *pageMeta) IsNode() bool {
   214  	return !p.IsPage()
   215  }
   216  
   217  func (p *pageMeta) IsPage() bool {
   218  	return p.Kind() == page.KindPage
   219  }
   220  
   221  // Param is a convenience method to do lookups in Page's and Site's Params map,
   222  // in that order.
   223  //
   224  // This method is also implemented on SiteInfo.
   225  // TODO(bep) interface
   226  func (p *pageMeta) Param(key interface{}) (interface{}, error) {
   227  	return resource.Param(p, p.s.Info.Params(), key)
   228  }
   229  
   230  func (p *pageMeta) Params() maps.Params {
   231  	return p.params
   232  }
   233  
   234  func (p *pageMeta) Path() string {
   235  	if !p.File().IsZero() {
   236  		const example = `
   237    {{ $path := "" }}
   238    {{ with .File }}
   239  	{{ $path = .Path }}
   240    {{ else }}
   241  	{{ $path = .Path }}
   242    {{ end }}
   243  `
   244  		helpers.Deprecated(".Path when the page is backed by a file", "We plan to use Path for a canonical source path and you probably want to check the source is a file. To get the current behaviour, you can use a construct similar to the one below:\n"+example, false)
   245  
   246  	}
   247  
   248  	return p.Pathc()
   249  }
   250  
   251  // This is just a bridge method, use Path in templates.
   252  func (p *pageMeta) Pathc() string {
   253  	if !p.File().IsZero() {
   254  		return p.File().Path()
   255  	}
   256  	return p.SectionsPath()
   257  }
   258  
   259  // RelatedKeywords implements the related.Document interface needed for fast page searches.
   260  func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
   261  	v, err := p.Param(cfg.Name)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  
   266  	return cfg.ToKeywords(v)
   267  }
   268  
   269  func (p *pageMeta) IsSection() bool {
   270  	return p.Kind() == page.KindSection
   271  }
   272  
   273  func (p *pageMeta) Section() string {
   274  	if p.IsHome() {
   275  		return ""
   276  	}
   277  
   278  	if p.IsNode() {
   279  		if len(p.sections) == 0 {
   280  			// May be a sitemap or similar.
   281  			return ""
   282  		}
   283  		return p.sections[0]
   284  	}
   285  
   286  	if !p.File().IsZero() {
   287  		return p.File().Section()
   288  	}
   289  
   290  	panic("invalid page state")
   291  }
   292  
   293  func (p *pageMeta) SectionsEntries() []string {
   294  	return p.sections
   295  }
   296  
   297  func (p *pageMeta) SectionsPath() string {
   298  	return path.Join(p.SectionsEntries()...)
   299  }
   300  
   301  func (p *pageMeta) Sitemap() config.Sitemap {
   302  	return p.sitemap
   303  }
   304  
   305  func (p *pageMeta) Title() string {
   306  	return p.title
   307  }
   308  
   309  const defaultContentType = "page"
   310  
   311  func (p *pageMeta) Type() string {
   312  	if p.contentType != "" {
   313  		return p.contentType
   314  	}
   315  
   316  	if sect := p.Section(); sect != "" {
   317  		return sect
   318  	}
   319  
   320  	return defaultContentType
   321  }
   322  
   323  func (p *pageMeta) Weight() int {
   324  	return p.weight
   325  }
   326  
   327  func (pm *pageMeta) mergeBucketCascades(b1, b2 *pagesMapBucket) {
   328  	if b1.cascade == nil {
   329  		b1.cascade = make(map[page.PageMatcher]maps.Params)
   330  	}
   331  
   332  	if b2 != nil && b2.cascade != nil {
   333  		for k, v := range b2.cascade {
   334  
   335  			vv, found := b1.cascade[k]
   336  			if !found {
   337  				b1.cascade[k] = v
   338  			} else {
   339  				// Merge
   340  				for ck, cv := range v {
   341  					if _, found := vv[ck]; !found {
   342  						vv[ck] = cv
   343  					}
   344  				}
   345  			}
   346  		}
   347  	}
   348  }
   349  
   350  func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, frontmatter map[string]interface{}) error {
   351  	pm.params = make(maps.Params)
   352  
   353  	if frontmatter == nil && (parentBucket == nil || parentBucket.cascade == nil) {
   354  		return nil
   355  	}
   356  
   357  	if frontmatter != nil {
   358  		// Needed for case insensitive fetching of params values
   359  		maps.PrepareParams(frontmatter)
   360  		if p.bucket != nil {
   361  			// Check for any cascade define on itself.
   362  			if cv, found := frontmatter["cascade"]; found {
   363  				var err error
   364  				p.bucket.cascade, err = page.DecodeCascade(cv)
   365  				if err != nil {
   366  					return err
   367  				}
   368  			}
   369  		}
   370  	} else {
   371  		frontmatter = make(map[string]interface{})
   372  	}
   373  
   374  	var cascade map[page.PageMatcher]maps.Params
   375  
   376  	if p.bucket != nil {
   377  		if parentBucket != nil {
   378  			// Merge missing keys from parent into this.
   379  			pm.mergeBucketCascades(p.bucket, parentBucket)
   380  		}
   381  		cascade = p.bucket.cascade
   382  	} else if parentBucket != nil {
   383  		cascade = parentBucket.cascade
   384  	}
   385  
   386  	for m, v := range cascade {
   387  		if !m.Matches(p) {
   388  			continue
   389  		}
   390  		for kk, vv := range v {
   391  			if _, found := frontmatter[kk]; !found {
   392  				frontmatter[kk] = vv
   393  			}
   394  		}
   395  	}
   396  
   397  	var mtime time.Time
   398  	var contentBaseName string
   399  	if !p.File().IsZero() {
   400  		contentBaseName = p.File().ContentBaseName()
   401  		if p.File().FileInfo() != nil {
   402  			mtime = p.File().FileInfo().ModTime()
   403  		}
   404  	}
   405  
   406  	var gitAuthorDate time.Time
   407  	if p.gitInfo != nil {
   408  		gitAuthorDate = p.gitInfo.AuthorDate
   409  	}
   410  
   411  	descriptor := &pagemeta.FrontMatterDescriptor{
   412  		Frontmatter:   frontmatter,
   413  		Params:        pm.params,
   414  		Dates:         &pm.Dates,
   415  		PageURLs:      &pm.urlPaths,
   416  		BaseFilename:  contentBaseName,
   417  		ModTime:       mtime,
   418  		GitAuthorDate: gitAuthorDate,
   419  		Location:      langs.GetLocation(pm.s.Language()),
   420  	}
   421  
   422  	// Handle the date separately
   423  	// TODO(bep) we need to "do more" in this area so this can be split up and
   424  	// more easily tested without the Page, but the coupling is strong.
   425  	err := pm.s.frontmatterHandler.HandleDates(descriptor)
   426  	if err != nil {
   427  		p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
   428  	}
   429  
   430  	pm.buildConfig, err = pagemeta.DecodeBuildConfig(frontmatter["_build"])
   431  	if err != nil {
   432  		return err
   433  	}
   434  
   435  	var sitemapSet bool
   436  
   437  	var draft, published, isCJKLanguage *bool
   438  	for k, v := range frontmatter {
   439  		loki := strings.ToLower(k)
   440  
   441  		if loki == "published" { // Intentionally undocumented
   442  			vv, err := cast.ToBoolE(v)
   443  			if err == nil {
   444  				published = &vv
   445  			}
   446  			// published may also be a date
   447  			continue
   448  		}
   449  
   450  		if pm.s.frontmatterHandler.IsDateKey(loki) {
   451  			continue
   452  		}
   453  
   454  		switch loki {
   455  		case "title":
   456  			pm.title = cast.ToString(v)
   457  			pm.params[loki] = pm.title
   458  		case "linktitle":
   459  			pm.linkTitle = cast.ToString(v)
   460  			pm.params[loki] = pm.linkTitle
   461  		case "summary":
   462  			pm.summary = cast.ToString(v)
   463  			pm.params[loki] = pm.summary
   464  		case "description":
   465  			pm.description = cast.ToString(v)
   466  			pm.params[loki] = pm.description
   467  		case "slug":
   468  			// Don't start or end with a -
   469  			pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-")
   470  			pm.params[loki] = pm.Slug()
   471  		case "url":
   472  			url := cast.ToString(v)
   473  			if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
   474  				return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle())
   475  			}
   476  			lang := p.s.GetLanguagePrefix()
   477  			if lang != "" && !strings.HasPrefix(url, "/") && strings.HasPrefix(url, lang+"/") {
   478  				if strings.HasPrefix(hugo.CurrentVersion.String(), "0.55") {
   479  					// We added support for page relative URLs in Hugo 0.55 and
   480  					// this may get its language path added twice.
   481  					// TODO(bep) eventually remove this.
   482  					p.s.Log.Warnf(`Front matter in %q with the url %q with no leading / has what looks like the language prefix added. In Hugo 0.55 we added support for page relative URLs in front matter, no language prefix needed. Check the URL and consider to either add a leading / or remove the language prefix.`, p.pathOrTitle(), url)
   483  				}
   484  			}
   485  			pm.urlPaths.URL = url
   486  			pm.params[loki] = url
   487  		case "type":
   488  			pm.contentType = cast.ToString(v)
   489  			pm.params[loki] = pm.contentType
   490  		case "keywords":
   491  			pm.keywords = cast.ToStringSlice(v)
   492  			pm.params[loki] = pm.keywords
   493  		case "headless":
   494  			// Legacy setting for leaf bundles.
   495  			// This is since Hugo 0.63 handled in a more general way for all
   496  			// pages.
   497  			isHeadless := cast.ToBool(v)
   498  			pm.params[loki] = isHeadless
   499  			if p.File().TranslationBaseName() == "index" && isHeadless {
   500  				pm.buildConfig.List = pagemeta.Never
   501  				pm.buildConfig.Render = pagemeta.Never
   502  			}
   503  		case "outputs":
   504  			o := cast.ToStringSlice(v)
   505  			if len(o) > 0 {
   506  				// Output formats are explicitly set in front matter, use those.
   507  				outFormats, err := p.s.outputFormatsConfig.GetByNames(o...)
   508  
   509  				if err != nil {
   510  					p.s.Log.Errorf("Failed to resolve output formats: %s", err)
   511  				} else {
   512  					pm.configuredOutputFormats = outFormats
   513  					pm.params[loki] = outFormats
   514  				}
   515  
   516  			}
   517  		case "draft":
   518  			draft = new(bool)
   519  			*draft = cast.ToBool(v)
   520  		case "layout":
   521  			pm.layout = cast.ToString(v)
   522  			pm.params[loki] = pm.layout
   523  		case "markup":
   524  			pm.markup = cast.ToString(v)
   525  			pm.params[loki] = pm.markup
   526  		case "weight":
   527  			pm.weight = cast.ToInt(v)
   528  			pm.params[loki] = pm.weight
   529  		case "aliases":
   530  			pm.aliases = cast.ToStringSlice(v)
   531  			for i, alias := range pm.aliases {
   532  				if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
   533  					return fmt.Errorf("http* aliases not supported: %q", alias)
   534  				}
   535  				pm.aliases[i] = filepath.ToSlash(alias)
   536  			}
   537  			pm.params[loki] = pm.aliases
   538  		case "sitemap":
   539  			p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, maps.ToStringMap(v))
   540  			pm.params[loki] = p.m.sitemap
   541  			sitemapSet = true
   542  		case "iscjklanguage":
   543  			isCJKLanguage = new(bool)
   544  			*isCJKLanguage = cast.ToBool(v)
   545  		case "translationkey":
   546  			pm.translationKey = cast.ToString(v)
   547  			pm.params[loki] = pm.translationKey
   548  		case "resources":
   549  			var resources []map[string]interface{}
   550  			handled := true
   551  
   552  			switch vv := v.(type) {
   553  			case []map[interface{}]interface{}:
   554  				for _, vvv := range vv {
   555  					resources = append(resources, maps.ToStringMap(vvv))
   556  				}
   557  			case []map[string]interface{}:
   558  				resources = append(resources, vv...)
   559  			case []interface{}:
   560  				for _, vvv := range vv {
   561  					switch vvvv := vvv.(type) {
   562  					case map[interface{}]interface{}:
   563  						resources = append(resources, maps.ToStringMap(vvvv))
   564  					case map[string]interface{}:
   565  						resources = append(resources, vvvv)
   566  					}
   567  				}
   568  			default:
   569  				handled = false
   570  			}
   571  
   572  			if handled {
   573  				pm.params[loki] = resources
   574  				pm.resourcesMetadata = resources
   575  				break
   576  			}
   577  			fallthrough
   578  
   579  		default:
   580  			// If not one of the explicit values, store in Params
   581  			switch vv := v.(type) {
   582  			case bool:
   583  				pm.params[loki] = vv
   584  			case string:
   585  				pm.params[loki] = vv
   586  			case int64, int32, int16, int8, int:
   587  				pm.params[loki] = vv
   588  			case float64, float32:
   589  				pm.params[loki] = vv
   590  			case time.Time:
   591  				pm.params[loki] = vv
   592  			default: // handle array of strings as well
   593  				switch vvv := vv.(type) {
   594  				case []interface{}:
   595  					if len(vvv) > 0 {
   596  						switch vvv[0].(type) {
   597  						case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter
   598  							pm.params[loki] = vvv
   599  						case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter
   600  							pm.params[loki] = vvv
   601  						case []interface{}:
   602  							pm.params[loki] = vvv
   603  						default:
   604  							a := make([]string, len(vvv))
   605  							for i, u := range vvv {
   606  								a[i] = cast.ToString(u)
   607  							}
   608  
   609  							pm.params[loki] = a
   610  						}
   611  					} else {
   612  						pm.params[loki] = []string{}
   613  					}
   614  				default:
   615  					pm.params[loki] = vv
   616  				}
   617  			}
   618  		}
   619  	}
   620  
   621  	if !sitemapSet {
   622  		pm.sitemap = p.s.siteCfg.sitemap
   623  	}
   624  
   625  	pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup)
   626  
   627  	if draft != nil && published != nil {
   628  		pm.draft = *draft
   629  		p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
   630  	} else if draft != nil {
   631  		pm.draft = *draft
   632  	} else if published != nil {
   633  		pm.draft = !*published
   634  	}
   635  	pm.params["draft"] = pm.draft
   636  
   637  	if isCJKLanguage != nil {
   638  		pm.isCJKLanguage = *isCJKLanguage
   639  	} else if p.s.siteCfg.hasCJKLanguage && p.source.parsed != nil {
   640  		if cjkRe.Match(p.source.parsed.Input()) {
   641  			pm.isCJKLanguage = true
   642  		} else {
   643  			pm.isCJKLanguage = false
   644  		}
   645  	}
   646  
   647  	pm.params["iscjklanguage"] = p.m.isCJKLanguage
   648  
   649  	return nil
   650  }
   651  
   652  func (p *pageMeta) noListAlways() bool {
   653  	return p.buildConfig.List != pagemeta.Always
   654  }
   655  
   656  func (p *pageMeta) getListFilter(local bool) contentTreeNodeCallback {
   657  	return newContentTreeFilter(func(n *contentNode) bool {
   658  		if n == nil {
   659  			return true
   660  		}
   661  
   662  		var shouldList bool
   663  		switch n.p.m.buildConfig.List {
   664  		case pagemeta.Always:
   665  			shouldList = true
   666  		case pagemeta.Never:
   667  			shouldList = false
   668  		case pagemeta.ListLocally:
   669  			shouldList = local
   670  		}
   671  
   672  		return !shouldList
   673  	})
   674  }
   675  
   676  func (p *pageMeta) noRender() bool {
   677  	return p.buildConfig.Render != pagemeta.Always
   678  }
   679  
   680  func (p *pageMeta) noLink() bool {
   681  	return p.buildConfig.Render == pagemeta.Never
   682  }
   683  
   684  func (p *pageMeta) applyDefaultValues(n *contentNode) error {
   685  	if p.buildConfig.IsZero() {
   686  		p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil)
   687  	}
   688  
   689  	if !p.s.isEnabled(p.Kind()) {
   690  		(&p.buildConfig).Disable()
   691  	}
   692  
   693  	if p.markup == "" {
   694  		if !p.File().IsZero() {
   695  			// Fall back to file extension
   696  			p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
   697  		}
   698  		if p.markup == "" {
   699  			p.markup = "markdown"
   700  		}
   701  	}
   702  
   703  	if p.title == "" && p.f.IsZero() {
   704  		switch p.Kind() {
   705  		case page.KindHome:
   706  			p.title = p.s.Info.title
   707  		case page.KindSection:
   708  			var sectionName string
   709  			if n != nil {
   710  				sectionName = n.rootSection()
   711  			} else {
   712  				sectionName = p.sections[0]
   713  			}
   714  
   715  			sectionName = helpers.FirstUpper(sectionName)
   716  			if p.s.Cfg.GetBool("pluralizeListTitles") {
   717  				p.title = flect.Pluralize(sectionName)
   718  			} else {
   719  				p.title = sectionName
   720  			}
   721  		case page.KindTerm:
   722  			// TODO(bep) improve
   723  			key := p.sections[len(p.sections)-1]
   724  			p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1)
   725  		case page.KindTaxonomy:
   726  			p.title = p.s.titleFunc(p.sections[0])
   727  		case kind404:
   728  			p.title = "404 Page not found"
   729  
   730  		}
   731  	}
   732  
   733  	if p.IsNode() {
   734  		p.bundleType = files.ContentClassBranch
   735  	} else {
   736  		source := p.File()
   737  		if fi, ok := source.(*fileInfo); ok {
   738  			class := fi.FileInfo().Meta().Classifier
   739  			switch class {
   740  			case files.ContentClassBranch, files.ContentClassLeaf:
   741  				p.bundleType = class
   742  			}
   743  		}
   744  	}
   745  
   746  	if !p.f.IsZero() {
   747  		var renderingConfigOverrides map[string]interface{}
   748  		bfParam := getParamToLower(p, "blackfriday")
   749  		if bfParam != nil {
   750  			renderingConfigOverrides = maps.ToStringMap(bfParam)
   751  		}
   752  
   753  		p.renderingConfigOverrides = renderingConfigOverrides
   754  
   755  	}
   756  
   757  	return nil
   758  }
   759  
   760  func (p *pageMeta) newContentConverter(ps *pageState, markup string, renderingConfigOverrides map[string]interface{}) (converter.Converter, error) {
   761  	if ps == nil {
   762  		panic("no Page provided")
   763  	}
   764  	cp := p.s.ContentSpec.Converters.Get(markup)
   765  	if cp == nil {
   766  		return converter.NopConverter, errors.Errorf("no content renderer found for markup %q", p.markup)
   767  	}
   768  
   769  	var id string
   770  	var filename string
   771  	var path string
   772  	if !p.f.IsZero() {
   773  		id = p.f.UniqueID()
   774  		filename = p.f.Filename()
   775  		path = p.f.Path()
   776  	} else {
   777  		path = p.Pathc()
   778  	}
   779  
   780  	cpp, err := cp.New(
   781  		converter.DocumentContext{
   782  			Document:        newPageForRenderHook(ps),
   783  			DocumentID:      id,
   784  			DocumentName:    path,
   785  			Filename:        filename,
   786  			ConfigOverrides: renderingConfigOverrides,
   787  		},
   788  	)
   789  	if err != nil {
   790  		return converter.NopConverter, err
   791  	}
   792  
   793  	return cpp, nil
   794  }
   795  
   796  // The output formats this page will be rendered to.
   797  func (m *pageMeta) outputFormats() output.Formats {
   798  	if len(m.configuredOutputFormats) > 0 {
   799  		return m.configuredOutputFormats
   800  	}
   801  
   802  	return m.s.outputFormats[m.Kind()]
   803  }
   804  
   805  func (p *pageMeta) Slug() string {
   806  	return p.urlPaths.Slug
   807  }
   808  
   809  func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) interface{} {
   810  	v := m.Params()[strings.ToLower(key)]
   811  
   812  	if v == nil {
   813  		return nil
   814  	}
   815  
   816  	switch val := v.(type) {
   817  	case bool:
   818  		return val
   819  	case string:
   820  		if stringToLower {
   821  			return strings.ToLower(val)
   822  		}
   823  		return val
   824  	case int64, int32, int16, int8, int:
   825  		return cast.ToInt(v)
   826  	case float64, float32:
   827  		return cast.ToFloat64(v)
   828  	case time.Time:
   829  		return val
   830  	case []string:
   831  		if stringToLower {
   832  			return helpers.SliceToLower(val)
   833  		}
   834  		return v
   835  	default:
   836  		return v
   837  	}
   838  }
   839  
   840  func getParamToLower(m resource.ResourceParamsProvider, key string) interface{} {
   841  	return getParam(m, key, true)
   842  }