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:
Bjørn Erik Pedersen
2022-02-17 13:04:00 +01:00
parent 2c20f5bc00
commit 08fdca9d93
73 changed files with 1887 additions and 1986 deletions

View File

@@ -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 {