66
|
1 // Package bfchroma provides an easy and extensible blackfriday renderer that
|
|
2 // uses the chroma syntax highlighter to render code blocks.
|
|
3 package bfchroma
|
|
4
|
|
5 import (
|
|
6 "io"
|
|
7
|
|
8 "github.com/alecthomas/chroma/v2"
|
|
9 "github.com/alecthomas/chroma/v2/formatters/html"
|
|
10 "github.com/alecthomas/chroma/v2/lexers"
|
|
11 "github.com/alecthomas/chroma/v2/styles"
|
|
12 bf "github.com/russross/blackfriday/v2"
|
|
13 )
|
|
14
|
|
15 // Option defines the functional option type
|
|
16 type Option func(r *Renderer)
|
|
17
|
|
18 // Style is a function option allowing to set the style used by chroma
|
|
19 // Default : "monokai"
|
|
20 func Style(s string) Option {
|
|
21 return func(r *Renderer) {
|
|
22 r.Style = styles.Get(s)
|
|
23 }
|
|
24 }
|
|
25
|
|
26 // ChromaStyle is an option to directly set the style of the renderer using a
|
|
27 // chroma style instead of a string
|
|
28 func ChromaStyle(s *chroma.Style) Option {
|
|
29 return func(r *Renderer) {
|
|
30 r.Style = s
|
|
31 }
|
|
32 }
|
|
33
|
|
34 // WithoutAutodetect disables chroma's language detection when no codeblock
|
|
35 // extra information is given. It will fallback to a sane default instead of
|
|
36 // trying to detect the language.
|
|
37 func WithoutAutodetect() Option {
|
|
38 return func(r *Renderer) {
|
|
39 r.Autodetect = false
|
|
40 }
|
|
41 }
|
|
42
|
|
43 // EmbedCSS will embed CSS needed for html.WithClasses() in beginning of the document
|
|
44 func EmbedCSS() Option {
|
|
45 return func(r *Renderer) {
|
|
46 r.embedCSS = true
|
|
47 }
|
|
48 }
|
|
49
|
|
50 // ChromaOptions allows to pass Chroma html.Option such as Standalone()
|
|
51 // WithClasses(), ClassPrefix(prefix)...
|
|
52 func ChromaOptions(options ...html.Option) Option {
|
|
53 return func(r *Renderer) {
|
|
54 r.ChromaOptions = options
|
|
55 }
|
|
56 }
|
|
57
|
|
58 // Extend allows to specify the blackfriday renderer which is extended
|
|
59 func Extend(br bf.Renderer) Option {
|
|
60 return func(r *Renderer) {
|
|
61 r.Base = br
|
|
62 }
|
|
63 }
|
|
64
|
|
65 // NewRenderer will return a new bfchroma renderer with sane defaults
|
|
66 func NewRenderer(options ...Option) *Renderer {
|
|
67 r := &Renderer{
|
|
68 Base: bf.NewHTMLRenderer(bf.HTMLRendererParameters{
|
|
69 Flags: bf.CommonHTMLFlags,
|
|
70 }),
|
|
71 Style: styles.Monokai,
|
|
72 Autodetect: true,
|
|
73 }
|
|
74 for _, option := range options {
|
|
75 option(r)
|
|
76 }
|
|
77 r.Formatter = html.New(r.ChromaOptions...)
|
|
78 return r
|
|
79 }
|
|
80
|
|
81 // RenderWithChroma will render the given text to the w io.Writer
|
|
82 func (r *Renderer) RenderWithChroma(w io.Writer, text []byte, data bf.CodeBlockData) error {
|
|
83 var lexer chroma.Lexer
|
|
84
|
|
85 // Determining the lexer to use
|
|
86 if len(data.Info) > 0 {
|
|
87 lexer = lexers.Get(string(data.Info))
|
|
88 } else if r.Autodetect {
|
|
89 lexer = lexers.Analyse(string(text))
|
|
90 }
|
|
91 if lexer == nil {
|
|
92 lexer = lexers.Fallback
|
|
93 }
|
|
94
|
|
95 // Tokenize the code
|
|
96 iterator, err := lexer.Tokenise(nil, string(text))
|
|
97 if err != nil {
|
|
98 return err
|
|
99 }
|
|
100 return r.Formatter.Format(w, r.Style, iterator)
|
|
101 }
|
|
102
|
|
103 // Renderer is a custom Blackfriday renderer that uses the capabilities of
|
|
104 // chroma to highlight code with triple backtick notation
|
|
105 type Renderer struct {
|
|
106 Base bf.Renderer
|
|
107 Autodetect bool
|
|
108 ChromaOptions []html.Option
|
|
109 Style *chroma.Style
|
|
110 Formatter *html.Formatter
|
|
111 embedCSS bool
|
|
112 }
|
|
113
|
|
114 // RenderNode satisfies the Renderer interface
|
|
115 func (r *Renderer) RenderNode(w io.Writer, node *bf.Node, entering bool) bf.WalkStatus {
|
|
116 switch node.Type {
|
|
117 case bf.Document:
|
|
118 if entering && r.embedCSS {
|
|
119 w.Write([]byte("<style>")) // nolint: errcheck
|
|
120 r.Formatter.WriteCSS(w, r.Style) // nolint: errcheck
|
|
121 w.Write([]byte("</style>")) // nolint: errcheck
|
|
122 }
|
|
123 return r.Base.RenderNode(w, node, entering)
|
|
124 case bf.CodeBlock:
|
|
125 if err := r.RenderWithChroma(w, node.Literal, node.CodeBlockData); err != nil {
|
|
126 return r.Base.RenderNode(w, node, entering)
|
|
127 }
|
|
128 return bf.SkipChildren
|
|
129 default:
|
|
130 return r.Base.RenderNode(w, node, entering)
|
|
131 }
|
|
132 }
|
|
133
|
|
134 // RenderHeader satisfies the Renderer interface
|
|
135 func (r *Renderer) RenderHeader(w io.Writer, ast *bf.Node) {
|
|
136 r.Base.RenderHeader(w, ast)
|
|
137 }
|
|
138
|
|
139 // RenderFooter satisfies the Renderer interface
|
|
140 func (r *Renderer) RenderFooter(w io.Writer, ast *bf.Node) {
|
|
141 r.Base.RenderFooter(w, ast)
|
|
142 }
|
|
143
|
|
144 // ChromaCSS returns CSS used with chroma's html.WithClasses() option
|
|
145 func (r *Renderer) ChromaCSS(w io.Writer) error {
|
|
146 return r.Formatter.WriteCSS(w, r.Style)
|
|
147 }
|