dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

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 }