github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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  
    38  	"github.com/gohugoio/hugo/common/maps"
    39  	"github.com/gohugoio/hugo/config"
    40  	"github.com/gohugoio/hugo/helpers"
    41  
    42  	"github.com/gohugoio/hugo/output"
    43  	"github.com/gohugoio/hugo/resources/page"
    44  	"github.com/gohugoio/hugo/resources/page/pagemeta"
    45  	"github.com/gohugoio/hugo/resources/resource"
    46  	"github.com/spf13/cast"
    47  )
    48  
    49  var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)
    50  
    51  type pageMeta struct {
    52  	// kind is the discriminator that identifies the different page types
    53  	// in the different page collections. This can, as an example, be used
    54  	// to to filter regular pages, find sections etc.
    55  	// Kind will, for the pages available to the templates, be one of:
    56  	// page, home, section, taxonomy and term.
    57  	// It is of string type to make it easy to reason about in
    58  	// the templates.
    59  	kind string
    60  
    61  	// This is a standalone page not part of any page collection. These
    62  	// include sitemap, robotsTXT and similar. It will have no pageOutputs, but
    63  	// a fixed pageOutput.
    64  	standalone bool
    65  
    66  	draft       bool // Only published when running with -D flag
    67  	buildConfig pagemeta.BuildConfig
    68  
    69  	bundleType files.ContentClass
    70  
    71  	// Params contains configuration defined in the params section of page frontmatter.
    72  	params map[string]any
    73  
    74  	title     string
    75  	linkTitle string
    76  
    77  	summary string
    78  
    79  	resourcePath string
    80  
    81  	weight int
    82  
    83  	markup      string
    84  	contentType string
    85  
    86  	// whether the content is in a CJK language.
    87  	isCJKLanguage bool
    88  
    89  	layout string
    90  
    91  	aliases []string
    92  
    93  	description string
    94  	keywords    []string
    95  
    96  	urlPaths pagemeta.URLPath
    97  
    98  	resource.Dates
    99  
   100  	// Set if this page is bundled inside another.
   101  	bundled bool
   102  
   103  	// A key that maps to translation(s) of this page. This value is fetched
   104  	// from the page front matter.
   105  	translationKey string
   106  
   107  	// From front matter.
   108  	configuredOutputFormats output.Formats
   109  
   110  	// This is the raw front matter metadata that is going to be assigned to
   111  	// the Resources above.
   112  	resourcesMetadata []map[string]any
   113  
   114  	f source.File
   115  
   116  	sections []string
   117  
   118  	// Sitemap overrides from front matter.
   119  	sitemap config.Sitemap
   120  
   121  	s *Site
   122  
   123  	contentConverterInit sync.Once
   124  	contentConverter     converter.Converter
   125  }
   126  
   127  func (p *pageMeta) Aliases() []string {
   128  	return p.aliases
   129  }
   130  
   131  func (p *pageMeta) Author() page.Author {
   132  	helpers.Deprecated(".Author", "Use taxonomies.", false)
   133  	authors := p.Authors()
   134  
   135  	for _, author := range authors {
   136  		return author
   137  	}
   138  	return page.Author{}
   139  }
   140  
   141  func (p *pageMeta) Authors() page.AuthorList {
   142  	helpers.Deprecated(".Authors", "Use taxonomies.", false)
   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 any) (any, 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]any) 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]any)
   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.IsZero() {
   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]any
   550  			handled := true
   551  
   552  			switch vv := v.(type) {
   553  			case []map[any]any:
   554  				for _, vvv := range vv {
   555  					resources = append(resources, maps.ToStringMap(vvv))
   556  				}
   557  			case []map[string]any:
   558  				resources = append(resources, vv...)
   559  			case []any:
   560  				for _, vvv := range vv {
   561  					switch vvvv := vvv.(type) {
   562  					case map[any]any:
   563  						resources = append(resources, maps.ToStringMap(vvvv))
   564  					case map[string]any:
   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 []any:
   583  				if len(vv) > 0 {
   584  					allStrings := true
   585  					for _, vvv := range vv {
   586  						if _, ok := vvv.(string); !ok {
   587  							allStrings = false
   588  							break
   589  						}
   590  					}
   591  					if allStrings {
   592  						// We need tags, keywords etc. to be []string, not []interface{}.
   593  						a := make([]string, len(vv))
   594  						for i, u := range vv {
   595  							a[i] = cast.ToString(u)
   596  						}
   597  						pm.params[loki] = a
   598  					} else {
   599  						pm.params[loki] = vv
   600  					}
   601  				} else {
   602  					pm.params[loki] = []string{}
   603  				}
   604  			default:
   605  				pm.params[loki] = vv
   606  			}
   607  		}
   608  	}
   609  
   610  	if !sitemapSet {
   611  		pm.sitemap = p.s.siteCfg.sitemap
   612  	}
   613  
   614  	pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup)
   615  
   616  	if draft != nil && published != nil {
   617  		pm.draft = *draft
   618  		p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
   619  	} else if draft != nil {
   620  		pm.draft = *draft
   621  	} else if published != nil {
   622  		pm.draft = !*published
   623  	}
   624  	pm.params["draft"] = pm.draft
   625  
   626  	if isCJKLanguage != nil {
   627  		pm.isCJKLanguage = *isCJKLanguage
   628  	} else if p.s.siteCfg.hasCJKLanguage && p.source.parsed != nil {
   629  		if cjkRe.Match(p.source.parsed.Input()) {
   630  			pm.isCJKLanguage = true
   631  		} else {
   632  			pm.isCJKLanguage = false
   633  		}
   634  	}
   635  
   636  	pm.params["iscjklanguage"] = p.m.isCJKLanguage
   637  
   638  	return nil
   639  }
   640  
   641  func (p *pageMeta) noListAlways() bool {
   642  	return p.buildConfig.List != pagemeta.Always
   643  }
   644  
   645  func (p *pageMeta) getListFilter(local bool) contentTreeNodeCallback {
   646  	return newContentTreeFilter(func(n *contentNode) bool {
   647  		if n == nil {
   648  			return true
   649  		}
   650  
   651  		var shouldList bool
   652  		switch n.p.m.buildConfig.List {
   653  		case pagemeta.Always:
   654  			shouldList = true
   655  		case pagemeta.Never:
   656  			shouldList = false
   657  		case pagemeta.ListLocally:
   658  			shouldList = local
   659  		}
   660  
   661  		return !shouldList
   662  	})
   663  }
   664  
   665  func (p *pageMeta) noRender() bool {
   666  	return p.buildConfig.Render != pagemeta.Always
   667  }
   668  
   669  func (p *pageMeta) noLink() bool {
   670  	return p.buildConfig.Render == pagemeta.Never
   671  }
   672  
   673  func (p *pageMeta) applyDefaultValues(n *contentNode) error {
   674  	if p.buildConfig.IsZero() {
   675  		p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil)
   676  	}
   677  
   678  	if !p.s.isEnabled(p.Kind()) {
   679  		(&p.buildConfig).Disable()
   680  	}
   681  
   682  	if p.markup == "" {
   683  		if !p.File().IsZero() {
   684  			// Fall back to file extension
   685  			p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
   686  		}
   687  		if p.markup == "" {
   688  			p.markup = "markdown"
   689  		}
   690  	}
   691  
   692  	if p.title == "" && p.f.IsZero() {
   693  		switch p.Kind() {
   694  		case page.KindHome:
   695  			p.title = p.s.Info.title
   696  		case page.KindSection:
   697  			var sectionName string
   698  			if n != nil {
   699  				sectionName = n.rootSection()
   700  			} else {
   701  				sectionName = p.sections[0]
   702  			}
   703  
   704  			sectionName = helpers.FirstUpper(sectionName)
   705  			if p.s.Cfg.GetBool("pluralizeListTitles") {
   706  				p.title = flect.Pluralize(sectionName)
   707  			} else {
   708  				p.title = sectionName
   709  			}
   710  		case page.KindTerm:
   711  			// TODO(bep) improve
   712  			key := p.sections[len(p.sections)-1]
   713  			p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1)
   714  		case page.KindTaxonomy:
   715  			p.title = p.s.titleFunc(p.sections[0])
   716  		case kind404:
   717  			p.title = "404 Page not found"
   718  
   719  		}
   720  	}
   721  
   722  	if p.IsNode() {
   723  		p.bundleType = files.ContentClassBranch
   724  	} else {
   725  		source := p.File()
   726  		if fi, ok := source.(*fileInfo); ok {
   727  			class := fi.FileInfo().Meta().Classifier
   728  			switch class {
   729  			case files.ContentClassBranch, files.ContentClassLeaf:
   730  				p.bundleType = class
   731  			}
   732  		}
   733  	}
   734  
   735  	return nil
   736  }
   737  
   738  func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) {
   739  	if ps == nil {
   740  		panic("no Page provided")
   741  	}
   742  	cp := p.s.ContentSpec.Converters.Get(markup)
   743  	if cp == nil {
   744  		return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q", p.markup)
   745  	}
   746  
   747  	var id string
   748  	var filename string
   749  	var path string
   750  	if !p.f.IsZero() {
   751  		id = p.f.UniqueID()
   752  		filename = p.f.Filename()
   753  		path = p.f.Path()
   754  	} else {
   755  		path = p.Pathc()
   756  	}
   757  
   758  	cpp, err := cp.New(
   759  		converter.DocumentContext{
   760  			Document:     newPageForRenderHook(ps),
   761  			DocumentID:   id,
   762  			DocumentName: path,
   763  			Filename:     filename,
   764  		},
   765  	)
   766  	if err != nil {
   767  		return converter.NopConverter, err
   768  	}
   769  
   770  	return cpp, nil
   771  }
   772  
   773  // The output formats this page will be rendered to.
   774  func (m *pageMeta) outputFormats() output.Formats {
   775  	if len(m.configuredOutputFormats) > 0 {
   776  		return m.configuredOutputFormats
   777  	}
   778  
   779  	return m.s.outputFormats[m.Kind()]
   780  }
   781  
   782  func (p *pageMeta) Slug() string {
   783  	return p.urlPaths.Slug
   784  }
   785  
   786  func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any {
   787  	v := m.Params()[strings.ToLower(key)]
   788  
   789  	if v == nil {
   790  		return nil
   791  	}
   792  
   793  	switch val := v.(type) {
   794  	case bool:
   795  		return val
   796  	case string:
   797  		if stringToLower {
   798  			return strings.ToLower(val)
   799  		}
   800  		return val
   801  	case int64, int32, int16, int8, int:
   802  		return cast.ToInt(v)
   803  	case float64, float32:
   804  		return cast.ToFloat64(v)
   805  	case time.Time:
   806  		return val
   807  	case []string:
   808  		if stringToLower {
   809  			return helpers.SliceToLower(val)
   810  		}
   811  		return v
   812  	default:
   813  		return v
   814  	}
   815  }
   816  
   817  func getParamToLower(m resource.ResourceParamsProvider, key string) any {
   818  	return getParam(m, key, true)
   819  }