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,47 +16,155 @@ package highlight
import (
"fmt"
gohtml "html"
"html/template"
"io"
"strconv"
"strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
hl "github.com/yuin/goldmark-highlighting"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/internal/attributes"
)
// Markdown attributes used by the Chroma hightlighter.
var chromaHightlightProcessingAttributes = map[string]bool{
"anchorLineNos": true,
"guessSyntax": true,
"hl_Lines": true,
"lineAnchors": true,
"lineNos": true,
"lineNoStart": true,
"lineNumbersInTable": true,
"noClasses": true,
"style": true,
"tabWidth": true,
}
func init() {
for k, v := range chromaHightlightProcessingAttributes {
chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
}
}
func New(cfg Config) Highlighter {
return Highlighter{
return chromaHighlighter{
cfg: cfg,
}
}
type Highlighter struct {
type Highlighter interface {
Highlight(code, lang string, opts interface{}) (string, error)
HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error)
hooks.CodeBlockRenderer
}
type chromaHighlighter struct {
cfg Config
}
func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) {
if optsStr == "" {
return highlight(code, lang, h.cfg)
}
func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (string, error) {
cfg := h.cfg
if err := applyOptionsFromString(optsStr, &cfg); err != nil {
if err := applyOptions(opts, &cfg); err != nil {
return "", err
}
var b strings.Builder
if err := highlight(&b, code, lang, nil, cfg); err != nil {
return "", err
}
return highlight(code, lang, cfg)
return b.String(), nil
}
func highlight(code, lang string, cfg Config) (string, error) {
w := &strings.Builder{}
func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) {
cfg := h.cfg
var b strings.Builder
attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
options := ctx.Options()
if err := applyOptionsFromMap(options, &cfg); err != nil {
return HightlightResult{}, err
}
// Apply these last so the user can override them.
if err := applyOptions(opts, &cfg); err != nil {
return HightlightResult{}, err
}
err := highlight(&b, ctx.Code(), ctx.Lang(), attributes, cfg)
if err != nil {
return HightlightResult{}, err
}
return HightlightResult{
Body: template.HTML(b.String()),
}, nil
}
func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
cfg := h.cfg
attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil {
return err
}
return highlight(w, ctx.Code(), ctx.Lang(), attributes, cfg)
}
var id = identity.NewPathIdentity("chroma", "highlight")
func (h chromaHighlighter) GetIdentity() identity.Identity {
return id
}
type HightlightResult struct {
Body template.HTML
}
func (h HightlightResult) Highlighted() template.HTML {
return h.Body
}
func (h chromaHighlighter) toHighlightOptionsAttributes(ctx hooks.CodeblockContext) (map[string]interface{}, map[string]interface{}) {
attributes := ctx.Attributes()
if attributes == nil || len(attributes) == 0 {
return nil, nil
}
options := make(map[string]interface{})
attrs := make(map[string]interface{})
for k, v := range attributes {
klow := strings.ToLower(k)
if chromaHightlightProcessingAttributes[klow] {
options[klow] = v
} else {
attrs[k] = v
}
}
const lineanchorsKey = "lineanchors"
if _, found := options[lineanchorsKey]; !found {
// Set it to the ordinal.
options[lineanchorsKey] = strconv.Itoa(ctx.Ordinal())
}
return options, attrs
}
func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) error {
var lexer chroma.Lexer
if lang != "" {
lexer = lexers.Get(lang)
}
if lexer == nil && cfg.GuessSyntax {
if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
lexer = lexers.Analyse(code)
if lexer == nil {
lexer = lexers.Fallback
@@ -69,7 +177,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
fmt.Fprint(w, wrapper.Start(true, ""))
fmt.Fprint(w, gohtml.EscapeString(code))
fmt.Fprint(w, wrapper.End(true))
return w.String(), nil
return nil
}
style := styles.Get(cfg.Style)
@@ -80,7 +188,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
iterator, err := lexer.Tokenise(nil, code)
if err != nil {
return "", err
return err
}
options := cfg.ToHTMLOptions()
@@ -88,25 +196,13 @@ func highlight(code, lang string, cfg Config) (string, error) {
formatter := html.New(options...)
fmt.Fprint(w, `<div class="highlight">`)
writeDivStart(w, attributes)
if err := formatter.Format(w, style, iterator); err != nil {
return "", err
return err
}
fmt.Fprint(w, `</div>`)
writeDivEnd(w)
return w.String(), nil
}
func GetCodeBlockOptions() func(ctx hl.CodeBlockContext) []html.Option {
return func(ctx hl.CodeBlockContext) []html.Option {
var language string
if l, ok := ctx.Language(); ok {
language = string(l)
}
return []html.Option{
getHtmlPreWrapper(language),
}
}
return nil
}
func getPreWrapper(language string) preWrapper {
@@ -150,3 +246,25 @@ func (p preWrapper) End(code bool) string {
func WritePreEnd(w io.Writer) {
fmt.Fprint(w, preEnd)
}
func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
w.WriteString(`<div class="highlight`)
if attrs != nil {
for _, attr := range attrs {
if attr.Name == "class" {
w.WriteString(" " + attr.ValueString())
break
}
}
_, _ = w.WriteString("\"")
attributes.RenderAttributes(w, true, attrs...)
} else {
_, _ = w.WriteString("\"")
}
w.WriteString(">")
}
func writeDivEnd(w hugio.FlexiWriter) {
w.WriteString("</div>")
}