Skip to content

Commit 6131782

Browse files
committed
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
1 parent 6142bc7 commit 6131782

File tree

6 files changed

+127
-24
lines changed

6 files changed

+127
-24
lines changed

hugolib/shortcode.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,17 +398,20 @@ func doRenderShortcode(
398398
return true
399399
}
400400
base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor()
401+
402+
// With shortcodes/mymarkdown.md (only), this allows {{% mymarkdown %}} when rendering HTML,
403+
// but will not resolve any template when doing {{< mymarkdown >}}.
404+
layoutDescriptor.AlwaysAllowPlainText = sc.doMarkup
401405
q := tplimpl.TemplateQuery{
402406
Path: base,
403407
Name: sc.name,
404408
Category: tplimpl.CategoryShortcode,
405409
Desc: layoutDescriptor,
406410
Consider: include,
407411
}
408-
v := s.TemplateStore.LookupShortcode(q)
412+
v, err := s.TemplateStore.LookupShortcode(q)
409413
if v == nil {
410-
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
411-
return zeroShortcode, nil
414+
return zeroShortcode, err
412415
}
413416
tmpl = v
414417
hasVariants = hasVariants || len(ofCount) > 1

hugolib/shortcode_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,7 @@ func TestShortcodeMarkdownOutputFormat(t *testing.T) {
918918
---
919919
title: "p1"
920920
---
921-
{{< foo >}}
921+
{{% foo %}}
922922
# The below would have failed using the HTML template parser.
923923
-- layouts/shortcodes/foo.md --
924924
§§§
@@ -930,9 +930,7 @@ title: "p1"
930930

931931
b := Test(t, files)
932932

933-
b.AssertFileContent("public/p1/index.html", `
934-
<x
935-
`)
933+
b.AssertFileContent("public/p1/index.html", "<code>&lt;x")
936934
}
937935

938936
func TestShortcodePreserveIndentation(t *testing.T) {

tpl/tplimpl/shortcodes_integration_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"strings"
1818
"testing"
1919

20+
qt "github.com/frankban/quicktest"
2021
"github.com/gohugoio/hugo/htesting/hqt"
2122
"github.com/gohugoio/hugo/hugolib"
2223
)
@@ -696,3 +697,35 @@ title: p2
696697
b.AssertFileContent("public/p1/index.html", "78eb19b5c6f3768f")
697698
b.AssertFileContent("public/p2/index.html", "a6db910a9cf54bc1")
698699
}
700+
701+
func TestShortcodePlainTextVsHTMLTemplateIssue13698(t *testing.T) {
702+
t.Parallel()
703+
704+
filesTemplate := `
705+
-- hugo.toml --
706+
markup.goldmark.renderer.unsafe = true
707+
-- layouts/all.html --
708+
Content: {{ .Content }}|
709+
-- layouts/_shortcodes/mymarkdown.md --
710+
<div>Foo bar</div>
711+
-- content/p1.md --
712+
---
713+
title: p1
714+
---
715+
## A shortcode
716+
717+
SHORTCODE
718+
719+
`
720+
721+
files := strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{% mymarkdown %}}")
722+
b := hugolib.Test(t, files)
723+
b.AssertFileContent("public/p1/index.html", "<div>Foo bar</div>")
724+
725+
files = strings.ReplaceAll(filesTemplate, "SHORTCODE", "{{< mymarkdown >}}")
726+
727+
var err error
728+
b, err = hugolib.TestE(t, files)
729+
b.Assert(err, qt.IsNotNil)
730+
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`)
731+
}

tpl/tplimpl/templatedescriptor.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type TemplateDescriptor struct {
3737
// Misc.
3838
LayoutFromUserMustMatch bool // If set, we only look for the exact layout.
3939
IsPlainText bool // Whether this is a plain text template.
40+
AlwaysAllowPlainText bool // Whether to e.g. allow plain text templates to be rendered in HTML.
4041
}
4142

4243
func (d *TemplateDescriptor) normalizeFromFile() {
@@ -64,7 +65,7 @@ func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool
6465
return weightNoMatch
6566
}
6667

67-
w := this.doCompare(category, isEmbedded, s.opts.DefaultContentLanguage, other)
68+
w := this.doCompare(category, s.opts.DefaultContentLanguage, other)
6869

6970
if w.w1 <= 0 {
7071
if category == CategoryMarkup && (this.Variant1 == other.Variant1) && (this.Variant2 == other.Variant2 || this.Variant2 != "" && other.Variant2 == "") {
@@ -74,21 +75,29 @@ func (s descriptorHandler) compareDescriptors(category Category, isEmbedded bool
7475
}
7576

7677
w.w1 = 1
77-
return w
78+
}
79+
80+
if category == CategoryShortcode {
81+
if (this.IsPlainText == other.IsPlainText || !other.IsPlainText) || this.AlwaysAllowPlainText {
82+
w.w1 = 1
83+
}
7884
}
7985
}
8086

8187
return w
8288
}
8389

8490
//lint:ignore ST1006 this vs other makes it easier to reason about.
85-
func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, defaultContentLanguage string, other TemplateDescriptor) weight {
91+
func (this TemplateDescriptor) doCompare(category Category, defaultContentLanguage string, other TemplateDescriptor) weight {
8692
w := weightNoMatch
8793

88-
// HTML in plain text is OK, but not the other way around.
89-
if other.IsPlainText && !this.IsPlainText {
90-
return w
94+
if !this.AlwaysAllowPlainText {
95+
// HTML in plain text is OK, but not the other way around.
96+
if other.IsPlainText && !this.IsPlainText {
97+
return w
98+
}
9199
}
100+
92101
if other.Kind != "" && other.Kind != this.Kind {
93102
return w
94103
}

tpl/tplimpl/templatestore.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"bytes"
2020
"context"
2121
"embed"
22+
"errors"
2223
"fmt"
2324
"io"
2425
"io/fs"
@@ -608,7 +609,7 @@ func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo {
608609
return ti
609610
}
610611

611-
func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
612+
func (s *TemplateStore) LookupShortcode(q TemplateQuery) (*TemplInfo, error) {
612613
q.init()
613614
k1 := s.key(q.Path)
614615

@@ -630,22 +631,37 @@ func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
630631
}
631632

632633
for k, vv := range v {
634+
best.candidates = append(best.candidates, vv)
633635
if !q.Consider(vv) {
634636
continue
635637
}
636638

637639
weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k)
638640
weight.distance = distance
639-
if best.isBetter(weight, vv) {
641+
isBetter := best.isBetter(weight, vv)
642+
if isBetter {
640643
best.updateValues(weight, k2, k, vv)
641644
}
642645
}
643646

644647
return false, nil
645648
})
646649

647-
// Any match will do.
648-
return best.templ
650+
if best.w.w1 <= 0 {
651+
var err error
652+
if s := best.candidatesAsStringSlice(); s != nil {
653+
msg := fmt.Sprintf("no compatible template found for shortcode %q in %s", q.Name, s)
654+
if !q.Desc.IsPlainText {
655+
msg += "; note that to use plain text template shortcodes in HTML you need to use the shortcode {{% delimiter"
656+
}
657+
err = errors.New(msg)
658+
} else {
659+
err = fmt.Errorf("no template found for shortcode %q", q.Name)
660+
}
661+
return nil, err
662+
}
663+
664+
return best.templ, nil
649665
}
650666

651667
// PrintDebug is for testing/debugging only.
@@ -1817,10 +1833,11 @@ type TextTemplatHandler interface {
18171833
}
18181834

18191835
type bestMatch struct {
1820-
templ *TemplInfo
1821-
desc TemplateDescriptor
1822-
w weight
1823-
key string
1836+
templ *TemplInfo
1837+
desc TemplateDescriptor
1838+
w weight
1839+
key string
1840+
candidates []*TemplInfo
18241841

18251842
// settings.
18261843
defaultOutputformat string
@@ -1831,6 +1848,18 @@ func (best *bestMatch) reset() {
18311848
best.w = weight{}
18321849
best.desc = TemplateDescriptor{}
18331850
best.key = ""
1851+
best.candidates = nil
1852+
}
1853+
1854+
func (best *bestMatch) candidatesAsStringSlice() []string {
1855+
if len(best.candidates) == 0 {
1856+
return nil
1857+
}
1858+
candidates := make([]string, len(best.candidates))
1859+
for i, v := range best.candidates {
1860+
candidates[i] = v.PathInfo.Path()
1861+
}
1862+
return candidates
18341863
}
18351864

18361865
func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
@@ -1840,7 +1869,6 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
18401869
}
18411870

18421871
if w.w1 <= 0 {
1843-
18441872
if best.w.w1 <= 0 {
18451873
return ti.PathInfo.Path() < best.templ.PathInfo.Path()
18461874
}

tpl/tplimpl/templatestore_integration_test.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,26 @@ func TestPartialHTML(t *testing.T) {
920920
b.AssertFileContent("public/index.html", "<link rel=\"stylesheet\" href=\"/css/style.css\">")
921921
}
922922

923+
func TestPartialPlainTextInHTML(t *testing.T) {
924+
t.Parallel()
925+
926+
files := `
927+
-- hugo.toml --
928+
-- layouts/all.html --
929+
<html>
930+
<head>
931+
{{ partial "mypartial.txt" . }}
932+
</head>
933+
</html>
934+
-- layouts/partials/mypartial.txt --
935+
My <div>partial</div>.
936+
`
937+
938+
b := hugolib.Test(t, files)
939+
940+
b.AssertFileContent("public/index.html", "My &lt;div&gt;partial&lt;/div&gt;.")
941+
}
942+
923943
// Issue #13593.
924944
func TestGoatAndNoGoat(t *testing.T) {
925945
t.Parallel()
@@ -1103,6 +1123,18 @@ All.
11031123
b.AssertLogContains("unrecognized render hook")
11041124
}
11051125

1126+
func TestLayoutNotFound(t *testing.T) {
1127+
t.Parallel()
1128+
1129+
files := `
1130+
-- hugo.toml --
1131+
-- layouts/single.html --
1132+
Single.
1133+
`
1134+
b := hugolib.Test(t, files, hugolib.TestOptWarn())
1135+
b.AssertLogContains("WARN found no layout file for \"html\" for kind \"home\"")
1136+
}
1137+
11061138
func TestLayoutOverrideThemeWhenThemeOnOldFormatIssue13715(t *testing.T) {
11071139
t.Parallel()
11081140

@@ -1214,8 +1246,8 @@ s2.
12141246
Category: tplimpl.CategoryShortcode,
12151247
Desc: desc,
12161248
}
1217-
v := store.LookupShortcode(q)
1218-
if v == nil {
1249+
v, err := store.LookupShortcode(q)
1250+
if v == nil || err != nil {
12191251
b.Fatal("not found")
12201252
}
12211253
}

0 commit comments

Comments
 (0)