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),
- ))
-}
--
⑨