diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2022-02-17 13:04:00 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2022-02-24 18:59:50 +0100 |
commit | 08fdca9d9365eaf1e496a12e2af5e18617bd0e66 (patch) | |
tree | 6c6942d1b74a4160d93a997860bafd52b92025f5 /markup/highlight | |
parent | 2c20f5bc00b604e72b3b7e401fbdbf9447fe3470 (diff) |
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
Diffstat (limited to 'markup/highlight')
-rw-r--r-- | markup/highlight/config.go | 99 | ||||
-rw-r--r-- | markup/highlight/highlight.go | 178 |
2 files changed, 234 insertions, 43 deletions
diff --git a/markup/highlight/config.go b/markup/highlight/config.go index 1dc1e28e2..86ac02c3d 100644 --- a/markup/highlight/config.go +++ b/markup/highlight/config.go @@ -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 diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go index 319426241..e9cbeb3c9 100644 --- a/markup/highlight/highlight.go +++ b/markup/highlight/highlight.go @@ -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 := applyOptions(opts, &cfg); err != nil { + return "", err } + var b strings.Builder - cfg := h.cfg - if err := applyOptionsFromString(optsStr, &cfg); err != nil { + 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>`) - - return w.String(), nil -} + writeDivEnd(w) -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>") +} |