From 277bc7119d77ab3764455d7ac19c28315ddd9339 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?=
Date: Sat, 8 Jun 2024 11:52:22 +0200
Subject: [PATCH] Implement defer
Closes #8086
---
cache/httpcache/httpcache.go | 1 -
common/herrors/errors.go | 3 +
common/hugio/hasBytesWriter.go | 41 +++-
common/hugio/hasBytesWriter_test.go | 13 +-
common/maps/cache.go | 20 ++
deps/deps.go | 59 +++++-
hugofs/hasbytes_fs.go | 23 ++-
hugolib/hugo_sites_build.go | 200 ++++++++++++++++----
hugolib/site.go | 12 +-
hugolib/site_new.go | 4 -
hugolib/site_render.go | 2 +-
tpl/template.go | 26 +++
tpl/templates/init.go | 10 +
tpl/templates/templates.go | 67 ++++++-
tpl/templates/templates_integration_test.go | 44 +++++
tpl/tplimpl/template.go | 35 ++++
tpl/tplimpl/template_ast_transformers.go | 63 +++++-
17 files changed, 543 insertions(+), 80 deletions(-)
diff --git a/cache/httpcache/httpcache.go b/cache/httpcache/httpcache.go
index ff360001f6a..98f7fedd4c5 100644
--- a/cache/httpcache/httpcache.go
+++ b/cache/httpcache/httpcache.go
@@ -83,7 +83,6 @@ func (c *Config) Compile() (ConfigCompiled, error) {
}
// PollConfig holds the configuration for polling remote resources to detect changes in watch mode.
-// TODO1 make sure this enabled only in watch mode.
type PollConfig struct {
// What remote resources to apply this configuration to.
For GlobMatcher
diff --git a/common/herrors/errors.go b/common/herrors/errors.go
index 7c389c1aeaf..98d2f41f830 100644
--- a/common/herrors/errors.go
+++ b/common/herrors/errors.go
@@ -131,6 +131,9 @@ func ImproveIfNilPointer(inErr error) (outErr error) {
call := m[1]
field := m[2]
parts := strings.Split(call, ".")
+ if len(parts) < 2 {
+ return
+ }
receiverName := parts[len(parts)-2]
receiver := strings.Join(parts[:len(parts)-1], ".")
s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
diff --git a/common/hugio/hasBytesWriter.go b/common/hugio/hasBytesWriter.go
index 5148c82f96a..d2bcd1bb408 100644
--- a/common/hugio/hasBytesWriter.go
+++ b/common/hugio/hasBytesWriter.go
@@ -17,24 +17,35 @@ import (
"bytes"
)
-// HasBytesWriter is a writer that will set Match to true if the given pattern
-// is found in the stream.
+// HasBytesWriter is a writer will match against a slice of patterns.
type HasBytesWriter struct {
- Match bool
- Pattern []byte
+ Patterns []*HasBytesPattern
i int
done bool
buff []byte
}
+type HasBytesPattern struct {
+ Match bool
+ Pattern []byte
+}
+
+func (h *HasBytesWriter) patternLen() int {
+ l := 0
+ for _, p := range h.Patterns {
+ l += len(p.Pattern)
+ }
+ return l
+}
+
func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
if h.done {
return len(p), nil
}
if len(h.buff) == 0 {
- h.buff = make([]byte, len(h.Pattern)*2)
+ h.buff = make([]byte, h.patternLen()*2)
}
for i := range p {
@@ -46,11 +57,23 @@ func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
h.i = len(h.buff) / 2
}
- if bytes.Contains(h.buff, h.Pattern) {
- h.Match = true
- h.done = true
- return len(p), nil
+ for _, pp := range h.Patterns {
+ if bytes.Contains(h.buff, pp.Pattern) {
+ pp.Match = true
+ done := true
+ for _, ppp := range h.Patterns {
+ if !ppp.Match {
+ done = false
+ break
+ }
+ }
+ if done {
+ h.done = true
+ }
+ return len(p), nil
+ }
}
+
}
return len(p), nil
diff --git a/common/hugio/hasBytesWriter_test.go b/common/hugio/hasBytesWriter_test.go
index af53fa5dd49..49487ab0bb7 100644
--- a/common/hugio/hasBytesWriter_test.go
+++ b/common/hugio/hasBytesWriter_test.go
@@ -34,8 +34,11 @@ func TestHasBytesWriter(t *testing.T) {
var b bytes.Buffer
h := &HasBytesWriter{
- Pattern: []byte("__foo"),
+ Patterns: []*HasBytesPattern{
+ {Pattern: []byte("__foo")},
+ },
}
+
return h, io.MultiWriter(&b, h)
}
@@ -46,19 +49,19 @@ func TestHasBytesWriter(t *testing.T) {
for i := 0; i < 22; i++ {
h, w := neww()
fmt.Fprintf(w, rndStr()+"abc __foobar"+rndStr())
- c.Assert(h.Match, qt.Equals, true)
+ c.Assert(h.Patterns[0].Match, qt.Equals, true)
h, w = neww()
fmt.Fprintf(w, rndStr()+"abc __f")
fmt.Fprintf(w, "oo bar"+rndStr())
- c.Assert(h.Match, qt.Equals, true)
+ c.Assert(h.Patterns[0].Match, qt.Equals, true)
h, w = neww()
fmt.Fprintf(w, rndStr()+"abc __moo bar")
- c.Assert(h.Match, qt.Equals, false)
+ c.Assert(h.Patterns[0].Match, qt.Equals, false)
}
h, w := neww()
fmt.Fprintf(w, "__foo")
- c.Assert(h.Match, qt.Equals, true)
+ c.Assert(h.Patterns[0].Match, qt.Equals, true)
}
diff --git a/common/maps/cache.go b/common/maps/cache.go
index 3723d318e45..7cd7410c216 100644
--- a/common/maps/cache.go
+++ b/common/maps/cache.go
@@ -74,6 +74,26 @@ func (c *Cache[K, T]) ForEeach(f func(K, T)) {
}
}
+func (c *Cache[K, T]) Drain() map[K]T {
+ c.Lock()
+ m := c.m
+ c.m = make(map[K]T)
+ c.Unlock()
+ return m
+}
+
+func (c *Cache[K, T]) Len() int {
+ c.RLock()
+ defer c.RUnlock()
+ return len(c.m)
+}
+
+func (c *Cache[K, T]) Reset() {
+ c.Lock()
+ c.m = make(map[K]T)
+ c.Unlock()
+}
+
// SliceCache is a simple thread safe cache backed by a map.
type SliceCache[T any] struct {
m map[string][]T
diff --git a/deps/deps.go b/deps/deps.go
index 678f8a2fccf..9200f2b12d2 100644
--- a/deps/deps.go
+++ b/deps/deps.go
@@ -15,6 +15,7 @@ import (
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
@@ -161,20 +162,29 @@ func (d *Deps) Init() error {
}
if d.PathSpec == nil {
- hashBytesReceiverFunc := func(name string, match bool) {
- if !match {
- return
+ hashBytesReceiverFunc := func(name string, match []byte) {
+ s := string(match)
+ switch s {
+ case postpub.PostProcessPrefix:
+ d.BuildState.AddFilenameWithPostPrefix(name)
+ case tpl.HugoDeferredTemplatePrefix:
+ d.BuildState.DeferredExecutions.FilenamesWithPostPrefix.Set(name, true)
}
- d.BuildState.AddFilenameWithPostPrefix(name)
}
// Skip binary files.
mediaTypes := d.Conf.GetConfigSection("mediaTypes").(media.Types)
- hashBytesSHouldCheck := func(name string) bool {
+ hashBytesShouldCheck := func(name string) bool {
ext := strings.TrimPrefix(filepath.Ext(name), ".")
return mediaTypes.IsTextSuffix(ext)
}
- d.Fs.PublishDir = hugofs.NewHasBytesReceiver(d.Fs.PublishDir, hashBytesSHouldCheck, hashBytesReceiverFunc, []byte(postpub.PostProcessPrefix))
+ d.Fs.PublishDir = hugofs.NewHasBytesReceiver(
+ d.Fs.PublishDir,
+ hashBytesShouldCheck,
+ hashBytesReceiverFunc,
+ []byte(tpl.HugoDeferredTemplatePrefix),
+ []byte(postpub.PostProcessPrefix))
+
pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log)
if err != nil {
return err
@@ -371,10 +381,41 @@ type BuildState struct {
// A set of filenames in /public that
// contains a post-processing prefix.
filenamesWithPostPrefix map[string]bool
+
+ filenamesWithDefferedExecution map[string]bool
+
+ DeferredExecutions *DeferredExecutions
+
+ // Deferred executions grouped by rendering context.
+ DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
+}
+
+type DeferredExecutions struct {
+ // A set of filenames in /public that
+ // contains a post-processing prefix.
+ FilenamesWithPostPrefix *maps.Cache[string, bool]
+
+ // Maps a placeholder to a deferred execution.
+ Executions *maps.Cache[string, *tpl.DeferredExecution]
}
var _ identity.SignalRebuilder = (*BuildState)(nil)
+func (b *BuildState) StartRenderingStage(r tpl.RenderingContext) {
+ if b.DeferredExecutionsGroupedByRenderingContext == nil {
+ b.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
+ }
+ b.DeferredExecutions = &DeferredExecutions{
+ Executions: maps.NewCache[string, *tpl.DeferredExecution](),
+ FilenamesWithPostPrefix: maps.NewCache[string, bool](),
+ }
+}
+
+func (b *BuildState) StopRenderingStage(r tpl.RenderingContext) {
+ b.DeferredExecutionsGroupedByRenderingContext[r] = b.DeferredExecutions
+ b.DeferredExecutions = nil
+}
+
func (b *BuildState) SignalRebuild(ids ...identity.Identity) {
b.OnSignalRebuild(ids...)
}
@@ -399,6 +440,12 @@ func (b *BuildState) GetFilenamesWithPostPrefix() []string {
return filenames
}
+func (b *BuildState) AddDeferredExecution(id string, d *tpl.DeferredExecution) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ b.DeferredExecutions.Executions.Set(id, d)
+}
+
func (b *BuildState) Incr() int {
return int(atomic.AddUint64(&b.counter, uint64(1)))
}
diff --git a/hugofs/hasbytes_fs.go b/hugofs/hasbytes_fs.go
index 238fbc9c475..ac9e881ef6e 100644
--- a/hugofs/hasbytes_fs.go
+++ b/hugofs/hasbytes_fs.go
@@ -28,12 +28,12 @@ var (
type hasBytesFs struct {
afero.Fs
shouldCheck func(name string) bool
- hasBytesCallback func(name string, match bool)
- pattern []byte
+ hasBytesCallback func(name string, match []byte)
+ patterns [][]byte
}
-func NewHasBytesReceiver(delegate afero.Fs, shouldCheck func(name string) bool, hasBytesCallback func(name string, match bool), pattern []byte) afero.Fs {
- return &hasBytesFs{Fs: delegate, shouldCheck: shouldCheck, hasBytesCallback: hasBytesCallback, pattern: pattern}
+func NewHasBytesReceiver(delegate afero.Fs, shouldCheck func(name string) bool, hasBytesCallback func(name string, match []byte), patterns ...[]byte) afero.Fs {
+ return &hasBytesFs{Fs: delegate, shouldCheck: shouldCheck, hasBytesCallback: hasBytesCallback, patterns: patterns}
}
func (fs *hasBytesFs) UnwrapFilesystem() afero.Fs {
@@ -60,10 +60,15 @@ func (fs *hasBytesFs) wrapFile(f afero.File) afero.File {
if !fs.shouldCheck(f.Name()) {
return f
}
+ patterns := make([]*hugio.HasBytesPattern, len(fs.patterns))
+ for i, p := range fs.patterns {
+ patterns[i] = &hugio.HasBytesPattern{Pattern: p}
+ }
+
return &hasBytesFile{
File: f,
hbw: &hugio.HasBytesWriter{
- Pattern: fs.pattern,
+ Patterns: patterns,
},
hasBytesCallback: fs.hasBytesCallback,
}
@@ -74,7 +79,7 @@ func (fs *hasBytesFs) Name() string {
}
type hasBytesFile struct {
- hasBytesCallback func(name string, match bool)
+ hasBytesCallback func(name string, match []byte)
hbw *hugio.HasBytesWriter
afero.File
}
@@ -88,6 +93,10 @@ func (h *hasBytesFile) Write(p []byte) (n int, err error) {
}
func (h *hasBytesFile) Close() error {
- h.hasBytesCallback(h.Name(), h.hbw.Match)
+ for _, p := range h.hbw.Patterns {
+ if p.Match {
+ h.hasBytesCallback(h.Name(), p.Pattern)
+ }
+ }
return h.File.Close()
}
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 12eb6a5f8d7..0992b0fa952 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -26,6 +26,7 @@ import (
"time"
"github.com/bep/logg"
+ "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
@@ -165,7 +166,8 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
}
if prepareErr == nil {
- if err := h.render(infol, conf); err != nil {
+ ctx := context.Background()
+ if err := h.render(ctx, infol, conf); err != nil {
h.SendError(fmt.Errorf("render: %w", err))
}
@@ -173,6 +175,16 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
h.SendError(fmt.Errorf("postRenderOnce: %w", err))
}
+ // Make sure to write any build stats to disk first so it's available
+ // to the post processors.
+ if err := h.writeBuildStats(); err != nil {
+ return err
+ }
+
+ if err := h.renderDeferred(infol); err != nil {
+ h.SendError(fmt.Errorf("renderDeferred: %w", err))
+ }
+
if err := h.postProcess(infol); err != nil {
h.SendError(fmt.Errorf("postProcess: %w", err))
}
@@ -327,7 +339,7 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil
}
// render renders the sites.
-func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
+func (h *HugoSites) render(ctx context.Context, l logg.LevelLogger, config *BuildCfg) error {
l = l.WithField("step", "render")
start := time.Now()
defer func() {
@@ -352,47 +364,167 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
continue
}
- siteRenderContext.outIdx = siteOutIdx
- siteRenderContext.sitesOutIdx = i
- i++
-
- select {
- case <-h.Done():
- return nil
- default:
- for _, s2 := range h.Sites {
- // We render site by site, but since the content is lazily rendered
- // and a site can "borrow" content from other sites, every site
- // needs this set.
- s2.rc = &siteRenderingContext{Format: renderFormat}
-
- if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
- return err
+ if err := func() error {
+ rc := tpl.RenderingContext{Site: s, SiteOutIdx: siteOutIdx}
+ h.BuildState.StartRenderingStage(rc)
+ defer h.BuildState.StopRenderingStage(rc)
+
+ siteRenderContext.outIdx = siteOutIdx
+ siteRenderContext.sitesOutIdx = i
+ i++
+
+ select {
+ case <-h.Done():
+ return nil
+ default:
+ for _, s2 := range h.Sites {
+ if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil {
+ return err
+ }
+ }
+ if !config.SkipRender {
+ ll := l.WithField("substep", "pages").
+ WithField("site", s.language.Lang).
+ WithField("outputFormat", renderFormat.Name)
+
+ start := time.Now()
+
+ if config.PartialReRender {
+ if err := s.renderPages(siteRenderContext); err != nil {
+ return err
+ }
+ } else {
+ if err := s.render(siteRenderContext); err != nil {
+ return err
+ }
+ }
+ loggers.TimeTrackf(ll, start, nil, "")
}
}
- if !config.SkipRender {
- ll := l.WithField("substep", "pages").
- WithField("site", s.language.Lang).
- WithField("outputFormat", renderFormat.Name)
+ return nil
+ }(); err != nil {
+ return err
+ }
- start := time.Now()
+ }
+ }
- if config.PartialReRender {
- if err := s.renderPages(siteRenderContext); err != nil {
- return err
- }
- } else {
- if err := s.render(siteRenderContext); err != nil {
+ return nil
+}
+
+func (h *HugoSites) renderDeferred(l logg.LevelLogger) error {
+ l = l.WithField("step", "render deferred")
+ start := time.Now()
+ defer func() {
+ // TODO1 fields.
+ loggers.TimeTrackf(l, start, logg.Fields{}, "")
+ }()
+
+ for rc, de := range h.Deps.BuildState.DeferredExecutionsGroupedByRenderingContext {
+ if de.FilenamesWithPostPrefix.Len() == 0 {
+ continue
+ }
+ s := rc.Site.(*Site)
+ for _, s2 := range h.Sites {
+ if err := s2.preparePagesForRender(s == s2, rc.SiteOutIdx); err != nil {
+ return err
+ }
+ }
+ if err := s.executeDeferredTemplates(de); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error {
+ handleFile := func(filename string) error {
+ content, err := afero.ReadFile(s.BaseFs.PublishFs, filename)
+ if err != nil {
+ return err
+ }
+
+ k := 0
+ changed := false
+
+ for {
+ if k >= len(content) {
+ break
+ }
+ l := bytes.Index(content[k:], []byte(tpl.HugoDeferredTemplatePrefix))
+ if l == -1 {
+ break
+ }
+ m := bytes.Index(content[k+l:], []byte(tpl.HugoDeferredTemplateSuffix)) + len(tpl.HugoDeferredTemplateSuffix)
+
+ low, high := k+l, k+l+m
+
+ forward := l + m
+ id := string(content[low:high])
+
+ if err := func() error {
+ deferred, found := de.Executions.Get(id)
+ if !found {
+ panic(fmt.Sprintf("deferred execution with id %q not found", id))
+ }
+ deferred.Mu.Lock()
+ defer deferred.Mu.Unlock()
+
+ if !deferred.Executed {
+ tmpl := s.Deps.Tmpl()
+ templ, found := tmpl.Lookup(deferred.TemplateName)
+ if !found {
+ panic(fmt.Sprintf("template %q not found", deferred.TemplateName))
+ }
+
+ if err := func() error {
+ buf := bufferpool.GetBuffer()
+ defer bufferpool.PutBuffer(buf)
+
+ err = tmpl.ExecuteWithContext(deferred.Ctx, templ, buf, deferred.Data)
+ if err != nil {
return err
}
+ deferred.Result = buf.String()
+ deferred.Executed = true
+
+ return nil
+ }(); err != nil {
+ return err
}
- loggers.TimeTrackf(ll, start, nil, "")
}
+
+ content = append(content[:low], append([]byte(deferred.Result), content[high:]...)...)
+ changed = true
+
+ return nil
+ }(); err != nil {
+ return err
}
+
+ k += forward
}
+
+ if changed {
+ return afero.WriteFile(s.BaseFs.PublishFs, filename, content, 0o666)
+ }
+
+ return nil
}
- return nil
+ g := rungroup.Run[string](context.Background(), rungroup.Config[string]{
+ NumWorkers: s.h.numWorkers,
+ Handle: func(ctx context.Context, filename string) error {
+ return handleFile(filename)
+ },
+ })
+
+ de.FilenamesWithPostPrefix.ForEeach(func(filename string, _ bool) {
+ g.Enqueue(filename)
+ })
+
+ return g.Wait()
}
// / postRenderOnce runs some post processing that only needs to be done once, e.g. printing of unused templates.
@@ -428,12 +560,6 @@ func (h *HugoSites) postProcess(l logg.LevelLogger) error {
l = l.WithField("step", "postProcess")
defer loggers.TimeTrackf(l, time.Now(), nil, "")
- // Make sure to write any build stats to disk first so it's available
- // to the post processors.
- if err := h.writeBuildStats(); err != nil {
- return err
- }
-
// This will only be set when js.Build have been triggered with
// imports that resolves to the project or a module.
// Write a jsconfig.json file to the project's /asset directory
diff --git a/hugolib/site.go b/hugolib/site.go
index b4b89975d84..145fd82d1cc 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -200,10 +200,6 @@ func (s *Site) prepareInits() {
})
}
-type siteRenderingContext struct {
- output.Format
-}
-
func (s *Site) Menus() navigation.Menus {
s.checkReady()
s.init.menus.Do(context.Background())
@@ -967,12 +963,12 @@ func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bo
return true
}
-func (s *Site) render(ctx *siteRenderContext) (err error) {
+func (s *Site) render(sctx *siteRenderContext) (err error) {
if err := page.Clear(); err != nil {
return err
}
- if ctx.outIdx == 0 {
+ if sctx.outIdx == 0 {
// Note that even if disableAliases is set, the aliases themselves are
// preserved on page. The motivation with this is to be able to generate
// 301 redirects in a .htaccess file and similar using a custom output format.
@@ -987,11 +983,11 @@ func (s *Site) render(ctx *siteRenderContext) (err error) {
}
}
- if err = s.renderPages(ctx); err != nil {
+ if err = s.renderPages(sctx); err != nil {
return
}
- if !ctx.shouldRenderStandalonePage("") {
+ if !sctx.shouldRenderStandalonePage("") {
return
}
diff --git a/hugolib/site_new.go b/hugolib/site_new.go
index 2ba5ef2fb3b..d5ca02044ee 100644
--- a/hugolib/site_new.go
+++ b/hugolib/site_new.go
@@ -88,10 +88,6 @@ type Site struct {
publisher publisher.Publisher
frontmatterHandler pagemeta.FrontMatterHandler
- // We render each site for all the relevant output formats in serial with
- // this rendering context pointing to the current one.
- rc *siteRenderingContext
-
// The output formats that we need to render this site in. This slice
// will be fixed once set.
// This will be the union of Site.Pages' outputFormats.
diff --git a/hugolib/site_render.go b/hugolib/site_render.go
index 1cd509fea96..e07815f30f5 100644
--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -226,7 +226,7 @@ func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error {
paginatePath := s.conf.PaginatePath
d := p.targetPathDescriptor
- f := p.s.rc.Format
+ f := p.outputFormat()
d.Type = f
if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() {
diff --git a/tpl/template.go b/tpl/template.go
index 0ab1abf2f93..7e42e2aa136 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -20,11 +20,13 @@ import (
"reflect"
"regexp"
"strings"
+ "sync"
"unicode"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/output/layouts"
"github.com/gohugoio/hugo/output"
@@ -160,6 +162,11 @@ type TemplateFuncGetter interface {
GetFunc(name string) (reflect.Value, bool)
}
+type RenderingContext struct {
+ Site site
+ SiteOutIdx int
+}
+
type contextKey string
// Context manages values passed in the context to templates.
@@ -191,6 +198,15 @@ type page interface {
IsNode() bool
}
+type site interface {
+ Language() *langs.Language
+}
+
+const (
+ HugoDeferredTemplatePrefix = "___hdeferred/"
+ HugoDeferredTemplateSuffix = "__d="
+)
+
const hugoNewLinePlaceholder = "___hugonl_"
var stripHTMLReplacerPre = strings.NewReplacer("\n", " ", "
", hugoNewLinePlaceholder, "
", hugoNewLinePlaceholder, "
", hugoNewLinePlaceholder)
@@ -228,3 +244,13 @@ func StripHTML(s string) string {
return s
}
+
+type DeferredExecution struct {
+ Mu sync.Mutex
+ Ctx context.Context
+ TemplateName string
+ Data any
+
+ Executed bool
+ Result string
+}
diff --git a/tpl/templates/init.go b/tpl/templates/init.go
index ff6acdabd0b..a64ca602956 100644
--- a/tpl/templates/init.go
+++ b/tpl/templates/init.go
@@ -39,6 +39,16 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.Defer,
+ []string{"defer"},
+ [][2]string{},
+ )
+
+ ns.AddMethodMapping(ctx.DoDefer,
+ []string{"doDefer"},
+ [][2]string{},
+ )
+
return ns
}
diff --git a/tpl/templates/templates.go b/tpl/templates/templates.go
index 8e40f3443a2..55cfb32078a 100644
--- a/tpl/templates/templates.go
+++ b/tpl/templates/templates.go
@@ -15,14 +15,24 @@
package templates
import (
+ "context"
+ "fmt"
+ "strconv"
+ "sync/atomic"
+
"github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/mitchellh/mapstructure"
)
// New returns a new instance of the templates-namespaced template functions.
func New(deps *deps.Deps) *Namespace {
- return &Namespace{
+ ns := &Namespace{
deps: deps,
}
+
+ return ns
}
// Namespace provides template functions for the "templates" namespace.
@@ -36,3 +46,58 @@ type Namespace struct {
func (ns *Namespace) Exists(name string) bool {
return ns.deps.Tmpl().HasTemplate(name)
}
+
+// Defer defers the execution of a template block.
+func (ns *Namespace) Defer(args ...any) (bool, error) {
+ if len(args) != 0 {
+ return false, fmt.Errorf("Defer does not take any arguments")
+ }
+ return true, nil
+}
+
+var defferedIDCounter atomic.Uint64
+
+type DeferOpts struct {
+ // Optional cache key. If set, the deferred block will be executed
+ // once per unique key.
+ Key string
+
+ // Optional data context to use when executing the deferred block.
+ Data any
+}
+
+// DoDefer defers the execution of a template block.
+// For internal use only.
+func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
+ var opts DeferOpts
+ if optsv != nil {
+ if err := mapstructure.WeakDecode(optsv, &opts); err != nil {
+ panic(err)
+ }
+ }
+
+ // TODO1 error: : error calling doDefer: '' expected a map, got 'string'`
+ // TODO1 render: execute of template failed: template: index.html:4:40: executing "___hdeferred/7791e2f5d53ae7dc4f431e48124f30cb" at <$var>: undefined variable: $var
+
+ templateName := id
+ var key string
+ if opts.Key != "" {
+ key = helpers.MD5String(opts.Key)
+ } else {
+ key = strconv.FormatUint(defferedIDCounter.Add(1), 10)
+ }
+
+ id = fmt.Sprintf("%s_%s%s", id, key, tpl.HugoDeferredTemplateSuffix)
+
+ _ = ns.deps.BuildState.DeferredExecutions.Executions.GetOrCreate(id,
+ func() *tpl.DeferredExecution {
+ return &tpl.DeferredExecution{
+ TemplateName: templateName,
+ Ctx: ctx,
+ Data: opts.Data,
+ Executed: false,
+ }
+ })
+
+ return id
+}
diff --git a/tpl/templates/templates_integration_test.go b/tpl/templates/templates_integration_test.go
index 301f783a573..c5176219c4d 100644
--- a/tpl/templates/templates_integration_test.go
+++ b/tpl/templates/templates_integration_test.go
@@ -93,3 +93,47 @@ Home: true
`)
}
+
+func TestDefer(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+[languages]
+[languages.en]
+weight = 1
+[languages.nn]
+weight = 2
+-- i18n/en.toml --
+[hello]
+other = "Hello"
+-- i18n/nn.toml --
+[hello]
+other = "Hei"
+-- content/_index.en.md --
+---
+title: "Home"
+outputs: ["html", "amp"]
+---
+-- content/_index.nn.md --
+---
+title: "Heim"
+outputs: ["html", "amp"]
+---
+-- layouts/index.html --
+HTML.
+{{ $data := dict "page" . }}
+{{ with (defer (dict "data" $data) ) }}Title: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}{{ end }}$
+-- layouts/index.amp.html --
+AMP.
+{{ $data := dict "page" . }}
+{{ with (defer (dict "data" $data) ) }}Title: {{ .page.Title }}|{{ .page.RelPermalink }}|Hello: {{ T "hello" }}{{ end }}$
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "Title: Home|/|Hello: Hello")
+ b.AssertFileContent("public/amp/index.html", "Title: Home|/amp/|Hello: Hello")
+ b.AssertFileContent("public/nn/index.html", "Title: Heim|/nn/|Hello: Hei")
+ b.AssertFileContent("public/nn/amp/index.html", "Title: Heim|/nn/amp/|Hello: Hei")
+}
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 63dc29662e4..b74a6bf1536 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -42,6 +42,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -745,6 +746,11 @@ func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *t
t.transformNotFound[k] = ts
}
+ for k, v := range c.deferNodes {
+ if err := t.main.fromListNode(k, ts.isText(), v); err != nil {
+ return nil, err
+ }
+ }
return c, err
}
@@ -1002,6 +1008,35 @@ func (t *templateNamespace) newTemplateLookup(in *templateState) func(name strin
}
}
+func (t *templateNamespace) fromListNode(name string, isText bool, n *parse.ListNode) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ var templ tpl.Template
+
+ if isText {
+ prototype := t.prototypeText
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return err
+ }
+ tt.Tree.Root = n
+ templ = tt
+ } else {
+ prototype := t.prototypeHTML
+ tt, err := prototype.New(name).Parse("")
+ if err != nil {
+ return err
+ }
+ tt.Tree.Root = n
+ templ = tt
+ }
+
+ t.templates[name] = newTemplateState(templ, templateInfo{name: name}, nil)
+
+ return nil
+}
+
func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
t.mu.Lock()
defer t.mu.Unlock()
diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go
index 92558a9037b..6dc64f64ae7 100644
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/template_ast_transformers.go
@@ -17,6 +17,7 @@ import (
"errors"
"fmt"
+ "github.com/gohugoio/hugo/helpers"
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -38,6 +39,7 @@ const (
type templateContext struct {
visited map[string]bool
templateNotFound map[string]bool
+ deferNodes map[string]*parse.ListNode
lookupFn func(name string) *templateState
// The last error encountered.
@@ -77,6 +79,7 @@ func newTemplateContext(
lookupFn: lookupFn,
visited: make(map[string]bool),
templateNotFound: make(map[string]bool),
+ deferNodes: make(map[string]*parse.ListNode),
}
}
@@ -116,9 +119,14 @@ const (
// "range" over a one-element slice so we can shift dot to the
// partial's argument, Arg, while allowing Arg to be falsy.
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
+
+ doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
)
-var partialReturnWrapper *parse.ListNode
+var (
+ partialReturnWrapper *parse.ListNode
+ doDefer *parse.ListNode
+)
func init() {
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
@@ -126,6 +134,12 @@ func init() {
panic(err)
}
partialReturnWrapper = templ.Tree.Root
+
+ templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
+ if err != nil {
+ panic(err)
+ }
+ doDefer = templ.Tree.Root
}
// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
@@ -158,6 +172,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
case *parse.IfNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.WithNode:
+ c.handleDefer(x)
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.RangeNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
@@ -191,6 +206,52 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
return true, c.err
}
+func (c *templateContext) handleDefer(withNode *parse.WithNode) {
+ if len(withNode.Pipe.Cmds) != 1 {
+ return
+ }
+ cmd := withNode.Pipe.Cmds[0]
+ if len(cmd.Args) != 1 {
+ return
+ }
+ idArg := cmd.Args[0]
+
+ p, ok := idArg.(*parse.PipeNode)
+ if !ok {
+ return
+ }
+
+ if len(p.Cmds) != 1 {
+ return
+ }
+
+ cmd = p.Cmds[0]
+ if len(cmd.Args) != 2 {
+ return
+ }
+
+ idArg = cmd.Args[0]
+
+ id, ok := idArg.(*parse.IdentifierNode)
+ if !ok || id.Ident != "defer" {
+ return
+ }
+ deferArg := cmd.Args[1]
+ cmd.Args = []parse.Node{idArg}
+
+ l := doDefer.CopyList()
+ n := l.Nodes[0].(*parse.ActionNode)
+
+ inner := withNode.List
+ innerHash := helpers.MD5String(inner.String())
+ deferredID := tpl.HugoDeferredTemplatePrefix + innerHash
+ c.deferNodes[tpl.HugoDeferredTemplatePrefix+innerHash] = inner
+ withNode.List = l
+
+ n.Pipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode).Text = deferredID
+ n.Pipe.Cmds[0].Args[2] = deferArg
+}
+
func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
for _, node := range nodes {
c.applyTransformations(node)