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}