-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
-
475
-
476
-
477
-
478
-
479
-
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
-
488
-
489
-
490
-
491
-
492
-
493
-
494
-
495
-
496
-
497
-
498
-
499
-
500
-
501
-
502
-
503
-
504
-
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
-
536
-
537
-
538
-
539
-
540
-
541
-
542
-
543
-
544
-
545
-
546
-
547
-
548
-
549
-
550
-
551
-
552
-
553
-
554
-
555
-
556
-
557
-
558
-
559
-
560
-
561
-
562
-
563
-
564
-
565
-
566
-
567
-
568
-
569
-
570
-
571
-
572
-
573
-
574
// 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).
package highlighting
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
// Pass in a custom Chroma style. If this is not nil, the Style string will be ignored
CustomStyle *chroma.Style
// If set, will try to guess language if none provided.
// If the guessing fails, we will fall back to a text lexer.
// Note that while Chroma's API supports language guessing, the implementation
// is not there yet, so you will currently always get the basic text lexer.
GuessLanguage bool
// 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
// WrapperRenderer 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 optCustomStyle:
c.CustomStyle = value.(*chroma.Style)
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)
case optGuessLanguage:
c.GuessLanguage = value.(bool)
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"
const optCustomStyle renderer.OptionName = "HighlightingCustomStyle"
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}
}
type withCustomStyle struct {
value *chroma.Style
}
func (o *withCustomStyle) SetConfig(c *renderer.Config) {
c.Options[optCustomStyle] = o.value
}
func (o *withCustomStyle) SetHighlightingOption(c *Config) {
c.CustomStyle = o.value
}
// WithStyle is a functional option that changes highlighting style.
func WithCustomStyle(style *chroma.Style) Option {
return &withCustomStyle{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 optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage"
type withGuessLanguage struct {
value bool
}
func (o *withGuessLanguage) SetConfig(c *renderer.Config) {
c.Options[optGuessLanguage] = o.value
}
func (o *withGuessLanguage) SetHighlightingOption(c *Config) {
c.GuessLanguage = o.value
}
// WithGuessLanguage is a functional option that toggles language guessing
// if none provided.
func WithGuessLanguage(b bool) Option {
return &withGuessLanguage{value: b}
}
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[optFormatOptions] = 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 := r.CustomStyle
if style == nil {
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))
}
if bytes.Equal(v, linenosTableAttrValue) {
chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true))
} else if bytes.Equal(v, linenosInlineAttrValue) {
chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false))
}
}
}
}
var lexer chroma.Lexer
if language != nil {
lexer = lexers.Get(string(language))
}
if !nohl && (lexer != nil || r.GuessLanguage) {
if style == nil {
style = styles.Fallback
}
var buffer bytes.Buffer
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
buffer.Write(line.Value(source))
}
if lexer == nil {
lexer = lexers.Analyse(buffer.String())
if lexer == nil {
lexer = lexers.Fallback
}
language = []byte(strings.ToLower(lexer.Config().Name))
}
lexer = chroma.Coalesce(lexer)
iterator, err := lexer.Tokenise(nil, buffer.String())
if err == nil {
c := newCodeBlockContext(language, true, attrs)
if r.CodeBlockOptions != nil {
chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...)
}
formatter := chromahtml.New(chromaFormatterOptions...)
if r.WrapperRenderer != nil {
r.WrapperRenderer(w, c, true)
}
_ = formatter.Format(w, style, iterator) == nil
if r.WrapperRenderer != nil {
r.WrapperRenderer(w, c, false)
}
if r.CSSWriter != nil {
_ = formatter.WriteCSS(r.CSSWriter, style)
}
return ast.WalkContinue, nil
}
}
var c CodeBlockContext
if r.WrapperRenderer != nil {
c = newCodeBlockContext(language, false, attrs)
r.WrapperRenderer(w, c, true)
} else {
_, _ = w.WriteString("<pre><code")
language := n.Language(source)
if language != nil {
_, _ = w.WriteString(" class=\"language-")
r.Writer.Write(w, language)
_, _ = w.WriteString("\"")
}
_ = w.WriteByte('>')
}
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.RawWrite(w, line.Value(source))
}
if r.WrapperRenderer != nil {
r.WrapperRenderer(w, c, false)
} else {
_, _ = w.WriteString("</code></pre>\n")
}
return ast.WalkContinue, nil
}
type highlighting struct {
options []Option
}
// Highlighting is a goldmark.Extender implementation.
var Highlighting = &highlighting{
options: []Option{},
}
// NewHighlighting returns a new extension with given options.
func NewHighlighting(opts ...Option) goldmark.Extender {
return &highlighting{
options: opts,
}
}
// Extend implements goldmark.Extender.
func (e *highlighting) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewHTMLRenderer(e.options...), 200),
))
}