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 | }
|
---|