summaryrefslogtreecommitdiffstats
path: root/tpl
diff options
context:
space:
mode:
Diffstat (limited to 'tpl')
-rw-r--r--tpl/internal/go_templates/htmltemplate/hugo_template.go14
-rw-r--r--tpl/internal/go_templates/texttemplate/hugo_template.go16
-rw-r--r--tpl/internal/go_templates/texttemplate/parse/parse.go2
-rw-r--r--tpl/partials/partials.go2
-rw-r--r--tpl/tplimpl/embedded/templates/_shortcodes/twitter.html2
-rw-r--r--tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html2
-rw-r--r--tpl/tplimpl/embedded/templates/_shortcodes/x.html2
-rw-r--r--tpl/tplimpl/shortcodes_integration_test.go99
-rw-r--r--tpl/tplimpl/templatedescriptor.go21
-rw-r--r--tpl/tplimpl/templates.go52
-rw-r--r--tpl/tplimpl/templatestore.go244
-rw-r--r--tpl/tplimpl/templatestore_integration_test.go53
-rw-r--r--tpl/transform/transform.go46
-rw-r--r--tpl/transform/transform_integration_test.go40
14 files changed, 474 insertions, 121 deletions
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/partials/partials.go b/tpl/partials/partials.go
index 19882e36a..57a2aa280 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -133,7 +133,7 @@ func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) {
if strings.HasPrefix(name, "partials/") {
// This is most likely not what the user intended.
// This worked before Hugo 0.146.0.
- ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
+ ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Doubtful use of partial function in {{ partial \"%s\"}}), this is most likely not what you want. Consider removing superfluous prefix \"partials/\" from template name given as first function argument.", name)
}
v := ns.deps.TemplateStore.LookupPartial(name)
if v == nil {
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
index ce356559d..849bad99e 100644
--- a/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/twitter.html
@@ -2,7 +2,7 @@
{{- $pc := site.Config.Privacy.Twitter -}}
{{- if not $pc.Disable -}}
{{- if $pc.Simple -}}
- {{- template "_internal/shortcodes/twitter_simple.html" . -}}
+ {{- template "_shortcodes/twitter_simple.html" . -}}
{{- else -}}
{{- $id := or (.Get "id") "" -}}
{{- $user := or (.Get "user") "" -}}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
index 2588ac86c..fb8ea0d97 100644
--- a/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html
@@ -18,7 +18,7 @@ title, then loading.
{{- $pc := site.Config.Privacy.Vimeo }}
{{- if not $pc.Disable }}
{{- if $pc.Simple }}
- {{- template "_internal/shortcodes/vimeo_simple.html" . }}
+ {{- template "_shortcodes/vimeo_simple.html" . }}
{{- else }}
{{- $dnt := cond $pc.EnableDNT 1 0 }}
diff --git a/tpl/tplimpl/embedded/templates/_shortcodes/x.html b/tpl/tplimpl/embedded/templates/_shortcodes/x.html
index 28a5e331b..87455530c 100644
--- a/tpl/tplimpl/embedded/templates/_shortcodes/x.html
+++ b/tpl/tplimpl/embedded/templates/_shortcodes/x.html
@@ -1,7 +1,7 @@
{{- $pc := site.Config.Privacy.X -}}
{{- if not $pc.Disable -}}
{{- if $pc.Simple -}}
- {{- template "_internal/shortcodes/x_simple.html" . -}}
+ {{- template "_shortcodes/x_simple.html" . -}}
{{- else -}}
{{- $id := or (.Get "id") "" -}}
{{- $user := or (.Get "user") "" -}}
diff --git a/tpl/tplimpl/shortcodes_integration_test.go b/tpl/tplimpl/shortcodes_integration_test.go
index 665760dec..86f6007ca 100644
--- a/tpl/tplimpl/shortcodes_integration_test.go
+++ b/tpl/tplimpl/shortcodes_integration_test.go
@@ -17,6 +17,7 @@ import (
"strings"
"testing"
+ qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
"github.com/gohugoio/hugo/hugolib"
)
@@ -460,7 +461,6 @@ title: p1 (de)
}
func TestVimeoShortcode(t *testing.T) {
- t.Skip("Fix me: Upstream API changes")
t.Parallel()
files := `
@@ -697,3 +697,100 @@ title: p2
b.AssertFileContent("public/p1/index.html", "78eb19b5c6f3768f")
b.AssertFileContent("public/p2/index.html", "a6db910a9cf54bc1")
}
+
+func TestShortcodePlainTextVsHTMLTemplateIssue13698(t *testing.T) {
+ t.Parallel()
+
+ filesTemplate := `
+-- hugo.toml --
+markup.goldmark.renderer.unsafe = true
+-- layouts/all.html --
+Content: {{ .Content }}|
+-- layouts/_shortcodes/mymarkdown.md --
+<div>Foo bar</div>
+-- content/p1.md --
+---
+title: p1
+---
+## A shortcode
+
+SHORTCODE
+
+`
+
+ files := strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{% mymarkdown %}}")
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html", "<div>Foo bar</div>")
+
+ files = strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{< mymarkdown >}}")
+
+ var err error
+ b, err = hugolib.TestE(t, files)
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err.Error(), qt.Contains, `no compatible template found for shortcode "mymarkdown" in [/_shortcodes/mymarkdown.md]; note that to use plain text template shortcodes in HTML you need to use the shortcode {{% delimiter`)
+}
+
+func TestShortcodeOnlyLanguageInBaseIssue13699And13740(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = 'https://example.org/'
+disableLanguages = ['de']
+[languages]
+[languages.en]
+weight = 1
+[languages.de]
+weight = 2
+-- layouts/_shortcodes/de.html --
+de.html
+-- layouts/all.html --
+{{ .Content }}
+-- content/_index.md --
+---
+title: home
+---
+{{< de >}}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "de.html")
+}
+
+func TestShortcodeLanguage13767(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+defaultContentLanguage = 'pl'
+defaultContentLanguageInSubdir = true
+[languages.pl]
+weight = 1
+[languages.en]
+weight = 2
+-- content/_index.md --
+---
+title: dom
+---
+{{< myshortcode >}}
+-- content/_index.en.md --
+---
+title: home
+---
+{{< myshortcode >}}
+-- layouts/_shortcodes/myshortcode.html --
+myshortcode.html
+-- layouts/_shortcodes/myshortcode.en.html --
+myshortcode.en.html
+-- layouts/all.html --
+{{ .Content }}
+
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/pl/index.html", "myshortcode.html")
+ b.AssertFileContent("public/en/index.html", "myshortcode.en.html")
+}
diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go
index ea47afc88..fd86f15fa 100644
--- a/tpl/tplimpl/templatedescriptor.go
+++ b/tpl/tplimpl/templatedescriptor.go
@@ -37,6 +37,7 @@ type TemplateDescriptor struct {
// Misc.
LayoutFromUserMustMatch bool // If set, we only look for the exact layout.
IsPlainText bool // Whether this is a plain text template.
+ AlwaysAllowPlainText bool // Whether to e.g. allow plain text templates to be rendered in HTML.
}
func (d *TemplateDescriptor) normalizeFromFile() {
@@ -64,7 +65,7 @@ func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool
return weightNoMatch
}
- w := this.doCompare(category, isEmbedded, s.opts.DefaultContentLanguage, other)
+ w := this.doCompare(category, s.opts.DefaultContentLanguage, other)
if w.w1 <= 0 {
if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
@@ -74,7 +75,12 @@ func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool
}
w.w1 = 1
- return w
+ }
+
+ if category == CategoryShortcode {
+ if (this.IsPlainText == other.IsPlainText || !other.IsPlainText) || this.AlwaysAllowPlainText {
+ w.w1 = 1
+ }
}
}
@@ -82,13 +88,16 @@ func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool
}
//lint:ignore ST1006 this vs other makes it easier to reason about.
-func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, defaultContentLanguage string, other TemplateDescriptor) weight {
+func (this TemplateDescriptor) doCompare(category Category, defaultContentLanguage string, other TemplateDescriptor) weight {
w := weightNoMatch
- // HTML in plain text is OK, but not the other way around.
- if other.IsPlainText && !this.IsPlainText {
- return w
+ if !this.AlwaysAllowPlainText {
+ // HTML in plain text is OK, but not the other way around.
+ if other.IsPlainText && !this.IsPlainText {
+ return w
+ }
}
+
if other.Kind != "" && other.Kind != this.Kind {
return w
}
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)
- }
- }
- 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")
+ }
+}
diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
index 53880eb33..bbb7f27cc 100644
--- a/tpl/tplimpl/templatestore.go
+++ b/tpl/tplimpl/templatestore.go
@@ -19,6 +19,7 @@ import (
"bytes"
"context"
"embed"
+ "errors"
"fmt"
"io"
"io/fs"
@@ -113,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),
@@ -142,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 {
@@ -423,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
@@ -608,7 +611,7 @@ func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo {
return ti
}
-func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
+func (s *TemplateStore) LookupShortcode(q TemplateQuery) (*TemplInfo, error) {
q.init()
k1 := s.key(q.Path)
@@ -630,13 +633,15 @@ func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
}
for k, vv := range v {
+ best.candidates = append(best.candidates, vv)
if !q.Consider(vv) {
continue
}
weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k)
weight.distance = distance
- if best.isBetter(weight, vv) {
+ isBetter := best.isBetter(weight, vv)
+ if isBetter {
best.updateValues(weight, k2, k, vv)
}
}
@@ -644,8 +649,21 @@ func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
return false, nil
})
- // Any match will do.
- return best.templ
+ if best.w.w1 <= 0 {
+ var err error
+ if s := best.candidatesAsStringSlice(); s != nil {
+ msg := fmt.Sprintf("no compatible template found for shortcode %q in %s", q.Name, s)
+ if !q.Desc.IsPlainText {
+ msg += "; note that to use plain text template shortcodes in HTML you need to use the shortcode {{% delimiter"
+ }
+ err = errors.New(msg)
+ } else {
+ err = fmt.Errorf("no template found for shortcode %q", q.Name)
+ }
+ return nil, err
+ }
+
+ return best.templ, nil
}
// PrintDebug is for testing/debugging only.
@@ -693,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
}
@@ -924,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 {
@@ -1008,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
}
@@ -1089,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 {
@@ -1103,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(
@@ -1111,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],
@@ -1133,12 +1174,26 @@ func (s *TemplateStore) insertTemplate2(
tree.Insert(key, m)
}
- if !replace {
- if v, found := m[nk]; found {
- if len(pi.Identifiers()) >= len(v.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
- }
+ nkExisting, existingFound := m[nk]
+ if !replace && existingFound && fi != nil && nkExisting.Fi != nil {
+ // See issue #13715.
+ // We do the merge on the file system level, but from Hugo v0.146.0 we have a situation where
+ // the project may well have a different layouts layout compared to the theme(s) it uses.
+ // We could possibly have fixed that on a lower (file system) level, but since this is just
+ // a temporary situation (until all projects are updated),
+ // do a replace here if the file comes from higher up in the module chain.
+ 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 nil, nil
}
}
@@ -1165,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
@@ -1347,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
}
@@ -1355,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
}
@@ -1394,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
}
@@ -1405,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 {
@@ -1431,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() {
@@ -1439,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
}
}
@@ -1459,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
@@ -1488,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
}
}
@@ -1808,10 +1872,11 @@ type TextTemplatHandler interface {
}
type bestMatch struct {
- templ *TemplInfo
- desc TemplateDescriptor
- w weight
- key string
+ templ *TemplInfo
+ desc TemplateDescriptor
+ w weight
+ key string
+ candidates []*TemplInfo
// settings.
defaultOutputformat string
@@ -1822,6 +1887,18 @@ func (best *bestMatch) reset() {
best.w = weight{}
best.desc = TemplateDescriptor{}
best.key = ""
+ best.candidates = nil
+}
+
+func (best *bestMatch) candidatesAsStringSlice() []string {
+ if len(best.candidates) == 0 {
+ return nil
+ }
+ candidates := make([]string, len(best.candidates))
+ for i, v := range best.candidates {
+ candidates[i] = v.PathInfo.Path()
+ }
+ return candidates
}
func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
@@ -1831,7 +1908,6 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
}
if w.w1 <= 0 {
-
if best.w.w1 <= 0 {
return ti.PathInfo.Path() < best.templ.PathInfo.Path()
}
diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go
index 75ec0376f..0b3ce7a56 100644
--- a/tpl/tplimpl/templatestore_integration_test.go
+++ b/tpl/tplimpl/templatestore_integration_test.go
@@ -920,6 +920,26 @@ func TestPartialHTML(t *testing.T) {
b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">")
}
+func TestPartialPlainTextInHTML(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/all.html --
+<html>
+<head>
+{{ partial "mypartial.txt" . }}
+</head>
+</html>
+-- layouts/partials/mypartial.txt --
+My <div>partial</div>.
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "My &lt;div&gt;partial&lt;/div&gt;.")
+}
+
// Issue #13593.
func TestGoatAndNoGoat(t *testing.T) {
t.Parallel()
@@ -1103,6 +1123,35 @@ All.
b.AssertLogContains("unrecognized render hook")
}
+func TestLayoutNotFound(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/single.html --
+Single.
+`
+ b := hugolib.Test(t, files, hugolib.TestOptWarn())
+ b.AssertLogContains("WARN found no layout file for \"html\" for kind \"home\"")
+}
+
+func TestLayoutOverrideThemeWhenThemeOnOldFormatIssue13715(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+theme = "mytheme"
+-- layouts/list.html --
+ layouts/list.html
+-- themes/mytheme/layouts/_default/list.html --
+mytheme/layouts/_default/list.html
+
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/index.html", "layouts/list.html")
+}
+
func BenchmarkExecuteWithContext(b *testing.B) {
files := `
-- hugo.toml --
@@ -1197,8 +1246,8 @@ s2.
Category: tplimpl.CategoryShortcode,
Desc: desc,
}
- v := store.LookupShortcode(q)
- if v == nil {
+ v, err := store.LookupShortcode(q)
+ if v == nil || err != nil {
b.Fatal("not found")
}
}
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index bc6d97cf2..380ea252b 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -17,8 +17,10 @@ package transform
import (
"bytes"
"context"
+ "encoding/json"
"encoding/xml"
"errors"
+ "fmt"
"html"
"html/template"
"io"
@@ -234,6 +236,7 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, er
MinRuleThickness: 0.04,
ErrorColor: "#cc0000",
ThrowOnError: true,
+ Strict: "error",
},
}
@@ -243,8 +246,23 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, er
}
}
+ switch katexInput.Options.Strict {
+ case "error", "ignore", "warn":
+ // Valid strict mode, continue
+ default:
+ return "", fmt.Errorf("invalid strict mode; expected one of error, ignore, or warn; received %s", katexInput.Options.Strict)
+ }
+
+ type fileCacheEntry struct {
+ Version string `json:"version"`
+ Output string `json:"output"`
+ Warnings []string `json:"warnings,omitempty"`
+ }
+
+ const fileCacheEntryVersion = "v1" // Increment on incompatible changes.
+
s := hashing.HashString(args...)
- key := "tomath/" + s[:2] + "/" + s[2:]
+ key := "tomath/" + fileCacheEntryVersion + "/" + s[:2] + "/" + s[2:]
fileCache := ns.deps.ResourceSpec.FileCaches.MiscCache()
v, err := ns.cacheMath.GetOrCreate(key, func(string) (template.HTML, error) {
@@ -265,15 +283,35 @@ func (ns *Namespace) ToMath(ctx context.Context, args ...any) (template.HTML, er
if err != nil {
return nil, err
}
- return hugio.NewReadSeekerNoOpCloserFromString(result.Data.Output), nil
+
+ e := fileCacheEntry{
+ Version: fileCacheEntryVersion,
+ Output: result.Data.Output,
+ Warnings: result.Header.Warnings,
+ }
+
+ buf := &bytes.Buffer{}
+ enc := json.NewEncoder(buf)
+ enc.SetEscapeHTML(false)
+ if err := enc.Encode(e); err != nil {
+ return nil, fmt.Errorf("failed to encode file cache entry: %w", err)
+ }
+ return hugio.NewReadSeekerNoOpCloserFromBytes(buf.Bytes()), nil
})
if err != nil {
return "", err
}
- s, err := hugio.ReadString(r)
+ var e fileCacheEntry
+ if err := json.NewDecoder(r).Decode(&e); err != nil {
+ return "", fmt.Errorf("failed to decode file cache entry: %w", err)
+ }
+
+ for _, warning := range e.Warnings {
+ ns.deps.Log.Warnf("transform.ToMath: %s", warning)
+ }
- return template.HTML(s), err
+ return template.HTML(e.Output), err
})
if err != nil {
return "", err
diff --git a/tpl/transform/transform_integration_test.go b/tpl/transform/transform_integration_test.go
index 2b3c7d40e..8197b3e3d 100644
--- a/tpl/transform/transform_integration_test.go
+++ b/tpl/transform/transform_integration_test.go
@@ -495,3 +495,43 @@ DATA
}
}
}
+
+// Issue 13729
+func TestToMathStrictMode(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- layouts/all.html --
+{{ transform.ToMath "a %" dict }}
+-- foo --
+`
+
+ // strict mode: default
+ f := strings.ReplaceAll(files, "dict", "")
+ b, err := hugolib.TestE(t, f)
+ b.Assert(err.Error(), qt.Contains, "[commentAtEnd]")
+
+ // strict mode: error
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "error")`)
+ b, err = hugolib.TestE(t, f)
+ b.Assert(err.Error(), qt.Contains, "[commentAtEnd]")
+
+ // strict mode: ignore
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "ignore")`)
+ b = hugolib.Test(t, f, hugolib.TestOptWarn())
+ b.AssertLogMatches("")
+ b.AssertFileContent("public/index.html", `<annotation encoding="application/x-tex">a %</annotation>`)
+
+ // strict: warn
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "warn")`)
+ b = hugolib.Test(t, f, hugolib.TestOptWarn())
+ b.AssertLogMatches("[commentAtEnd]")
+ b.AssertFileContent("public/index.html", `<annotation encoding="application/x-tex">a %</annotation>`)
+
+ // strict mode: invalid value
+ f = strings.ReplaceAll(files, "dict", `(dict "strict" "foo")`)
+ b, err = hugolib.TestE(t, f)
+ b.Assert(err.Error(), qt.Contains, "invalid strict mode")
+}