diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go index f5c0befcc..0b22b9aa7 100644 --- 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") diff --git a/tpl/internal/go_templates/htmltemplate/hugo_template.go b/tpl/internal/go_templates/htmltemplate/hugo_template.go index cb7d0dc25..d91baac70 100644 --- 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) diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go index d179cb8c9..4f505d8c5 100644 --- 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 + } + } + } +} diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go index 27c84f31e..e05a33d6f 100644 --- 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", diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go index 19de48e38..7aeb7e2b9 100644 --- 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) +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 + } + } } } - if htmlt, ok := in.(*htmltemplate.Template); ok { - for _, t := range htmlt.Templates() { - templs = append(templs, t) - } - } - 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") + } +} diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index 2ea337274..bbb7f27cc 100644 --- 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,59 +946,77 @@ 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 } - 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 { + 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 } - } - for _, t := range p.baseofTextClones { - if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil { - return err + + if ti != nil { + ti.Template = templ + ti.noBaseOf = true + ti.subCategory = SubCategoryInline + ti.D.IsPlainText = isText(templ) } } + 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.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 + } + } + } + } +} + func (s *TemplateStore) insertEmbedded() error { return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -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 } }