mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
70db201ed4
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>
986 lines
24 KiB
Go
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
|
|
}
|