Repository: gohugoio/hugo (language: go)

Issue:
stack overflow in goldmark.(*hookedRenderer).renderAutoLink
<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.88.1+extended darwin/amd64 BuildDate=unknown
</pre>

### Does this issue reproduce with the latest release?

Yes.

This issue appeared in the 0.86. It is not presented in 0.85.

The stack trace is really huge (2k lines), as far as I understand routine that overflows does that executing the `goldmark.(*hookedRenderer).renderAutoLink` (infinite recursion?).

```
Start building sites … 
hugo v0.88.1+extended darwin/amd64 BuildDate=unknown
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc041980ef0 stack=[0xc041980000, 0xc061980000]
fatal error: stack overflow

runtime stack:
runtime.throw({0x5c434c3, 0x6f02dc0})
        runtime/panic.go:1198 +0x71
runtime.newstack()
        runtime/stack.go:1088 +0x5ac
runtime.morestack()
        runtime/asm_amd64.s:461 +0x8b

...
```


Discussion / hints:
This is a little odd as we do have test for this.

Thanks, but we need more than 5 lines of the error stack to be able to fix this. Could you at least paste as long so it start repeating itself.
Ok, whole stacktrace:

https://gist.github.com/meownoid/9b7eeb881c8fa1f7450937734c933dc0
@meownoid Thanks! Can you add a generic/minimal version of the template you’re using here (plus a generic version of any relevant content file)? Otherwise it’ll be a bit difficult to write a test for the inputs that caused the infinite recursion.
Minimal reproducible example:
```
hugo new site test-site
cd test-site
hugo new theme test-theme
mkdir -p themes/test-theme/layouts/_default/_markup
echo '<a href="{{ .Destination | safeURL }}">{{ .Text | .Page.RenderString }}</a>' > themes/test-theme/layouts/_default/_markup/render-link.html
echo 'theme = "test-theme"' >> config.toml
hugo new test-post.md
echo "https://en.wikipedia.org/wiki/Unit_testing" >> content/test-post.md
hugo -D
```
markdown

```md
https://example.org

a@b.com
```
layouts/_default/_markup/render-link.html

Code|Result
:--|:--
`<a href="{{ .Destination \| safeURL }}">{{ .Text \| .Page.RenderString }}</a>`|FAIL
`<a href="{{ .Destination \| safeURL }}">{{ .Text \| markdownify }}</a>`|PASS

This also works:

```text
{{ $text := .Text | replaceRE "/" "&#47;" | replaceRE "@" "&#64;" }}
<a href="{{ .Destination | safeURL }}">{{ $text | .Page.RenderString }}</a>
```

When `.Page.RenderString` is invoked, I think the link text is recognized as an autolink, and it's rendered again, and again, and again, or something like that. By replacing / and @ with their HTML entities, the link text is no longer recognized as an autolink.

><a href="{{ .Destination \| safeURL }}">{{ .Text \| .Page.RenderString }}</a>

OK, I think I understand what _technically_ is going on here -- but what is the motivation behind the above construct?

> what is the motivation behind the above construct?

I have no idea. The link text is already passed through the markdown renderer, so passing it through `.RenderString` or `markdownify` seems superfluous.
@bep @jmooring 

`.Page.RenderString` is needed for image links to work:

```
[![image](https://example.com/image_preview.jpg)](https://example.com/image_large.jpg)
```

`.RenderString` is needed because `markdownify ` does not support render hooks (`render-image.html` in my case). From the [documentation](https://gohugo.io/functions/markdownify/):

```
Note: if you need Render Hooks, which markdownify doesn’t currently support, use .RenderString instead.
```
Sorry for the noise on this...

@meownoid I would like to revisit this. As far as I can tell, there is no reason to pass `.Text` in a link render hook through either `markdownify` or `.PageRenderString`. 

`.Text` is already rendered, and that means images wrapped in links are rendered with the image render hook if present.

Here's a simple example with both link and image render hooks. The markdown contains an image wrapped in a link, as well as bare links (autolinks).

```bash
git clone --single-branch -b hugo-github-issue-8959 https://github.com/jmooring/hugo-testing hugo-github-issue-8959
cd hugo-github-issue-8959
hugo server
```

View source in your browser (look for the title attributes) to verify that links and images were passed through their respective hooks.

Can we close this issue, or am I missing something?
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Reopening based on this:
<https://discourse.gohugo.io/t/error-during-hugo-serve-v-0-93-3-with-goldmark-extension/37567/5>

In v0.93.0 we changed `markdownify` to use `site.Home.RenderString` instead, so anyone doing this in their markdown render hook for links will run into the problem:

```
<a href="{{ .Destination }}" >{{ .Text | markdownify | safeHTML }}</a>
```

Although there's no reason to use `markdownify` here (`.Text` has already been rendered), that doesn't stop users from doing it.
The only way I can think of to fix this would be to change:

```
type LinkContext interface {
	Text() string
	PlainText() string
}
```

To something ala:


```
type LinkContext interface {
	Text() RenderedString
	PlainText() string
}
```

And check for that inside `markdownify`. Will think.



Fix patch:
diff --git a/common/types/hstring/stringtypes.go b/common/types/hstring/stringtypes.go
new file mode 100644
index 00000000000..601218e0ed4
--- /dev/null
+++ b/common/types/hstring/stringtypes.go
@@ -0,0 +1,20 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hstring
+
+type RenderedString string
+
+func (s RenderedString) String() string {
+	return string(s)
+}
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index d79b152f3b7..a7ad2a245a4 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -24,6 +24,7 @@ import (
 	"unicode/utf8"
 
 	"github.com/gohugoio/hugo/common/text"
+	"github.com/gohugoio/hugo/common/types/hstring"
 	"github.com/gohugoio/hugo/identity"
 	"github.com/mitchellh/mapstructure"
 	"github.com/pkg/errors"
@@ -351,8 +352,16 @@ func (p *pageContentOutput) RenderString(args ...interface{}) (template.HTML, er
 		}
 	}
 
+	contentToRender := args[sidx]
+
+	if _, ok := contentToRender.(hstring.RenderedString); ok {
+		// This content is already rendered, this is potentially
+		// a infinite recursion.
+		return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
+	}
+
 	var err error
-	s, err = cast.ToStringE(args[sidx])
+	s, err = cast.ToStringE(contentToRender)
 	if err != nil {
 		return "", err
 	}
@@ -515,7 +524,6 @@ func (p *pageContentOutput) initRenderHooks() error {
 					}
 				}
 			}
-
 			if !found1 {
 				if tp == hooks.CodeBlockRendererType {
 					// No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster.
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 54ebf405ec5..2f2d5e6cbed 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -18,6 +18,7 @@ import (
 
 	"github.com/gohugoio/hugo/common/hugio"
 	"github.com/gohugoio/hugo/common/text"
+	"github.com/gohugoio/hugo/common/types/hstring"
 	"github.com/gohugoio/hugo/identity"
 	"github.com/gohugoio/hugo/markup/internal/attributes"
 )
@@ -32,7 +33,7 @@ type LinkContext interface {
 	Page() interface{}
 	Destination() string
 	Title() string
-	Text() string
+	Text() hstring.RenderedString
 	PlainText() string
 }
 
@@ -75,7 +76,7 @@ type HeadingContext interface {
 	// Anchor is the HTML id assigned to the heading.
 	Anchor() string
 	// Text is the rendered (HTML) heading text, excluding the heading marker.
-	Text() string
+	Text() hstring.RenderedString
 	// PlainText is the unrendered version of Text.
 	PlainText() string
 
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index 138a60d263b..9300d49abea 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -17,6 +17,7 @@ import (
 	"bytes"
 	"strings"
 
+	"github.com/gohugoio/hugo/common/types/hstring"
 	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
 	"github.com/gohugoio/hugo/markup/internal/attributes"
@@ -47,7 +48,7 @@ type linkContext struct {
 	page        interface{}
 	destination string
 	title       string
-	text        string
+	text        hstring.RenderedString
 	plainText   string
 }
 
@@ -63,7 +64,7 @@ func (ctx linkContext) Page() interface{} {
 	return ctx.page
 }
 
-func (ctx linkContext) Text() string {
+func (ctx linkContext) Text() hstring.RenderedString {
 	return ctx.text
 }
 
@@ -79,7 +80,7 @@ type headingContext struct {
 	page      interface{}
 	level     int
 	anchor    string
-	text      string
+	text      hstring.RenderedString
 	plainText string
 	*attributes.AttributesHolder
 }
@@ -96,7 +97,7 @@ func (ctx headingContext) Anchor() string {
 	return ctx.anchor
 }
 
-func (ctx headingContext) Text() string {
+func (ctx headingContext) Text() hstring.RenderedString {
 	return ctx.text
 }
 
@@ -153,7 +154,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
 			page:        ctx.DocumentContext().Document,
 			destination: string(n.Destination),
 			title:       string(n.Title),
-			text:        string(text),
+			text:        hstring.RenderedString(text),
 			plainText:   string(n.Text(source)),
 		},
 	)
@@ -223,7 +224,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
 			page:        ctx.DocumentContext().Document,
 			destination: string(n.Destination),
 			title:       string(n.Title),
-			text:        string(text),
+			text:        hstring.RenderedString(text),
 			plainText:   string(n.Text(source)),
 		},
 	)
@@ -290,7 +291,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
 		linkContext{
 			page:        ctx.DocumentContext().Document,
 			destination: url,
-			text:        label,
+			text:        hstring.RenderedString(label),
 			plainText:   label,
 		},
 	)
@@ -366,7 +367,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
 			page:             ctx.DocumentContext().Document,
 			level:            n.Level,
 			anchor:           string(anchor),
-			text:             string(text),
+			text:             hstring.RenderedString(text),
 			plainText:        string(n.Text(source)),
 			AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
 		},
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 48cfaffffe0..498c0d674c5 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -20,7 +20,6 @@ import (
 
 	"github.com/alecthomas/chroma/lexers"
 	"github.com/gohugoio/hugo/cache/namedmemcache"
-	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/gohugoio/hugo/markup/highlight"
 
@@ -119,20 +118,18 @@ func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) {
 
 // Markdownify renders a given input from Markdown to HTML.
 func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
-	defer herrors.Recover()
-	ss, err := cast.ToStringE(s)
-	if err != nil {
-		return "", err
-	}
 
 	home := ns.deps.Site.Home()
 	if home == nil {
 		panic("home must not be nil")
 	}
-	sss, err := home.RenderString(ss)
+	ss, err := home.RenderString(s)
+	if err != nil {
+		return "", err
+	}
 
 	// Strip if this is a short inline type of text.
-	bb := ns.deps.ContentSpec.TrimShortHTML([]byte(sss))
+	bb := ns.deps.ContentSpec.TrimShortHTML([]byte(ss))
 
 	return helpers.BytesToHTML(bb), nil
 }


Test patch:
diff --git a/common/types/hstring/stringtypes_test.go b/common/types/hstring/stringtypes_test.go
new file mode 100644
index 00000000000..8ff477f6311
--- /dev/null
+++ b/common/types/hstring/stringtypes_test.go
@@ -0,0 +1,30 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hstring
+
+import (
+	"html/template"
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/spf13/cast"
+)
+
+func TestStringTypes(t *testing.T) {
+	c := qt.New(t)
+
+	// Validate that it will behave like a string in Hugo settings.
+	c.Assert(cast.ToString(RenderedString("Hugo")), qt.Equals, "Hugo")
+	c.Assert(template.HTML(RenderedString("Hugo")), qt.Equals, template.HTML("Hugo"))
+}
diff --git a/markup/goldmark/integration_test.go b/markup/goldmark/integration_test.go
index 89cd5bbb644..3e4764cac82 100644
--- a/markup/goldmark/integration_test.go
+++ b/markup/goldmark/integration_test.go
@@ -18,6 +18,8 @@ import (
 	"strings"
 	"testing"
 
+	qt "github.com/frankban/quicktest"
+
 	"github.com/gohugoio/hugo/hugolib"
 )
 
@@ -395,6 +397,49 @@ FENCE
 	}
 }
 
+// Iisse #8959
+func TestHookInfiniteRecursion(t *testing.T) {
+	t.Parallel()
+
+	for _, renderFunc := range []string{"markdownify", ".Page.RenderString"} {
+		t.Run(renderFunc, func(t *testing.T) {
+
+			files := `
+-- config.toml --
+-- layouts/_default/_markup/render-link.html --
+<a href="{{ .Destination | safeURL }}">{{ .Text | RENDERFUNC }}</a>	
+-- layouts/_default/single.html --
+{{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+https://example.org
+
+a@b.com
+		
+			
+			`
+
+			files = strings.ReplaceAll(files, "RENDERFUNC", renderFunc)
+
+			b, err := hugolib.NewIntegrationTestBuilder(
+				hugolib.IntegrationTestConfig{
+					T:           t,
+					TxtarString: files,
+				},
+			).BuildE()
+
+			b.Assert(err, qt.IsNotNil)
+			b.Assert(err.Error(), qt.Contains, "text is already rendered, repeating it may cause infinite recursion")
+
+		})
+
+	}
+
+}
+
 // Issue 9594
 func TestQuotesInImgAltAttr(t *testing.T) {
 	t.Parallel()