Fix live reload when editing inline partials - hugo - [fork] hugo port for 9front
(HTM) git clone git@git.drkhsh.at/hugo.git
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) Submodules
(DIR) README
(DIR) LICENSE
---
(DIR) commit 0c7b1a3f2679755b0fde3a230e15471f404b3aa4
(DIR) parent 970b887ba1bba83ca843b9ece16dbf01fa1a022d
(HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date: Wed, 21 May 2025 11:25:32 +0200
Fix live reload when editing inline partials
Fixes #13723
Diffstat:
M hugolib/rebuild_test.go | 54 +++++++++++++++++++++++++++++++
M tpl/internal/go_templates/htmltemp… | 14 ++++++++++++++
M tpl/internal/go_templates/texttemp… | 16 ++++++++++++++++
M tpl/internal/go_templates/texttemp… | 2 +-
M tpl/tplimpl/templates.go | 52 ++++++++++++++++++++-----------
M tpl/tplimpl/templatestore.go | 177 +++++++++++++++++++------------
6 files changed, 226 insertions(+), 89 deletions(-)
---
(DIR) diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go
@@ -1766,6 +1766,60 @@ MyTemplate: {{ partial "MyTemplate.html" . }}|
b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate Edited")
}
+func TestRebuildEditInlinePartial13723(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableLiveReload = true
+title = "Foo"
+-- layouts/baseof.html --
+{{ block "main" . }}Main.{{ end }}
+{{ partial "myinlinepartialinbaseof.html" . }}|
+ {{- define "_partials/myinlinepartialinbaseof.html" }}
+ My inline partial in baseof.
+ {{ end }}
+-- layouts/_partials/mypartial.html --
+Mypartial.
+{{ partial "myinlinepartial.html" . }}|
+{{- define "_partials/myinlinepartial.html" }}
+Mypartial Inline.|{{ .Title }}|
+{{ end }}
+-- layouts/_partials/myotherpartial.html --
+Myotherpartial.
+{{ partial "myotherinlinepartial.html" . }}|
+{{- define "_partials/myotherinlinepartial.html" }}
+Myotherpartial Inline.|{{ .Title }}|
+{{ return "myotherinlinepartial" }}
+{{ end }}
+-- layouts/all.html --
+{{ define "main" }}
+{{ partial "mypartial.html" . }}|
+{{ partial "myotherpartial.html" . }}|
+ {{ partial "myinlinepartialinall.html" . }}|
+{{ end }}
+ {{- define "_partials/myinlinepartialinall.html" }}
+ My inline partial in all.
+ {{ end }}
+
+`
+ b := TestRunning(t, files)
+ b.AssertFileContent("public/index.html", "Mypartial.", "Mypartial Inline.|Foo")
+
+ // Edit inline partial in partial.
+ b.EditFileReplaceAll("layouts/_partials/mypartial.html", "Mypartial Inline.", "Mypartial Inline Edited.").Build()
+ b.AssertFileContent("public/index.html", "Mypartial Inline Edited.|Foo")
+
+ // Edit inline partial in baseof.
+ b.EditFileReplaceAll("layouts/baseof.html", "My inline partial in baseof.", "My inline partial in baseof Edited.").Build()
+ b.AssertFileContent("public/index.html", "My inline partial in baseof Edited.")
+
+ // Edit inline partial in all.
+ b.EditFileReplaceAll("layouts/all.html", "My inline partial in all.", "My inline partial in all Edited.").Build()
+ b.AssertFileContent("public/index.html", "My inline partial in all Edited.")
+}
+
func TestRebuildEditAsciidocContentFile(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("skip asciidoc")
(DIR) diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go
@@ -15,6 +15,7 @@ package template
import (
"fmt"
+ "iter"
"github.com/gohugoio/hugo/common/types"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
@@ -38,6 +39,19 @@ func (t *Template) Prepare() (*template.Template, error) {
return t.text, nil
}
+func (t *Template) All() iter.Seq[*Template] {
+ return func(yield func(t *Template) bool) {
+ ns := t.nameSpace
+ ns.mu.Lock()
+ defer ns.mu.Unlock()
+ for _, v := range ns.set {
+ if !yield(v) {
+ return
+ }
+ }
+ }
+}
+
// See https://github.com/golang/go/issues/5884
func StripTags(html string) string {
return stripTags(html)
(DIR) diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -17,6 +17,7 @@ import (
"context"
"fmt"
"io"
+ "iter"
"reflect"
"github.com/gohugoio/hugo/common/herrors"
@@ -433,3 +434,18 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
func isTrue(val reflect.Value) (truth, ok bool) {
return hreflect.IsTruthfulValue(val), true
}
+
+func (t *Template) All() iter.Seq[*Template] {
+ return func(yield func(t *Template) bool) {
+ if t.common == nil {
+ return
+ }
+ t.muTmpl.RLock()
+ defer t.muTmpl.RUnlock()
+ for _, v := range t.tmpl {
+ if !yield(v) {
+ return
+ }
+ }
+ }
+}
(DIR) diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go
@@ -533,7 +533,7 @@ func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode,
t.rangeDepth--
}
switch next.Type() {
- case nodeEnd: //done
+ case nodeEnd: // done
case nodeElse:
// Special case for "else if" and "else with".
// If the "else" is followed immediately by an "if" or "with",
(DIR) diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go
@@ -2,6 +2,7 @@ package tplimpl
import (
"io"
+ "iter"
"regexp"
"strconv"
"strings"
@@ -44,16 +45,15 @@ var embeddedTemplatesAliases = map[string][]string{
"_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
}
-func (s *TemplateStore) parseTemplate(ti *TemplInfo) error {
- err := s.tns.doParseTemplate(ti)
+func (s *TemplateStore) parseTemplate(ti *TemplInfo, replace bool) error {
+ err := s.tns.doParseTemplate(ti, replace)
if err != nil {
return s.addFileContext(ti, "parse of template failed", err)
}
-
return err
}
-func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error {
+func (t *templateNamespace) doParseTemplate(ti *TemplInfo, replace bool) error {
if !ti.noBaseOf || ti.category == CategoryBaseof {
// Delay parsing until we have the base template.
return nil
@@ -68,7 +68,7 @@ func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error {
if ti.D.IsPlainText {
prototype := t.parseText
- if prototype.Lookup(name) != nil {
+ if !replace && prototype.Lookup(name) != nil {
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
}
templ, err = prototype.New(name).Parse(ti.content)
@@ -77,7 +77,7 @@ func (t *templateNamespace) doParseTemplate(ti *TemplInfo) error {
}
} else {
prototype := t.parseHTML
- if prototype.Lookup(name) != nil {
+ if !replace && prototype.Lookup(name) != nil {
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
}
templ, err = prototype.New(name).Parse(ti.content)
@@ -181,19 +181,24 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla
return nil
}
-func (t *templateNamespace) templatesIn(in tpl.Template) []tpl.Template {
- var templs []tpl.Template
- if textt, ok := in.(*texttemplate.Template); ok {
- for _, t := range textt.Templates() {
- templs = append(templs, t)
- }
- }
- if htmlt, ok := in.(*htmltemplate.Template); ok {
- for _, t := range htmlt.Templates() {
- templs = append(templs, t)
+func (t *templateNamespace) templatesIn(in tpl.Template) iter.Seq[tpl.Template] {
+ return func(yield func(t tpl.Template) bool) {
+ switch in := in.(type) {
+ case *htmltemplate.Template:
+ for t := range in.All() {
+ if !yield(t) {
+ return
+ }
+ }
+
+ case *texttemplate.Template:
+ for t := range in.All() {
+ if !yield(t) {
+ return
+ }
+ }
}
}
- return templs
}
/*
@@ -337,8 +342,6 @@ func (t *templateNamespace) createPrototypes(init bool) error {
t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
t.prototypeText = texttemplate.Must(t.parseText.Clone())
}
- // t.execHTML = htmltemplate.Must(t.parseHTML.Clone())
- // t.execText = texttemplate.Must(t.parseText.Clone())
return nil
}
@@ -350,3 +353,14 @@ func newTemplateNamespace(funcs map[string]any) *templateNamespace {
standaloneText: texttemplate.New("").Funcs(funcs),
}
}
+
+func isText(t tpl.Template) bool {
+ switch t.(type) {
+ case *texttemplate.Template:
+ return true
+ case *htmltemplate.Template:
+ return false
+ default:
+ panic("unknown template type")
+ }
+}
(DIR) diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
@@ -114,17 +114,18 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
panic("HTML output format not found")
}
s := &TemplateStore{
- opts: opts,
- siteOpts: siteOpts,
- optsOrig: opts,
- siteOptsOrig: siteOpts,
- htmlFormat: html,
- storeSite: configureSiteStorage(siteOpts, opts.Watching),
- treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
- treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
- templatesByPath: maps.NewCache[string, *TemplInfo](),
- shortcodesByName: maps.NewCache[string, *TemplInfo](),
- cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
+ opts: opts,
+ siteOpts: siteOpts,
+ optsOrig: opts,
+ siteOptsOrig: siteOpts,
+ htmlFormat: html,
+ storeSite: configureSiteStorage(siteOpts, opts.Watching),
+ treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
+ treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
+ templatesByPath: maps.NewCache[string, *TemplInfo](),
+ shortcodesByName: maps.NewCache[string, *TemplInfo](),
+ cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
+ templatesSnapshotSet: maps.NewCache[*parse.Tree, struct{}](),
// Note that the funcs passed below is just for name validation.
tns: newTemplateNamespace(siteOpts.TemplateFuncs),
@@ -143,10 +144,10 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
if err := s.insertEmbedded(); err != nil {
return nil, err
}
- if err := s.parseTemplates(); err != nil {
+ if err := s.parseTemplates(false); err != nil {
return nil, err
}
- if err := s.extractInlinePartials(); err != nil {
+ if err := s.extractInlinePartials(false); err != nil {
return nil, err
}
if err := s.transformTemplates(); err != nil {
@@ -424,10 +425,11 @@ type TemplateStore struct {
siteOpts SiteOptions
htmlFormat output.Format
- treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
- treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
- templatesByPath *maps.Cache[string, *TemplInfo]
- shortcodesByName *maps.Cache[string, *TemplInfo]
+ treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
+ treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
+ templatesByPath *maps.Cache[string, *TemplInfo]
+ shortcodesByName *maps.Cache[string, *TemplInfo]
+ templatesSnapshotSet *maps.Cache[*parse.Tree, struct{}]
dh descriptorHandler
@@ -709,12 +711,16 @@ func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool)
if err := s.insertTemplates(include, true); err != nil {
return err
}
- if err := s.parseTemplates(); err != nil {
+ if err := s.createTemplatesSnapshot(); err != nil {
return err
}
- if err := s.extractInlinePartials(); err != nil {
+ if err := s.parseTemplates(true); err != nil {
return err
}
+ if err := s.extractInlinePartials(true); err != nil {
+ return err
+ }
+
if err := s.transformTemplates(); err != nil {
return err
}
@@ -940,57 +946,75 @@ func (s *TemplateStore) extractIdentifiers(line string) []string {
return identifiers
}
-func (s *TemplateStore) extractInlinePartials() error {
+func (s *TemplateStore) extractInlinePartials(rebuild bool) error {
isPartialName := func(s string) bool {
return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
}
- p := s.tns
// We may find both inline and external partials in the current template namespaces,
// so only add the ones we have not seen before.
- addIfNotSeen := func(isText bool, templs ...tpl.Template) error {
- for _, templ := range templs {
- if templ.Name() == "" || !isPartialName(templ.Name()) {
- continue
- }
- name := templ.Name()
- if !paths.HasExt(name) {
- // Assume HTML. This in line with how the lookup works.
- name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
- }
- if !strings.HasPrefix(name, "_") {
- name = "_" + name
- }
- pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
- ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
- if err != nil {
- return err
- }
-
- if ti != nil {
- ti.Template = templ
- ti.noBaseOf = true
- ti.subCategory = SubCategoryInline
- ti.D.IsPlainText = isText
- }
+ for templ := range s.allRawTemplates() {
+ if templ.Name() == "" || !isPartialName(templ.Name()) {
+ continue
+ }
+ if rebuild && s.templatesSnapshotSet.Contains(getParseTree(templ)) {
+ // This partial was not created during this build.
+ continue
+ }
+ name := templ.Name()
+ if !paths.HasExt(name) {
+ // Assume HTML. This in line with how the lookup works.
+ name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
+ }
+ if !strings.HasPrefix(name, "_") {
+ name = "_" + name
+ }
+ pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
+ ti, err := s.insertTemplate(pi, nil, SubCategoryInline, false, s.treeMain)
+ if err != nil {
+ return err
+ }
+ if ti != nil {
+ ti.Template = templ
+ ti.noBaseOf = true
+ ti.subCategory = SubCategoryInline
+ ti.D.IsPlainText = isText(templ)
}
- return nil
}
- addIfNotSeen(false, p.templatesIn(p.parseHTML)...)
- addIfNotSeen(true, p.templatesIn(p.parseText)...)
- for _, t := range p.baseofHtmlClones {
- if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil {
- return err
+ return nil
+}
+
+func (s *TemplateStore) allRawTemplates() iter.Seq[tpl.Template] {
+ p := s.tns
+ return func(yield func(tpl.Template) bool) {
+ for t := range p.templatesIn(p.parseHTML) {
+ if !yield(t) {
+ return
+ }
}
- }
- for _, t := range p.baseofTextClones {
- if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil {
- return err
+ for t := range p.templatesIn(p.parseText) {
+ if !yield(t) {
+ return
+ }
+ }
+
+ for _, tt := range p.baseofHtmlClones {
+ for t := range p.templatesIn(tt) {
+ if !yield(t) {
+ return
+ }
+ }
+ }
+ for _, tt := range p.baseofTextClones {
+ for t := range p.templatesIn(tt) {
+ if !yield(t) {
+ return
+ }
+ }
}
}
- return nil
}
func (s *TemplateStore) insertEmbedded() error {
@@ -1024,7 +1048,7 @@ func (s *TemplateStore) insertEmbedded() error {
return err
}
} else {
- ti, err = s.insertTemplate(pi, nil, false, s.treeMain)
+ ti, err = s.insertTemplate(pi, nil, SubCategoryEmbedded, false, s.treeMain)
if err != nil {
return err
}
@@ -1105,7 +1129,7 @@ func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo,
return ti, nil
}
-func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
+func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, subCategory SubCategory, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
key, _, category, d, err := s.toKeyCategoryAndDescriptor(pi)
// See #13577. Warn for now.
if err != nil {
@@ -1119,7 +1143,7 @@ func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, r
return nil, nil
}
- return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree)
+ return s.insertTemplate2(pi, fi, key, category, subCategory, d, replace, false, tree)
}
func (s *TemplateStore) insertTemplate2(
@@ -1127,6 +1151,7 @@ func (s *TemplateStore) insertTemplate2(
fi hugofs.FileMetaInfo,
key string,
category Category,
+ subCategory SubCategory,
d TemplateDescriptor,
replace, isLegacyMapped bool,
tree doctree.Tree[map[nodeKey]*TemplInfo],
@@ -1161,6 +1186,11 @@ func (s *TemplateStore) insertTemplate2(
}
if !replace && existingFound {
+ // Always replace inline partials to allow for reloading.
+ replace = subCategory == SubCategoryInline && nkExisting.subCategory == SubCategoryInline
+ }
+
+ if !replace && existingFound {
if len(pi.Identifiers()) >= len(nkExisting.PathInfo.Identifiers()) {
// e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site.
return nil, nil
@@ -1190,7 +1220,7 @@ func (s *TemplateStore) insertTemplate2(
return ti, nil
}
-func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error {
+func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, partialRebuild bool) error {
if include == nil {
include = func(fi hugofs.FileMetaInfo) bool {
return true
@@ -1372,7 +1402,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
}
- if replace && pi.NameNoIdentifier() == baseNameBaseof {
+ if partialRebuild && pi.NameNoIdentifier() == baseNameBaseof {
// A baseof file has changed.
resetBaseVariants = true
}
@@ -1380,12 +1410,12 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
var ti *TemplInfo
var err error
if pi.Type() == paths.TypeShortcode {
- ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes)
+ ti, err = s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes)
if err != nil || ti == nil {
return err
}
} else {
- ti, err = s.insertTemplate(pi, fi, replace, s.treeMain)
+ ti, err = s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain)
if err != nil || ti == nil {
return err
}
@@ -1419,7 +1449,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
desc.IsPlainText = outputFormat.IsPlainText
desc.MediaType = mediaType.Type
- ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain)
+ ti, err := s.insertTemplate2(pi, fi, targetPath, category, SubCategoryMain, desc, true, true, s.treeMain)
if err != nil {
return err
}
@@ -1430,6 +1460,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
if err := s.tns.readTemplateInto(ti); err != nil {
return err
}
+
}
if resetBaseVariants {
@@ -1456,7 +1487,15 @@ func (s *TemplateStore) key(dir string) string {
return paths.TrimTrailing(dir)
}
-func (s *TemplateStore) parseTemplates() error {
+func (s *TemplateStore) createTemplatesSnapshot() error {
+ s.templatesSnapshotSet.Reset()
+ for t := range s.allRawTemplates() {
+ s.templatesSnapshotSet.Set(getParseTree(t), struct{}{})
+ }
+ return nil
+}
+
+func (s *TemplateStore) parseTemplates(replace bool) error {
if err := func() error {
// Read and parse all templates.
for _, v := range s.treeMain.All() {
@@ -1464,7 +1503,7 @@ func (s *TemplateStore) parseTemplates() error {
if vv.state == processingStateTransformed {
continue
}
- if err := s.parseTemplate(vv); err != nil {
+ if err := s.parseTemplate(vv, replace); err != nil {
return err
}
}
@@ -1484,7 +1523,7 @@ func (s *TemplateStore) parseTemplates() error {
// The regular expression used to detect if a template needs a base template has some
// rare false positives. Assume we don't need one.
vv.noBaseOf = true
- if err := s.parseTemplate(vv); err != nil {
+ if err := s.parseTemplate(vv, replace); err != nil {
return err
}
continue
@@ -1513,7 +1552,7 @@ func (s *TemplateStore) parseTemplates() error {
if vvv.state == processingStateTransformed {
continue
}
- if err := s.parseTemplate(vvv); err != nil {
+ if err := s.parseTemplate(vvv, replace); err != nil {
return err
}
}