Repository: gohugoio/hugo (language: go)

Issue:
TableOfContentsc causes timeout when {{ exists in code block.
<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.110.0-e32a493b7826d02763c3b79623952e625402b168+extended linux/amd64 BuildDate=2023-01-17T12:16:09Z VendorInfo=gohugoio
</pre>

### Does this issue reproduce with the latest release?

Yes.

### Description

I discovered this while using the [geekdocs](https://github.com/thegeeklab/hugo-geekdoc) theme, but as far as I can tell the bug is not specific to that theme.

If your page has:
    1. A code block with the sequence `{{` somewhere in it
    2. That code block is wrapped in a shortcode that renders it to html
    3. A table of contents
then, it will cause a timeout and fail to render.

Oddly enough, removing the table of contents from such a page fixes the problem. As does, having the code block not be wrapped in a shortcode.

### Error message

```
$ hugo server
Start building sites …
hugo v0.110.0-e32a493b7826d02763c3b79623952e625402b168+extended linux/amd64 BuildDate=2023-01-17T12:16:09Z VendorInfo=gohugoio
Error: Error building site: "/tmp/my_hugo/content/posts/my-first-post.md:1:1": timed out initializing value. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file.
Built in 30068 ms
```

### How to reproduce

`layouts/shortcodes/noop.html`
```
{{ .Inner | $.Page.RenderString }}
```

`layouts/shortcodes/toc.html`
```
{{ .Page.TableOfContents }}
```

`content/posts/my-first-post.md`
<pre>
---
title: "My First Post"
date: 2023-01-24T13:08:10-07:00
draft: false
---

{{< toc >}}

## this is a heading

{{< noop >}}
```
this is some code
   containing
     {{ sequence
```
{{< /noop >}}
</pre>


Discussion / hints:
When you include `.TableOfContents` in a shortcode, it forces the current page to be rendered, which includes the shortcode...

:chicken: → :egg: → :chicken: → :egg: → …

See <https://discourse.gohugo.io/t/36758/2>
Re-opening.
To be clear:

This should work:

In a shortcode in page "foo":

```
{{ .Page.TableOfContents }}
```

Should work.

```
{{ (site.GetPage "foo" ).TableOfContents }}
```

Will create an infinite recursion.

What I suspect is creating a infinite recursion above is the noop shortcode with:

```
{{ .Inner | $.Page.RenderString }}

```

I'm pretty sure it's not about any `{{` in the code block, and I suspect you need to simplify your shortcode setup to make this work.



I can reproduce this as described with v0.100.0 and later, but not with v0.99.1.

Git bisect says 9e904d756be02ca30e4cd9abb1eae8ba01f9c8af is the first bad commit.

```text
git clone --single-branch -b hugo-github-issue-10654 https://github.com/jmooring/hugo-testing hugo-github-issue-10654
cd hugo-github-issue-10654
hugo server
```


Ah, OK, yea then I understand, thanks for the clarification...

Fix patch:
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index 60e3a7f5984..97e9cc46564 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/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go
index 0a9fc61af41..7687479078c 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)
+}


Test patch:
diff --git a/hugolib/renderstring_test.go b/hugolib/renderstring_test.go
index 1be0cdffb53..af66156e692 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_test.go b/parser/pageparser/pageparser_test.go
index a21b97970cf..de817d1fb6e 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("{{</* foo */>}}"), 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)
+		}
+	})
+
+}