diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index cc8a145d9..56bf1ff9e 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -398,6 +398,10 @@ func doRenderShortcode( return true } base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor() + + // With shortcodes/mymarkdown.md (only), this allows {{% mymarkdown %}} when rendering HTML, + // but will not resolve any template when doing {{< mymarkdown >}}. + layoutDescriptor.AlwaysAllowPlainText = sc.doMarkup q := tplimpl.TemplateQuery{ Path: base, Name: sc.name, @@ -405,10 +409,9 @@ func doRenderShortcode( Desc: layoutDescriptor, Consider: include, } - v := s.TemplateStore.LookupShortcode(q) + v, err := s.TemplateStore.LookupShortcode(q) if v == nil { - s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path()) - return zeroShortcode, nil + return zeroShortcode, err } tmpl = v hasVariants = hasVariants || len(ofCount) > 1 diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index f1d90e22e..a1f12e77a 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -918,7 +918,7 @@ func TestShortcodeMarkdownOutputFormat(t *testing.T) { --- title: "p1" --- -{{< foo >}} +{{% foo %}} # The below would have failed using the HTML template parser. -- layouts/shortcodes/foo.md -- ยงยงยง @@ -930,9 +930,7 @@ title: "p1" b := Test(t, files) - b.AssertFileContent("public/p1/index.html", ` -<x") } func TestShortcodePreserveIndentation(t *testing.T) { diff --git a/tpl/tplimpl/shortcodes_integration_test.go b/tpl/tplimpl/shortcodes_integration_test.go index 838dc16d7..e65f82eab 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" ) @@ -696,3 +697,35 @@ 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 -- +
Foo bar
+-- 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", "
Foo bar
") + + 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`) +} 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/templatestore.go b/tpl/tplimpl/templatestore.go index c6a6d4cd5..2ea337274 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -19,6 +19,7 @@ import ( "bytes" "context" "embed" + "errors" "fmt" "io" "io/fs" @@ -608,7 +609,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 +631,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 +647,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. @@ -1817,10 +1833,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 @@ -1831,6 +1848,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 { @@ -1840,7 +1869,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 e10d7149a..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", "") } +func TestPartialPlainTextInHTML(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/all.html -- + + +{{ partial "mypartial.txt" . }} + + +-- layouts/partials/mypartial.txt -- +My
partial
. +` + + 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,18 @@ 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() @@ -1214,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") } }