]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix live reload when editing inline partials
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 21 May 2025 09:25:32 +0000 (11:25 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 22 May 2025 11:15:41 +0000 (13:15 +0200)
Fixes #13723

hugolib/rebuild_test.go
tpl/internal/go_templates/htmltemplate/hugo_template.go
tpl/internal/go_templates/texttemplate/hugo_template.go
tpl/internal/go_templates/texttemplate/parse/parse.go
tpl/tplimpl/templates.go
tpl/tplimpl/templatestore.go

index f5c0befcc0d3cf21e63a230c73c93fea1309bc20..0b22b9aa7c627bd3a55a8dd5cb9823be44701e6b 100644 (file)
@@ -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")
index cb7d0dc25065529b6235d26067133cda7ec86fc0..d91baac7059c9ca74cafe113ff9ae03bd5ca22cf 100644 (file)
@@ -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)
index d179cb8c92db4274b4557d48426a1e5ed4bc7188..4f505d8c55e818cfa1bf0a3fe82c67899a565f2f 100644 (file)
@@ -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
+                       }
+               }
+       }
+}
index 27c84f31eb3a9b423a4a7c26dac6aed59b6271b2..e05a33d6fddac7ca8730a0f9565981e4493aec70 100644 (file)
@@ -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",
index 19de48e3801af6d2e9cd1ceb107fd202563a4c44..7aeb7e2b94bde6f5fe90f1254136828eabf543f8 100644 (file)
@@ -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")
+       }
+}
index 2ea33727438dbd02ce90e4a1081dec919cadbc73..bbb7f27cc57a63f18056794070a5b3d4d6d60f46 100644 (file)
@@ -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],
@@ -1160,6 +1185,11 @@ func (s *TemplateStore) insertTemplate2(
                replace = fi.Meta().ModuleOrdinal < nkExisting.Fi.Meta().ModuleOrdinal
        }
 
+       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.
@@ -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
                                }
                        }