1// package highlighting is a extension for the goldmark(http://github.com/yuin/goldmark).
  2//
  3// This extension adds syntax-highlighting to the fenced code blocks using
  4// chroma(https://github.com/alecthomas/chroma).
  5package highlighting
  6
  7import (
  8	"bytes"
  9	"io"
 10	"strconv"
 11	"strings"
 12
 13	"github.com/yuin/goldmark"
 14	"github.com/yuin/goldmark/ast"
 15	"github.com/yuin/goldmark/parser"
 16	"github.com/yuin/goldmark/renderer"
 17	"github.com/yuin/goldmark/renderer/html"
 18	"github.com/yuin/goldmark/text"
 19	"github.com/yuin/goldmark/util"
 20
 21	"github.com/alecthomas/chroma/v2"
 22	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
 23	"github.com/alecthomas/chroma/v2/lexers"
 24	"github.com/alecthomas/chroma/v2/styles"
 25)
 26
 27// ImmutableAttributes is a read-only interface for ast.Attributes.
 28type ImmutableAttributes interface {
 29	// Get returns (value, true) if an attribute associated with given
 30	// name exists, otherwise (nil, false)
 31	Get(name []byte) (interface{}, bool)
 32
 33	// GetString returns (value, true) if an attribute associated with given
 34	// name exists, otherwise (nil, false)
 35	GetString(name string) (interface{}, bool)
 36
 37	// All returns all attributes.
 38	All() []ast.Attribute
 39}
 40
 41type immutableAttributes struct {
 42	n ast.Node
 43}
 44
 45func (a *immutableAttributes) Get(name []byte) (interface{}, bool) {
 46	return a.n.Attribute(name)
 47}
 48
 49func (a *immutableAttributes) GetString(name string) (interface{}, bool) {
 50	return a.n.AttributeString(name)
 51}
 52
 53func (a *immutableAttributes) All() []ast.Attribute {
 54	if a.n.Attributes() == nil {
 55		return []ast.Attribute{}
 56	}
 57	return a.n.Attributes()
 58}
 59
 60// CodeBlockContext holds contextual information of code highlighting.
 61type CodeBlockContext interface {
 62	// Language returns (language, true) if specified, otherwise (nil, false).
 63	Language() ([]byte, bool)
 64
 65	// Highlighted returns true if this code block can be highlighted, otherwise false.
 66	Highlighted() bool
 67
 68	// Attributes return attributes of the code block.
 69	Attributes() ImmutableAttributes
 70}
 71
 72type codeBlockContext struct {
 73	language    []byte
 74	highlighted bool
 75	attributes  ImmutableAttributes
 76}
 77
 78func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext {
 79	return &codeBlockContext{
 80		language:    language,
 81		highlighted: highlighted,
 82		attributes:  attrs,
 83	}
 84}
 85
 86func (c *codeBlockContext) Language() ([]byte, bool) {
 87	if c.language != nil {
 88		return c.language, true
 89	}
 90	return nil, false
 91}
 92
 93func (c *codeBlockContext) Highlighted() bool {
 94	return c.highlighted
 95}
 96
 97func (c *codeBlockContext) Attributes() ImmutableAttributes {
 98	return c.attributes
 99}
100
101// WrapperRenderer renders wrapper elements like div, pre, etc.
102type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool)
103
104// CodeBlockOptions creates Chroma options per code block.
105type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option
106
107// Config struct holds options for the extension.
108type Config struct {
109	html.Config
110
111	// Style is a highlighting style.
112	// Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters.
113	Style string
114
115	// Pass in a custom Chroma style. If this is not nil, the Style string will be ignored
116	CustomStyle *chroma.Style
117
118	// If set, will try to guess language if none provided.
119	// If the guessing fails, we will fall back to a text lexer.
120	// Note that while Chroma's API supports language guessing, the implementation
121	// is not there yet, so you will currently always get the basic text lexer.
122	GuessLanguage bool
123
124	// FormatOptions is a option related to output formats.
125	// See https://github.com/alecthomas/chroma#the-html-formatter for details.
126	FormatOptions []chromahtml.Option
127
128	// CSSWriter is an io.Writer that will be used as CSS data output buffer.
129	// If WithClasses() is enabled, you can get CSS data corresponds to the style.
130	CSSWriter io.Writer
131
132	// CodeBlockOptions allows set Chroma options per code block.
133	CodeBlockOptions CodeBlockOptions
134
135	// WrapperRenderer allows you to change wrapper elements.
136	WrapperRenderer WrapperRenderer
137}
138
139// NewConfig returns a new Config with defaults.
140func NewConfig() Config {
141	return Config{
142		Config:           html.NewConfig(),
143		Style:            "github",
144		FormatOptions:    []chromahtml.Option{},
145		CSSWriter:        nil,
146		WrapperRenderer:  nil,
147		CodeBlockOptions: nil,
148	}
149}
150
151// SetOption implements renderer.SetOptioner.
152func (c *Config) SetOption(name renderer.OptionName, value interface{}) {
153	switch name {
154	case optStyle:
155		c.Style = value.(string)
156	case optCustomStyle:
157		c.CustomStyle = value.(*chroma.Style)
158	case optFormatOptions:
159		if value != nil {
160			c.FormatOptions = value.([]chromahtml.Option)
161		}
162	case optCSSWriter:
163		c.CSSWriter = value.(io.Writer)
164	case optWrapperRenderer:
165		c.WrapperRenderer = value.(WrapperRenderer)
166	case optCodeBlockOptions:
167		c.CodeBlockOptions = value.(CodeBlockOptions)
168	case optGuessLanguage:
169		c.GuessLanguage = value.(bool)
170	default:
171		c.Config.SetOption(name, value)
172	}
173}
174
175// Option interface is a functional option interface for the extension.
176type Option interface {
177	renderer.Option
178	// SetHighlightingOption sets given option to the extension.
179	SetHighlightingOption(*Config)
180}
181
182type withHTMLOptions struct {
183	value []html.Option
184}
185
186func (o *withHTMLOptions) SetConfig(c *renderer.Config) {
187	if o.value != nil {
188		for _, v := range o.value {
189			v.(renderer.Option).SetConfig(c)
190		}
191	}
192}
193
194func (o *withHTMLOptions) SetHighlightingOption(c *Config) {
195	if o.value != nil {
196		for _, v := range o.value {
197			v.SetHTMLOption(&c.Config)
198		}
199	}
200}
201
202// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
203func WithHTMLOptions(opts ...html.Option) Option {
204	return &withHTMLOptions{opts}
205}
206
207const optStyle renderer.OptionName = "HighlightingStyle"
208const optCustomStyle renderer.OptionName = "HighlightingCustomStyle"
209
210var highlightLinesAttrName = []byte("hl_lines")
211
212var styleAttrName = []byte("hl_style")
213var nohlAttrName = []byte("nohl")
214var linenosAttrName = []byte("linenos")
215var linenosTableAttrValue = []byte("table")
216var linenosInlineAttrValue = []byte("inline")
217var linenostartAttrName = []byte("linenostart")
218
219type withStyle struct {
220	value string
221}
222
223func (o *withStyle) SetConfig(c *renderer.Config) {
224	c.Options[optStyle] = o.value
225}
226
227func (o *withStyle) SetHighlightingOption(c *Config) {
228	c.Style = o.value
229}
230
231// WithStyle is a functional option that changes highlighting style.
232func WithStyle(style string) Option {
233	return &withStyle{style}
234}
235
236type withCustomStyle struct {
237	value *chroma.Style
238}
239
240func (o *withCustomStyle) SetConfig(c *renderer.Config) {
241	c.Options[optCustomStyle] = o.value
242}
243
244func (o *withCustomStyle) SetHighlightingOption(c *Config) {
245	c.CustomStyle = o.value
246}
247
248// WithStyle is a functional option that changes highlighting style.
249func WithCustomStyle(style *chroma.Style) Option {
250	return &withCustomStyle{style}
251}
252
253const optCSSWriter renderer.OptionName = "HighlightingCSSWriter"
254
255type withCSSWriter struct {
256	value io.Writer
257}
258
259func (o *withCSSWriter) SetConfig(c *renderer.Config) {
260	c.Options[optCSSWriter] = o.value
261}
262
263func (o *withCSSWriter) SetHighlightingOption(c *Config) {
264	c.CSSWriter = o.value
265}
266
267// WithCSSWriter is a functional option that sets io.Writer for CSS data.
268func WithCSSWriter(w io.Writer) Option {
269	return &withCSSWriter{w}
270}
271
272const optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage"
273
274type withGuessLanguage struct {
275	value bool
276}
277
278func (o *withGuessLanguage) SetConfig(c *renderer.Config) {
279	c.Options[optGuessLanguage] = o.value
280}
281
282func (o *withGuessLanguage) SetHighlightingOption(c *Config) {
283	c.GuessLanguage = o.value
284}
285
286// WithGuessLanguage is a functional option that toggles language guessing
287// if none provided.
288func WithGuessLanguage(b bool) Option {
289	return &withGuessLanguage{value: b}
290}
291
292const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer"
293
294type withWrapperRenderer struct {
295	value WrapperRenderer
296}
297
298func (o *withWrapperRenderer) SetConfig(c *renderer.Config) {
299	c.Options[optWrapperRenderer] = o.value
300}
301
302func (o *withWrapperRenderer) SetHighlightingOption(c *Config) {
303	c.WrapperRenderer = o.value
304}
305
306// WithWrapperRenderer is a functional option that sets WrapperRenderer that
307// renders wrapper elements like div, pre, etc.
308func WithWrapperRenderer(w WrapperRenderer) Option {
309	return &withWrapperRenderer{w}
310}
311
312const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions"
313
314type withCodeBlockOptions struct {
315	value CodeBlockOptions
316}
317
318func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) {
319	c.Options[optWrapperRenderer] = o.value
320}
321
322func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) {
323	c.CodeBlockOptions = o.value
324}
325
326// WithCodeBlockOptions is a functional option that sets CodeBlockOptions that
327// allows setting Chroma options per code block.
328func WithCodeBlockOptions(c CodeBlockOptions) Option {
329	return &withCodeBlockOptions{value: c}
330}
331
332const optFormatOptions renderer.OptionName = "HighlightingFormatOptions"
333
334type withFormatOptions struct {
335	value []chromahtml.Option
336}
337
338func (o *withFormatOptions) SetConfig(c *renderer.Config) {
339	if _, ok := c.Options[optFormatOptions]; !ok {
340		c.Options[optFormatOptions] = []chromahtml.Option{}
341	}
342	c.Options[optFormatOptions] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...)
343}
344
345func (o *withFormatOptions) SetHighlightingOption(c *Config) {
346	c.FormatOptions = append(c.FormatOptions, o.value...)
347}
348
349// WithFormatOptions is a functional option that wraps chroma HTML formatter options.
350func WithFormatOptions(opts ...chromahtml.Option) Option {
351	return &withFormatOptions{opts}
352}
353
354// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension.
355type HTMLRenderer struct {
356	Config
357}
358
359// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it.
360func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer {
361	r := &HTMLRenderer{
362		Config: NewConfig(),
363	}
364	for _, opt := range opts {
365		opt.SetHighlightingOption(&r.Config)
366	}
367	return r
368}
369
370// RegisterFuncs implements NodeRenderer.RegisterFuncs.
371func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
372	reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
373}
374
375func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes {
376	if node.Attributes() != nil {
377		return &immutableAttributes{node}
378	}
379	if infostr != nil {
380		attrStartIdx := -1
381
382		for idx, char := range infostr {
383			if char == '{' {
384				attrStartIdx = idx
385				break
386			}
387		}
388		if attrStartIdx > 0 {
389			n := ast.NewTextBlock() // dummy node for storing attributes
390			attrStr := infostr[attrStartIdx:]
391			if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr {
392				for _, attr := range attrs {
393					n.SetAttribute(attr.Name, attr.Value)
394				}
395				return &immutableAttributes{n}
396			}
397		}
398	}
399	return nil
400}
401
402func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
403	n := node.(*ast.FencedCodeBlock)
404	if !entering {
405		return ast.WalkContinue, nil
406	}
407	language := n.Language(source)
408
409	chromaFormatterOptions := make([]chromahtml.Option, len(r.FormatOptions))
410	copy(chromaFormatterOptions, r.FormatOptions)
411
412	style := r.CustomStyle
413	if style == nil {
414		style = styles.Get(r.Style)
415	}
416	nohl := false
417
418	var info []byte
419	if n.Info != nil {
420		info = n.Info.Segment.Value(source)
421	}
422	attrs := getAttributes(n, info)
423	if attrs != nil {
424		baseLineNumber := 1
425		if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok {
426			if linenostart, ok := linenostartAttr.(float64); ok {
427				baseLineNumber = int(linenostart)
428				chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber))
429			}
430		}
431		if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr {
432			if lines, ok := linesAttr.([]interface{}); ok {
433				var hlRanges [][2]int
434				for _, l := range lines {
435					if ln, ok := l.(float64); ok {
436						hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1})
437					}
438					if rng, ok := l.([]uint8); ok {
439						slices := strings.Split(string([]byte(rng)), "-")
440						lhs, err := strconv.Atoi(slices[0])
441						if err != nil {
442							continue
443						}
444						rhs := lhs
445						if len(slices) > 1 {
446							rhs, err = strconv.Atoi(slices[1])
447							if err != nil {
448								continue
449							}
450						}
451						hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1})
452					}
453				}
454				chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges))
455			}
456		}
457		if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr {
458			if st, ok := styleAttr.([]uint8); ok {
459				styleStr := string([]byte(st))
460				style = styles.Get(styleStr)
461			}
462		}
463		if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr {
464			nohl = true
465		}
466
467		if linenosAttr, ok := attrs.Get(linenosAttrName); ok {
468			switch v := linenosAttr.(type) {
469			case bool:
470				chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v))
471			case []uint8:
472				if v != nil {
473					chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true))
474				}
475				if bytes.Equal(v, linenosTableAttrValue) {
476					chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true))
477				} else if bytes.Equal(v, linenosInlineAttrValue) {
478					chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false))
479				}
480			}
481		}
482	}
483
484	var lexer chroma.Lexer
485	if language != nil {
486		lexer = lexers.Get(string(language))
487	}
488	if !nohl && (lexer != nil || r.GuessLanguage) {
489		if style == nil {
490			style = styles.Fallback
491		}
492		var buffer bytes.Buffer
493		l := n.Lines().Len()
494		for i := 0; i < l; i++ {
495			line := n.Lines().At(i)
496			buffer.Write(line.Value(source))
497		}
498
499		if lexer == nil {
500			lexer = lexers.Analyse(buffer.String())
501			if lexer == nil {
502				lexer = lexers.Fallback
503			}
504			language = []byte(strings.ToLower(lexer.Config().Name))
505		}
506		lexer = chroma.Coalesce(lexer)
507
508		iterator, err := lexer.Tokenise(nil, buffer.String())
509		if err == nil {
510			c := newCodeBlockContext(language, true, attrs)
511
512			if r.CodeBlockOptions != nil {
513				chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...)
514			}
515			formatter := chromahtml.New(chromaFormatterOptions...)
516			if r.WrapperRenderer != nil {
517				r.WrapperRenderer(w, c, true)
518			}
519			_ = formatter.Format(w, style, iterator) == nil
520			if r.WrapperRenderer != nil {
521				r.WrapperRenderer(w, c, false)
522			}
523			if r.CSSWriter != nil {
524				_ = formatter.WriteCSS(r.CSSWriter, style)
525			}
526			return ast.WalkContinue, nil
527		}
528	}
529
530	var c CodeBlockContext
531	if r.WrapperRenderer != nil {
532		c = newCodeBlockContext(language, false, attrs)
533		r.WrapperRenderer(w, c, true)
534	} else {
535		_, _ = w.WriteString("<pre><code")
536		language := n.Language(source)
537		if language != nil {
538			_, _ = w.WriteString(" class=\"language-")
539			r.Writer.Write(w, language)
540			_, _ = w.WriteString("\"")
541		}
542		_ = w.WriteByte('>')
543	}
544	l := n.Lines().Len()
545	for i := 0; i < l; i++ {
546		line := n.Lines().At(i)
547		r.Writer.RawWrite(w, line.Value(source))
548	}
549	if r.WrapperRenderer != nil {
550		r.WrapperRenderer(w, c, false)
551	} else {
552		_, _ = w.WriteString("</code></pre>\n")
553	}
554	return ast.WalkContinue, nil
555}
556
557type highlighting struct {
558	options []Option
559}
560
561// Highlighting is a goldmark.Extender implementation.
562var Highlighting = &highlighting{
563	options: []Option{},
564}
565
566// NewHighlighting returns a new extension with given options.
567func NewHighlighting(opts ...Option) goldmark.Extender {
568	return &highlighting{
569		options: opts,
570	}
571}
572
573// Extend implements goldmark.Extender.
574func (e *highlighting) Extend(m goldmark.Markdown) {
575	m.Renderer().AddOptions(renderer.WithNodeRenderers(
576		util.Prioritized(NewHTMLRenderer(e.options...), 200),
577	))
578}