summaryrefslogtreecommitdiffstats
path: root/markup/highlight
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-02-17 13:04:00 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-02-24 18:59:50 +0100
commit08fdca9d9365eaf1e496a12e2af5e18617bd0e66 (patch)
tree6c6942d1b74a4160d93a997860bafd52b92025f5 /markup/highlight
parent2c20f5bc00b604e72b3b7e401fbdbf9447fe3470 (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.go99
-rw-r--r--markup/highlight/highlight.go178
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>")
+}