renderer.go (3955B)
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 bf "dkforest/pkg/blackfriday/v2" 9 "github.com/alecthomas/chroma" 10 "github.com/alecthomas/chroma/formatters/html" 11 "github.com/alecthomas/chroma/lexers" 12 "github.com/alecthomas/chroma/styles" 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>")) 120 r.Formatter.WriteCSS(w, r.Style) 121 w.Write([]byte("</style>")) 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 }