mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-21 21:35:28 +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:
@@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/formatters/html"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
@@ -46,6 +47,9 @@ type Config struct {
|
||||
// Use inline CSS styles.
|
||||
NoClasses bool
|
||||
|
||||
// No highlighting.
|
||||
NoHl bool
|
||||
|
||||
// When set, line numbers will be printed.
|
||||
LineNos bool
|
||||
LineNumbersInTable bool
|
||||
@@ -60,6 +64,9 @@ type Config struct {
|
||||
// A space separated list of line numbers, e.g. “3-8 10-20”.
|
||||
Hl_Lines string
|
||||
|
||||
// A parsed and ready to use list of line ranges.
|
||||
HL_lines_parsed [][2]int
|
||||
|
||||
// TabWidth sets the number of characters for a tab. Defaults to 4.
|
||||
TabWidth int
|
||||
|
||||
@@ -80,9 +87,19 @@ func (cfg Config) ToHTMLOptions() []html.Option {
|
||||
html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
|
||||
}
|
||||
|
||||
if cfg.Hl_Lines != "" {
|
||||
ranges, err := hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
|
||||
if err == nil {
|
||||
if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
|
||||
var ranges [][2]int
|
||||
if cfg.HL_lines_parsed != nil {
|
||||
ranges = cfg.HL_lines_parsed
|
||||
} else {
|
||||
var err error
|
||||
ranges, err = hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
|
||||
if err != nil {
|
||||
ranges = nil
|
||||
}
|
||||
}
|
||||
|
||||
if ranges != nil {
|
||||
options = append(options, html.HighlightLines(ranges))
|
||||
}
|
||||
}
|
||||
@@ -90,14 +107,32 @@ func (cfg Config) ToHTMLOptions() []html.Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func applyOptions(opts interface{}, cfg *Config) error {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
switch vv := opts.(type) {
|
||||
case map[string]interface{}:
|
||||
return applyOptionsFromMap(vv, cfg)
|
||||
case string:
|
||||
return applyOptionsFromString(vv, cfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyOptionsFromString(opts string, cfg *Config) error {
|
||||
optsm, err := parseOptions(opts)
|
||||
optsm, err := parseHightlightOptions(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mapstructure.WeakDecode(optsm, cfg)
|
||||
}
|
||||
|
||||
func applyOptionsFromMap(optsm map[string]interface{}, cfg *Config) error {
|
||||
normalizeHighlightOptions(optsm)
|
||||
return mapstructure.WeakDecode(optsm, cfg)
|
||||
}
|
||||
|
||||
// ApplyLegacyConfig applies legacy config from back when we had
|
||||
// Pygments.
|
||||
func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
|
||||
@@ -128,7 +163,7 @@ func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOptions(in string) (map[string]interface{}, error) {
|
||||
func parseHightlightOptions(in string) (map[string]interface{}, error) {
|
||||
in = strings.Trim(in, " ")
|
||||
opts := make(map[string]interface{})
|
||||
|
||||
@@ -142,19 +177,57 @@ func parseOptions(in string) (map[string]interface{}, error) {
|
||||
if len(keyVal) != 2 {
|
||||
return opts, fmt.Errorf("invalid Highlight option: %s", key)
|
||||
}
|
||||
if key == "linenos" {
|
||||
opts[key] = keyVal[1] != "false"
|
||||
if keyVal[1] == "table" || keyVal[1] == "inline" {
|
||||
opts["lineNumbersInTable"] = keyVal[1] == "table"
|
||||
}
|
||||
} else {
|
||||
opts[key] = keyVal[1]
|
||||
}
|
||||
opts[key] = keyVal[1]
|
||||
|
||||
}
|
||||
|
||||
normalizeHighlightOptions(opts)
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func normalizeHighlightOptions(m map[string]interface{}) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
lineNosKey = "linenos"
|
||||
hlLinesKey = "hl_lines"
|
||||
linosStartKey = "linenostart"
|
||||
noHlKey = "nohl"
|
||||
)
|
||||
|
||||
baseLineNumber := 1
|
||||
if v, ok := m[linosStartKey]; ok {
|
||||
baseLineNumber = cast.ToInt(v)
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
switch k {
|
||||
case noHlKey:
|
||||
m[noHlKey] = cast.ToBool(v)
|
||||
case lineNosKey:
|
||||
if v == "table" || v == "inline" {
|
||||
m["lineNumbersInTable"] = v == "table"
|
||||
}
|
||||
if vs, ok := v.(string); ok {
|
||||
m[k] = vs != "false"
|
||||
}
|
||||
|
||||
case hlLinesKey:
|
||||
if hlRanges, ok := v.([][2]int); ok {
|
||||
for i := range hlRanges {
|
||||
hlRanges[i][0] += baseLineNumber
|
||||
hlRanges[i][1] += baseLineNumber
|
||||
}
|
||||
delete(m, k)
|
||||
m[k+"_parsed"] = hlRanges
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startLine compensates for https://github.com/alecthomas/chroma/issues/30
|
||||
func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
|
||||
var ranges [][2]int
|
||||
|
@@ -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>")
|
||||
}
|
||||
|
Reference in New Issue
Block a user