github.com/neohugo/neohugo@v0.123.8/hugolib/page__meta.go (about)

     1  // Copyright 2024 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  	"context"
    18  	"fmt"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/gobuffalo/flect"
    25  	"github.com/neohugo/neohugo/identity"
    26  	"github.com/neohugo/neohugo/langs"
    27  	"github.com/neohugo/neohugo/markup/converter"
    28  	"github.com/neohugo/neohugo/related"
    29  	xmaps "golang.org/x/exp/maps"
    30  
    31  	"github.com/neohugo/neohugo/source"
    32  
    33  	"github.com/neohugo/neohugo/common/constants"
    34  	"github.com/neohugo/neohugo/common/loggers"
    35  	"github.com/neohugo/neohugo/common/maps"
    36  	"github.com/neohugo/neohugo/common/neohugo"
    37  	"github.com/neohugo/neohugo/common/paths"
    38  	"github.com/neohugo/neohugo/config"
    39  	"github.com/neohugo/neohugo/helpers"
    40  
    41  	"github.com/neohugo/neohugo/output"
    42  	"github.com/neohugo/neohugo/resources/kinds"
    43  	"github.com/neohugo/neohugo/resources/page"
    44  	"github.com/neohugo/neohugo/resources/page/pagemeta"
    45  	"github.com/neohugo/neohugo/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  	term     string // Set for kind == KindTerm.
    53  	singular string // Set for kind == KindTerm and kind == KindTaxonomy.
    54  
    55  	resource.Staler
    56  	pageMetaParams
    57  	pageMetaFrontMatter
    58  
    59  	// Set for standalone pages, e.g. robotsTXT.
    60  	standaloneOutputFormat output.Format
    61  
    62  	resourcePath string // Set for bundled pages; path relative to its bundle root.
    63  	bundled      bool   // Set if this page is bundled inside another.
    64  
    65  	pathInfo *paths.Path // Always set. This the canonical path to the Page.
    66  	f        *source.File
    67  
    68  	content *cachedContent // The source and the parsed page content.
    69  
    70  	s *Site // The site this page belongs to.
    71  }
    72  
    73  // Prepare for a rebuild of the data passed in from front matter.
    74  func (m *pageMeta) setMetaPostPrepareRebuild() {
    75  	params := xmaps.Clone[map[string]any](m.paramsOriginal)
    76  	m.pageMetaParams.pageConfig.Params = params
    77  	m.pageMetaFrontMatter = pageMetaFrontMatter{}
    78  }
    79  
    80  type pageMetaParams struct {
    81  	setMetaPostCount          int
    82  	setMetaPostCascadeChanged bool
    83  
    84  	pageConfig *pagemeta.PageConfig
    85  
    86  	// These are only set in watch mode.
    87  	datesOriginal   pagemeta.Dates
    88  	paramsOriginal  map[string]any                   // contains the original params as defined in the front matter.
    89  	cascadeOriginal map[page.PageMatcher]maps.Params // contains the original cascade as defined in the front matter.
    90  }
    91  
    92  // From page front matter.
    93  type pageMetaFrontMatter struct {
    94  	configuredOutputFormats output.Formats // outputs defined in front matter.
    95  }
    96  
    97  func (m *pageMetaParams) init(preserveOringal bool) {
    98  	if preserveOringal {
    99  		m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
   100  		m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade)
   101  	}
   102  }
   103  
   104  func (p *pageMeta) Aliases() []string {
   105  	return p.pageConfig.Aliases
   106  }
   107  
   108  // Deprecated: use taxonomies.
   109  func (p *pageMeta) Author() page.Author {
   110  	neohugo.Deprecate(".Author", "Use taxonomies.", "v0.98.0")
   111  	authors := p.Authors()
   112  
   113  	for _, author := range authors {
   114  		return author
   115  	}
   116  	return page.Author{}
   117  }
   118  
   119  // Deprecated: use taxonomies.
   120  func (p *pageMeta) Authors() page.AuthorList {
   121  	neohugo.Deprecate(".Author", "Use taxonomies.", "v0.112.0")
   122  	return nil
   123  }
   124  
   125  func (p *pageMeta) BundleType() string {
   126  	switch p.pathInfo.BundleType() {
   127  	case paths.PathTypeLeaf:
   128  		return "leaf"
   129  	case paths.PathTypeBranch:
   130  		return "branch"
   131  	default:
   132  		return ""
   133  	}
   134  }
   135  
   136  func (p *pageMeta) Date() time.Time {
   137  	return p.pageConfig.Date
   138  }
   139  
   140  func (p *pageMeta) PublishDate() time.Time {
   141  	return p.pageConfig.PublishDate
   142  }
   143  
   144  func (p *pageMeta) Lastmod() time.Time {
   145  	return p.pageConfig.Lastmod
   146  }
   147  
   148  func (p *pageMeta) ExpiryDate() time.Time {
   149  	return p.pageConfig.ExpiryDate
   150  }
   151  
   152  func (p *pageMeta) Description() string {
   153  	return p.pageConfig.Description
   154  }
   155  
   156  func (p *pageMeta) Lang() string {
   157  	return p.s.Lang()
   158  }
   159  
   160  func (p *pageMeta) Draft() bool {
   161  	return p.pageConfig.Draft
   162  }
   163  
   164  func (p *pageMeta) File() *source.File {
   165  	return p.f
   166  }
   167  
   168  func (p *pageMeta) IsHome() bool {
   169  	return p.Kind() == kinds.KindHome
   170  }
   171  
   172  func (p *pageMeta) Keywords() []string {
   173  	return p.pageConfig.Keywords
   174  }
   175  
   176  func (p *pageMeta) Kind() string {
   177  	return p.pageConfig.Kind
   178  }
   179  
   180  func (p *pageMeta) Layout() string {
   181  	return p.pageConfig.Layout
   182  }
   183  
   184  func (p *pageMeta) LinkTitle() string {
   185  	if p.pageConfig.LinkTitle != "" {
   186  		return p.pageConfig.LinkTitle
   187  	}
   188  
   189  	return p.Title()
   190  }
   191  
   192  func (p *pageMeta) Name() string {
   193  	if p.resourcePath != "" {
   194  		return p.resourcePath
   195  	}
   196  	if p.pageConfig.Kind == kinds.KindTerm {
   197  		return p.pathInfo.Unnormalized().BaseNameNoIdentifier()
   198  	}
   199  	return p.Title()
   200  }
   201  
   202  func (p *pageMeta) IsNode() bool {
   203  	return !p.IsPage()
   204  }
   205  
   206  func (p *pageMeta) IsPage() bool {
   207  	return p.Kind() == kinds.KindPage
   208  }
   209  
   210  // Param is a convenience method to do lookups in Page's and Site's Params map,
   211  // in that order.
   212  //
   213  // This method is also implemented on SiteInfo.
   214  // TODO(bep) interface
   215  func (p *pageMeta) Param(key any) (any, error) {
   216  	return resource.Param(p, p.s.Params(), key)
   217  }
   218  
   219  func (p *pageMeta) Params() maps.Params {
   220  	return p.pageConfig.Params
   221  }
   222  
   223  func (p *pageMeta) Path() string {
   224  	return p.pathInfo.Base()
   225  }
   226  
   227  func (p *pageMeta) PathInfo() *paths.Path {
   228  	return p.pathInfo
   229  }
   230  
   231  // RelatedKeywords implements the related.Document interface needed for fast page searches.
   232  func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
   233  	v, err := p.Param(cfg.Name)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	return cfg.ToKeywords(v)
   239  }
   240  
   241  func (p *pageMeta) IsSection() bool {
   242  	return p.Kind() == kinds.KindSection
   243  }
   244  
   245  func (p *pageMeta) Section() string {
   246  	return p.pathInfo.Section()
   247  }
   248  
   249  func (p *pageMeta) Sitemap() config.SitemapConfig {
   250  	return p.pageConfig.Sitemap
   251  }
   252  
   253  func (p *pageMeta) Title() string {
   254  	return p.pageConfig.Title
   255  }
   256  
   257  const defaultContentType = "page"
   258  
   259  func (p *pageMeta) Type() string {
   260  	if p.pageConfig.Type != "" {
   261  		return p.pageConfig.Type
   262  	}
   263  
   264  	if sect := p.Section(); sect != "" {
   265  		return sect
   266  	}
   267  
   268  	return defaultContentType
   269  }
   270  
   271  func (p *pageMeta) Weight() int {
   272  	return p.pageConfig.Weight
   273  }
   274  
   275  func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf config.AllProvider) error {
   276  	frontmatter := pi.frontMatter
   277  	if frontmatter != nil {
   278  		pcfg := p.pageConfig
   279  		if pcfg == nil {
   280  			panic("pageConfig not set")
   281  		}
   282  		// Needed for case insensitive fetching of params values
   283  		maps.PrepareParams(frontmatter)
   284  		pcfg.Params = frontmatter
   285  		// Check for any cascade define on itself.
   286  		if cv, found := frontmatter["cascade"]; found {
   287  			var err error
   288  			cascade, err := page.DecodeCascade(logger, cv)
   289  			if err != nil {
   290  				return err
   291  			}
   292  			pcfg.Cascade = cascade
   293  		}
   294  
   295  		// Look for path, lang and kind, all of which values we need early on.
   296  		if v, found := frontmatter["path"]; found {
   297  			pcfg.Path = paths.ToSlashPreserveLeading(cast.ToString(v))
   298  			pcfg.Params["path"] = pcfg.Path
   299  		}
   300  		if v, found := frontmatter["lang"]; found {
   301  			lang := strings.ToLower(cast.ToString(v))
   302  			if _, ok := conf.PathParser().LanguageIndex[lang]; ok {
   303  				pcfg.Lang = lang
   304  				pcfg.Params["lang"] = pcfg.Lang
   305  			}
   306  		}
   307  		if v, found := frontmatter["kind"]; found {
   308  			s := cast.ToString(v)
   309  			if s != "" {
   310  				pcfg.Kind = kinds.GetKindMain(s)
   311  				if pcfg.Kind == "" {
   312  					return fmt.Errorf("unknown kind %q in front matter", s)
   313  				}
   314  				pcfg.Params["kind"] = pcfg.Kind
   315  			}
   316  		}
   317  	} else if p.pageMetaParams.pageConfig.Params == nil {
   318  		p.pageConfig.Params = make(maps.Params)
   319  	}
   320  
   321  	p.pageMetaParams.init(conf.Watching())
   322  
   323  	return nil
   324  }
   325  
   326  func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error {
   327  	ps.m.setMetaPostCount++
   328  	var cascadeHashPre uint64
   329  	if ps.m.setMetaPostCount > 1 {
   330  		cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade)
   331  		ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
   332  
   333  	}
   334  
   335  	// Apply cascades first so they can be overridden later.
   336  	if cascade != nil {
   337  		if ps.m.pageConfig.Cascade != nil {
   338  			for k, v := range cascade {
   339  				vv, found := ps.m.pageConfig.Cascade[k]
   340  				if !found {
   341  					ps.m.pageConfig.Cascade[k] = v
   342  				} else {
   343  					// Merge
   344  					for ck, cv := range v {
   345  						if _, found := vv[ck]; !found {
   346  							vv[ck] = cv
   347  						}
   348  					}
   349  				}
   350  			}
   351  			cascade = ps.m.pageConfig.Cascade
   352  		} else {
   353  			ps.m.pageConfig.Cascade = cascade
   354  		}
   355  	}
   356  
   357  	if cascade == nil {
   358  		cascade = ps.m.pageConfig.Cascade
   359  	}
   360  
   361  	if ps.m.setMetaPostCount > 1 {
   362  		ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.Cascade)
   363  		if !ps.m.setMetaPostCascadeChanged {
   364  			// No changes, restore any value that may be changed by aggregation.
   365  			ps.m.pageConfig.Dates = ps.m.datesOriginal
   366  			return nil
   367  		}
   368  		ps.m.setMetaPostPrepareRebuild()
   369  
   370  	}
   371  
   372  	// Cascade is also applied to itself.
   373  	for m, v := range cascade {
   374  		if !m.Matches(ps) {
   375  			continue
   376  		}
   377  		for kk, vv := range v {
   378  			if _, found := ps.m.pageConfig.Params[kk]; !found {
   379  				ps.m.pageConfig.Params[kk] = vv
   380  			}
   381  		}
   382  	}
   383  
   384  	if err := ps.setMetaPostParams(); err != nil {
   385  		return err
   386  	}
   387  
   388  	if err := ps.m.applyDefaultValues(); err != nil {
   389  		return err
   390  	}
   391  
   392  	// Store away any original values that may be changed from aggregation.
   393  	ps.m.datesOriginal = ps.m.pageConfig.Dates
   394  
   395  	return nil
   396  }
   397  
   398  func (p *pageState) setMetaPostParams() error {
   399  	pm := p.m
   400  	var mtime time.Time
   401  	var contentBaseName string
   402  	if p.File() != nil {
   403  		contentBaseName = p.File().ContentBaseName()
   404  		if p.File().FileInfo() != nil {
   405  			mtime = p.File().FileInfo().ModTime()
   406  		}
   407  	}
   408  
   409  	var gitAuthorDate time.Time
   410  	if !p.gitInfo.IsZero() {
   411  		gitAuthorDate = p.gitInfo.AuthorDate
   412  	}
   413  
   414  	descriptor := &pagemeta.FrontMatterDescriptor{
   415  		PageConfig:    pm.pageConfig,
   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  	var buildConfig any
   431  	var isNewBuildKeyword bool
   432  	if v, ok := pm.pageConfig.Params["_build"]; ok {
   433  		buildConfig = v
   434  	} else {
   435  		buildConfig = pm.pageConfig.Params["build"]
   436  		isNewBuildKeyword = true
   437  	}
   438  	pm.pageConfig.Build, err = pagemeta.DecodeBuildConfig(buildConfig)
   439  	if err != nil {
   440  		var msgDetail string
   441  		if isNewBuildKeyword {
   442  			msgDetail = `. We renamed the _build keyword to build in Hugo 0.123.0. We recommend putting user defined params in the params section, e.g.:
   443  ---
   444  title: "My Title"
   445  params:
   446    build: "My Build"
   447  ---
   448  ยด
   449  
   450  `
   451  		}
   452  		return fmt.Errorf("failed to decode build config in front matter: %s%s", err, msgDetail)
   453  	}
   454  
   455  	var sitemapSet bool
   456  
   457  	pcfg := pm.pageConfig
   458  
   459  	params := pcfg.Params
   460  
   461  	var draft, published, isCJKLanguage *bool
   462  	var userParams map[string]any
   463  	for k, v := range pcfg.Params {
   464  		loki := strings.ToLower(k)
   465  
   466  		if loki == "params" {
   467  			vv, err := maps.ToStringMapE(v)
   468  			if err != nil {
   469  				return err
   470  			}
   471  			userParams = vv
   472  			delete(pcfg.Params, k)
   473  			continue
   474  		}
   475  
   476  		if loki == "published" { // Intentionally undocumented
   477  			vv, err := cast.ToBoolE(v)
   478  			if err == nil {
   479  				published = &vv
   480  			}
   481  			// published may also be a date
   482  			continue
   483  		}
   484  
   485  		if pm.s.frontmatterHandler.IsDateKey(loki) {
   486  			continue
   487  		}
   488  
   489  		switch loki {
   490  		case "title":
   491  			pcfg.Title = cast.ToString(v)
   492  			params[loki] = pcfg.Title
   493  		case "linktitle":
   494  			pcfg.LinkTitle = cast.ToString(v)
   495  			params[loki] = pcfg.LinkTitle
   496  		case "summary":
   497  			pcfg.Summary = cast.ToString(v)
   498  			params[loki] = pcfg.Summary
   499  		case "description":
   500  			pcfg.Description = cast.ToString(v)
   501  			params[loki] = pcfg.Description
   502  		case "slug":
   503  			// Don't start or end with a -
   504  			pcfg.Slug = strings.Trim(cast.ToString(v), "-")
   505  			params[loki] = pm.Slug()
   506  		case "url":
   507  			url := cast.ToString(v)
   508  			if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
   509  				return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle())
   510  			}
   511  			pcfg.URL = url
   512  			params[loki] = url
   513  		case "type":
   514  			pcfg.Type = cast.ToString(v)
   515  			params[loki] = pcfg.Type
   516  		case "keywords":
   517  			pcfg.Keywords = cast.ToStringSlice(v)
   518  			params[loki] = pcfg.Keywords
   519  		case "headless":
   520  			// Legacy setting for leaf bundles.
   521  			// This is since Hugo 0.63 handled in a more general way for all
   522  			// pages.
   523  			isHeadless := cast.ToBool(v)
   524  			params[loki] = isHeadless
   525  			if isHeadless {
   526  				pm.pageConfig.Build.List = pagemeta.Never
   527  				pm.pageConfig.Build.Render = pagemeta.Never
   528  			}
   529  		case "outputs":
   530  			o := cast.ToStringSlice(v)
   531  			// lower case names:
   532  			for i, s := range o {
   533  				o[i] = strings.ToLower(s)
   534  			}
   535  			if len(o) > 0 {
   536  				// Output formats are explicitly set in front matter, use those.
   537  				outFormats, err := p.s.conf.OutputFormats.Config.GetByNames(o...)
   538  				if err != nil {
   539  					p.s.Log.Errorf("Failed to resolve output formats: %s", err)
   540  				} else {
   541  					pm.configuredOutputFormats = outFormats
   542  					params[loki] = outFormats
   543  				}
   544  			}
   545  		case "draft":
   546  			draft = new(bool)
   547  			*draft = cast.ToBool(v)
   548  		case "layout":
   549  			pcfg.Layout = cast.ToString(v)
   550  			params[loki] = pcfg.Layout
   551  		case "markup":
   552  			pcfg.Markup = cast.ToString(v)
   553  			params[loki] = pcfg.Markup
   554  		case "weight":
   555  			pcfg.Weight = cast.ToInt(v)
   556  			params[loki] = pcfg.Weight
   557  		case "aliases":
   558  			pcfg.Aliases = cast.ToStringSlice(v)
   559  			for i, alias := range pcfg.Aliases {
   560  				if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
   561  					return fmt.Errorf("http* aliases not supported: %q", alias)
   562  				}
   563  				pcfg.Aliases[i] = filepath.ToSlash(alias)
   564  			}
   565  			params[loki] = pcfg.Aliases
   566  		case "sitemap":
   567  			pcfg.Sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v))
   568  			if err != nil {
   569  				return fmt.Errorf("failed to decode sitemap config in front matter: %s", err)
   570  			}
   571  			sitemapSet = true
   572  		case "iscjklanguage":
   573  			isCJKLanguage = new(bool)
   574  			*isCJKLanguage = cast.ToBool(v)
   575  		case "translationkey":
   576  			pcfg.TranslationKey = cast.ToString(v)
   577  			params[loki] = pcfg.TranslationKey
   578  		case "resources":
   579  			var resources []map[string]any
   580  			handled := true
   581  
   582  			switch vv := v.(type) {
   583  			case []map[any]any:
   584  				for _, vvv := range vv {
   585  					resources = append(resources, maps.ToStringMap(vvv))
   586  				}
   587  			case []map[string]any:
   588  				resources = append(resources, vv...)
   589  			case []any:
   590  				for _, vvv := range vv {
   591  					switch vvvv := vvv.(type) {
   592  					case map[any]any:
   593  						resources = append(resources, maps.ToStringMap(vvvv))
   594  					case map[string]any:
   595  						resources = append(resources, vvvv)
   596  					}
   597  				}
   598  			default:
   599  				handled = false
   600  			}
   601  
   602  			if handled {
   603  				pcfg.Resources = resources
   604  				break
   605  			}
   606  			fallthrough
   607  		default:
   608  			// If not one of the explicit values, store in Params
   609  			switch vv := v.(type) {
   610  			case []any:
   611  				if len(vv) > 0 {
   612  					allStrings := true
   613  					for _, vvv := range vv {
   614  						if _, ok := vvv.(string); !ok {
   615  							allStrings = false
   616  							break
   617  						}
   618  					}
   619  					if allStrings {
   620  						// We need tags, keywords etc. to be []string, not []interface{}.
   621  						a := make([]string, len(vv))
   622  						for i, u := range vv {
   623  							a[i] = cast.ToString(u)
   624  						}
   625  						params[loki] = a
   626  					} else {
   627  						params[loki] = vv
   628  					}
   629  				} else {
   630  					params[loki] = []string{}
   631  				}
   632  
   633  			default:
   634  				params[loki] = vv
   635  			}
   636  		}
   637  	}
   638  
   639  	for k, v := range userParams {
   640  		if _, found := params[k]; found {
   641  			p.s.Log.Warnidf(constants.WarnFrontMatterParamsOverrides, "Hugo front matter key %q is overridden in params section.", k)
   642  		}
   643  		params[strings.ToLower(k)] = v
   644  	}
   645  
   646  	if !sitemapSet {
   647  		pcfg.Sitemap = p.s.conf.Sitemap
   648  	}
   649  
   650  	pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup)
   651  
   652  	if draft != nil && published != nil {
   653  		pcfg.Draft = *draft
   654  		p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
   655  	} else if draft != nil {
   656  		pcfg.Draft = *draft
   657  	} else if published != nil {
   658  		pcfg.Draft = !*published
   659  	}
   660  	params["draft"] = pcfg.Draft
   661  
   662  	if isCJKLanguage != nil {
   663  		pcfg.IsCJKLanguage = *isCJKLanguage
   664  	} else if p.s.conf.HasCJKLanguage && p.m.content.pi.openSource != nil {
   665  		if cjkRe.Match(p.m.content.mustSource()) {
   666  			pcfg.IsCJKLanguage = true
   667  		} else {
   668  			pcfg.IsCJKLanguage = false
   669  		}
   670  	}
   671  
   672  	params["iscjklanguage"] = pcfg.IsCJKLanguage
   673  
   674  	return nil
   675  }
   676  
   677  // shouldList returns whether this page should be included in the list of pages.
   678  // glogal indicates site.Pages etc.
   679  func (p *pageMeta) shouldList(global bool) bool {
   680  	if p.isStandalone() {
   681  		// Never list 404, sitemap and similar.
   682  		return false
   683  	}
   684  
   685  	switch p.pageConfig.Build.List {
   686  	case pagemeta.Always:
   687  		return true
   688  	case pagemeta.Never:
   689  		return false
   690  	case pagemeta.ListLocally:
   691  		return !global
   692  	}
   693  	return false
   694  }
   695  
   696  func (p *pageMeta) shouldListAny() bool {
   697  	return p.shouldList(true) || p.shouldList(false)
   698  }
   699  
   700  func (p *pageMeta) isStandalone() bool {
   701  	return !p.standaloneOutputFormat.IsZero()
   702  }
   703  
   704  func (p *pageMeta) shouldBeCheckedForMenuDefinitions() bool {
   705  	if !p.shouldList(false) {
   706  		return false
   707  	}
   708  
   709  	return p.pageConfig.Kind == kinds.KindHome || p.pageConfig.Kind == kinds.KindSection || p.pageConfig.Kind == kinds.KindPage
   710  }
   711  
   712  func (p *pageMeta) noRender() bool {
   713  	return p.pageConfig.Build.Render != pagemeta.Always
   714  }
   715  
   716  func (p *pageMeta) noLink() bool {
   717  	return p.pageConfig.Build.Render == pagemeta.Never
   718  }
   719  
   720  func (p *pageMeta) applyDefaultValues() error {
   721  	if p.pageConfig.Build.IsZero() {
   722  		p.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil)
   723  	}
   724  
   725  	if !p.s.conf.IsKindEnabled(p.Kind()) {
   726  		(&p.pageConfig.Build).Disable()
   727  	}
   728  
   729  	if p.pageConfig.Markup == "" {
   730  		if p.File() != nil {
   731  			// Fall back to file extension
   732  			p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
   733  		}
   734  		if p.pageConfig.Markup == "" {
   735  			p.pageConfig.Markup = "markdown"
   736  		}
   737  	}
   738  
   739  	if p.pageConfig.Title == "" && p.f == nil {
   740  		switch p.Kind() {
   741  		case kinds.KindHome:
   742  			p.pageConfig.Title = p.s.Title()
   743  		case kinds.KindSection:
   744  			sectionName := p.pathInfo.Unnormalized().BaseNameNoIdentifier()
   745  			if p.s.conf.PluralizeListTitles {
   746  				sectionName = flect.Pluralize(sectionName)
   747  			}
   748  			if p.s.conf.CapitalizeListTitles {
   749  				sectionName = p.s.conf.C.CreateTitle(sectionName)
   750  			}
   751  			p.pageConfig.Title = sectionName
   752  		case kinds.KindTerm:
   753  			if p.term != "" {
   754  				if p.s.conf.CapitalizeListTitles {
   755  					p.pageConfig.Title = p.s.conf.C.CreateTitle(p.term)
   756  				} else {
   757  					p.pageConfig.Title = p.term
   758  				}
   759  			} else {
   760  				panic("term not set")
   761  			}
   762  		case kinds.KindTaxonomy:
   763  			if p.s.conf.CapitalizeListTitles {
   764  				p.pageConfig.Title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unnormalized().BaseNameNoIdentifier()), "-", " ", -1)
   765  			} else {
   766  				p.pageConfig.Title = strings.Replace(p.pathInfo.Unnormalized().BaseNameNoIdentifier(), "-", " ", -1)
   767  			}
   768  		case kinds.KindStatus404:
   769  			p.pageConfig.Title = "404 Page not found"
   770  		}
   771  	}
   772  
   773  	return nil
   774  }
   775  
   776  func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) {
   777  	if ps == nil {
   778  		panic("no Page provided")
   779  	}
   780  	cp := p.s.ContentSpec.Converters.Get(markup)
   781  	if cp == nil {
   782  		return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q, page: %s", markup, ps.getPageInfoForError())
   783  	}
   784  
   785  	var id string
   786  	var filename string
   787  	var path string
   788  	if p.f != nil {
   789  		id = p.f.UniqueID()
   790  		filename = p.f.Filename()
   791  		path = p.f.Path()
   792  	} else {
   793  		path = p.Path()
   794  	}
   795  
   796  	cpp, err := cp.New(
   797  		converter.DocumentContext{
   798  			Document:     newPageForRenderHook(ps),
   799  			DocumentID:   id,
   800  			DocumentName: path,
   801  			Filename:     filename,
   802  		},
   803  	)
   804  	if err != nil {
   805  		return converter.NopConverter, err
   806  	}
   807  
   808  	return cpp, nil
   809  }
   810  
   811  // The output formats this page will be rendered to.
   812  func (m *pageMeta) outputFormats() output.Formats {
   813  	if len(m.configuredOutputFormats) > 0 {
   814  		return m.configuredOutputFormats
   815  	}
   816  	return m.s.conf.C.KindOutputFormats[m.Kind()]
   817  }
   818  
   819  func (p *pageMeta) Slug() string {
   820  	return p.pageConfig.Slug
   821  }
   822  
   823  func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any {
   824  	v := m.Params()[strings.ToLower(key)]
   825  
   826  	if v == nil {
   827  		return nil
   828  	}
   829  
   830  	switch val := v.(type) {
   831  	case bool:
   832  		return val
   833  	case string:
   834  		if stringToLower {
   835  			return strings.ToLower(val)
   836  		}
   837  		return val
   838  	case int64, int32, int16, int8, int:
   839  		return cast.ToInt(v)
   840  	case float64, float32:
   841  		return cast.ToFloat64(v)
   842  	case time.Time:
   843  		return val
   844  	case []string:
   845  		if stringToLower {
   846  			return helpers.SliceToLower(val)
   847  		}
   848  		return v
   849  	default:
   850  		return v
   851  	}
   852  }
   853  
   854  func getParamToLower(m resource.ResourceParamsProvider, key string) any {
   855  	return getParam(m, key, true)
   856  }
   857  
   858  func (ps *pageState) initLazyProviders() error {
   859  	ps.init.Add(func(ctx context.Context) (any, error) {
   860  		pp, err := newPagePaths(ps)
   861  		if err != nil {
   862  			return nil, err
   863  		}
   864  
   865  		var outputFormatsForPage output.Formats
   866  		var renderFormats output.Formats
   867  
   868  		if ps.m.standaloneOutputFormat.IsZero() {
   869  			outputFormatsForPage = ps.m.outputFormats()
   870  			renderFormats = ps.s.h.renderFormats
   871  		} else {
   872  			// One of the fixed output format pages, e.g. 404.
   873  			outputFormatsForPage = output.Formats{ps.m.standaloneOutputFormat}
   874  			renderFormats = outputFormatsForPage
   875  		}
   876  
   877  		// Prepare output formats for all sites.
   878  		// We do this even if this page does not get rendered on
   879  		// its own. It may be referenced via one of the site collections etc.
   880  		// it will then need an output format.
   881  		ps.pageOutputs = make([]*pageOutput, len(renderFormats))
   882  		created := make(map[string]*pageOutput)
   883  		shouldRenderPage := !ps.m.noRender()
   884  
   885  		for i, f := range renderFormats {
   886  
   887  			if po, found := created[f.Name]; found {
   888  				ps.pageOutputs[i] = po
   889  				continue
   890  			}
   891  
   892  			render := shouldRenderPage
   893  			if render {
   894  				_, render = outputFormatsForPage.GetByName(f.Name)
   895  			}
   896  
   897  			po := newPageOutput(ps, pp, f, render)
   898  
   899  			// Create a content provider for the first,
   900  			// we may be able to reuse it.
   901  			if i == 0 {
   902  				contentProvider, err := newPageContentOutput(po)
   903  				if err != nil {
   904  					return nil, err
   905  				}
   906  				po.setContentProvider(contentProvider)
   907  			}
   908  
   909  			ps.pageOutputs[i] = po
   910  			created[f.Name] = po
   911  
   912  		}
   913  
   914  		if err := ps.initCommonProviders(pp); err != nil {
   915  			return nil, err
   916  		}
   917  
   918  		return nil, nil
   919  	})
   920  
   921  	return nil
   922  }