tpl: Narrow down the usage of plain text shortcodes when rendering HTML

After this commit, if you want to resolve `layouts/_shortcodes/myshortcode.txt` when rendering HTML content, you need to use the `{{%` shortcode delimiter:

```
{{% myshortcode %}}
```

This should be what people would do anyway, but we have also as part of this improved the error message to inform about what needs to be done.

Note that this is not relevant for partials.

Fixes #13698
This commit is contained in:
Bjørn Erik Pedersen
2025-05-16 10:36:05 +02:00
parent 6142bc701c
commit 61317821e4
6 changed files with 127 additions and 24 deletions

View File

@@ -398,6 +398,10 @@ func doRenderShortcode(
return true return true
} }
base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor() 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{ q := tplimpl.TemplateQuery{
Path: base, Path: base,
Name: sc.name, Name: sc.name,
@@ -405,10 +409,9 @@ func doRenderShortcode(
Desc: layoutDescriptor, Desc: layoutDescriptor,
Consider: include, Consider: include,
} }
v := s.TemplateStore.LookupShortcode(q) v, err := s.TemplateStore.LookupShortcode(q)
if v == nil { if v == nil {
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path()) return zeroShortcode, err
return zeroShortcode, nil
} }
tmpl = v tmpl = v
hasVariants = hasVariants || len(ofCount) > 1 hasVariants = hasVariants || len(ofCount) > 1

View File

@@ -918,7 +918,7 @@ func TestShortcodeMarkdownOutputFormat(t *testing.T) {
--- ---
title: "p1" title: "p1"
--- ---
{{< foo >}} {{% foo %}}
# The below would have failed using the HTML template parser. # The below would have failed using the HTML template parser.
-- layouts/shortcodes/foo.md -- -- layouts/shortcodes/foo.md --
§§§ §§§
@@ -930,9 +930,7 @@ title: "p1"
b := Test(t, files) b := Test(t, files)
b.AssertFileContent("public/p1/index.html", ` b.AssertFileContent("public/p1/index.html", "<code>&lt;x")
<x
`)
} }
func TestShortcodePreserveIndentation(t *testing.T) { func TestShortcodePreserveIndentation(t *testing.T) {

View File

@@ -17,6 +17,7 @@ import (
"strings" "strings"
"testing" "testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt" "github.com/gohugoio/hugo/htesting/hqt"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
) )
@@ -696,3 +697,35 @@ title: p2
b.AssertFileContent("public/p1/index.html", "78eb19b5c6f3768f") b.AssertFileContent("public/p1/index.html", "78eb19b5c6f3768f")
b.AssertFileContent("public/p2/index.html", "a6db910a9cf54bc1") 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`)
}

View File

@@ -37,6 +37,7 @@ type TemplateDescriptor struct {
// Misc. // Misc.
LayoutFromUserMustMatch bool // If set, we only look for the exact layout. LayoutFromUserMustMatch bool // If set, we only look for the exact layout.
IsPlainText bool // Whether this is a plain text template. 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() { func (d *TemplateDescriptor) normalizeFromFile() {
@@ -64,7 +65,7 @@ func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool
return weightNoMatch return weightNoMatch
} }
w := this.doCompare(category, isEmbedded, s.opts.DefaultContentLanguage, other) w := this.doCompare(category, s.opts.DefaultContentLanguage, other)
if w.w1 <= 0 { if w.w1 <= 0 {
if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") { 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 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. //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 w := weightNoMatch
// HTML in plain text is OK, but not the other way around. if !this.AlwaysAllowPlainText {
if other.IsPlainText && !this.IsPlainText { // HTML in plain text is OK, but not the other way around.
return w if other.IsPlainText && !this.IsPlainText {
return w
}
} }
if other.Kind != "" && other.Kind != this.Kind { if other.Kind != "" && other.Kind != this.Kind {
return w return w
} }

View File

@@ -19,6 +19,7 @@ import (
"bytes" "bytes"
"context" "context"
"embed" "embed"
"errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -608,7 +609,7 @@ func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo {
return ti return ti
} }
func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo { func (s *TemplateStore) LookupShortcode(q TemplateQuery) (*TemplInfo, error) {
q.init() q.init()
k1 := s.key(q.Path) k1 := s.key(q.Path)
@@ -630,13 +631,15 @@ func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
} }
for k, vv := range v { for k, vv := range v {
best.candidates = append(best.candidates, vv)
if !q.Consider(vv) { if !q.Consider(vv) {
continue continue
} }
weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k) weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k)
weight.distance = distance weight.distance = distance
if best.isBetter(weight, vv) { isBetter := best.isBetter(weight, vv)
if isBetter {
best.updateValues(weight, k2, k, vv) best.updateValues(weight, k2, k, vv)
} }
} }
@@ -644,8 +647,21 @@ func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
return false, nil return false, nil
}) })
// Any match will do. if best.w.w1 <= 0 {
return best.templ 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. // PrintDebug is for testing/debugging only.
@@ -1817,10 +1833,11 @@ type TextTemplatHandler interface {
} }
type bestMatch struct { type bestMatch struct {
templ *TemplInfo templ *TemplInfo
desc TemplateDescriptor desc TemplateDescriptor
w weight w weight
key string key string
candidates []*TemplInfo
// settings. // settings.
defaultOutputformat string defaultOutputformat string
@@ -1831,6 +1848,18 @@ func (best *bestMatch) reset() {
best.w = weight{} best.w = weight{}
best.desc = TemplateDescriptor{} best.desc = TemplateDescriptor{}
best.key = "" 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 { 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 w.w1 <= 0 {
if best.w.w1 <= 0 { if best.w.w1 <= 0 {
return ti.PathInfo.Path() < best.templ.PathInfo.Path() return ti.PathInfo.Path() < best.templ.PathInfo.Path()
} }

View File

@@ -920,6 +920,26 @@ func TestPartialHTML(t *testing.T) {
b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">") 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. // Issue #13593.
func TestGoatAndNoGoat(t *testing.T) { func TestGoatAndNoGoat(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1103,6 +1123,18 @@ All.
b.AssertLogContains("unrecognized render hook") 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) { func TestLayoutOverrideThemeWhenThemeOnOldFormatIssue13715(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1214,8 +1246,8 @@ s2.
Category: tplimpl.CategoryShortcode, Category: tplimpl.CategoryShortcode,
Desc: desc, Desc: desc,
} }
v := store.LookupShortcode(q) v, err := store.LookupShortcode(q)
if v == nil { if v == nil || err != nil {
b.Fatal("not found") b.Fatal("not found")
} }
} }