From 423b8f2fb834139cf31514b14b1c1bf28e43b384 Mon Sep 17 00:00:00 2001 From: "Eli W. Hunter" Date: Sat, 14 Mar 2020 10:43:10 -0400 Subject: [PATCH] Add render template hooks for headings This commit also * Renames previous types to be non-specific. (e.g. hookedRenderer rather than linkRenderer) Resolves #6713 --- hugolib/page.go | 46 ++--- hugolib/page__per_output.go | 2 +- hugolib/site.go | 12 +- markup/converter/converter.go | 2 +- markup/converter/hooks/hooks.go | 47 +++-- .../{render_link.go => render_hooks.go} | 168 ++++++++++++++---- 6 files changed, 208 insertions(+), 69 deletions(-) rename markup/goldmark/{render_link.go => render_hooks.go} (58%) 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 } -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 @@ func (r *Render) Eq(other interface{}) bool { return false } + if r.HeadingRenderer.GetIdentity() != ro.HeadingRenderer.GetIdentity() { + return false + } + return true } - -type LinkRenderer interface { - Render(w io.Writer, ctx LinkContext) error - identity.Provider -} diff --git a/markup/goldmark/render_link.go b/markup/goldmark/render_hooks.go similarity index 58% rename from markup/goldmark/render_link.go rename to 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,31 +152,9 @@ func (r *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node 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) { +func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Image) - var h *hooks.Render + var h *hooks.Renderers ctx, ok := w.(*renderContext) if ok { @@ -156,7 +175,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.ImageRenderer.RenderLink( w, linkContext{ page: ctx.DocumentContext().Document, @@ -173,9 +192,31 @@ func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Nod } -func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { +// 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) - var h *hooks.Render + 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 { @@ -196,7 +237,7 @@ func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node text := ctx.Buffer.Bytes()[ctx.pos:] ctx.Buffer.Truncate(ctx.pos) - err := h.LinkRenderer.Render( + err := h.LinkRenderer.RenderLink( w, linkContext{ page: ctx.DocumentContext().Document, @@ -210,7 +251,66 @@ func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node 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 {