From: Eli W. Hunter Date: Sat, 14 Mar 2020 14:43:10 +0000 (-0400) Subject: Add render template hooks for headings X-Git-Tag: v0.71.0~10 X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=423b8f2fb834139cf31514b14b1c1bf28e43b384;p=brevno-suite%2Fhugo Add render template hooks for headings This commit also * Renames previous types to be non-specific. (e.g. hookedRenderer rather than linkRenderer) Resolves #6713 --- diff --git a/hugolib/page.go b/hugolib/page.go index fddc25fa..bd518c1e 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 d7841f17..77a01801 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 5688b5fa..34671443 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 35377582..df4bad95 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 5dfb09e2..ab26a6f1 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_hooks.go b/markup/goldmark/render_hooks.go new file mode 100644 index 00000000..aaae68e7 --- /dev/null +++ b/markup/goldmark/render_hooks.go @@ -0,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("`)
+	_, _ = w.Write(n.Text(source))
+	_ = w.WriteByte('") + } 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("') + } else { + _, _ = w.WriteString("") + } + 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("') + } else { + _, _ = 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), + )) +} diff --git a/markup/goldmark/render_link.go b/markup/goldmark/render_link.go deleted file mode 100644 index c0269bed..00000000 --- a/markup/goldmark/render_link.go +++ /dev/null @@ -1,224 +0,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("`)
-	_, _ = w.Write(n.Text(source))
-	_ = w.WriteByte('") - } 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("') - } else { - _, _ = w.WriteString("") - } - 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), - )) -}