summaryrefslogtreecommitdiffstats
path: root/markup/highlight
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-11-06 20:10:47 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-11-23 14:12:24 +0100
commitbfb9613a14ab2d93a4474e5486d22e52a9d5e2b3 (patch)
tree81c4dbd10505e952489e1dbcf1d7bafc88b57c28 /markup/highlight
parenta3fe5e5e35f311f22b6b4fc38abfcf64cd2c7d6f (diff)
Add Goldmark as the new default markdown handler
This commit adds the fast and CommonMark compliant Goldmark as the new default markdown handler in Hugo. If you want to continue using BlackFriday as the default for md/markdown extensions, you can use this configuration: ```toml [markup] defaultMarkdownHandler="blackfriday" ``` Fixes #5963 Fixes #1778 Fixes #6355
Diffstat (limited to 'markup/highlight')
-rw-r--r--markup/highlight/config.go188
-rw-r--r--markup/highlight/config_test.go59
-rw-r--r--markup/highlight/highlight.go132
-rw-r--r--markup/highlight/highlight_test.go87
-rw-r--r--markup/highlight/temphighlighting/highlighting.go512
-rw-r--r--markup/highlight/temphighlighting/highlighting_test.go335
6 files changed, 1313 insertions, 0 deletions
diff --git a/markup/highlight/config.go b/markup/highlight/config.go
new file mode 100644
index 000000000..56e38fd85
--- /dev/null
+++ b/markup/highlight/config.go
@@ -0,0 +1,188 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package highlight provides code highlighting.
+package highlight
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/alecthomas/chroma/formatters/html"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+var DefaultConfig = Config{
+ // The highlighter style to use.
+ // See https://xyproto.github.io/splash/docs/all.html
+ Style: "monokai",
+ LineNoStart: 1,
+ CodeFences: true,
+ NoClasses: true,
+ LineNumbersInTable: true,
+ TabWidth: 4,
+}
+
+//
+type Config struct {
+ Style string
+
+ CodeFences bool
+
+ // Use inline CSS styles.
+ NoClasses bool
+
+ // When set, line numbers will be printed.
+ LineNos bool
+ LineNumbersInTable bool
+
+ // Start the line numbers from this value (default is 1).
+ LineNoStart int
+
+ // A space separated list of line numbers, e.g. “3-8 10-20”.
+ Hl_Lines string
+
+ // TabWidth sets the number of characters for a tab. Defaults to 4.
+ TabWidth int
+}
+
+func (cfg Config) ToHTMLOptions() []html.Option {
+ var options = []html.Option{
+ html.TabWidth(cfg.TabWidth),
+ html.WithLineNumbers(cfg.LineNos),
+ html.BaseLineNumber(cfg.LineNoStart),
+ html.LineNumbersInTable(cfg.LineNumbersInTable),
+ html.WithClasses(!cfg.NoClasses),
+ }
+
+ if cfg.Hl_Lines != "" {
+ ranges, err := hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
+ if err == nil {
+ options = append(options, html.HighlightLines(ranges))
+ }
+ }
+
+ return options
+}
+
+func applyOptionsFromString(opts string, cfg *Config) error {
+ optsm, err := parseOptions(opts)
+ if err != nil {
+ return err
+ }
+ return mapstructure.WeakDecode(optsm, cfg)
+}
+
+// ApplyLegacyConfig applies legacy config from back when we had
+// Pygments.
+func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
+ if conf.Style == DefaultConfig.Style {
+ if s := cfg.GetString("pygmentsStyle"); s != "" {
+ conf.Style = s
+ }
+ }
+
+ if conf.NoClasses == DefaultConfig.NoClasses && cfg.IsSet("pygmentsUseClasses") {
+ conf.NoClasses = !cfg.GetBool("pygmentsUseClasses")
+ }
+
+ if conf.CodeFences == DefaultConfig.CodeFences && cfg.IsSet("pygmentsCodeFences") {
+ conf.CodeFences = cfg.GetBool("pygmentsCodeFences")
+ }
+
+ if cfg.IsSet("pygmentsOptions") {
+ if err := applyOptionsFromString(cfg.GetString("pygmentsOptions"), conf); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+func parseOptions(in string) (map[string]interface{}, error) {
+ in = strings.Trim(in, " ")
+ opts := make(map[string]interface{})
+
+ if in == "" {
+ return opts, nil
+ }
+
+ for _, v := range strings.Split(in, ",") {
+ keyVal := strings.Split(v, "=")
+ key := strings.ToLower(strings.Trim(keyVal[0], " "))
+ 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]
+ }
+ }
+
+ return opts, nil
+}
+
+// startLine compansates for https://github.com/alecthomas/chroma/issues/30
+func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
+ var ranges [][2]int
+ s = strings.TrimSpace(s)
+
+ if s == "" {
+ return ranges, nil
+ }
+
+ // Variants:
+ // 1 2 3 4
+ // 1-2 3-4
+ // 1-2 3
+ // 1 3-4
+ // 1 3-4
+ fields := strings.Split(s, " ")
+ for _, field := range fields {
+ field = strings.TrimSpace(field)
+ if field == "" {
+ continue
+ }
+ numbers := strings.Split(field, "-")
+ var r [2]int
+ first, err := strconv.Atoi(numbers[0])
+ if err != nil {
+ return ranges, err
+ }
+ first = first + startLine - 1
+ r[0] = first
+ if len(numbers) > 1 {
+ second, err := strconv.Atoi(numbers[1])
+ if err != nil {
+ return ranges, err
+ }
+ second = second + startLine - 1
+ r[1] = second
+ } else {
+ r[1] = first
+ }
+
+ ranges = append(ranges, r)
+ }
+ return ranges, nil
+
+}
diff --git a/markup/highlight/config_test.go b/markup/highlight/config_test.go
new file mode 100644
index 000000000..0d4bb2f97
--- /dev/null
+++ b/markup/highlight/config_test.go
@@ -0,0 +1,59 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package highlight provides code highlighting.
+package highlight
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestConfig(t *testing.T) {
+ c := qt.New(t)
+
+ c.Run("applyLegacyConfig", func(c *qt.C) {
+ v := viper.New()
+ v.Set("pygmentsStyle", "hugo")
+ v.Set("pygmentsUseClasses", false)
+ v.Set("pygmentsCodeFences", false)
+ v.Set("pygmentsOptions", "linenos=inline")
+
+ cfg := DefaultConfig
+ err := ApplyLegacyConfig(v, &cfg)
+ c.Assert(err, qt.IsNil)
+ c.Assert(cfg.Style, qt.Equals, "hugo")
+ c.Assert(cfg.NoClasses, qt.Equals, true)
+ c.Assert(cfg.CodeFences, qt.Equals, false)
+ c.Assert(cfg.LineNos, qt.Equals, true)
+ c.Assert(cfg.LineNumbersInTable, qt.Equals, false)
+
+ })
+
+ c.Run("parseOptions", func(c *qt.C) {
+ cfg := DefaultConfig
+ opts := "noclasses=true,linenos=inline,linenostart=32,hl_lines=3-8 10-20"
+ err := applyOptionsFromString(opts, &cfg)
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(cfg.NoClasses, qt.Equals, true)
+ c.Assert(cfg.LineNos, qt.Equals, true)
+ c.Assert(cfg.LineNumbersInTable, qt.Equals, false)
+ c.Assert(cfg.LineNoStart, qt.Equals, 32)
+ c.Assert(cfg.Hl_Lines, qt.Equals, "3-8 10-20")
+
+ })
+}
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
new file mode 100644
index 000000000..99a0fa154
--- /dev/null
+++ b/markup/highlight/highlight.go
@@ -0,0 +1,132 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package highlight
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/alecthomas/chroma"
+ "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/alecthomas/chroma/styles"
+
+ hl "github.com/gohugoio/hugo/markup/highlight/temphighlighting"
+)
+
+func New(cfg Config) Highlighter {
+ return Highlighter{
+ cfg: cfg,
+ }
+}
+
+type Highlighter struct {
+ cfg Config
+}
+
+func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) {
+ cfg := h.cfg
+ if optsStr != "" {
+ if err := applyOptionsFromString(optsStr, &cfg); err != nil {
+ return "", err
+ }
+ }
+ return highlight(code, lang, cfg)
+}
+
+func highlight(code, lang string, cfg Config) (string, error) {
+ w := &strings.Builder{}
+ var lexer chroma.Lexer
+ if lang != "" {
+ lexer = lexers.Get(lang)
+ }
+
+ if lexer == nil {
+ wrapper := getPreWrapper(lang)
+ fmt.Fprint(w, wrapper.Start(true, ""))
+ fmt.Fprint(w, code)
+ fmt.Fprint(w, wrapper.End(true))
+ return w.String(), nil
+ }
+
+ style := styles.Get(cfg.Style)
+ if style == nil {
+ style = styles.Fallback
+ }
+
+ iterator, err := lexer.Tokenise(nil, code)
+ if err != nil {
+ return "", err
+ }
+
+ options := cfg.ToHTMLOptions()
+ options = append(options, getHtmlPreWrapper(lang))
+
+ formatter := html.New(options...)
+
+ fmt.Fprintf(w, `<div class="highlight">`)
+ if err := formatter.Format(w, style, iterator); err != nil {
+ return "", err
+ }
+ fmt.Fprintf(w, `</div>`)
+
+ 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),
+ }
+ }
+}
+
+func getPreWrapper(language string) preWrapper {
+ return preWrapper{language: language}
+}
+func getHtmlPreWrapper(language string) html.Option {
+ return html.WithPreWrapper(getPreWrapper(language))
+}
+
+type preWrapper struct {
+ language string
+}
+
+func (p preWrapper) Start(code bool, styleAttr string) string {
+ w := &strings.Builder{}
+ fmt.Fprintf(w, "<pre%s>", styleAttr)
+ var language string
+ if code {
+ language = p.language
+ }
+ WriteCodeTag(w, language)
+ return w.String()
+}
+
+func WriteCodeTag(w io.Writer, language string) {
+ fmt.Fprint(w, "<code")
+ if language != "" {
+ fmt.Fprintf(w, " class=\"language-"+language+"\"")
+ fmt.Fprintf(w, " data-lang=\""+language+"\"")
+ }
+ fmt.Fprint(w, ">")
+}
+
+func (p preWrapper) End(code bool) string {
+ return "</code></pre>"
+}
diff --git a/markup/highlight/highlight_test.go b/markup/highlight/highlight_test.go
new file mode 100644
index 000000000..58bd9c119
--- /dev/null
+++ b/markup/highlight/highlight_test.go
@@ -0,0 +1,87 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package highlight provides code highlighting.
+package highlight
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestHighlight(t *testing.T) {
+ c := qt.New(t)
+
+ lines := `LINE1
+LINE2
+LINE3
+LINE4
+LINE5
+`
+
+ c.Run("Basic", func(c *qt.C) {
+ cfg := DefaultConfig
+ cfg.NoClasses = false
+ h := New(cfg)
+
+ result, _ := h.Highlight(`echo "Hugo Rocks!"`, "bash", "")
+ c.Assert(result, qt.Equals, `<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nb">echo</span> <span class="s2">&#34;Hugo Rocks!&#34;</span></code></pre></div>`)
+ result, _ = h.Highlight(`echo "Hugo Rocks!"`, "unknown", "")
+ c.Assert(result, qt.Equals, `<pre><code class="language-unknown" data-lang="unknown">echo "Hugo Rocks!"</code></pre>`)
+
+ })
+
+ c.Run("Highlight lines, default config", func(c *qt.C) {
+ cfg := DefaultConfig
+ cfg.NoClasses = false
+ h := New(cfg)
+
+ result, _ := h.Highlight(lines, "bash", "linenos=table,hl_lines=2 4-5,linenostart=3")
+ c.Assert(result, qt.Contains, "<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre class=\"chroma\"><code><span class")
+ c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">4")
+
+ result, _ = h.Highlight(lines, "bash", "linenos=inline,hl_lines=2")
+ c.Assert(result, qt.Contains, "<span class=\"ln\">2</span>LINE2\n</span>")
+ c.Assert(result, qt.Not(qt.Contains), "<table")
+
+ result, _ = h.Highlight(lines, "bash", "linenos=true,hl_lines=2")
+ c.Assert(result, qt.Contains, "<table")
+ c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">2\n</span>")
+ })
+
+ c.Run("Highlight lines, linenumbers default on", func(c *qt.C) {
+ cfg := DefaultConfig
+ cfg.NoClasses = false
+ cfg.LineNos = true
+ h := New(cfg)
+
+ result, _ := h.Highlight(lines, "bash", "")
+ c.Assert(result, qt.Contains, "<span class=\"lnt\">2\n</span>")
+ result, _ = h.Highlight(lines, "bash", "linenos=false,hl_lines=2")
+ c.Assert(result, qt.Not(qt.Contains), "class=\"lnt\"")
+ })
+
+ c.Run("Highlight lines, linenumbers default on, linenumbers in table default off", func(c *qt.C) {
+ cfg := DefaultConfig
+ cfg.NoClasses = false
+ cfg.LineNos = true
+ cfg.LineNumbersInTable = false
+ h := New(cfg)
+
+ result, _ := h.Highlight(lines, "bash", "")
+ c.Assert(result, qt.Contains, "<span class=\"ln\">2</span>LINE2\n<")
+ result, _ = h.Highlight(lines, "bash", "linenos=table")
+ c.Assert(result, qt.Contains, "<span class=\"lnt\">1\n</span>")
+ })
+}
diff --git a/markup/highlight/temphighlighting/highlighting.go b/markup/highlight/temphighlighting/highlighting.go
new file mode 100644
index 000000000..d2f16c506
--- /dev/null
+++ b/markup/highlight/temphighlighting/highlighting.go
@@ -0,0 +1,512 @@
+// package highlighting is a extension for the goldmark(http://github.com/yuin/goldmark).
+//
+// This extension adds syntax-highlighting to the fenced code blocks using
+// chroma(https://github.com/alecthomas/chroma).
+//
+// TODO(bep) this is a very temporary fork based on https://github.com/yuin/goldmark-highlighting/pull/10
+// MIT Licensed, Copyright Yusuke Inuzuka
+package temphighlighting
+
+import (
+ "bytes"
+ "io"
+ "strconv"
+ "strings"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+
+ "github.com/alecthomas/chroma"
+ chromahtml "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/alecthomas/chroma/styles"
+)
+
+// ImmutableAttributes is a read-only interface for ast.Attributes.
+type ImmutableAttributes interface {
+ // Get returns (value, true) if an attribute associated with given
+ // name exists, otherwise (nil, false)
+ Get(name []byte) (interface{}, bool)
+
+ // GetString returns (value, true) if an attribute associated with given
+ // name exists, otherwise (nil, false)
+ GetString(name string) (interface{}, bool)
+
+ // All returns all attributes.
+ All() []ast.Attribute
+}
+
+type immutableAttributes struct {
+ n ast.Node
+}
+
+func (a *immutableAttributes) Get(name []byte) (interface{}, bool) {
+ return a.n.Attribute(name)
+}
+
+func (a *immutableAttributes) GetString(name string) (interface{}, bool) {
+ return a.n.AttributeString(name)
+}
+
+func (a *immutableAttributes) All() []ast.Attribute {
+ if a.n.Attributes() == nil {
+ return []ast.Attribute{}
+ }
+ return a.n.Attributes()
+}
+
+// CodeBlockContext holds contextual information of code highlighting.
+type CodeBlockContext interface {
+ // Language returns (language, true) if specified, otherwise (nil, false).
+ Language() ([]byte, bool)
+
+ // Highlighted returns true if this code block can be highlighted, otherwise false.
+ Highlighted() bool
+
+ // Attributes return attributes of the code block.
+ Attributes() ImmutableAttributes
+}
+
+type codeBlockContext struct {
+ language []byte
+ highlighted bool
+ attributes ImmutableAttributes
+}
+
+func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext {
+ return &codeBlockContext{
+ language: language,
+ highlighted: highlighted,
+ attributes: attrs,
+ }
+}
+
+func (c *codeBlockContext) Language() ([]byte, bool) {
+ if c.language != nil {
+ return c.language, true
+ }
+ return nil, false
+}
+
+func (c *codeBlockContext) Highlighted() bool {
+ return c.highlighted
+}
+
+func (c *codeBlockContext) Attributes() ImmutableAttributes {
+ return c.attributes
+}
+
+// WrapperRenderer renders wrapper elements like div, pre, etc.
+type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool)
+
+// CodeBlockOptions creates Chroma options per code block.
+type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option
+
+// Config struct holds options for the extension.
+type Config struct {
+ html.Config
+
+ // Style is a highlighting style.
+ // Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters.
+ Style string
+
+ // FormatOptions is a option related to output formats.
+ // See https://github.com/alecthomas/chroma#the-html-formatter for details.
+ FormatOptions []chromahtml.Option
+
+ // CSSWriter is an io.Writer that will be used as CSS data output buffer.
+ // If WithClasses() is enabled, you can get CSS data corresponds to the style.
+ CSSWriter io.Writer
+
+ // CodeBlockOptions allows set Chroma options per code block.
+ CodeBlockOptions CodeBlockOptions
+
+ // WrapperRendererCodeBlockOptions allows you to change wrapper elements.
+ WrapperRenderer WrapperRenderer
+}
+
+// NewConfig returns a new Config with defaults.
+func NewConfig() Config {
+ return Config{
+ Config: html.NewConfig(),
+ Style: "github",
+ FormatOptions: []chromahtml.Option{},
+ CSSWriter: nil,
+ WrapperRenderer: nil,
+ CodeBlockOptions: nil,
+ }
+}
+
+// SetOption implements renderer.SetOptioner.
+func (c *Config) SetOption(name renderer.OptionName, value interface{}) {
+ switch name {
+ case optStyle:
+ c.Style = value.(string)
+ case optFormatOptions:
+ if value != nil {
+ c.FormatOptions = value.([]chromahtml.Option)
+ }
+ case optCSSWriter:
+ c.CSSWriter = value.(io.Writer)
+ case optWrapperRenderer:
+ c.WrapperRenderer = value.(WrapperRenderer)
+ case optCodeBlockOptions:
+ c.CodeBlockOptions = value.(CodeBlockOptions)
+ default:
+ c.Config.SetOption(name, value)
+ }
+}
+
+// Option interface is a functional option interface for the extension.
+type Option interface {
+ renderer.Option
+ // SetHighlightingOption sets given option to the extension.
+ SetHighlightingOption(*Config)
+}
+
+type withHTMLOptions struct {
+ value []html.Option
+}
+
+func (o *withHTMLOptions) SetConfig(c *renderer.Config) {
+ if o.value != nil {
+ for _, v := range o.value {
+ v.(renderer.Option).SetConfig(c)
+ }
+ }
+}
+
+func (o *withHTMLOptions) SetHighlightingOption(c *Config) {
+ if o.value != nil {
+ for _, v := range o.value {
+ v.SetHTMLOption(&c.Config)
+ }
+ }
+}
+
+// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
+func WithHTMLOptions(opts ...html.Option) Option {
+ return &withHTMLOptions{opts}
+}
+
+const optStyle renderer.OptionName = "HighlightingStyle"
+
+var highlightLinesAttrName = []byte("hl_lines")
+
+var styleAttrName = []byte("hl_style")
+var nohlAttrName = []byte("nohl")
+var linenosAttrName = []byte("linenos")
+var linenosTableAttrValue = []byte("table")
+var linenosInlineAttrValue = []byte("inline")
+var linenostartAttrName = []byte("linenostart")
+
+type withStyle struct {
+ value string
+}
+
+func (o *withStyle) SetConfig(c *renderer.Config) {
+ c.Options[optStyle] = o.value
+}
+
+func (o *withStyle) SetHighlightingOption(c *Config) {
+ c.Style = o.value
+}
+
+// WithStyle is a functional option that changes highlighting style.
+func WithStyle(style string) Option {
+ return &withStyle{style}
+}
+
+const optCSSWriter renderer.OptionName = "HighlightingCSSWriter"
+
+type withCSSWriter struct {
+ value io.Writer
+}
+
+func (o *withCSSWriter) SetConfig(c *renderer.Config) {
+ c.Options[optCSSWriter] = o.value
+}
+
+func (o *withCSSWriter) SetHighlightingOption(c *Config) {
+ c.CSSWriter = o.value
+}
+
+// WithCSSWriter is a functional option that sets io.Writer for CSS data.
+func WithCSSWriter(w io.Writer) Option {
+ return &withCSSWriter{w}
+}
+
+const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer"
+
+type withWrapperRenderer struct {
+ value WrapperRenderer
+}
+
+func (o *withWrapperRenderer) SetConfig(c *renderer.Config) {
+ c.Options[optWrapperRenderer] = o.value
+}
+
+func (o *withWrapperRenderer) SetHighlightingOption(c *Config) {
+ c.WrapperRenderer = o.value
+}
+
+// WithWrapperRenderer is a functional option that sets WrapperRenderer that
+// renders wrapper elements like div, pre, etc.
+func WithWrapperRenderer(w WrapperRenderer) Option {
+ return &withWrapperRenderer{w}
+}
+
+const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions"
+
+type withCodeBlockOptions struct {
+ value CodeBlockOptions
+}
+
+func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) {
+ c.Options[optWrapperRenderer] = o.value
+}
+
+func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) {
+ c.CodeBlockOptions = o.value
+}
+
+// WithCodeBlockOptions is a functional option that sets CodeBlockOptions that
+// allows setting Chroma options per code block.
+func WithCodeBlockOptions(c CodeBlockOptions) Option {
+ return &withCodeBlockOptions{value: c}
+}
+
+const optFormatOptions renderer.OptionName = "HighlightingFormatOptions"
+
+type withFormatOptions struct {
+ value []chromahtml.Option
+}
+
+func (o *withFormatOptions) SetConfig(c *renderer.Config) {
+ if _, ok := c.Options[optFormatOptions]; !ok {
+ c.Options[optFormatOptions] = []chromahtml.Option{}
+ }
+ c.Options[optStyle] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...)
+}
+
+func (o *withFormatOptions) SetHighlightingOption(c *Config) {
+ c.FormatOptions = append(c.FormatOptions, o.value...)
+}
+
+// WithFormatOptions is a functional option that wraps chroma HTML formatter options.
+func WithFormatOptions(opts ...chromahtml.Option) Option {
+ return &withFormatOptions{opts}
+}
+
+// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension.
+type HTMLRenderer struct {
+ Config
+}
+
+// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it.
+func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer {
+ r := &HTMLRenderer{
+ Config: NewConfig(),
+ }
+ for _, opt := range opts {
+ opt.SetHighlightingOption(&r.Config)
+ }
+ return r
+}
+
+// RegisterFuncs implements NodeRenderer.RegisterFuncs.
+func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
+}
+
+func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes {
+ if node.Attributes() != nil {
+ return &immutableAttributes{node}
+ }
+ if infostr != nil {
+ attrStartIdx := -1
+
+ for idx, char := range infostr {
+ if char == '{' {
+ attrStartIdx = idx
+ break
+ }
+ }
+ if attrStartIdx > 0 {
+ n := ast.NewTextBlock() // dummy node for storing attributes
+ attrStr := infostr[attrStartIdx:]
+ if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr {
+ for _, attr := range attrs {
+ n.SetAttribute(attr.Name, attr.Value)
+ }
+ return &immutableAttributes{n}
+ }
+ }
+ }
+ return nil
+}
+
+func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.FencedCodeBlock)
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ language := n.Language(source)
+
+ chromaFormatterOptions := make([]chromahtml.Option, len(r.FormatOptions))
+ copy(chromaFormatterOptions, r.FormatOptions)
+ style := styles.Get(r.Style)
+ nohl := false
+
+ var info []byte
+ if n.Info != nil {
+ info = n.Info.Segment.Value(source)
+ }
+ attrs := getAttributes(n, info)
+ if attrs != nil {
+ baseLineNumber := 1
+ if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok {
+ baseLineNumber = int(linenostartAttr.(float64))
+ chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber))
+ }
+ if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr {
+ if lines, ok := linesAttr.([]interface{}); ok {
+ var hlRanges [][2]int
+ for _, l := range lines {
+ if ln, ok := l.(float64); ok {
+ hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1})
+ }
+ if rng, ok := l.([]uint8); ok {
+ slices := strings.Split(string([]byte(rng)), "-")
+ lhs, err := strconv.Atoi(slices[0])
+ if err != nil {
+ continue
+ }
+ rhs := lhs
+ if len(slices) > 1 {
+ rhs, err = strconv.Atoi(slices[1])
+ if err != nil {
+ continue
+ }
+ }
+ hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1})
+ }
+ }
+ chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges))
+ }
+ }
+ if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr {
+ styleStr := string([]byte(styleAttr.([]uint8)))
+ style = styles.Get(styleStr)
+ }
+ if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr {
+ nohl = true
+ }
+
+ if linenosAttr, ok := attrs.Get(linenosAttrName); ok {
+ switch v := linenosAttr.(type) {
+ case bool:
+ chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v))
+ case []uint8:
+ if v != nil {
+ chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true))
+