aboutsummaryrefslogtreecommitdiffhomepage
path: root/hugolib/pagesfromdata/pagesfromgotmpl.go
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib/pagesfromdata/pagesfromgotmpl.go')
-rw-r--r--hugolib/pagesfromdata/pagesfromgotmpl.go331
1 files changed, 331 insertions, 0 deletions
diff --git a/hugolib/pagesfromdata/pagesfromgotmpl.go b/hugolib/pagesfromdata/pagesfromgotmpl.go
new file mode 100644
index 000000000..4107f9e33
--- /dev/null
+++ b/hugolib/pagesfromdata/pagesfromgotmpl.go
@@ -0,0 +1,331 @@
+// 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 pagesfromdata
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/resources/page"
+ "github.com/gohugoio/hugo/resources/page/pagemeta"
+ "github.com/gohugoio/hugo/resources/resource"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
+ "github.com/spf13/cast"
+)
+
+type PagesFromDataTemplateContext interface {
+ // AddPage adds a new page to the site.
+ // The first return value will always be an empty string.
+ AddPage(any) (string, error)
+
+ // AddResource adds a new resource to the site.
+ // The first return value will always be an empty string.
+ AddResource(any) (string, error)
+
+ // The site to which the pages will be added.
+ Site() page.Site
+
+ // The same template may be executed multiple times for multiple languages.
+ // The Store can be used to store state between these invocations.
+ Store() *maps.Scratch
+
+ // By default, the template will be executed for the language
+ // defined by the _content.gotmpl file (e.g. its mount definition).
+ // This method can be used to activate the template for all languages.
+ // The return value will always be an empty string.
+ EnableAllLanguages() string
+}
+
+var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil)
+
+type pagesFromDataTemplateContext struct {
+ p *PagesFromTemplate
+}
+
+func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) {
+ m, err := maps.ToStringMapE(v)
+ if err != nil {
+ return "", nil, err
+ }
+ pathv, ok := m["path"]
+ if !ok {
+ return "", nil, fmt.Errorf("path not set")
+ }
+ path, err := cast.ToStringE(pathv)
+ if err != nil || path == "" {
+ return "", nil, fmt.Errorf("invalid path %q", path)
+ }
+ return path, m, nil
+}
+
+func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) {
+ path, m, err := p.toPathMap(v)
+ if err != nil {
+ return "", err
+ }
+
+ if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
+ return "", nil
+ }
+
+ pd := pagemeta.DefaultPageConfig
+ pd.IsFromContentAdapter = true
+
+ if err := mapstructure.WeakDecode(m, &pd); err != nil {
+ return "", fmt.Errorf("failed to decode page map: %w", err)
+ }
+
+ p.p.buildState.NumPagesAdded++
+
+ if err := pd.Validate(true); err != nil {
+ return "", err
+ }
+
+ return "", p.p.HandlePage(p.p, &pd)
+}
+
+func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) {
+ path, m, err := p.toPathMap(v)
+ if err != nil {
+ return "", err
+ }
+
+ if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
+ return "", nil
+ }
+
+ var rd pagemeta.ResourceConfig
+ if err := mapstructure.WeakDecode(m, &rd); err != nil {
+ return "", err
+ }
+
+ p.p.buildState.NumResourcesAdded++
+
+ if err := rd.Validate(); err != nil {
+ return "", err
+ }
+
+ return "", p.p.HandleResource(p.p, &rd)
+}
+
+func (p *pagesFromDataTemplateContext) Site() page.Site {
+ return p.p.Site
+}
+
+func (p *pagesFromDataTemplateContext) Store() *maps.Scratch {
+ return p.p.store
+}
+
+func (p *pagesFromDataTemplateContext) EnableAllLanguages() string {
+ p.p.buildState.EnableAllLanguages = true
+ return ""
+}
+
+func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate {
+ return &PagesFromTemplate{
+ PagesFromTemplateOptions: opts,
+ PagesFromTemplateDeps: opts.DepsFromSite(opts.Site),
+ buildState: &BuildState{
+ sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
+ },
+ store: maps.NewScratch(),
+ }
+}
+
+type PagesFromTemplateOptions struct {
+ Site page.Site
+ DepsFromSite func(page.Site) PagesFromTemplateDeps
+
+ DependencyManager identity.Manager
+
+ Watching bool
+
+ HandlePage func(pt *PagesFromTemplate, p *pagemeta.PageConfig) error
+ HandleResource func(pt *PagesFromTemplate, p *pagemeta.ResourceConfig) error
+
+ GoTmplFi hugofs.FileMetaInfo
+}
+
+type PagesFromTemplateDeps struct {
+ TmplFinder tpl.TemplateParseFinder
+ TmplExec tpl.TemplateExecutor
+}
+
+var _ resource.Staler = (*PagesFromTemplate)(nil)
+
+type PagesFromTemplate struct {
+ PagesFromTemplateOptions
+ PagesFromTemplateDeps
+ buildState *BuildState
+ store *maps.Scratch
+}
+
+func (b *PagesFromTemplate) AddChange(id identity.Identity) {
+ b.buildState.ChangedIdentities = append(b.buildState.ChangedIdentities, id)
+}
+
+func (b *PagesFromTemplate) MarkStale() {
+ b.buildState.StaleVersion++
+}
+
+func (b *PagesFromTemplate) StaleVersion() uint32 {
+ return b.buildState.StaleVersion
+}
+
+type BuildInfo struct {
+ NumPagesAdded uint64
+ NumResourcesAdded uint64
+ EnableAllLanguages bool
+ ChangedIdentities []identity.Identity
+ DeletedPaths []string
+ Path *paths.Path
+}
+
+type BuildState struct {
+ StaleVersion uint32
+
+ EnableAllLanguages bool
+
+ // Paths deleted in the current build.
+ DeletedPaths []string
+
+ // Changed identities in the current build.
+ ChangedIdentities []identity.Identity
+
+ NumPagesAdded uint64
+ NumResourcesAdded uint64
+
+ sourceInfosCurrent *maps.Cache[string, *sourceInfo]
+ sourceInfosPrevious *maps.Cache[string, *sourceInfo]
+}
+
+func (b *BuildState) hash(v any) uint64 {
+ return identity.HashUint64(v)
+}
+
+func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool {
+ h := b.hash(v)
+ si, found := b.sourceInfosPrevious.Get(changedPath)
+ if found {
+ b.sourceInfosCurrent.Set(changedPath, si)
+ if si.hash == h {
+ return false
+ }
+ } else {
+ si = &sourceInfo{}
+ b.sourceInfosCurrent.Set(changedPath, si)
+ }
+ si.hash = h
+ return true
+}
+
+func (b *BuildState) resolveDeletedPaths() {
+ if b.sourceInfosPrevious == nil {
+ b.DeletedPaths = nil
+ return
+ }
+ var paths []string
+ b.sourceInfosPrevious.ForEeach(func(k string, _ *sourceInfo) {
+ if _, found := b.sourceInfosCurrent.Get(k); !found {
+ paths = append(paths, k)
+ }
+ })
+
+ b.DeletedPaths = paths
+}
+
+func (b *BuildState) PrepareNextBuild() {
+ b.sourceInfosPrevious = b.sourceInfosCurrent
+ b.sourceInfosCurrent = maps.NewCache[string, *sourceInfo]()
+ b.StaleVersion = 0
+ b.DeletedPaths = nil
+ b.ChangedIdentities = nil
+ b.NumPagesAdded = 0
+ b.NumResourcesAdded = 0
+}
+
+type sourceInfo struct {
+ hash uint64
+}
+
+func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate {
+ // We deliberately make them share the same DepenencyManager and Store.
+ p.PagesFromTemplateOptions.Site = s
+ p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s)
+ p.buildState = &BuildState{
+ sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
+ }
+ return &p
+}
+
+func (p PagesFromTemplate) CloneForGoTmpl(fi hugofs.FileMetaInfo) *PagesFromTemplate {
+ p.PagesFromTemplateOptions.GoTmplFi = fi
+ return &p
+}
+
+func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager {
+ return p.DependencyManager
+}
+
+func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
+ defer func() {
+ p.buildState.PrepareNextBuild()
+ }()
+
+ f, err := p.GoTmplFi.Meta().Open()
+ if err != nil {
+ return BuildInfo{}, err
+ }
+ defer f.Close()
+
+ tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
+ if err != nil {
+ return BuildInfo{}, err
+ }
+
+ data := &pagesFromDataTemplateContext{
+ p: p,
+ }
+
+ ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
+
+ if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
+ return BuildInfo{}, err
+ }
+
+ if p.Watching {
+ p.buildState.resolveDeletedPaths()
+ }
+
+ bi := BuildInfo{
+ NumPagesAdded: p.buildState.NumPagesAdded,
+ NumResourcesAdded: p.buildState.NumResourcesAdded,
+ EnableAllLanguages: p.buildState.EnableAllLanguages,
+ ChangedIdentities: p.buildState.ChangedIdentities,
+ DeletedPaths: p.buildState.DeletedPaths,
+ Path: p.GoTmplFi.Meta().PathInfo,
+ }
+
+ return bi, nil
+}
+
+//////////////