shithub: hugo

Download patch

ref: 423b8f2fb834139cf31514b14b1c1bf28e43b384
parent: 991934497e88dcd4134a369a213bb5072c51c139
author: Eli W. Hunter <elihunter173@gmail.com>
date: Sat Mar 14 06:43:10 EDT 2020

Add render template hooks for headings

This commit also

* Renames previous types to be non-specific. (e.g. hookedRenderer rather
  than linkRenderer)

Resolves #6713

--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -375,48 +375,54 @@
 	return nil
 }
 
-func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) {
-
+func (p *pageState) createRenderHooks(f output.Format) (*hooks.Renderers, error) {
 	layoutDescriptor := p.getLayoutDescriptor()
 	layoutDescriptor.RenderingHook = true
 	layoutDescriptor.LayoutOverride = false
 	layoutDescriptor.Layout = ""
 
+	var renderers hooks.Renderers
+
 	layoutDescriptor.Kind = "render-link"
-	linkTempl, linkTemplFound, err := p.s.Tmpl().LookupLayout(layoutDescriptor, f)
+	templ, templFound, err := p.s.Tmpl().LookupLayout(layoutDescriptor, f)
 	if err != nil {
 		return nil, err
 	}
+	if templFound {
+		renderers.LinkRenderer = hookRenderer{
+			templateHandler: p.s.Tmpl(),
+			Provider:        templ.(tpl.Info),
+			templ:           templ,
+		}
+	}
 
 	layoutDescriptor.Kind = "render-image"
-	imgTempl, imgTemplFound, err := p.s.Tmpl().LookupLayout(layoutDescriptor, f)
+	templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
 	if err != nil {
 		return nil, err
 	}
-
-	var linkRenderer hooks.LinkRenderer
-	var imageRenderer hooks.LinkRenderer
-
-	if linkTemplFound {
-		linkRenderer = contentLinkRenderer{
+	if templFound {
+		renderers.ImageRenderer = hookRenderer{
 			templateHandler: p.s.Tmpl(),
-			Provider:        linkTempl.(tpl.Info),
-			templ:           linkTempl,
+			Provider:        templ.(tpl.Info),
+			templ:           templ,
 		}
 	}
 
-	if imgTemplFound {
-		imageRenderer = contentLinkRenderer{
+	layoutDescriptor.Kind = "render-heading"
+	templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
+	if err != nil {
+		return nil, err
+	}
+	if templFound {
+		renderers.HeadingRenderer = hookRenderer{
 			templateHandler: p.s.Tmpl(),
-			Provider:        imgTempl.(tpl.Info),
-			templ:           imgTempl,
+			Provider:        templ.(tpl.Info),
+			templ:           templ,
 		}
 	}
 
-	return &hooks.Render{
-		LinkRenderer:  linkRenderer,
-		ImageRenderer: imageRenderer,
-	}, nil
+	return &renderers, nil
 }
 
 func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -245,7 +245,7 @@
 	placeholdersEnabledInit sync.Once
 
 	// May be nil.
-	renderHooks *hooks.Render
+	renderHooks *hooks.Renderers
 	// Set if there are more than one output format variant
 	renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
 
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1650,14 +1650,20 @@
 	"404": true,
 }
 
-type contentLinkRenderer struct {
+// hookRenderer is the canonical implementation of all hooks.ITEMRenderer,
+// where ITEM is the thing being hooked.
+type hookRenderer struct {
 	templateHandler tpl.TemplateHandler
 	identity.Provider
 	templ tpl.Template
 }
 
-func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error {
-	return r.templateHandler.Execute(r.templ, w, ctx)
+func (hr hookRenderer) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
+	return hr.templateHandler.Execute(hr.templ, w, ctx)
+}
+
+func (hr hookRenderer) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
+	return hr.templateHandler.Execute(hr.templ, w, ctx)
 }
 
 func (s *Site) renderForTemplate(name, outputFormat string, d interface{}, w io.Writer, templ tpl.Template) (err error) {
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -126,7 +126,7 @@
 type RenderContext struct {
 	Src         []byte
 	RenderTOC   bool
-	RenderHooks *hooks.Render
+	RenderHooks *hooks.Renderers
 }
 
 var (
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -27,13 +27,41 @@
 	PlainText() string
 }
 
-type Render struct {
-	LinkRenderer  LinkRenderer
-	ImageRenderer LinkRenderer
+type LinkRenderer interface {
+	RenderLink(w io.Writer, ctx LinkContext) error
+	identity.Provider
 }
 
-func (r *Render) Eq(other interface{}) bool {
-	ro, ok := other.(*Render)
+// HeadingContext contains accessors to all attributes that a HeadingRenderer
+// can use to render a heading.
+type HeadingContext interface {
+	// Page is the page containing the heading.
+	Page() interface{}
+	// Level is the level of the header (i.e. 1 for top-level, 2 for sub-level, etc.).
+	Level() int
+	// Anchor is the HTML id assigned to the heading.
+	Anchor() string
+	// Text is the rendered (HTML) heading text, excluding the heading marker.
+	Text() string
+	// PlainText is the unrendered version of Text.
+	PlainText() string
+}
+
+// HeadingRenderer describes a uniquely identifiable rendering hook.
+type HeadingRenderer interface {
+	// Render writes the renderered content to w using the data in w.
+	RenderHeading(w io.Writer, ctx HeadingContext) error
+	identity.Provider
+}
+
+type Renderers struct {
+	LinkRenderer    LinkRenderer
+	ImageRenderer   LinkRenderer
+	HeadingRenderer HeadingRenderer
+}
+
+func (r *Renderers) Eq(other interface{}) bool {
+	ro, ok := other.(*Renderers)
 	if !ok {
 		return false
 	}
@@ -49,10 +77,9 @@
 		return false
 	}
 
-	return true
-}
+	if r.HeadingRenderer.GetIdentity() != ro.HeadingRenderer.GetIdentity() {
+		return false
+	}
 
-type LinkRenderer interface {
-	Render(w io.Writer, ctx LinkContext) error
-	identity.Provider
+	return true
 }
--- /dev/null
+++ b/markup/goldmark/render_hooks.go
@@ -1,0 +1,324 @@
+// Copyright 2019 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 goldmark
+
+import (
+	"github.com/gohugoio/hugo/markup/converter/hooks"
+
+	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/renderer"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/util"
+)
+
+var _ renderer.SetOptioner = (*hookedRenderer)(nil)
+
+func newLinkRenderer() renderer.NodeRenderer {
+	r := &hookedRenderer{
+		Config: html.Config{
+			Writer: html.DefaultWriter,
+		},
+	}
+	return r
+}
+
+func newLinks() goldmark.Extender {
+	return &links{}
+}
+
+type linkContext struct {
+	page        interface{}
+	destination string
+	title       string
+	text        string
+	plainText   string
+}
+
+func (ctx linkContext) Destination() string {
+	return ctx.destination
+}
+
+func (ctx linkContext) Resolved() bool {
+	return false
+}
+
+func (ctx linkContext) Page() interface{} {
+	return ctx.page
+}
+
+func (ctx linkContext) Text() string {
+	return ctx.text
+}
+
+func (ctx linkContext) PlainText() string {
+	return ctx.plainText
+}
+
+func (ctx linkContext) Title() string {
+	return ctx.title
+}
+
+type headingContext struct {
+	page      interface{}
+	level     int
+	anchor    string
+	text      string
+	plainText string
+}
+
+func (ctx headingContext) Page() interface{} {
+	return ctx.page
+}
+
+func (ctx headingContext) Level() int {
+	return ctx.level
+}
+
+func (ctx headingContext) Anchor() string {
+	return ctx.anchor
+}
+
+func (ctx headingContext) Text() string {
+	return ctx.text
+}
+
+func (ctx headingContext) PlainText() string {
+	return ctx.plainText
+}
+
+type hookedRenderer struct {
+	html.Config
+}
+
+func (r *hookedRenderer) SetOption(name renderer.OptionName, value interface{}) {
+	r.Config.SetOption(name, value)
+}
+
+// RegisterFuncs implements NodeRenderer.RegisterFuncs.
+func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+	reg.Register(ast.KindLink, r.renderLink)
+	reg.Register(ast.KindImage, r.renderImage)
+	reg.Register(ast.KindHeading, r.renderHeading)
+}
+
+// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
+func (r *hookedRenderer) RenderAttributes(w util.BufWriter, node ast.Node) {
+
+	for _, attr := range node.Attributes() {
+		_, _ = w.WriteString(" ")
+		_, _ = w.Write(attr.Name)
+		_, _ = w.WriteString(`="`)
+		_, _ = w.Write(util.EscapeHTML(attr.Value.([]byte)))
+		_ = w.WriteByte('"')
+	}
+}
+
+// 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) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+	n := node.(*ast.Image)
+	_, _ = w.WriteString("<img src=\"")
+	if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+	}
+	_, _ = w.WriteString(`" alt="`)
+	_, _ = w.Write(n.Text(source))
+	_ = w.WriteByte('"')
+	if n.Title != nil {
+		_, _ = w.WriteString(` title="`)
+		r.Writer.Write(w, n.Title)
+		_ = w.WriteByte('"')
+	}
+	if r.XHTML {
+		_, _ = w.WriteString(" />")
+	} else {
+		_, _ = w.WriteString(">")
+	}
+	return ast.WalkSkipChildren, nil
+}
+
+func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Image)
+	var h *hooks.Renderers
+
+	ctx, ok := w.(*renderContext)
+	if ok {
+		h = ctx.RenderContext().RenderHooks
+		ok = h != nil && h.ImageRenderer != nil
+	}
+
+	if !ok {
+		return r.renderDefaultImage(w, source, node, entering)
+	}
+
+	if entering {
+		// Store the current pos so we can capture the rendered text.
+		ctx.pos = ctx.Buffer.Len()
+		return ast.WalkContinue, nil
+	}
+
+	text := ctx.Buffer.Bytes()[ctx.pos:]
+	ctx.Buffer.Truncate(ctx.pos)
+
+	err := h.ImageRenderer.RenderLink(
+		w,
+		linkContext{
+			page:        ctx.DocumentContext().Document,
+			destination: string(n.Destination),
+			title:       string(n.Title),
+			text:        string(text),
+			plainText:   string(n.Text(source)),
+		},
+	)
+
+	ctx.AddIdentity(h.ImageRenderer.GetIdentity())
+
+	return ast.WalkContinue, err
+
+}
+
+// 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) renderDefaultLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Link)
+	if entering {
+		_, _ = w.WriteString("<a href=\"")
+		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+		}
+		_ = w.WriteByte('"')
+		if n.Title != nil {
+			_, _ = w.WriteString(` title="`)
+			r.Writer.Write(w, n.Title)
+			_ = w.WriteByte('"')
+		}
+		_ = w.WriteByte('>')
+	} else {
+		_, _ = w.WriteString("</a>")
+	}
+	return ast.WalkContinue, nil
+}
+
+func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Link)
+	var h *hooks.Renderers
+
+	ctx, ok := w.(*renderContext)
+	if ok {
+		h = ctx.RenderContext().RenderHooks
+		ok = h != nil && h.LinkRenderer != nil
+	}
+
+	if !ok {
+		return r.renderDefaultLink(w, source, node, entering)
+	}
+
+	if entering {
+		// Store the current pos so we can capture the rendered text.
+		ctx.pos = ctx.Buffer.Len()
+		return ast.WalkContinue, nil
+	}
+
+	text := ctx.Buffer.Bytes()[ctx.pos:]
+	ctx.Buffer.Truncate(ctx.pos)
+
+	err := h.LinkRenderer.RenderLink(
+		w,
+		linkContext{
+			page:        ctx.DocumentContext().Document,
+			destination: string(n.Destination),
+			title:       string(n.Title),
+			text:        string(text),
+			plainText:   string(n.Text(source)),
+		},
+	)
+
+	ctx.AddIdentity(h.LinkRenderer.GetIdentity())
+
+	return ast.WalkContinue, err
+}
+
+func (r *hookedRenderer) renderDefaultHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Heading)
+	if entering {
+		_, _ = w.WriteString("<h")
+		_ = w.WriteByte("0123456"[n.Level])
+		if n.Attributes() != nil {
+			r.RenderAttributes(w, node)
+		}
+		_ = w.WriteByte('>')
+	} else {
+		_, _ = w.WriteString("</h")
+		_ = w.WriteByte("0123456"[n.Level])
+		_, _ = w.WriteString(">\n")
+	}
+	return ast.WalkContinue, nil
+}
+
+func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Heading)
+	var h *hooks.Renderers
+
+	ctx, ok := w.(*renderContext)
+	if ok {
+		h = ctx.RenderContext().RenderHooks
+		ok = h != nil && h.HeadingRenderer != nil
+	}
+
+	if !ok {
+		return r.renderDefaultHeading(w, source, node, entering)
+	}
+
+	if entering {
+		// Store the current pos so we can capture the rendered text.
+		ctx.pos = ctx.Buffer.Len()
+		return ast.WalkContinue, nil
+	}
+
+	text := ctx.Buffer.Bytes()[ctx.pos:]
+	ctx.Buffer.Truncate(ctx.pos)
+	// All ast.Heading nodes are guaranteed to have an attribute called "id"
+	// that is an array of bytes that encode a valid string.
+	anchori, _ := n.AttributeString("id")
+	anchor := anchori.([]byte)
+
+	err := h.HeadingRenderer.RenderHeading(
+		w,
+		headingContext{
+			page:      ctx.DocumentContext().Document,
+			level:     n.Level,
+			anchor:    string(anchor),
+			text:      string(text),
+			plainText: string(n.Text(source)),
+		},
+	)
+
+	ctx.AddIdentity(h.HeadingRenderer.GetIdentity())
+
+	return ast.WalkContinue, err
+}
+
+type links struct {
+}
+
+// Extend implements goldmark.Extender.
+func (e *links) Extend(m goldmark.Markdown) {
+	m.Renderer().AddOptions(renderer.WithNodeRenderers(
+		util.Prioritized(newLinkRenderer(), 100),
+	))
+}
--- a/markup/goldmark/render_link.go
+++ /dev/null
@@ -1,224 +1,0 @@
-// Copyright 2019 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 goldmark
-
-import (
-	"github.com/gohugoio/hugo/markup/converter/hooks"
-
-	"github.com/yuin/goldmark"
-	"github.com/yuin/goldmark/ast"
-	"github.com/yuin/goldmark/renderer"
-	"github.com/yuin/goldmark/renderer/html"
-	"github.com/yuin/goldmark/util"
-)
-
-var _ renderer.SetOptioner = (*linkRenderer)(nil)
-
-func newLinkRenderer() renderer.NodeRenderer {
-	r := &linkRenderer{
-		Config: html.Config{
-			Writer: html.DefaultWriter,
-		},
-	}
-	return r
-}
-
-func newLinks() goldmark.Extender {
-	return &links{}
-}
-
-type linkContext struct {
-	page        interface{}
-	destination string
-	title       string
-	text        string
-	plainText   string
-}
-
-func (ctx linkContext) Destination() string {
-	return ctx.destination
-}
-
-func (ctx linkContext) Resolved() bool {
-	return false
-}
-
-func (ctx linkContext) Page() interface{} {
-	return ctx.page
-}
-
-func (ctx linkContext) Text() string {
-	return ctx.text
-}
-
-func (ctx linkContext) PlainText() string {
-	return ctx.plainText
-}
-
-func (ctx linkContext) Title() string {
-	return ctx.title
-}
-
-type linkRenderer struct {
-	html.Config
-}
-
-func (r *linkRenderer) SetOption(name renderer.OptionName, value interface{}) {
-	r.Config.SetOption(name, value)
-}
-
-// RegisterFuncs implements NodeRenderer.RegisterFuncs.
-func (r *linkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
-	reg.Register(ast.KindLink, r.renderLink)
-	reg.Register(ast.KindImage, r.renderImage)
-}
-
-// 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 *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	if !entering {
-		return ast.WalkContinue, nil
-	}
-	n := node.(*ast.Image)
-	_, _ = w.WriteString("<img src=\"")
-	if r.Unsafe || !html.IsDangerousURL(n.Destination) {
-		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
-	}
-	_, _ = w.WriteString(`" alt="`)
-	_, _ = w.Write(n.Text(source))
-	_ = w.WriteByte('"')
-	if n.Title != nil {
-		_, _ = w.WriteString(` title="`)
-		r.Writer.Write(w, n.Title)
-		_ = w.WriteByte('"')
-	}
-	if r.XHTML {
-		_, _ = w.WriteString(" />")
-	} else {
-		_, _ = w.WriteString(">")
-	}
-	return ast.WalkSkipChildren, nil
-}
-
-// 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 *linkRenderer) renderDefaultLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*ast.Link)
-	if entering {
-		_, _ = w.WriteString("<a href=\"")
-		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
-			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
-		}
-		_ = w.WriteByte('"')
-		if n.Title != nil {
-			_, _ = w.WriteString(` title="`)
-			r.Writer.Write(w, n.Title)
-			_ = w.WriteByte('"')
-		}
-		_ = w.WriteByte('>')
-	} else {
-		_, _ = w.WriteString("</a>")
-	}
-	return ast.WalkContinue, nil
-}
-
-func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*ast.Image)
-	var h *hooks.Render
-
-	ctx, ok := w.(*renderContext)
-	if ok {
-		h = ctx.RenderContext().RenderHooks
-		ok = h != nil && h.ImageRenderer != nil
-	}
-
-	if !ok {
-		return r.renderDefaultImage(w, source, node, entering)
-	}
-
-	if entering {
-		// Store the current pos so we can capture the rendered text.
-		ctx.pos = ctx.Buffer.Len()
-		return ast.WalkContinue, nil
-	}
-
-	text := ctx.Buffer.Bytes()[ctx.pos:]
-	ctx.Buffer.Truncate(ctx.pos)
-
-	err := h.ImageRenderer.Render(
-		w,
-		linkContext{
-			page:        ctx.DocumentContext().Document,
-			destination: string(n.Destination),
-			title:       string(n.Title),
-			text:        string(text),
-			plainText:   string(n.Text(source)),
-		},
-	)
-
-	ctx.AddIdentity(h.ImageRenderer.GetIdentity())
-
-	return ast.WalkContinue, err
-
-}
-
-func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
-	n := node.(*ast.Link)
-	var h *hooks.Render
-
-	ctx, ok := w.(*renderContext)
-	if ok {
-		h = ctx.RenderContext().RenderHooks
-		ok = h != nil && h.LinkRenderer != nil
-	}
-
-	if !ok {
-		return r.renderDefaultLink(w, source, node, entering)
-	}
-
-	if entering {
-		// Store the current pos so we can capture the rendered text.
-		ctx.pos = ctx.Buffer.Len()
-		return ast.WalkContinue, nil
-	}
-
-	text := ctx.Buffer.Bytes()[ctx.pos:]
-	ctx.Buffer.Truncate(ctx.pos)
-
-	err := h.LinkRenderer.Render(
-		w,
-		linkContext{
-			page:        ctx.DocumentContext().Document,
-			destination: string(n.Destination),
-			title:       string(n.Title),
-			text:        string(text),
-			plainText:   string(n.Text(source)),
-		},
-	)
-
-	ctx.AddIdentity(h.LinkRenderer.GetIdentity())
-
-	return ast.WalkContinue, err
-
-}
-
-type links struct {
-}
-
-// Extend implements goldmark.Extender.
-func (e *links) Extend(m goldmark.Markdown) {
-	m.Renderer().AddOptions(renderer.WithNodeRenderers(
-		util.Prioritized(newLinkRenderer(), 100),
-	))
-}