mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
1f23b4949c
This issue fixes two cases where `{{__hugo_ctx` artifacts were left in the rendered output: 1. Inclusion when `.RenderShortcodes` is wrapped in HTML. 2. Inclusion of Markdown file without a trailing newline in some cases. Closes #12854 Updates #12998
782 lines
19 KiB
Go
782 lines
19 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"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
"github.com/gohugoio/hugo/hugolib/doctree"
|
|
"github.com/gohugoio/hugo/hugolib/segments"
|
|
"github.com/gohugoio/hugo/identity"
|
|
"github.com/gohugoio/hugo/media"
|
|
"github.com/gohugoio/hugo/output"
|
|
"github.com/gohugoio/hugo/output/layouts"
|
|
"github.com/gohugoio/hugo/related"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/gohugoio/hugo/markup/converter"
|
|
"github.com/gohugoio/hugo/markup/tableofcontents"
|
|
|
|
"github.com/gohugoio/hugo/tpl"
|
|
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
"github.com/gohugoio/hugo/common/maps"
|
|
"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)
|
|
_ contentNodeI = (*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
|
|
|
|
// 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
|
|
resourcesPublishInit *sync.Once
|
|
}
|
|
|
|
func (p *pageState) IdentifierBase() string {
|
|
return p.Path()
|
|
}
|
|
|
|
func (p *pageState) GetIdentity() identity.Identity {
|
|
return p
|
|
}
|
|
|
|
func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) bool {
|
|
return f(p)
|
|
}
|
|
|
|
func (p *pageState) GetDependencyManager() identity.Manager {
|
|
return p.dependencyManager
|
|
}
|
|
|
|
func (p *pageState) GetDependencyManagerForScope(scope int) identity.Manager {
|
|
switch scope {
|
|
case pageDependencyScopeDefault:
|
|
return p.dependencyManagerOutput
|
|
case pageDependencyScopeGlobal:
|
|
return p.dependencyManager
|
|
default:
|
|
return identity.NopManager
|
|
}
|
|
}
|
|
|
|
func (p *pageState) Key() string {
|
|
return "page-" + strconv.FormatUint(p.pid, 10)
|
|
}
|
|
|
|
func (p *pageState) resetBuildState() {
|
|
p.Scratcher = maps.NewScratcher()
|
|
}
|
|
|
|
func (p *pageState) reusePageOutputContent() bool {
|
|
return p.pageOutputTemplateVariationsState.Load() == 1
|
|
}
|
|
|
|
func (p *pageState) skipRender() bool {
|
|
b := p.s.conf.C.SegmentFilter.ShouldExcludeFine(
|
|
segments.SegmentMatcherFields{
|
|
Path: p.Path(),
|
|
Kind: p.Kind(),
|
|
Lang: p.Lang(),
|
|
Output: p.pageOutput.f.Name,
|
|
},
|
|
)
|
|
|
|
return b
|
|
}
|
|
|
|
func (po *pageState) isRenderedAny() bool {
|
|
for _, o := range po.pageOutputs {
|
|
if o.isRendered() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *pageState) isContentNodeBranch() bool {
|
|
return p.IsNode()
|
|
}
|
|
|
|
func (p *pageState) Err() resource.ResourceError {
|
|
return nil
|
|
}
|
|
|
|
// Eq returns whether the current page equals the given page.
|
|
// This is what's invoked when doing `{{ if eq $page $otherPage }}`
|
|
func (p *pageState) Eq(other any) bool {
|
|
pp, err := unwrapPage(other)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return p == pp
|
|
}
|
|
|
|
func (p *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 (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document {
|
|
fragments := p.pageOutput.pco.c().Fragments(ctx)
|
|
headings := fragments.Headings.FilterBy(fn)
|
|
return &pageHeadingsFiltered{
|
|
pageState: p,
|
|
headings: headings,
|
|
}
|
|
}
|
|
|
|
func (p *pageState) GitInfo() source.GitInfo {
|
|
return p.gitInfo
|
|
}
|
|
|
|
func (p *pageState) CodeOwners() []string {
|
|
return p.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 (p *pageState) GetTerms(taxonomy string) page.Pages {
|
|
return p.s.pageMap.getTermsForPageInTaxonomy(p.Path(), taxonomy)
|
|
}
|
|
|
|
func (p *pageState) MarshalJSON() ([]byte, error) {
|
|
return page.MarshalPageToJSON(p)
|
|
}
|
|
|
|
func (p *pageState) RegularPagesRecursive() page.Pages {
|
|
switch p.Kind() {
|
|
case kinds.KindSection, kinds.KindHome:
|
|
return p.s.pageMap.getPagesInSection(
|
|
pageMapQueryPagesInSection{
|
|
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
|
Path: p.Path(),
|
|
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage),
|
|
},
|
|
Recursive: true,
|
|
},
|
|
)
|
|
default:
|
|
return p.RegularPages()
|
|
}
|
|
}
|
|
|
|
func (p *pageState) PagesRecursive() page.Pages {
|
|
return nil
|
|
}
|
|
|
|
func (p *pageState) RegularPages() page.Pages {
|
|
switch p.Kind() {
|
|
case kinds.KindPage:
|
|
case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy:
|
|
return p.s.pageMap.getPagesInSection(
|
|
pageMapQueryPagesInSection{
|
|
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
|
Path: p.Path(),
|
|
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage),
|
|
},
|
|
},
|
|
)
|
|
case kinds.KindTerm:
|
|
return p.s.pageMap.getPagesWithTerm(
|
|
pageMapQueryPagesBelowPath{
|
|
Path: p.Path(),
|
|
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage),
|
|
},
|
|
)
|
|
default:
|
|
return p.s.RegularPages()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *pageState) Pages() page.Pages {
|
|
switch p.Kind() {
|
|
case kinds.KindPage:
|
|
case kinds.KindSection, kinds.KindHome:
|
|
return p.s.pageMap.getPagesInSection(
|
|
pageMapQueryPagesInSection{
|
|
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
|
Path: p.Path(),
|
|
KeyPart: "page-section",
|
|
Include: pagePredicates.ShouldListLocal.And(
|
|
pagePredicates.KindPage.Or(pagePredicates.KindSection),
|
|
),
|
|
},
|
|
},
|
|
)
|
|
case kinds.KindTerm:
|
|
return p.s.pageMap.getPagesWithTerm(
|
|
pageMapQueryPagesBelowPath{
|
|
Path: p.Path(),
|
|
},
|
|
)
|
|
case kinds.KindTaxonomy:
|
|
return p.s.pageMap.getPagesInSection(
|
|
pageMapQueryPagesInSection{
|
|
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
|
Path: p.Path(),
|
|
KeyPart: "term",
|
|
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm),
|
|
},
|
|
Recursive: true,
|
|
},
|
|
)
|
|
default:
|
|
return p.s.Pages()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RawContent returns the un-rendered source content without
|
|
// any leading front matter.
|
|
func (p *pageState) RawContent() string {
|
|
if p.m.content.pi.itemsStep2 == nil {
|
|
return ""
|
|
}
|
|
start := p.m.content.pi.posMainContent
|
|
if start == -1 {
|
|
start = 0
|
|
}
|
|
source, err := p.m.content.pi.contentSource(p.m.content)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(source[start:])
|
|
}
|
|
|
|
func (p *pageState) Resources() resource.Resources {
|
|
return p.s.pageMap.getOrCreateResourcesForPage(p)
|
|
}
|
|
|
|
func (p *pageState) HasShortcode(name string) bool {
|
|
if p.m.content.shortcodeState == nil {
|
|
return false
|
|
}
|
|
|
|
return p.m.content.shortcodeState.hasName(name)
|
|
}
|
|
|
|
func (p *pageState) Site() page.Site {
|
|
return p.sWrapped
|
|
}
|
|
|
|
func (p *pageState) String() string {
|
|
var sb strings.Builder
|
|
if p.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(p.File().Filename()))
|
|
if p.File().IsContentAdapter() {
|
|
// Also include the path.
|
|
sb.WriteString(":")
|
|
sb.WriteString(p.Path())
|
|
}
|
|
} else {
|
|
sb.WriteString(p.Path())
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// IsTranslated returns whether this content file is translated to
|
|
// other language(s).
|
|
func (p *pageState) IsTranslated() bool {
|
|
return len(p.Translations()) > 0
|
|
}
|
|
|
|
// TranslationKey returns the key used to identify a translation of this content.
|
|
func (p *pageState) TranslationKey() string {
|
|
if p.m.pageConfig.TranslationKey != "" {
|
|
return p.m.pageConfig.TranslationKey
|
|
}
|
|
return p.Path()
|
|
}
|
|
|
|
// AllTranslations returns all translations, including the current Page.
|
|
func (p *pageState) AllTranslations() page.Pages {
|
|
key := p.Path() + "/" + "translations-all"
|
|
// This is called from Translations, so we need to use a different partition, cachePages2,
|
|
// to avoid potential deadlocks.
|
|
pages, err := p.s.pageMap.getOrCreatePagesFromCache(p.s.pageMap.cachePages2, key, func(string) (page.Pages, error) {
|
|
if p.m.pageConfig.TranslationKey != "" {
|
|
// translationKey set by user.
|
|
pas, _ := p.s.h.translationKeyPages.Get(p.m.pageConfig.TranslationKey)
|
|
pasc := make(page.Pages, len(pas))
|
|
copy(pasc, pas)
|
|
page.SortByLanguage(pasc)
|
|
return pasc, nil
|
|
}
|
|
var pas page.Pages
|
|
p.s.pageMap.treePages.ForEeachInDimension(p.Path(), doctree.DimensionLanguage.Index(),
|
|
func(n contentNodeI) bool {
|
|
if n != nil {
|
|
pas = append(pas, n.(page.Page))
|
|
}
|
|
return false
|
|
},
|
|
)
|
|
|
|
pas = pagePredicates.ShouldLink.Filter(pas)
|
|
page.SortByLanguage(pas)
|
|
return pas, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return pages
|
|
}
|
|
|
|
// Translations returns the translations excluding the current Page.
|
|
func (p *pageState) Translations() page.Pages {
|
|
key := p.Path() + "/" + "translations"
|
|
pages, err := p.s.pageMap.getOrCreatePagesFromCache(nil, key, func(string) (page.Pages, error) {
|
|
var pas page.Pages
|
|
for _, pp := range p.AllTranslations() {
|
|
if !pp.Eq(p) {
|
|
pas = append(pas, pp)
|
|
}
|
|
}
|
|
return pas, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return pages
|
|
}
|
|
|
|
func (ps *pageState) initCommonProviders(pp pagePaths) error {
|
|
if ps.IsPage() {
|
|
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
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor {
|
|
p.layoutDescriptorInit.Do(func() {
|
|
var section string
|
|
sections := p.SectionsEntries()
|
|
|
|
switch p.Kind() {
|
|
case kinds.KindSection:
|
|
if len(sections) > 0 {
|
|
section = sections[0]
|
|
}
|
|
case kinds.KindTaxonomy, kinds.KindTerm:
|
|
|
|
if p.m.singular != "" {
|
|
section = p.m.singular
|
|
} else if len(sections) > 0 {
|
|
section = sections[0]
|
|
}
|
|
default:
|
|
}
|
|
|
|
p.layoutDescriptor = layouts.LayoutDescriptor{
|
|
Kind: p.Kind(),
|
|
Type: p.Type(),
|
|
Lang: p.Language().Lang,
|
|
Layout: p.Layout(),
|
|
Section: section,
|
|
}
|
|
})
|
|
|
|
return p.layoutDescriptor
|
|
}
|
|
|
|
func (p *pageState) resolveTemplate(layouts ...string) (tpl.Template, bool, error) {
|
|
f := p.outputFormat()
|
|
|
|
d := p.getLayoutDescriptor()
|
|
|
|
if len(layouts) > 0 {
|
|
d.Layout = layouts[0]
|
|
d.LayoutOverride = true
|
|
}
|
|
|
|
return p.s.Tmpl().LookupLayout(d, f)
|
|
}
|
|
|
|
// Must be run after the site section tree etc. is built and ready.
|
|
func (p *pageState) initPage() error {
|
|
if _, err := p.init.Do(context.Background()); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *pageState) renderResources() error {
|
|
var initErr error
|
|
|
|
p.resourcesPublishInit.Do(func() {
|
|
for _, r := range p.Resources() {
|
|
if _, ok := r.(page.Page); ok {
|
|
// Pages gets rendered with the owning page but we count them here.
|
|
p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages)
|
|
continue
|
|
}
|
|
|
|
if _, isWrapper := r.(resource.ResourceWrapper); isWrapper {
|
|
// Skip resources that are wrapped.
|
|
// These gets published on its own.
|
|
continue
|
|
}
|
|
|
|
src, ok := r.(resource.Source)
|
|
if !ok {
|
|
initErr = fmt.Errorf("resource %T does not support resource.Source", r)
|
|
return
|
|
}
|
|
|
|
if err := src.Publish(); err != nil {
|
|
if !herrors.IsNotExist(err) {
|
|
p.s.Log.Errorf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err)
|
|
}
|
|
} else {
|
|
p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files)
|
|
}
|
|
}
|
|
})
|
|
|
|
return initErr
|
|
}
|
|
|
|
func (p *pageState) AlternativeOutputFormats() page.OutputFormats {
|
|
f := p.outputFormat()
|
|
var o page.OutputFormats
|
|
for _, of := range p.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 (p *pageMeta) wrapError(err error, sourceFs afero.Fs) error {
|
|
if err == nil {
|
|
panic("wrapError with nil")
|
|
}
|
|
|
|
if p.File() == nil {
|
|
// No more details to add.
|
|
return fmt.Errorf("%q: %w", p.Path(), err)
|
|
}
|
|
|
|
return hugofs.AddFileInfoToError(err, p.File().FileInfo(), sourceFs)
|
|
}
|
|
|
|
// wrapError adds some more context to the given error if possible/needed
|
|
func (p *pageState) wrapError(err error) error {
|
|
return p.m.wrapError(err, p.s.h.SourceFs)
|
|
}
|
|
|
|
func (p *pageState) getPageInfoForError() string {
|
|
s := fmt.Sprintf("kind: %q, path: %q", p.Kind(), p.Path())
|
|
if p.File() != nil {
|
|
s += fmt.Sprintf(", file: %q", p.File().Filename())
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (p *pageState) getContentConverter() converter.Converter {
|
|
var err error
|
|
p.contentConverterInit.Do(func() {
|
|
if p.m.pageConfig.ContentMediaType.IsZero() {
|
|
panic("ContentMediaType not set")
|
|
}
|
|
markup := p.m.pageConfig.ContentMediaType.SubType
|
|
|
|
if markup == "html" {
|
|
// Only used for shortcode inner content.
|
|
markup = "markdown"
|
|
}
|
|
p.contentConverter, err = p.m.newContentConverter(p, markup)
|
|
})
|
|
|
|
if err != nil {
|
|
p.s.Log.Errorln("Failed to create content converter:", err)
|
|
}
|
|
return p.contentConverter
|
|
}
|
|
|
|
func (p *pageState) errorf(err error, format string, a ...any) error {
|
|
if herrors.UnwrapFileError(err) != nil {
|
|
// More isn't always better.
|
|
return err
|
|
}
|
|
args := append([]any{p.Language().Lang, p.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 (p *pageState) outputFormat() (f output.Format) {
|
|
if p.pageOutput == nil {
|
|
panic("no pageOutput")
|
|
}
|
|
return p.pageOutput.f
|
|
}
|
|
|
|
func (p *pageState) parseError(err error, input []byte, offset int) error {
|
|
pos := posFromInput("", input, offset)
|
|
return herrors.NewFileErrorFromName(err, p.File().Filename()).UpdatePosition(pos)
|
|
}
|
|
|
|
func (p *pageState) pathOrTitle() string {
|
|
if p.File() != nil {
|
|
return p.File().Filename()
|
|
}
|
|
|
|
if p.Path() != "" {
|
|
return p.Path()
|
|
}
|
|
|
|
return p.Title()
|
|
}
|
|
|
|
func (p *pageState) posFromInput(input []byte, offset int) text.Position {
|
|
return posFromInput(p.pathOrTitle(), input, offset)
|
|
}
|
|
|
|
func (p *pageState) posOffset(offset int) text.Position {
|
|
return p.posFromInput(p.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 (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
|
|
if err := p.initPage(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(p.pageOutputs) == 1 {
|
|
idx = 0
|
|
}
|
|
|
|
p.pageOutputIdx = idx
|
|
p.pageOutput = p.pageOutputs[idx]
|
|
if p.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 && p.pageOutput.paginator != nil && p.pageOutput.paginator.current != nil {
|
|
p.pageOutput.paginator.reset()
|
|
}
|
|
|
|
if isRenderingSite {
|
|
cp := p.pageOutput.pco
|
|
if cp == nil && p.reusePageOutputContent() {
|
|
// Look for content to reuse.
|
|
for i := 0; i < len(p.pageOutputs); i++ {
|
|
if i == idx {
|
|
continue
|
|
}
|
|
po := p.pageOutputs[i]
|
|
|
|
if po.pco != nil {
|
|
cp = po.pco
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if cp == nil {
|
|
var err error
|
|
cp, err = newPageContentOutput(p.pageOutput)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
p.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 := (p.pageOutput.ContentProvider.(*page.LazyContentProvider)); ok {
|
|
lcp.Reset()
|
|
} else {
|
|
lcp = page.NewLazyContentProvider(func() (page.OutputFormatContentProvider, error) {
|
|
cp, err := newPageContentOutput(p.pageOutput)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cp, nil
|
|
})
|
|
p.pageOutput.contentRenderer = lcp
|
|
p.pageOutput.ContentProvider = lcp
|
|
p.pageOutput.MarkupProvider = lcp
|
|
p.pageOutput.PageRenderProvider = lcp
|
|
p.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
|
|
}
|