mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -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
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -126,7 +126,7 @@ type DocumentContext struct {
|
|||
type RenderContext struct {
|
||||
Src []byte
|
||||
RenderTOC bool
|
||||
RenderHooks *hooks.Render
|
||||
RenderHooks *hooks.Renderers
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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("<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) {
|
||||
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("<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 {
|
||||
|
@ -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("<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 {
|
Loading…
Reference in a new issue