summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEli W. Hunter <elihunter173@gmail.com>2020-03-14 10:43:10 -0400
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2020-05-15 21:12:43 +0200
commit423b8f2fb834139cf31514b14b1c1bf28e43b384 (patch)
treedcd2d3c2dfcc5e2960056462c2d048906b1acc55
parent991934497e88dcd4134a369a213bb5072c51c139 (diff)
Add render template hooks for headings
This commit also * Renames previous types to be non-specific. (e.g. hookedRenderer rather than linkRenderer) Resolves #6713
-rw-r--r--hugolib/page.go46
-rw-r--r--hugolib/page__per_output.go2
-rw-r--r--hugolib/site.go12
-rw-r--r--markup/converter/converter.go2
-rw-r--r--markup/converter/hooks/hooks.go47
-rw-r--r--markup/goldmark/render_hooks.go (renamed from markup/goldmark/render_link.go)156
6 files changed, 202 insertions, 63 deletions
diff --git a/hugolib/page.go b/hugolib/page.go
index fddc25fa0..bd518c1e1 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -375,48 +375,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
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 {
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index d7841f178..77a01801d 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -245,7 +245,7 @@ type pageContentOutput struct {
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
diff --git a/hugolib/site.go b/hugolib/site.go
index 5688b5fac..34671443e 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1650,14 +1650,20 @@ var infoOnMissingLayout = map[string]bool{
"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) {
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
index 353775826..df4bad95c 100644
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -126,7 +126,7 @@ type DocumentContext struct {
type RenderContext struct {
Src []byte
RenderTOC bool
- RenderHooks *hooks.Render
+ RenderHooks *hooks.Renderers
}
var (
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 5dfb09e2d..ab26a6f1a 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -27,13 +27,41 @@ type LinkContext interface {
PlainText() string
}
-type Render struct {
- LinkRenderer LinkRenderer
- ImageRenderer LinkRenderer
+type LinkRenderer interface {
+ RenderLink(w io.Writer, ctx LinkContext) error
+ identity.Provider
+}
+
+// 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
}
-func (r *Render) Eq(other interface{}) bool {
- ro, ok := other.(*Render)
+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 @@ func (r *Render) Eq(other interface{}) bool {
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
}
diff --git a/markup/goldmark/render_link.go b/markup/goldmark/render_hooks.go
index c0269bedf..aaae68e7f 100644
--- a/markup/goldmark/render_link.go
+++ b/markup/goldmark/render_hooks.go
@@ -23,10 +23,10 @@ import (
"github.com/yuin/goldmark/util"
)
-var _ renderer.SetOptioner = (*linkRenderer)(nil)
+var _ renderer.SetOptioner = (*hookedRenderer)(nil)
func newLinkRenderer() renderer.NodeRenderer {
- r := &linkRenderer{
+ r := &hookedRenderer{
Config: html.Config{
Writer: html.DefaultWriter,
},
@@ -70,23 +70,64 @@ func (ctx linkContext) Title() string {
return ctx.title
}
-type linkRenderer struct {
+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 *linkRenderer) SetOption(name renderer.OptionName, value interface{}) {
+func (r *hookedRenderer) SetOption(name renderer.OptionName, value interface{}) {
r.Config.SetOption(name, value)
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
-func (r *linkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+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 *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+func (r *hookedRenderer) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
@@ -111,9 +152,49 @@ func (r *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node
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 *linkRenderer) renderDefaultLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+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=\"")
@@ -133,18 +214,18 @@ func (r *linkRenderer) renderDefaultLink(w util.BufWriter, source []byte, node 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
+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.ImageRenderer != nil
+ ok = h != nil && h.LinkRenderer != nil
}
if !ok {
- return r.renderDefaultImage(w, source, node, entering)
+ return r.renderDefaultLink(w, source, node, entering)
}
if entering {
@@ -156,7 +237,7 @@ func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Nod
text := ctx.Buffer.Bytes()[ctx.pos:]
ctx.Buffer.Truncate(ctx.pos)
- err := h.ImageRenderer.Render(
+ err := h.LinkRenderer.RenderLink(
w,
linkContext{
page: ctx.DocumentContext().Document,
@@ -167,24 +248,40 @@ func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Nod
},
)
- ctx.AddIdentity(h.ImageRenderer.GetIdentity())
+ 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 *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- n := node.(*ast.Link)
- var h *hooks.Render
+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.LinkRenderer != nil
+ ok = h != nil && h.HeadingRenderer != nil
}
if !ok {
- return r.renderDefaultLink(w, source, node, entering)
+ return r.renderDefaultHeading(w, source, node, entering)
}
if entering {
@@ -195,22 +292,25 @@ func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node
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.LinkRenderer.Render(
+ err := h.HeadingRenderer.RenderHeading(
w,
- linkContext{
- page: ctx.DocumentContext().Document,
- destination: string(n.Destination),
- title: string(n.Title),
- text: string(text),
- plainText: string(n.Text(source)),
+ headingContext{
+ page: ctx.DocumentContext().Document,
+ level: n.Level,
+ anchor: string(anchor),
+ text: string(text),
+ plainText: string(n.Text(source)),
},
)
- ctx.AddIdentity(h.LinkRenderer.GetIdentity())
+ ctx.AddIdentity(h.HeadingRenderer.GetIdentity())
return ast.WalkContinue, err
-
}
type links struct {