markup/goldmark: Add removeSurroundingParagraph for Markdown images

* Removes any surrounding paragraph nodes
* And transfers any attributes from the surrounding paragraph down to the image node
* Adds IsBlock and Ordinal (zero based) field to the image context passed to the image render hooks

IsBlock is set to true if `wrapStandAloneImageWithinParagraph = false` and  the image's parent node has only one child.

Closes #8362
Fixes #10492
Fixes #10494
Fixes #10501
This commit is contained in:
Bjørn Erik Pedersen
2022-12-03 12:33:48 +01:00
parent 535ea8cc9b
commit 63126c6359
9 changed files with 469 additions and 24 deletions

View File

@@ -0,0 +1,113 @@
package images_test
import (
"strings"
"testing"
"github.com/gohugoio/hugo/hugolib"
)
func TestDisableWrapStandAloneImageWithinParagraph(t *testing.T) {
t.Parallel()
filesTemplate := `
-- config.toml --
[markup.goldmark.renderer]
unsafe = false
[markup.goldmark.parser]
wrapStandAloneImageWithinParagraph = CONFIG_VALUE
[markup.goldmark.parser.attribute]
block = true
title = true
-- content/p1.md --
---
title: "p1"
---
This is an inline image: ![Inline Image](/inline.jpg). Some more text.
![Block Image](/block.jpg)
{.b}
-- layouts/_default/single.html --
{{ .Content }}
`
t.Run("With Hook, no wrap", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "false")
files = files + `-- layouts/_default/_markup/render-image.html --
{{ if .IsBlock }}
<figure class="{{ .Attributes.class }}">
<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
</figure>
{{ else }}
<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
{{ end }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: false,
},
).Build()
b.AssertFileContent("public/p1/index.html",
"This is an inline image: \n\t<img src=\"/inline.jpg\" alt=\"Inline Image\" />\n. Some more text.</p>",
"<figure class=\"b\">\n\t<img src=\"/block.jpg\" alt=\"Block Image\" />",
)
})
t.Run("With Hook, wrap", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "true")
files = files + `-- layouts/_default/_markup/render-image.html --
{{ if .IsBlock }}
<figure class="{{ .Attributes.class }}">
<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
</figure>
{{ else }}
<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" />
{{ end }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: false,
},
).Build()
b.AssertFileContent("public/p1/index.html",
"This is an inline image: \n\t<img src=\"/inline.jpg\" alt=\"Inline Image\" />\n. Some more text.</p>",
"<p class=\"b\">\n\t<img src=\"/block.jpg\" alt=\"Block Image\" />\n</p>",
)
})
t.Run("No Hook, no wrap", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "false")
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: false,
},
).Build()
b.AssertFileContent("public/p1/index.html", "<p>This is an inline image: <img src=\"/inline.jpg\" alt=\"Inline Image\">. Some more text.</p>\n<img src=\"/block.jpg\" alt=\"Block Image\" class=\"b\">")
})
t.Run("No Hook, wrap", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "true")
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: false,
},
).Build()
b.AssertFileContent("public/p1/index.html", "<p class=\"b\"><img src=\"/block.jpg\" alt=\"Block Image\"></p>")
})
}

View File

@@ -0,0 +1,77 @@
package images
import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type (
imagesExtension struct {
wrapStandAloneImageWithinParagraph bool
}
)
const (
// Used to signal to the rendering step that an image is used in a block context.
// Dont's change this; the prefix must match the internalAttrPrefix in the root goldmark package.
AttrIsBlock = "_h__isBlock"
AttrOrdinal = "_h__ordinal"
)
func New(wrapStandAloneImageWithinParagraph bool) goldmark.Extender {
return &imagesExtension{wrapStandAloneImageWithinParagraph: wrapStandAloneImageWithinParagraph}
}
func (e *imagesExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithASTTransformers(
util.Prioritized(&Transformer{wrapStandAloneImageWithinParagraph: e.wrapStandAloneImageWithinParagraph}, 300),
),
)
}
type Transformer struct {
wrapStandAloneImageWithinParagraph bool
}
// Transform transforms the provided Markdown AST.
func (t *Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser.Context) {
var ordinal int
ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
if n, ok := node.(*ast.Image); ok {
parent := n.Parent()
n.SetAttributeString(AttrOrdinal, ordinal)
ordinal++
if !t.wrapStandAloneImageWithinParagraph {
isBlock := parent.ChildCount() == 1
if isBlock {
n.SetAttributeString(AttrIsBlock, true)
}
if isBlock && parent.Kind() == ast.KindParagraph {
for _, attr := range parent.Attributes() {
// Transfer any attribute set down to the image.
// Image elements does not support attributes on its own,
// so it's safe to just set without checking first.
n.SetAttribute(attr.Name, attr.Value)
}
grandParent := parent.Parent()
grandParent.ReplaceChild(grandParent, parent, n)
}
}
}
return ast.WalkContinue, nil
})
}