Files
hugo/hugolib/page.go
Bjørn Erik Pedersen 70db201ed4 Add importContext option to css.Build, js.Build, css.Sass and css.TailwindCSS
This allows @import statements to be resolved in a set of user provided resources (e.g. from resources.FromString or css.ChromaStyles) before the assets filesystem.

The option also applies to css.PostCSS via the shared import inlining, and css.Sass requires the dartsass transpiler. The import context is part of the transformation cache key.

Fixes #15103

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:20:06 +02:00

986 lines
24 KiB
Go

// Copyright 2024 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 (
"context"
"fmt"
"iter"
"path/filepath"
"slices"
"strings"
"sync/atomic"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
var (
_ page.Page = (*pageState)(nil)
_ collections.Grouper = (*pageState)(nil)
_ collections.Slicer = (*pageState)(nil)
_ identity.DependencyManagerScopedProvider = (*pageState)(nil)
_ pageContext = (*pageState)(nil)
)
var (
pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType)
nopPageOutput = &pageOutput{
pagePerOutputProviders: nopPagePerOutput,
MarkupProvider: page.NopPage,
ContentProvider: page.NopPage,
}
)
// pageContext provides contextual information about this page, for error
// logging and similar.
type pageContext interface {
posOffset(offset int) text.Position
wrapError(err error) error
getContentConverter() converter.Converter
}
type pageSiteAdapter struct {
p page.Page
s *Site
}
func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) {
p, err := pa.s.getPage(pa.p, ref)
if p == nil {
// The nil struct has meaning in some situations, mostly to avoid breaking
// existing sites doing $nilpage.IsDescendant($p), which will always return
// false.
p = page.NilPage
}
return p, err
}
type pageState struct {
// Incremented for each new page created.
// Note that this will change between builds for a given Page.
pid uint64
s *Site
// This slice will be of same length as the number of global slice of output
// formats (for all sites).
pageOutputs []*pageOutput
// Used to determine if we can reuse content across output formats.
pageOutputTemplateVariationsState atomic.Uint32
// This will be shifted out when we start to render a new output format.
pageOutputIdx int
*pageOutput
// Common for all output formats.
*pageCommon
resource.Staler
dependencyManager identity.Manager
}
// This is not accurate and only used for progress reporting.
// We can do better, but this will do for now.
func (p *pageState) hasRenderableOutput() bool {
for _, po := range p.pageOutputs {
if po.render {
return true
}
}
return false
}
func (p *pageState) incrPageOutputTemplateVariation() {
p.pageOutputTemplateVariationsState.Add(1)
}
func (ps *pageState) canReusePageOutputContent() bool {
return ps.pageOutputTemplateVariationsState.Load() == 1
}
func (ps *pageState) IdentifierBase() string {
return ps.Path()
}
func (ps *pageState) GetIdentity() identity.Identity {
return ps
}
func (ps *pageState) ForEeachIdentity(f func(identity.Identity) bool) bool {
return f(ps)
}
func (ps *pageState) GetDependencyManager() identity.Manager {
return ps.dependencyManager
}
func (ps *pageState) GetDependencyManagerForScope(scope int) identity.Manager {
switch scope {
case pageDependencyScopeDefault:
return ps.dependencyManagerOutput
case pageDependencyScopeGlobal:
return ps.dependencyManager
default:
return identity.NopManager
}
}
func (ps *pageState) GetDependencyManagerForScopesAll() []identity.Manager {
return []identity.Manager{ps.dependencyManager, ps.dependencyManagerOutput}
}
// 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.
func (ps *pageState) Param(key any) (any, error) {
return resource.Param(ps, ps.s.Params(), key)
}
// RelatedKeywords implements the related.Document interface needed for fast page searches.
func (ps *pageState) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
v, found, err := page.NamedPageMetaValue(ps, cfg.Name)
if err != nil {
return nil, err
}
if !found {
return nil, nil
}
return cfg.ToKeywords(v)
}
func (ps *pageState) resetBuildState() {
ps.m.prepareRebuild()
}
func (ps *pageState) skipRender() bool {
b := ps.s.conf.Segments.Config.SegmentFilter.ShouldExcludeFine(
segments.SegmentQuery{
Path: ps.Path(),
Kind: ps.Kind(),
Site: ps.s.siteVector,
Output: ps.pageOutput.f.Name,
},
)
return b
}
func (ps *pageState) isRenderedAny() bool {
for _, o := range ps.pageOutputs {
if o.isRendered() {
return true
}
}
return false
}
// Implements contentNode.
func (ps *pageState) forEeachContentNode(f func(v sitesmatrix.Vector, n contentNode) bool) bool {
return f(ps.s.siteVector, ps)
}
func (ps *pageState) contentWeight() int {
return ps.m.pageMetaSource.contentWeight()
}
func (ps *pageState) nodeSourceEntryID() any {
return ps.m.nodeSourceEntryID()
}
func (ps *pageState) nodeCategoryPage() {
// Marker method.
}
func (m *pageState) nodeCategorySingle() {
// Marker method.
}
func (ps *pageState) lookupContentNode(v sitesmatrix.Vector) contentNode {
pc := ps.m.pageConfigSource
if pc.MatchSiteVector(v) {
return ps
}
return nil
}
func (ps *pageState) lookupContentNodes(vec sitesmatrix.Vector, fallback bool) iter.Seq[contentNodeForSite] {
nop := func(yield func(n contentNodeForSite) bool) {}
pc := ps.m.pageConfigSource
if !fallback {
if !pc.MatchSiteVector(vec) {
return nop
}
return func(yield func(n contentNodeForSite) bool) {
yield(ps)
}
}
if !pc.MatchLanguageCoarse(vec) {
return nop
}
if !pc.MatchVersionCoarse(vec) {
return nop
}
if !pc.MatchRoleCoarse(vec) {
return nop
}
return func(yield func(n contentNodeForSite) bool) {
yield(ps)
}
}
// Eq returns whether the current page equals the given page.
// This is what's invoked when doing `{{ if eq $page $otherPage }}`
func (ps *pageState) Eq(other any) bool {
pp, err := unwrapPage(other)
if err != nil {
return false
}
return ps == pp
}
func (ps *pageState) HeadingsFiltered(context.Context) tableofcontents.Headings {
return nil
}
type pageHeadingsFiltered struct {
*pageState
headings tableofcontents.Headings
}
func (p *pageHeadingsFiltered) HeadingsFiltered(context.Context) tableofcontents.Headings {
return p.headings
}
func (p *pageHeadingsFiltered) page() page.Page {
return p.pageState
}
// For internal use by the related content feature.
func (ps *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document {
fragments := ps.pageOutput.pco.c().Fragments(ctx)
headings := fragments.Headings.FilterBy(fn)
return &pageHeadingsFiltered{
pageState: ps,
headings: headings,
}
}
func (ps *pageState) GitInfo() *source.GitInfo {
return ps.gitInfo
}
func (ps *pageState) CodeOwners() []string {
return ps.codeowners
}
// GetTerms gets the terms defined on this page in the given taxonomy.
// The pages returned will be ordered according to the front matter.
func (ps *pageState) GetTerms(taxonomy string) page.Pages {
return ps.s.pageMap.getTermsForPageInTaxonomy(ps.Path(), taxonomy)
}
func (ps *pageState) MarshalJSON() ([]byte, error) {
return page.MarshalPageToJSON(ps)
}
func (ps *pageState) RegularPagesRecursive() page.Pages {
switch ps.Kind() {
case kinds.KindSection, kinds.KindHome:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
Recursive: true,
},
)
default:
return ps.RegularPages()
}
}
func (ps *pageState) PagesRecursive() page.Pages {
return nil
}
func (ps *pageState) RegularPages() page.Pages {
switch ps.Kind() {
case kinds.KindPage:
case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
},
)
case kinds.KindTerm:
return ps.s.pageMap.getPagesWithTerm(
pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
)
default:
return ps.s.RegularPages()
}
return nil
}
func (ps *pageState) Pages() page.Pages {
switch ps.Kind() {
case kinds.KindPage:
case kinds.KindSection, kinds.KindHome:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
KeyPart: "page-section",
Include: pagePredicates.ShouldListLocal.And(
pagePredicates.KindPage.Or(pagePredicates.KindSection),
).BoolFunc(),
},
},
)
case kinds.KindTerm:
return ps.s.pageMap.getPagesWithTerm(
pageMapQueryPagesBelowPath{
Path: ps.Path(),
},
)
case kinds.KindTaxonomy:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
KeyPart: "term",
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
},
Recursive: true,
},
)
default:
return ps.s.Pages()
}
return nil
}
// RawContent returns the un-rendered source content without
// any leading front matter.
func (ps *pageState) RawContent() string {
if ps.m.content.pi.itemsStep2 == nil {
return ""
}
start := ps.m.content.pi.posMainContent
if start == -1 {
start = 0
}
source, err := ps.m.content.pi.contentSource(ps.m.content)
if err != nil {
panic(err)
}
return string(source[start:])
}
func (ps *pageState) Resources() resource.Resources {
return ps.s.pageMap.getOrCreateResourcesForPage(ps)
}
func (ps *pageState) HasShortcode(name string) bool {
p := ps.m.content.hasShortcode.Load()
if p == nil {
return false
}
hasShortcode := *p
return hasShortcode(name)
}
func (ps *pageState) Site() page.Site {
return ps.s.siteWrapped
}
func (ps *pageState) String() string {
var sb strings.Builder
if ps.File() != nil {
// The forward slashes even on Windows is motivated by
// getting stable tests.
// This information is meant for getting positional information in logs,
// so the direction of the slashes should not matter.
sb.WriteString(filepath.ToSlash(ps.File().Filename()))
if ps.File().IsContentAdapter() {
// Also include the path.
sb.WriteString(":")
sb.WriteString(ps.Path())
}
} else {
sb.WriteString(ps.Path())
}
return sb.String()
}
// IsTranslated returns whether this content file is translated to
// other language(s).
func (ps *pageState) IsTranslated() bool {
return len(ps.AllTranslations()) > 1
}
// TranslationKey returns the key used to identify a translation of this content.
func (ps *pageState) TranslationKey() string {
if ps.m.pageConfig.TranslationKey != "" {
return ps.m.pageConfig.TranslationKey
}
return ps.Path()
}
// AllTranslations returns all translations, including the current Page.
func (ps *pageState) AllTranslations() page.Pages {
key := ps.Path() + "/" + "translations-all"
// This is called from Translations, so we need to use a different partition, cachePages2,
// to avoid potential deadlocks.
pages, err := ps.s.pageMap.getOrCreatePagesFromCache(ps.s.pageMap.cachePages2, key, func(string) (page.Pages, error) {
if ps.m.pageConfig.TranslationKey != "" {
// translationKey set by user.
pas, _ := ps.s.h.translationKeyPages.Get(ps.m.pageConfig.TranslationKey)
pasc := slices.Clone(pas)
page.SortByLanguage(pasc)
return pasc, nil
}
var pas page.Pages
ps.s.pageMap.treePages.ForEeachInDimension(ps.Path(), ps.s.siteVector, sitesmatrix.Language,
func(n contentNode) bool {
if n != nil {
pas = append(pas, n.(page.Page))
}
return true
},
)
pas = pagePredicates.ShouldLink.Filter(pas)
page.SortByLanguage(pas)
return pas, nil
})
if err != nil {
panic(err)
}
return pages
}
// For internal use only.
func (ps *pageState) SiteVector() sitesmatrix.Vector {
return ps.s.siteVector
}
func (ps *pageState) siteVector() sitesmatrix.Vector {
return ps.s.siteVector
}
func (ps *pageState) siteVectors() sitesmatrix.VectorIterator {
return ps.s.siteVector
}
// Rotate returns all pages in the given dimension for this page.
func (ps *pageState) Rotate(dimensionStr string) (page.Pages, error) {
dimensionStr = strings.ToLower(dimensionStr)
key := ps.Path() + "/" + "rotate-" + dimensionStr
d, err := sitesmatrix.ParseDimension(dimensionStr)
if err != nil {
return nil, fmt.Errorf("failed to parse dimension %q: %w", dimensionStr, err)
}
pages, err := ps.s.pageMap.getOrCreatePagesFromCache(ps.s.pageMap.cachePages2, key, func(string) (page.Pages, error) {
var pas page.Pages
ps.s.pageMap.treePages.ForEeachInDimension(ps.Path(), ps.s.siteVector, d,
func(n contentNode) bool {
if n != nil {
p := n.(page.Page)
pas = append(pas, p)
}
return true
},
)
if dimensionStr == "language" && ps.m.pageConfig.TranslationKey != "" {
// translationKey set by user.
// This is an old construct back from when languages were introduced.
// We keep it for backward compatibility.
// Also see AllTranslations.
pas, _ := ps.s.h.translationKeyPages.Get(ps.m.pageConfig.TranslationKey)
pasc := slices.Clone(pas)
page.SortByLanguage(pasc)
return pasc, nil
}
pas = pagePredicates.ShouldLink.Filter(pas)
page.SortByDims(pas)
return pas, nil
})
return pages, err
}
// Translations returns the translations excluding the current Page.
func (ps *pageState) Translations() page.Pages {
key := ps.Path() + "/" + "translations"
pages, err := ps.s.pageMap.getOrCreatePagesFromCache(nil, key, func(string) (page.Pages, error) {
var pas page.Pages
for _, pp := range ps.AllTranslations() {
if !pp.Eq(ps) {
pas = append(pas, pp)
}
}
return pas, nil
})
if err != nil {
panic(err)
}
return pages
}
func (ps *pageState) initCommonProviders(pp pagePaths) error {
if ps.IsPage() {
if ps.s == nil {
panic("no site")
}
ps.posNextPrev = &nextPrev{init: ps.s.init.prevNext}
ps.posNextPrevSection = &nextPrev{init: ps.s.init.prevNextInSection}
ps.InSectionPositioner = newPagePositionInSection(ps.posNextPrevSection)
ps.Positioner = newPagePosition(ps.posNextPrev)
}
ps.OutputFormatsProvider = pp
ps.targetPathDescriptor = pp.targetPathDescriptor
ps.RefProvider = newPageRef(ps)
ps.SitesProvider = ps.s
ps.SiteProvider = ps
return nil
}
// Exported so it can be used in integration tests.
func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) {
p := po.p
f := po.f
base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type)
return base, tplimpl.TemplateDescriptor{
Kind: p.Kind(),
LayoutFromUser: p.Layout(),
OutputFormat: f.Name,
MediaType: f.MediaType.Type,
IsPlainText: f.IsPlainText,
}
}
func (ps *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
pth, d := ps.GetInternalTemplateBasePathAndDescriptor()
var subPath string
if len(layouts) > 0 {
layout := layouts[0]
if i := strings.LastIndexByte(layout, '/'); i != -1 {
// A layout in a sub path, e.g. "foo/mylayout".
subPath, layout = layout[:i], layout[i+1:]
if layout == "" {
return nil, false, nil
}
}
d.LayoutFromUser = layout
d.LayoutFromUserMustMatch = true
}
q := tplimpl.TemplateQuery{
Path: pth,
SubPath: subPath,
Category: tplimpl.CategoryLayout,
Sites: ps.s.siteVector,
Desc: d,
}
tinfo := ps.s.TemplateStore.LookupPagesLayout(q)
if tinfo == nil {
return nil, false, nil
}
return tinfo, true, nil
}
// Must be run after the site section tree etc. is built and ready.
func (ps *pageState) initPage() error {
var initErr error
ps.pageInit.Do(func() {
var pp pagePaths
pp, initErr = newPagePaths(ps)
if initErr != nil {
return
}
var outputFormatsForPage output.Formats
var renderFormats output.Formats
if ps.m.standaloneOutputFormat.IsZero() {
outputFormatsForPage = ps.outputFormats()
renderFormats = ps.s.h.renderFormats
} else {
// One of the fixed output format pages, e.g. 404.
outputFormatsForPage = output.Formats{ps.m.standaloneOutputFormat}
renderFormats = outputFormatsForPage
}
// Prepare output formats for all sites.
// We do this even if this page does not get rendered on
// its own. It may be referenced via one of the site collections etc.
// it will then need an output format.
ps.pageOutputs = make([]*pageOutput, len(renderFormats))
created := make(map[string]*pageOutput)
shouldRenderPage := !ps.m.noRender()
for i, f := range renderFormats {
if po, found := created[f.Name]; found {
ps.pageOutputs[i] = po
continue
}
render := shouldRenderPage
if render {
_, render = outputFormatsForPage.GetByName(f.Name)
}
po := newPageOutput(ps, pp, f, render)
// Create a content provider for the first,
// we may be able to reuse it.
if i == 0 {
var contentProvider *pageContentOutput
contentProvider, initErr = newPageContentOutput(po)
if initErr != nil {
return
}
po.setContentProvider(contentProvider)
}
ps.pageOutputs[i] = po
created[f.Name] = po
}
if initErr = ps.initCommonProviders(pp); initErr != nil {
return
}
})
return initErr
}
func (ps *pageState) renderResources() error {
for _, r := range ps.Resources() {
if _, ok := r.(page.Page); ok {
if !ps.s.h.BuildState.IsRebuild() {
// Pages gets rendered with the owning page but we count them here.
ps.s.PathSpec.ProcessingStats.Incr(&ps.s.PathSpec.ProcessingStats.Pages)
}
continue
}
if resources.IsPublished(r) {
continue
}
src, ok := r.(resource.Source)
if !ok {
return fmt.Errorf("resource %T does not support resource.Source", r)
}
if err := src.Publish(); err != nil {
if !herrors.IsNotExist(err) {
ps.s.Log.Errorf("Failed to publish Resource for page %q: %s", ps.pathOrTitle(), err)
}
} else {
ps.s.PathSpec.ProcessingStats.Incr(&ps.s.PathSpec.ProcessingStats.Files)
}
}
return nil
}
func (ps *pageState) AlternativeOutputFormats() page.OutputFormats {
f := ps.outputFormat()
var o page.OutputFormats
for _, of := range ps.OutputFormats() {
if of.Format.NotAlternative || of.Format.Name == f.Name {
continue
}
o = append(o, of)
}
return o
}
type renderStringOpts struct {
Display string
Markup string
}
var defaultRenderStringOpts = renderStringOpts{
Display: "inline",
Markup: "", // Will inherit the page's value when not set.
}
func (m *pageMetaSource) wrapError(err error, sourceFs afero.Fs) error {
if err == nil {
panic("wrapError with nil")
}
if m.f == nil {
// No more details to add.
return fmt.Errorf("%q: %w", m.Path(), err)
}
return hugofs.AddFileInfoToError(err, m.f.FileInfo(), sourceFs)
}
// wrapError adds some more context to the given error if possible/needed
func (ps *pageState) wrapError(err error) error {
return ps.m.wrapError(err, ps.s.h.SourceFs)
}
func (ps *pageState) getPageInfoForError() string {
s := fmt.Sprintf("kind: %q, path: %q", ps.Kind(), ps.Path())
if ps.File() != nil {
s += fmt.Sprintf(", file: %q", ps.File().Filename())
}
return s
}
func (ps *pageState) getContentConverter() converter.Converter {
var err error
ps.contentConverterInit.Do(func() {
if ps.m.pageConfigSource.ContentMediaType.IsZero() {
panic("ContentMediaType not set")
}
markup := ps.m.pageConfigSource.ContentMediaType.SubType
if markup == "html" {
// Only reachable for shortcode inner content rendering; file-based
// HTML pages are gated at initFrontMatter via security.allowContent.
markup = "markdown"
}
ps.contentConverter, err = ps.m.newContentConverter(ps, markup)
})
if err != nil {
ps.s.Log.Errorln("Failed to create content converter:", err)
}
return ps.contentConverter
}
func (ps *pageState) errorf(err error, format string, a ...any) error {
if herrors.UnwrapFileError(err) != nil {
// More isn't always better.
return err
}
args := append([]any{ps.Language().Lang, ps.pathOrTitle()}, a...)
args = append(args, err)
format = "[%s] page %q: " + format + ": %w"
if err == nil {
return fmt.Errorf(format, args...)
}
return fmt.Errorf(format, args...)
}
func (ps *pageState) outputFormat() (f output.Format) {
if ps.pageOutput == nil {
panic("no pageOutput")
}
return ps.pageOutput.f
}
func (ps *pageState) parseError(err error, input []byte, offset int) error {
pos := posFromInput("", input, offset)
return herrors.NewFileErrorFromName(err, ps.File().Filename()).UpdatePosition(pos)
}
func (ps *pageState) pathOrTitle() string {
if ps.File() != nil {
return ps.File().Filename()
}
if ps.Path() != "" {
return ps.Path()
}
return ps.Title()
}
func (ps *pageState) posFromInput(input []byte, offset int) text.Position {
return posFromInput(ps.pathOrTitle(), input, offset)
}
func (ps *pageState) posOffset(offset int) text.Position {
return ps.posFromInput(ps.m.content.mustSource(), offset)
}
// shiftToOutputFormat is serialized. The output format idx refers to the
// full set of output formats for all sites.
// This is serialized.
func (ps *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
if err := ps.initPage(); err != nil {
return err
}
if len(ps.pageOutputs) == 1 {
idx = 0
}
ps.pageOutputIdx = idx
ps.pageOutput = ps.pageOutputs[idx]
if ps.pageOutput == nil {
panic(fmt.Sprintf("pageOutput is nil for output idx %d", idx))
}
// Reset any built paginator. This will trigger when re-rendering pages in
// server mode.
if isRenderingSite && ps.pageOutput.paginator != nil && ps.pageOutput.paginator.current != nil {
ps.pageOutput.paginator.reset()
}
if isRenderingSite {
cp := ps.pageOutput.pco
if cp == nil && ps.canReusePageOutputContent() {
// Look for content to reuse.
for i := range ps.pageOutputs {
if i == idx {
continue
}
po := ps.pageOutputs[i]
if po.pco != nil {
cp = po.pco
break
}
}
}
if cp == nil {
var err error
cp, err = newPageContentOutput(ps.pageOutput)
if err != nil {
return err
}
}
ps.pageOutput.setContentProvider(cp)
} else {
// We attempt to assign pageContentOutputs while preparing each site
// for rendering and before rendering each site. This lets us share
// content between page outputs to conserve resources. But if a template
// unexpectedly calls a method of a ContentProvider that is not yet
// initialized, we assign a LazyContentProvider that performs the
// initialization just in time.
if lcp, ok := (ps.pageOutput.ContentProvider.(*page.LazyContentProvider)); ok {
lcp.Reset()
} else {
lcp = page.NewLazyContentProvider(func(context.Context) page.OutputFormatContentProvider {
cp, err := newPageContentOutput(ps.pageOutput)
if err != nil {
panic(err)
}
return cp
})
ps.pageOutput.contentRenderer = lcp
ps.pageOutput.ContentProvider = lcp
ps.pageOutput.MarkupProvider = lcp
ps.pageOutput.PageRenderProvider = lcp
ps.pageOutput.TableOfContentsProvider = lcp
}
}
return nil
}
var (
_ page.Page = (*pageWithOrdinal)(nil)
_ collections.Order = (*pageWithOrdinal)(nil)
_ pageWrapper = (*pageWithOrdinal)(nil)
)
type pageWithOrdinal struct {
ordinal int
*pageState
}
func (p pageWithOrdinal) Ordinal() int {
return p.ordinal
}
func (p pageWithOrdinal) page() page.Page {
return p.pageState
}
type pageWithWeight0 struct {
weight0 int
*pageState
}
func (p pageWithWeight0) Weight0() int {
return p.weight0
}
func (p pageWithWeight0) page() page.Page {
return p.pageState
}
var _ types.Unwrapper = (*pageWithWeight0)(nil)
func (p pageWithWeight0) Unwrapv() any {
return p.pageState
}