summaryrefslogtreecommitdiffstats
path: root/markup
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-12-03 12:33:48 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-12-05 15:00:47 +0100
commit63126c6359a693345a3a81b22e0f95660b248044 (patch)
tree7f2fdf6af58735564cf3be57efbdad594150fa05 /markup
parent535ea8cc9bf20b1ba6f656f9f3eee3818c6dc537 (diff)
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
Diffstat (limited to 'markup')
-rw-r--r--markup/converter/hooks/hooks.go6
-rw-r--r--markup/goldmark/convert.go11
-rw-r--r--markup/goldmark/goldmark_config/config.go8
-rw-r--r--markup/goldmark/images/integration_test.go113
-rw-r--r--markup/goldmark/images/transform.go77
-rw-r--r--markup/goldmark/links/integration_test.go113
-rw-r--r--markup/goldmark/links/transform.go73
-rw-r--r--markup/goldmark/render_hooks.go89
-rw-r--r--markup/internal/attributes/attributes.go3
9 files changed, 469 insertions, 24 deletions
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index a8666bdf0..a59da939e 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -37,6 +37,12 @@ type LinkContext interface {
PlainText() string
}
+type ImageLinkContext interface {
+ LinkContext
+ IsBlock() bool
+ Ordinal() int
+}
+
type CodeblockContext interface {
AttributesProvider
text.Positioner
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
index ba85831b0..a179cd233 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -17,12 +17,12 @@ package goldmark
import (
"bytes"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
+ "github.com/gohugoio/hugo/markup/goldmark/images"
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
- "github.com/gohugoio/hugo/identity"
-
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/yuin/goldmark"
@@ -33,6 +33,10 @@ import (
"github.com/yuin/goldmark/text"
)
+const (
+ internalAttrPrefix = "_h__"
+)
+
// Provider is the package entry point.
var Provider converter.ProviderProvider = provide{}
@@ -92,6 +96,8 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
parserOptions []parser.Option
)
+ extensions = append(extensions, images.New(cfg.Parser.WrapStandAloneImageWithinParagraph))
+
if mcfg.Highlight.CodeFences {
extensions = append(extensions, codeblocks.New())
}
@@ -131,7 +137,6 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
if cfg.Parser.Attribute.Title {
parserOptions = append(parserOptions, parser.WithAttribute())
}
-
if cfg.Parser.Attribute.Block {
extensions = append(extensions, attributes.New())
}
diff --git a/markup/goldmark/goldmark_config/config.go b/markup/goldmark/goldmark_config/config.go
index a3238091b..ff0b6bbef 100644
--- a/markup/goldmark/goldmark_config/config.go
+++ b/markup/goldmark/goldmark_config/config.go
@@ -36,8 +36,9 @@ var Default = Config{
Unsafe: false,
},
Parser: Parser{
- AutoHeadingID: true,
- AutoHeadingIDType: AutoHeadingIDTypeGitHub,
+ AutoHeadingID: true,
+ AutoHeadingIDType: AutoHeadingIDTypeGitHub,
+ WrapStandAloneImageWithinParagraph: true,
Attribute: ParserAttribute{
Title: true,
Block: false,
@@ -88,6 +89,9 @@ type Parser struct {
// Enables custom attributes.
Attribute ParserAttribute
+
+ // Whether to wrap stand-alone images within a paragraph or not.
+ WrapStandAloneImageWithinParagraph bool
}
type ParserAttribute struct {
diff --git a/markup/goldmark/images/integration_test.go b/markup/goldmark/images/integration_test.go
new file mode 100644
index 000000000..e8d1b880e
--- /dev/null
+++ b/markup/goldmark/images/integration_test.go
@@ -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>")
+ })
+
+}
diff --git a/markup/goldmark/images/transform.go b/markup/goldmark/images/transform.go
new file mode 100644
index 000000000..4eb793ae1
--- /dev/null
+++ b/markup/goldmark/images/transform.go
@@ -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
+
+ })
+
+}
diff --git a/markup/goldmark/links/integration_test.go b/markup/goldmark/links/integration_test.go
new file mode 100644
index 000000000..20d4d74b1
--- /dev/null
+++ b/markup/goldmark/links/integration_test.go
@@ -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 }}|{{ .Ordinal }}" />
+</figure>
+{{ else }}
+ <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}|{{ .Ordinal }}" />
+{{ 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|0\" />\n. Some more text.</p>",
+ "<figure class=\"b\">\n\t<img src=\"/block.jpg\" alt=\"Block Image|1\" />",
+ )
+ })
+
+ 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>")
+ })
+
+}
diff --git a/markup/goldmark/links/transform.go b/markup/goldmark/links/transform.go
new file mode 100644
index 000000000..b4f6b6dc5
--- /dev/null
+++ b/markup/goldmark/links/transform.go
@@ -0,0 +1,73 @@
+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 (
+ linksExtension 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"
+)
+
+func New(wrapStandAloneImageWithinParagraph bool) goldmark.Extender {
+ return &linksExtension{wrapStandAloneImageWithinParagraph: wrapStandAloneImageWithinParagraph}
+}
+
+func (e *linksExtension) 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) {
+ 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()
+
+ 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
+
+ })
+
+}
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index e28f816d6..f36f9f4e6 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -20,6 +20,7 @@ import (
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
+ "github.com/gohugoio/hugo/markup/goldmark/images"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/gohugoio/hugo/markup/internal/attributes"
@@ -52,16 +53,13 @@ type linkContext struct {
title string
text hstring.RenderedString
plainText string
+ *attributes.AttributesHolder
}
func (ctx linkContext) Destination() string {
return ctx.destination
}
-func (ctx linkContext) Resolved() bool {
- return false
-}
-
func (ctx linkContext) Page() any {
return ctx.page
}
@@ -78,6 +76,20 @@ func (ctx linkContext) Title() string {
return ctx.title
}
+type imageLinkContext struct {
+ linkContext
+ ordinal int
+ isBlock bool
+}
+
+func (ctx imageLinkContext) IsBlock() bool {
+ return ctx.isBlock
+}
+
+func (ctx imageLinkContext) Ordinal() int {
+ return ctx.ordinal
+}
+
type headingContext struct {
page any
level int
@@ -151,14 +163,36 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos)
+ var (
+ isBlock bool
+ ordinal int
+ )
+ if b, ok := n.AttributeString(images.AttrIsBlock); ok && b.(bool) {
+ isBlock = true
+ }
+ if n, ok := n.AttributeString(images.AttrOrdinal); ok {
+ ordinal = n.(int)
+ }
+
+ // We use the attributes to signal from the parser whether the image is in
+ // a block context or not.
+ // We may find a better way to do that, but for now, we'll need to remove any
+ // internal attributes before rendering.
+ attrs := r.filterInternalAttributes(n.Attributes())
+
err := lr.RenderLink(
w,
- linkContext{
- page: ctx.DocumentContext().Document,
- destination: string(n.Destination),
- title: string(n.Title),
- text: hstring.RenderedString(text),
- plainText: string(n.Text(source)),
+ imageLinkContext{
+ linkContext: linkContext{
+ page: ctx.DocumentContext().Document,
+ destination: string(n.Destination),
+ title: string(n.Title),
+ text: hstring.RenderedString(text),
+ plainText: string(n.Text(source)),
+ AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
+ },
+ ordinal: ordinal,
+ isBlock: isBlock,
},
)
@@ -167,6 +201,17 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, err
}
+func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
+ n := 0
+ for _, x := range attrs {
+ if !bytes.HasPrefix(x.Name, []byte(internalAttrPrefix)) {
+ attrs[n] = x
+ n++
+ }
+ }
+ return attrs[:n]
+}
+
// Fall back to the default Goldmark render funcs. Method below borrowed from:
// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -186,6 +231,10 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
r.Writer.Write(w, n.Title)
_ = w.WriteByte('"')
}
+ if n.Attributes() != nil {
+ attrs := r.filterInternalAttributes(n.Attributes())
+ attributes.RenderASTAttributes(w, attrs...)
+ }
if r.XHTML {
_, _ = w.WriteString(" />")
} else {
@@ -224,11 +273,12 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
err := lr.RenderLink(
w,
linkContext{
- page: ctx.DocumentContext().Document,
- destination: string(n.Destination),
- title: string(n.Title),
- text: hstring.RenderedString(text),
- plainText: string(n.Text(source)),
+ page: ctx.DocumentContext().Document,
+ destination: string(n.Destination),
+ title: string(n.Title),
+ text: hstring.RenderedString(text),
+ plainText: string(n.Text(source)),
+ AttributesHolder: attributes.Empty,
},
)
@@ -292,10 +342,11 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
err := lr.RenderLink(
w,
linkContext{
- page: ctx.DocumentContext().Document,
- destination: url,
- text: hstring.RenderedString(label),
- plainText: label,
+ page: ctx.DocumentContext().Document,
+ destination: url,
+ text: hstring.RenderedString(label),
+ plainText: label,
+ AttributesHolder: attributes.Empty,
},
)
diff --git a/markup/internal/attributes/attributes.go b/markup/internal/attributes/attributes.go
index 688740983..a67b51acb 100644
--- a/markup/internal/attributes/attributes.go
+++ b/markup/internal/attributes/attributes.go
@@ -126,6 +126,9 @@ func (a Attribute) ValueString() string {
return cast.ToString(a.Value)
}
+// Empty holds no attributes.
+var Empty = &AttributesHolder{}
+
type AttributesHolder struct {
// What we get from Goldmark.
attributes []Attribute