diff options
Diffstat (limited to 'tpl')
-rw-r--r-- | tpl/internal/go_templates/htmltemplate/hugo_template.go | 14 | ||||
-rw-r--r-- | tpl/internal/go_templates/texttemplate/hugo_template.go | 16 | ||||
-rw-r--r-- | tpl/internal/go_templates/texttemplate/parse/parse.go | 2 | ||||
-rw-r--r-- | tpl/partials/partials.go | 2 | ||||
-rw-r--r-- | tpl/tplimpl/embedded/templates/_shortcodes/twitter.html | 2 | ||||
-rw-r--r-- | tpl/tplimpl/embedded/templates/_shortcodes/vimeo.html | 2 | ||||
-rw-r--r-- | tpl/tplimpl/embedded/templates/_shortcodes/x.html | 2 | ||||
-rw-r--r-- | tpl/tplimpl/shortcodes_integration_test.go | 99 | ||||
-rw-r--r-- | tpl/tplimpl/templatedescriptor.go | 21 | ||||
-rw-r--r-- | tpl/tplimpl/templates.go | 52 | ||||
-rw-r--r-- | tpl/tplimpl/templatestore.go | 244 | ||||
-rw-r--r-- | tpl/tplimpl/templatestore_integration_test.go | 53 | ||||
-rw-r--r-- | tpl/transform/transform.go | 46 | ||||
-rw-r--r-- | tpl/transform/transform_integration_test.go | 40 |
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 <div>partial</div>.") +} + // 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") +} |