Repository: gohugoio/hugo (language: go)

Issue:
Decode typographer HTML entities in hook's .PlainText
This is the opposite take on #13291.

After fixing #13286 then

```
![A's is > B's](sunsets.jpg)
```

Will become `PlainText=`

```
A&rsquo;s is > B&rsquo;s
```

This will in a hook similar to Hugo's internal:

```handlebars
<img src="{{ $src }}" alt="{{ .PlainText }}"...
```

End up as


```
<img src="sunsets.jpg" alt="A&amp;rsquo;s is &;gt; B&amp;rsquo;s"></p>
```

Which is invalid.

Note that this is currently only (I think) an issue when using the typographer extension (default on) and having apostrophes in your image alt texts eg.

We really want `.PlainText` to be format agnostic, and the only current way I can think about fixing this, is to convert these HTML entities back to their real characters. Goldmark has an utility func for doing that.




Discussion / hints:

Fix patch:
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
index cd64fc94498..32304fafaa3 100644
--- a/markup/goldmark/internal/render/context.go
+++ b/markup/goldmark/internal/render/context.go
@@ -27,6 +27,7 @@ import (
 	"github.com/gohugoio/hugo/markup/converter"
 	"github.com/gohugoio/hugo/markup/converter/hooks"
 	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/util"
 )
 
 type BufWriter struct {
@@ -264,6 +265,8 @@ func (c *hookBase) PositionerSourceTarget() []byte {
 }
 
 // TextPlain returns a plain text representation of the given node.
+// This will resolve any lefover HTML entities. This will typically be
+// entities inserted by e.g. the typographer extension.
 // Goldmark's Node.Text was deprecated in 1.7.8.
 func TextPlain(n ast.Node, source []byte) string {
 	buf := bp.GetBuffer()
@@ -272,7 +275,7 @@ func TextPlain(n ast.Node, source []byte) string {
 	for c := n.FirstChild(); c != nil; c = c.NextSibling() {
 		textPlainTo(c, source, buf)
 	}
-	return buf.String()
+	return string(util.ResolveEntityNames(buf.Bytes()))
 }
 
 func textPlainTo(c ast.Node, source []byte, buf *bytes.Buffer) {
@@ -283,6 +286,8 @@ func textPlainTo(c ast.Node, source []byte, buf *bytes.Buffer) {
 	case *ast.RawHTML:
 		s := strings.TrimSpace(tpl.StripHTML(string(c.Segments.Value(source))))
 		buf.WriteString(s)
+	case *ast.String:
+		buf.Write(c.Value)
 	case *ast.Text:
 		buf.Write(c.Segment.Value(source))
 	default:


Test patch:
diff --git a/markup/goldmark/goldmark_integration_test.go b/markup/goldmark/goldmark_integration_test.go
index 67348d894d8..184d0d7fc8c 100644
--- a/markup/goldmark/goldmark_integration_test.go
+++ b/markup/goldmark/goldmark_integration_test.go
@@ -527,6 +527,39 @@ a <!-- b --> c
 	)
 }
 
+// Issue 13286
+func TestImageAltApostrophesWithTypographer(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+[markup.goldmark.extensions.typographer]
+disable = false
+ [markup.goldmark.renderHooks.image]
+ enableDefault = true
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Image
+
+![A's is > than B's](some-image.png)
+
+
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContentExact("public/p1/index.html",
+		// Note that this markup is slightly different than the one produced by the default Goldmark renderer,
+		// see issue 13292.
+		"alt=\"A’s is &gt; than B’s\">",
+	)
+}
+
 // Issue #7332
 // Issue #11587
 func TestGoldmarkEmojiExtension(t *testing.T) {