Repository: gohugoio/hugo (language: go)

Issue:
Quotation marks and apostrophes in image alt-text are double-escaped with Goldmark Typographer
### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.112.5-48e33e10af2feb2a0e1c32d04880ed016619f2d8 linux/amd64 BuildDate=2023-05-29T07:27:36Z VendorInfo=gohugoio
</pre>

### Does this issue reproduce with the latest release?

Yes

### Description

Quotation marks and apostrophes appearing in image alt-text are double-escaped.

The following Markdown:

```
!["They didn't even say 'hello'!" I exclaimed.](https://example.com/image.jpg)
```

is rendered as:

```
<p><img src="https://example.com/image.jpg" alt="&amp;ldquo;They didn&amp;rsquo;t even say &amp;lsquo;hello&amp;rsquo;!&amp;rdquo; I exclaimed."></p>
```

Expected output is:

```
<p><img src="https://example.com/image.jpg" alt="&ldquo;They didn&rsquo;t even say &lsquo;hello&rsquo;!&rdquo; I exclaimed."></p>
```

This is not a bug in Goldmark itself, as using that library directly produces the expected output. The bug actually was previously present in Goldmark ([GitHub issue](https://github.com/yuin/goldmark/issues/321)) but it has been fixed, so now it appears that this bug now has to do with how Hugo handles the data.


Discussion / hints:
Confirmed that Goldmark outptut is correct:

<details>
<summary>main.go</summary>

```text
import (
	"bytes"
	"fmt"

	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/extension"
)

func main() {
	md := goldmark.New(
		goldmark.WithExtensions(
			extension.Typographer,
		),
	)

	input := `![He said, "Eat at Joe's."](x)`

	var buf bytes.Buffer
	if err := md.Convert([]byte(input), &buf); err != nil {
		panic(err)
	}

	fmt.Println(buf.String())

}
```
</details>
<br>

Confirmed that this is not Go's [html/template](https://pkg.go.dev/html/template) package doing its thing by setting:

```text
[outputFormats.html]
isPlainText = true
```

Note that this only affects the attribute value.

#### Markdown

```text
![He said, "Eat at Joe's."](x)

[He said, "Eat at Joe's."](x)
```

#### Rendered

```html
<img src="x" alt="He said, &amp;ldquo;Eat at Joe&amp;rsquo;s.&amp;rdquo;">

<a href="x">He said, &ldquo;Eat at Joe&rsquo;s.&rdquo;</a>
```

### Example

```text
git clone --single-branch -b hugo-github-issue-11045 https://github.com/jmooring/hugo-testing hugo-github-issue-11045
cd hugo-github-issue-11045
hugo server
```



Fix patch:
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index 0bd800dc0d9..60a3a1efee3 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -225,7 +225,7 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
 		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
 	}
 	_, _ = w.WriteString(`" alt="`)
-	_, _ = w.Write(util.EscapeHTML(n.Text(source)))
+	_, _ = w.Write(nodeToHTMLText(n, source))
 	_ = w.WriteByte('"')
 	if n.Title != nil {
 		_, _ = w.WriteString(` title="`)
@@ -475,3 +475,18 @@ func (e *links) Extend(m goldmark.Markdown) {
 		util.Prioritized(newLinkRenderer(e.cfg), 100),
 	))
 }
+
+// Borrowed from Goldmark.
+func nodeToHTMLText(n ast.Node, source []byte) []byte {
+	var buf bytes.Buffer
+	for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+		if s, ok := c.(*ast.String); ok && s.IsCode() {
+			buf.Write(s.Text(source))
+		} else if !c.HasChildren() {
+			buf.Write(util.EscapeHTML(c.Text(source)))
+		} else {
+			buf.Write(nodeToHTMLText(c, source))
+		}
+	}
+	return buf.Bytes()
+}


Test patch:
diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go
index 05279cd2c58..66652987705 100644
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -584,6 +584,29 @@ rightDoubleQuote = "&raquo;"
 	c.Assert(got, qt.Contains, "<p>A &laquo;quote&raquo; and &lsquo;another quote&rsquo; and a &laquo;quote with a &rsquo;nested&rsquo; quote&raquo; and a &lsquo;quote with a &laquo;nested&raquo; quote&rsquo; and an ellipsis&hellip;</p>\n")
 }
 
+// Issue #11045
+func TestTypographerImageAltText(t *testing.T) {
+	c := qt.New(t)
+
+	content := `
+!["They didn't even say 'hello'!" I exclaimed.](https://example.com/image.jpg)
+`
+
+	confStr := `
+[markup]
+[markup.goldmark]
+
+`
+
+	cfg := config.FromTOMLConfigString(confStr)
+	conf := testconfig.GetTestConfig(nil, cfg)
+
+	b := convert(c, conf, content)
+	got := string(b.Bytes())
+
+	c.Assert(got, qt.Contains, "&ldquo;They didn&rsquo;t even say &lsquo;hello&rsquo;!&rdquo; I exclaimed.")
+}
+
 func unsafeConf() config.AllProvider {
 	cfg := config.FromTOMLConfigString(`
 [markup]