diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 60e3a7f59..97e9cc465 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -384,8 +384,8 @@ func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) { var rendered []byte - if strings.Contains(contentToRender, "{{") { - // Probably a shortcode. + if pageparser.HasShortcode(contentToRender) { + // String contains a shortcode. parsed, err := pageparser.ParseMain(strings.NewReader(contentToRender), pageparser.Config{}) if err != nil { return "", err diff --git a/hugolib/renderstring_test.go b/hugolib/renderstring_test.go index 1be0cdffb..af66156e6 100644 --- a/hugolib/renderstring_test.go +++ b/hugolib/renderstring_test.go @@ -190,3 +190,39 @@ Has other: false `) } + +func TestRenderStringWithShortcodeIssue10654(t *testing.T) { + t.Parallel() + + files := ` +-- config.toml -- +timeout = '300ms' +-- content/p1.md -- +--- +title: "P1" +--- +{{< toc >}} + +## Heading 1 + +{{< noop >}} + {{ not a shortcode +{{< /noop >}} +} +-- layouts/shortcodes/noop.html -- +{{ .Inner | $.Page.RenderString }} +-- layouts/shortcodes/toc.html -- +{{ .Page.TableOfContents }} +-- layouts/_default/single.html -- +{{ .Content }} +` + + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + b.AssertFileContent("public/p1/index.html", `TableOfContents`) +} diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go index 0a9fc61af..768747907 100644 --- a/parser/pageparser/pageparser.go +++ b/parser/pageparser/pageparser.go @@ -19,6 +19,8 @@ import ( "fmt" "io" "io/ioutil" + "regexp" + "strings" "github.com/gohugoio/hugo/parser/metadecoders" ) @@ -234,3 +236,14 @@ func IsProbablySourceOfItems(source []byte, items Items) bool { return true } + +var hasShortcodeRe = regexp.MustCompile(`{{[%,<][^\/]`) + +// HasShortcode returns true if the given string contains a shortcode. +func HasShortcode(s string) bool { + // Fast path for the common case. + if !strings.Contains(s, "{{") { + return false + } + return hasShortcodeRe.MatchString(s) +} diff --git a/parser/pageparser/pageparser_test.go b/parser/pageparser/pageparser_test.go index a21b97970..de817d1fb 100644 --- a/parser/pageparser/pageparser_test.go +++ b/parser/pageparser/pageparser_test.go @@ -101,3 +101,30 @@ func TestIsProbablyItemsSource(t *testing.T) { c.Assert(IsProbablySourceOfItems([]byte(`{{< foo >}} `), items), qt.IsFalse) c.Assert(IsProbablySourceOfItems([]byte(``), items), qt.IsFalse) } + +func TestHasShortcode(t *testing.T) { + c := qt.New(t) + + c.Assert(HasShortcode("{{< foo >}}"), qt.IsTrue) + c.Assert(HasShortcode("aSDasd SDasd aSD\n\nasdfadf{{% foo %}}\nasdf"), qt.IsTrue) + c.Assert(HasShortcode("{{}}"), qt.IsFalse) + c.Assert(HasShortcode("{{%/* foo */%}}"), qt.IsFalse) + +} + +func BenchmarkHasShortcode(b *testing.B) { + withShortcode := strings.Repeat("this is text", 30) + "{{< myshortcode >}}This is some inner content.{{< /myshortcode >}}" + strings.Repeat("this is text", 30) + withoutShortcode := strings.Repeat("this is text", 30) + "This is some inner content." + strings.Repeat("this is text", 30) + b.Run("Match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + HasShortcode(withShortcode) + } + }) + + b.Run("NoMatch", func(b *testing.B) { + for i := 0; i < b.N; i++ { + HasShortcode(withoutShortcode) + } + }) + +}