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