]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
tpl: Narrow down the usage of plain text shortcodes when rendering HTML
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 16 May 2025 08:36:05 +0000 (10:36 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 18 May 2025 10:48:24 +0000 (12:48 +0200)
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

hugolib/shortcode.go
hugolib/shortcode_test.go
tpl/tplimpl/shortcodes_integration_test.go
tpl/tplimpl/templatedescriptor.go
tpl/tplimpl/templatestore.go
tpl/tplimpl/templatestore_integration_test.go

index cc8a145d995d304843e2f360fd338b2ba185cd3b..56bf1ff9e959a11d760a33d8e2b5baa1cd92af45 100644 (file)
@@ -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
index f1d90e22ed6e09fb5a11153a09455b3128406d43..a1f12e77ab12f807237d2df42610194d45f4722f 100644 (file)
@@ -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
-       `)
+       b.AssertFileContent("public/p1/index.html", "<code>&lt;x")
 }
 
 func TestShortcodePreserveIndentation(t *testing.T) {
index 838dc16d745316dc162443cd4c0838f81706e9c3..e65f82eab058cbac3a758ee02bb47ba6acb39c08 100644 (file)
@@ -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 --
+<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`)
+}
index ea47afc88d3407d5fe3f6a8d96691deedf8fd039..fd86f15fadb35de1ad987b969d2354c21b60bf94 100644 (file)
@@ -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
        }
index c6a6d4cd58fd092fff0ee111dc22f06c581d8618..2ea33727438dbd02ce90e4a1081dec919cadbc73 100644 (file)
@@ -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()
                }
index e10d7149a9d7332a5de6e827846a1024ca7dc01a..0b3ce7a563a2d035273754c641d629191d9c63a2 100644 (file)
@@ -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,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")
                }
        }