mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
Add render template hooks for headings
This commit also * Renames previous types to be non-specific. (e.g. hookedRenderer rather than linkRenderer) Resolves #6713
This commit is contained in:
parent
991934497e
commit
423b8f2fb8
6 changed files with 208 additions and 69 deletions
|
@ -375,48 +375,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
|
||||||
return nil
|
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 := p.getLayoutDescriptor()
|
||||||
layoutDescriptor.RenderingHook = true
|
layoutDescriptor.RenderingHook = true
|
||||||
layoutDescriptor.LayoutOverride = false
|
layoutDescriptor.LayoutOverride = false
|
||||||
layoutDescriptor.Layout = ""
|
layoutDescriptor.Layout = ""
|
||||||
|
|
||||||
|
var renderers hooks.Renderers
|
||||||
|
|
||||||
layoutDescriptor.Kind = "render-link"
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if templFound {
|
||||||
|
renderers.LinkRenderer = hookRenderer{
|
||||||
|
templateHandler: p.s.Tmpl(),
|
||||||
|
Provider: templ.(tpl.Info),
|
||||||
|
templ: templ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
layoutDescriptor.Kind = "render-image"
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if templFound {
|
||||||
var linkRenderer hooks.LinkRenderer
|
renderers.ImageRenderer = hookRenderer{
|
||||||
var imageRenderer hooks.LinkRenderer
|
|
||||||
|
|
||||||
if linkTemplFound {
|
|
||||||
linkRenderer = contentLinkRenderer{
|
|
||||||
templateHandler: p.s.Tmpl(),
|
templateHandler: p.s.Tmpl(),
|
||||||
Provider: linkTempl.(tpl.Info),
|
Provider: templ.(tpl.Info),
|
||||||
templ: linkTempl,
|
templ: templ,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if imgTemplFound {
|
layoutDescriptor.Kind = "render-heading"
|
||||||
imageRenderer = contentLinkRenderer{
|
templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if templFound {
|
||||||
|
renderers.HeadingRenderer = hookRenderer{
|
||||||
templateHandler: p.s.Tmpl(),
|
templateHandler: p.s.Tmpl(),
|
||||||
Provider: imgTempl.(tpl.Info),
|
Provider: templ.(tpl.Info),
|
||||||
templ: imgTempl,
|
templ: templ,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &hooks.Render{
|
return &renderers, nil
|
||||||
LinkRenderer: linkRenderer,
|
|
||||||
ImageRenderer: imageRenderer,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
|
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
|
||||||
|
|
|
@ -245,7 +245,7 @@ type pageContentOutput struct {
|
||||||
placeholdersEnabledInit sync.Once
|
placeholdersEnabledInit sync.Once
|
||||||
|
|
||||||
// May be nil.
|
// May be nil.
|
||||||
renderHooks *hooks.Render
|
renderHooks *hooks.Renderers
|
||||||
// Set if there are more than one output format variant
|
// Set if there are more than one output format variant
|
||||||
renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
|
renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
|
||||||
|
|
||||||
|
|
|
@ -1650,14 +1650,20 @@ var infoOnMissingLayout = map[string]bool{
|
||||||
"404": true,
|
"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
|
templateHandler tpl.TemplateHandler
|
||||||
identity.Provider
|
identity.Provider
|
||||||
templ tpl.Template
|
templ tpl.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error {
|
func (hr hookRenderer) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
|
||||||
return r.templateHandler.Execute(r.templ, w, ctx)
|
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) {
|
func (s *Site) renderForTemplate(name, outputFormat string, d interface{}, w io.Writer, templ tpl.Template) (err error) {
|
||||||
|
|
|
@ -126,7 +126,7 @@ type DocumentContext struct {
|
||||||
type RenderContext struct {
|
type RenderContext struct {
|
||||||
Src []byte
|
Src []byte
|
||||||
RenderTOC bool
|
RenderTOC bool
|
||||||
RenderHooks *hooks.Render
|
RenderHooks *hooks.Renderers
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -27,13 +27,41 @@ type LinkContext interface {
|
||||||
PlainText() string
|
PlainText() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Render struct {
|
type LinkRenderer interface {
|
||||||
LinkRenderer LinkRenderer
|
RenderLink(w io.Writer, ctx LinkContext) error
|
||||||
ImageRenderer LinkRenderer
|
identity.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Render) Eq(other interface{}) bool {
|
// HeadingContext contains accessors to all attributes that a HeadingRenderer
|
||||||
ro, ok := other.(*Render)
|
// 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 {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -49,10 +77,9 @@ func (r *Render) Eq(other interface{}) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.HeadingRenderer.GetIdentity() != ro.HeadingRenderer.GetIdentity() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type LinkRenderer interface {
|
|
||||||
Render(w io.Writer, ctx LinkContext) error
|
|
||||||
identity.Provider
|
|
||||||
}
|
|
||||||
|
|
|
@ -23,10 +23,10 @@ import (
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ renderer.SetOptioner = (*linkRenderer)(nil)
|
var _ renderer.SetOptioner = (*hookedRenderer)(nil)
|
||||||
|
|
||||||
func newLinkRenderer() renderer.NodeRenderer {
|
func newLinkRenderer() renderer.NodeRenderer {
|
||||||
r := &linkRenderer{
|
r := &hookedRenderer{
|
||||||
Config: html.Config{
|
Config: html.Config{
|
||||||
Writer: html.DefaultWriter,
|
Writer: html.DefaultWriter,
|
||||||
},
|
},
|
||||||
|
@ -70,23 +70,64 @@ func (ctx linkContext) Title() string {
|
||||||
return ctx.title
|
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
|
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)
|
r.Config.SetOption(name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
|
// 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.KindLink, r.renderLink)
|
||||||
reg.Register(ast.KindImage, r.renderImage)
|
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:
|
// Fall back to the default Goldmark render funcs. Method below borrowed from:
|
||||||
// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
|
// 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 {
|
if !entering {
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
@ -111,31 +152,9 @@ func (r *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node
|
||||||
return ast.WalkSkipChildren, nil
|
return ast.WalkSkipChildren, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to the default Goldmark render funcs. Method below borrowed from:
|
func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
// 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)
|
n := node.(*ast.Image)
|
||||||
var h *hooks.Render
|
var h *hooks.Renderers
|
||||||
|
|
||||||
ctx, ok := w.(*renderContext)
|
ctx, ok := w.(*renderContext)
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -156,7 +175,7 @@ func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Nod
|
||||||
text := ctx.Buffer.Bytes()[ctx.pos:]
|
text := ctx.Buffer.Bytes()[ctx.pos:]
|
||||||
ctx.Buffer.Truncate(ctx.pos)
|
ctx.Buffer.Truncate(ctx.pos)
|
||||||
|
|
||||||
err := h.ImageRenderer.Render(
|
err := h.ImageRenderer.RenderLink(
|
||||||
w,
|
w,
|
||||||
linkContext{
|
linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
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)
|
n := node.(*ast.Link)
|
||||||
var h *hooks.Render
|
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)
|
ctx, ok := w.(*renderContext)
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -196,7 +237,7 @@ func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node
|
||||||
text := ctx.Buffer.Bytes()[ctx.pos:]
|
text := ctx.Buffer.Bytes()[ctx.pos:]
|
||||||
ctx.Buffer.Truncate(ctx.pos)
|
ctx.Buffer.Truncate(ctx.pos)
|
||||||
|
|
||||||
err := h.LinkRenderer.Render(
|
err := h.LinkRenderer.RenderLink(
|
||||||
w,
|
w,
|
||||||
linkContext{
|
linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
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())
|
ctx.AddIdentity(h.LinkRenderer.GetIdentity())
|
||||||
|
|
||||||
return ast.WalkContinue, err
|
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 {
|
type links struct {
|
Loading…
Reference in a new issue