mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-13 20:24:00 +02:00
Add Markdown diagrams and render hooks for code blocks
You can now create custom hook templates for code blocks, either one for all (`render-codeblock.html`) or for a given code language (e.g. `render-codeblock-go.html`). We also used this new hook to add support for diagrams in Hugo: * Goat (Go ASCII Tool) is built-in and enabled by default; just create a fenced code block with the language `goat` and start draw your Ascii diagrams. * Another popular alternative for diagrams in Markdown, Mermaid (supported by GitHub), can also be implemented with a simple template. See the Hugo documentation for more information. Updates #7765 Closes #9538 Fixes #9553 Fixes #8520 Fixes #6702 Fixes #9558
This commit is contained in:
@@ -16,11 +16,10 @@ package goldmark
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
||||
"github.com/gohugoio/hugo/markup/internal/attributes"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@@ -44,28 +43,6 @@ func newLinks() goldmark.Extender {
|
||||
return &links{}
|
||||
}
|
||||
|
||||
type attributesHolder struct {
|
||||
// What we get from Goldmark.
|
||||
astAttributes []ast.Attribute
|
||||
|
||||
// What we send to the the render hooks.
|
||||
attributesInit sync.Once
|
||||
attributes map[string]string
|
||||
}
|
||||
|
||||
func (a *attributesHolder) Attributes() map[string]string {
|
||||
a.attributesInit.Do(func() {
|
||||
a.attributes = make(map[string]string)
|
||||
for _, attr := range a.astAttributes {
|
||||
if strings.HasPrefix(string(attr.Name), "on") {
|
||||
continue
|
||||
}
|
||||
a.attributes[string(attr.Name)] = string(util.EscapeHTML(attr.Value.([]byte)))
|
||||
}
|
||||
})
|
||||
return a.attributes
|
||||
}
|
||||
|
||||
type linkContext struct {
|
||||
page interface{}
|
||||
destination string
|
||||
@@ -104,7 +81,7 @@ type headingContext struct {
|
||||
anchor string
|
||||
text string
|
||||
plainText string
|
||||
*attributesHolder
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
|
||||
func (ctx headingContext) Page() interface{} {
|
||||
@@ -143,52 +120,17 @@ func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
|
||||
reg.Register(ast.KindHeading, r.renderHeading)
|
||||
}
|
||||
|
||||
func (r *hookedRenderer) renderAttributesForNode(w util.BufWriter, node ast.Node) {
|
||||
renderAttributes(w, false, node.Attributes()...)
|
||||
}
|
||||
|
||||
// Attributes with special meaning that does not make sense to render in HTML.
|
||||
var attributeExcludes = map[string]bool{
|
||||
"hl_lines": true,
|
||||
"hl_style": true,
|
||||
"linenos": true,
|
||||
"linenostart": true,
|
||||
}
|
||||
|
||||
func renderAttributes(w util.BufWriter, skipClass bool, attributes ...ast.Attribute) {
|
||||
for _, attr := range attributes {
|
||||
if skipClass && bytes.Equal(attr.Name, []byte("class")) {
|
||||
continue
|
||||
}
|
||||
|
||||
a := strings.ToLower(string(attr.Name))
|
||||
if attributeExcludes[a] || strings.HasPrefix(a, "on") {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = w.WriteString(" ")
|
||||
_, _ = w.Write(attr.Name)
|
||||
_, _ = w.WriteString(`="`)
|
||||
|
||||
switch v := attr.Value.(type) {
|
||||
case []byte:
|
||||
_, _ = w.Write(util.EscapeHTML(v))
|
||||
default:
|
||||
w.WriteString(cast.ToString(v))
|
||||
}
|
||||
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
var lr hooks.LinkRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.ImageRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
lr = h.(hooks.LinkRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -197,15 +139,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.pushPos(ctx.Buffer.Len())
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.popPos()
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
|
||||
err := h.ImageRenderer.RenderLink(
|
||||
err := lr.RenderLink(
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -216,7 +158,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
},
|
||||
)
|
||||
|
||||
ctx.AddIdentity(h.ImageRenderer)
|
||||
ctx.AddIdentity(lr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -250,12 +192,15 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
|
||||
|
||||
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
|
||||
var lr hooks.LinkRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.LinkRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
lr = h.(hooks.LinkRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -264,15 +209,15 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.pushPos(ctx.Buffer.Len())
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.popPos()
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
|
||||
err := h.LinkRenderer.RenderLink(
|
||||
err := lr.RenderLink(
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -286,7 +231,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||
// TODO(bep) I have a working branch that fixes these rather confusing identity types,
|
||||
// but for now it's important that it's not .GetIdentity() that's added here,
|
||||
// to make sure we search the entire chain on changes.
|
||||
ctx.AddIdentity(h.LinkRenderer)
|
||||
ctx.AddIdentity(lr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -319,12 +264,15 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
}
|
||||
|
||||
n := node.(*ast.AutoLink)
|
||||
var h hooks.Renderers
|
||||
var lr hooks.LinkRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.LinkRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
lr = h.(hooks.LinkRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -337,7 +285,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
url = "mailto:" + url
|
||||
}
|
||||
|
||||
err := h.LinkRenderer.RenderLink(
|
||||
err := lr.RenderLink(
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -350,7 +298,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
// TODO(bep) I have a working branch that fixes these rather confusing identity types,
|
||||
// but for now it's important that it's not .GetIdentity() that's added here,
|
||||
// to make sure we search the entire chain on changes.
|
||||
ctx.AddIdentity(h.LinkRenderer)
|
||||
ctx.AddIdentity(lr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -383,12 +331,15 @@ func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte,
|
||||
|
||||
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
|
||||
var hr hooks.HeadingRenderer
|
||||
|
||||
ctx, ok := w.(*renderContext)
|
||||
ctx, ok := w.(*render.Context)
|
||||
if ok {
|
||||
h = ctx.RenderContext().RenderHooks
|
||||
ok = h.HeadingRenderer != nil
|
||||
h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
|
||||
ok = h != nil
|
||||
if ok {
|
||||
hr = h.(hooks.HeadingRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
@@ -397,11 +348,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.pushPos(ctx.Buffer.Len())
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.popPos()
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
// All ast.Heading nodes are guaranteed to have an attribute called "id"
|
||||
@@ -409,7 +360,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
anchori, _ := n.AttributeString("id")
|
||||
anchor := anchori.([]byte)
|
||||
|
||||
err := h.HeadingRenderer.RenderHeading(
|
||||
err := hr.RenderHeading(
|
||||
w,
|
||||
headingContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
@@ -417,11 +368,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
anchor: string(anchor),
|
||||
text: string(text),
|
||||
plainText: string(n.Text(source)),
|
||||
attributesHolder: &attributesHolder{astAttributes: n.Attributes()},
|
||||
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
|
||||
},
|
||||
)
|
||||
|
||||
ctx.AddIdentity(h.HeadingRenderer)
|
||||
ctx.AddIdentity(hr)
|
||||
|
||||
return ast.WalkContinue, err
|
||||
}
|
||||
@@ -432,7 +383,7 @@ func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, n
|
||||
_, _ = w.WriteString("<h")
|
||||
_ = w.WriteByte("0123456"[n.Level])
|
||||
if n.Attributes() != nil {
|
||||
r.renderAttributesForNode(w, node)
|
||||
attributes.RenderASTAttributes(w, node.Attributes()...)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user