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")
import (
"io"
+ "iter"
"regexp"
"strconv"
"strings"
"_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
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)
}
} 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)
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
}
/*
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
}
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")
+ }
+}
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),
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 {
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
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
}
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 {
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
}
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 {
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(
fi hugofs.FileMetaInfo,
key string,
category Category,
+ subCategory SubCategory,
d TemplateDescriptor,
replace, isLegacyMapped bool,
tree doctree.Tree[map[nodeKey]*TemplInfo],
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.
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
}
- if replace && pi.NameNoIdentifier() == baseNameBaseof {
+ if partialRebuild && pi.NameNoIdentifier() == baseNameBaseof {
// A baseof file has changed.
resetBaseVariants = true
}
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
}
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
}
if err := s.tns.readTemplateInto(ti); err != nil {
return err
}
+
}
if resetBaseVariants {
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() {
if vv.state == processingStateTransformed {
continue
}
- if err := s.parseTemplate(vv); err != nil {
+ if err := s.parseTemplate(vv, replace); err != nil {
return err
}
}
// 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
if vvv.state == processingStateTransformed {
continue
}
- if err := s.parseTemplate(vvv); err != nil {
+ if err := s.parseTemplate(vvv, replace); err != nil {
return err
}
}