├── .github ├── renovate.json └── workflows │ └── golang.yml ├── .gitignore ├── LICENSE ├── README.md ├── _example └── main.go ├── go.mod ├── go.sum ├── renderer.go └── renderer_test.go /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:best-practices", ":automergeMinor", ":automergeDigest"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/golang.yml: -------------------------------------------------------------------------------- 1 | name: golang 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | issues: write 14 | pull-requests: write 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | with: 18 | fetch-depth: 10 19 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 20 | with: 21 | go-version-file: "go.mod" 22 | - uses: lazyguru/go-coverage-action@9b496d245c69f85ab8cef4ce57d857cc727506ee 23 | with: 24 | fail-coverage: never 25 | cover-pkg: ./... 26 | 27 | golangci: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: read 31 | pull-requests: read 32 | checks: write 33 | steps: 34 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 35 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 36 | with: 37 | go-version-file: "go.mod" 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8 40 | with: 41 | version: latest 42 | args: --timeout=3m 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | vendor/ 16 | coverage.txt 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bfchroma 2 | 3 | [![forthebadge](https://forthebadge.com/images/badges/made-with-go.svg)](https://forthebadge.com)[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/Depado/bfchroma)](https://goreportcard.com/report/github.com/Depado/bfchroma) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Depado/bfchroma/blob/master/LICENSE) 7 | [![Godoc](https://godoc.org/github.com/Depado/bfchroma?status.svg)](https://godoc.org/github.com/Depado/bfchroma) 8 | [![Sourcegraph](https://sourcegraph.com/github.com/Depado/bfchroma/-/badge.svg)](https://sourcegraph.com/github.com/Depado/bfchroma?badge) 9 | [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/Depado) 10 | 11 | 12 | Integrating [Chroma](https://github.com/alecthomas/chroma) syntax highlighter as 13 | a [Blackfriday](https://github.com/russross/blackfriday) renderer. 14 | 15 | ## Install and prerequisites 16 | 17 | This project requires and uses the `v2` version of 18 | [Blackfriday](https://github.com/russross/blackfriday/tree/v2). 19 | 20 | ``` 21 | $ go get github.com/Depado/bfchroma/v2 22 | ``` 23 | 24 | ## Features 25 | 26 | This renderer integrates chroma to highlight code with triple backtick notation. 27 | It will try to use the given language when available otherwise it will try to 28 | detect the language. If none of these two method works it will fallback to sane 29 | defaults. 30 | 31 | ## Usage 32 | 33 | bfchroma uses the functional options approach so you can customize the behavior 34 | of the renderer. It uses sane defaults when no option is passed so you can use 35 | the renderer simply by doing so : 36 | 37 | ```go 38 | html := bf.Run([]byte(md), bf.WithRenderer(bfchroma.NewRenderer())) 39 | ``` 40 | 41 | ### Options 42 | 43 | - `Style(s string)` 44 | Define the style used by chroma for the rendering. The full list can be found [here](https://github.com/alecthomas/chroma/tree/master/styles) 45 | - `ChromaStyle(*chroma.Style)` 46 | This option can be used to passe directly a `*chroma.Style` instead of the 47 | string representing the style as with the `Style(string)` option. 48 | - `WithoutAutodetect()` 49 | By default when no language information is written in the code block, this 50 | renderer will try to auto-detect the used language. This option disables 51 | this behavior and will fallback to a sane default when no language 52 | information is available. 53 | - `EmbedCSS()` 54 | This option will embed CSS needed for chroma's `html.WithClasses()` at the beginning of blackfriday document. 55 | CSS can also be extracted separately by calling `Renderer`'s.`ChromaCSS(w)` method, which will return styleshet for currently set style 56 | - `Extend(bf.Renderer)` 57 | This option allows to define the base blackfriday that will be extended. 58 | - `ChromaOptions(...html.Option)` 59 | This option allows you to pass Chroma's html options in the renderer. Such 60 | options can be found [here](https://github.com/alecthomas/chroma#the-html-formatter). 61 | 62 | ### Option examples 63 | 64 | Disabling language auto-detection and displaying line numbers 65 | 66 | ```go 67 | r := bfchroma.NewRenderer( 68 | bfchroma.WithoutAutodetect(), 69 | bfchroma.ChromaOptions(html.WithLineNumbers()), 70 | ) 71 | ``` 72 | 73 | Extend a blackfriday renderer 74 | 75 | ```go 76 | b := bf.NewHTMLRenderer(bf.HTMLRendererParameters{ 77 | Flags: bf.CommonHTMLFlags, 78 | }) 79 | 80 | r := bfchroma.NewRenderer(bfchroma.Extend(b)) 81 | ``` 82 | 83 | Use a different style 84 | 85 | ```go 86 | r := bfchroma.NewRenderer(bfchroma.Style("dracula")) 87 | // Or 88 | r = bfchroma.NewRenderer(bfchroma.ChromaStyle(styles.Dracula)) 89 | ``` 90 | 91 | 92 | 93 | ## Examples 94 | 95 | ```go 96 | package main 97 | 98 | import ( 99 | "fmt" 100 | 101 | "github.com/Depado/bfchroma/v2" 102 | 103 | bf "github.com/russross/blackfriday/v2" 104 | ) 105 | 106 | var md = "This is some sample code.\n\n```go\n" + 107 | `func main() { 108 | fmt.Println("Hi") 109 | } 110 | ` + "```" 111 | 112 | func main() { 113 | html := bf.Run([]byte(md), bf.WithRenderer(bfchroma.NewRenderer())) 114 | fmt.Println(string(html)) 115 | } 116 | ``` 117 | 118 | 119 | Will output : 120 | 121 | ```html 122 |

This is some sample code.

123 |
func main() {
124 | fmt.Println("Hi")
125 | }
126 | 
127 | ``` 128 | 129 | ## Real-life example 130 | 131 | In [smallblog](https://github.com/Depado/smallblog) I'm using bfchroma to render 132 | my articles. It's using a combination of both bfchroma's options and blackfriday 133 | extensions and flags. 134 | 135 | ```go 136 | package main 137 | 138 | import ( 139 | "github.com/Depado/bfchroma/v2" 140 | 141 | "github.com/alecthomas/chroma/v2/formatters/html" 142 | bf "github.com/russross/blackfriday/v2" 143 | ) 144 | 145 | // Defines the extensions that are used 146 | var exts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink | 147 | bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak | 148 | bf.DefinitionLists | bf.Footnotes 149 | 150 | // Defines the HTML rendering flags that are used 151 | var flags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions | 152 | bf.SmartypantsDashes | bf.SmartypantsLatexDashes | bf.TOC 153 | 154 | // render will take a []byte input and will render it using a new renderer each 155 | // time because reusing the same can mess with TOC and header IDs 156 | func render(input []byte) []byte { 157 | return bf.Run( 158 | input, 159 | bf.WithRenderer( 160 | bfchroma.NewRenderer( 161 | bfchroma.WithoutAutodetect(), 162 | bfchroma.ChromaOptions( 163 | html.WithLineNumbers(), 164 | ), 165 | bfchroma.Extend( 166 | bf.NewHTMLRenderer(bf.HTMLRendererParameters{ 167 | Flags: flags, 168 | }), 169 | ), 170 | ), 171 | ), 172 | bf.WithExtensions(exts), 173 | ) 174 | } 175 | ``` 176 | 177 | ## Classes 178 | 179 | If you have loads of code in your markdown, you might want to consider using 180 | `html.WithClasses()` in your `bfchroma.ChromaOptions()`. The CSS of the style 181 | you chose can then be accessed like this : 182 | 183 | ```go 184 | r := bfchroma.NewRenderer( 185 | bfchroma.WithoutAutodetect(), 186 | bfchroma.Extend( 187 | bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: flags}), 188 | ), 189 | bfchroma.Style("monokai"), 190 | bfchroma.ChromaOptions(html.WithClasses()), 191 | ) 192 | 193 | var css template.CSS 194 | 195 | b := new(bytes.Buffer) 196 | if err := r.ChromaCSS(b); err != nil { 197 | logrus.WithError(err).Warning("Couldn't write CSS") 198 | } 199 | css = template.CSS(b.String()) 200 | 201 | bf.Run(input, bf.WithRenderer(r), bf.WithExtensions(exts)) 202 | ``` 203 | 204 | This way, you can pass your `css` var to any template and render it along the 205 | rendered markdown. 206 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Depado/bfchroma" 7 | "github.com/alecthomas/chroma/v2/formatters/html" 8 | 9 | bf "github.com/russross/blackfriday/v2" 10 | ) 11 | 12 | var md = "This is some sample code.\n\n```go\n" + 13 | `func main() { 14 | fmt.Println("Hi") 15 | } 16 | ` + "```" 17 | 18 | func main() { 19 | var r *bfchroma.Renderer 20 | var h []byte 21 | 22 | // Basic usage 23 | r = bfchroma.NewRenderer() 24 | h = bf.Run([]byte(md), bf.WithRenderer(r)) 25 | fmt.Println(string(h)) 26 | 27 | // Option examples and extending a specific blackfriday renderer 28 | b := bf.NewHTMLRenderer(bf.HTMLRendererParameters{ 29 | Flags: bf.CommonHTMLFlags, 30 | }) 31 | r = bfchroma.NewRenderer( 32 | bfchroma.WithoutAutodetect(), 33 | bfchroma.Extend(b), 34 | bfchroma.ChromaOptions(html.WithLineNumbers(true)), 35 | ) 36 | h = bf.Run([]byte(md), bf.WithRenderer(r)) 37 | fmt.Println(string(h)) 38 | 39 | md := "```\npackage main\n\nfunc main() {\n}\n```" 40 | r = bfchroma.NewRenderer(bfchroma.WithoutAutodetect()) 41 | h = bf.Run([]byte(md), bf.WithRenderer(r)) 42 | fmt.Println(string(h)) 43 | 44 | md = "```go\npackage main\n\nfunc main() {\n}\n```" 45 | r = bfchroma.NewRenderer(bfchroma.ChromaOptions(html.WithLineNumbers(true))) 46 | h = bf.Run([]byte(md), bf.WithRenderer(r)) 47 | fmt.Println(string(h)) 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Depado/bfchroma/v2 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/alecthomas/chroma/v2 v2.18.0 7 | github.com/russross/blackfriday/v2 v2.1.0 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/dlclark/regexp2 v1.11.5 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 2 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= 4 | github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= 5 | github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= 6 | github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 7 | github.com/alecthomas/chroma/v2 v2.17.0 h1:3r2Cgk+nXNICMBxIFGnTRTbQFUwMiLisW+9uos0TtUI= 8 | github.com/alecthomas/chroma/v2 v2.17.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 9 | github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= 10 | github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 11 | github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4= 12 | github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 13 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 14 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 18 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 19 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 20 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 21 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 22 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 26 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 27 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 28 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /renderer.go: -------------------------------------------------------------------------------- 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.Get("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("")) // 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 | } 148 | -------------------------------------------------------------------------------- /renderer_test.go: -------------------------------------------------------------------------------- 1 | package bfchroma 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/alecthomas/chroma/v2" 9 | "github.com/alecthomas/chroma/v2/formatters/html" 10 | "github.com/alecthomas/chroma/v2/styles" 11 | bf "github.com/russross/blackfriday/v2" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestExtend(t *testing.T) { 17 | var b bf.Renderer 18 | var r *Renderer 19 | b = bf.NewHTMLRenderer(bf.HTMLRendererParameters{ 20 | Flags: bf.CommonHTMLFlags, 21 | }) 22 | r = NewRenderer(Extend(b)) 23 | assert.Equal(t, r.Base, b, "should be the same renderer") 24 | } 25 | 26 | func ExampleExtend() { 27 | md := "```go\npackage main\n\nfunc main() {\n}\n```" 28 | 29 | b := bf.NewHTMLRenderer(bf.HTMLRendererParameters{ 30 | Flags: bf.CommonHTMLFlags, 31 | }) 32 | r := NewRenderer(Extend(b)) 33 | 34 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 35 | fmt.Println(string(h)) 36 | } 37 | 38 | func TestStyle(t *testing.T) { 39 | var r *Renderer 40 | for k, v := range styles.Registry { 41 | r = NewRenderer(Style(k)) 42 | assert.Equal(t, r.Style, v, "Style should match") 43 | } 44 | for _, v := range []string{"♥", "inexistent", "fallback!"} { 45 | r = NewRenderer(Style(v)) 46 | assert.Equal(t, r.Style, styles.Fallback) 47 | } 48 | } 49 | 50 | func ExampleStyle() { 51 | md := "```go\npackage main\n\nfunc main() {\n}\n```" 52 | 53 | r := NewRenderer(Style("github")) 54 | 55 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 56 | fmt.Println(string(h)) 57 | } 58 | 59 | func TestChromaStyle(t *testing.T) { 60 | var r *Renderer 61 | for _, v := range styles.Registry { 62 | r = NewRenderer(ChromaStyle(v)) 63 | assert.Equal(t, r.Style, v, "Style should match") 64 | } 65 | } 66 | 67 | func ExampleChromaStyle() { 68 | md := "```go\npackage main\n\nfunc main() {\n}\n```" 69 | 70 | r := NewRenderer(ChromaStyle(styles.Get("github"))) 71 | 72 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 73 | fmt.Println(string(h)) 74 | } 75 | 76 | func TestWithoutAutodetect(t *testing.T) { 77 | r := NewRenderer(WithoutAutodetect()) 78 | assert.False(t, r.Autodetect, "Should set Autodetect to false") 79 | r = NewRenderer() 80 | assert.True(t, r.Autodetect, "Not using option should leave Autodetect to true") 81 | } 82 | 83 | func ExampleWithoutAutodetect() { 84 | md := "```\npackage main\n\nfunc main() {\n}\n```" 85 | 86 | r := NewRenderer(WithoutAutodetect()) 87 | 88 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 89 | fmt.Println(string(h)) 90 | } 91 | 92 | func TestChromaOptions(t *testing.T) { 93 | NewRenderer(ChromaOptions(html.WithClasses(true))) 94 | } 95 | 96 | func ExampleChromaOptions() { 97 | md := "```go\npackage main\n\nfunc main() {\n}\n```" 98 | 99 | r := NewRenderer(ChromaOptions(html.WithLineNumbers(true))) 100 | 101 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 102 | fmt.Println(string(h)) 103 | } 104 | 105 | func TestRenderWithChroma(t *testing.T) { 106 | var err error 107 | var b *bytes.Buffer 108 | r := NewRenderer() 109 | tests := []struct { 110 | in []byte 111 | cbd bf.CodeBlockData 112 | out string 113 | }{ 114 | {[]byte{0}, bf.CodeBlockData{}, "
\x00
"}, 115 | {[]byte{0, 1, 2}, bf.CodeBlockData{}, "
\x00\x01\x02
"}, 116 | {[]byte("Hello World"), bf.CodeBlockData{}, "
Hello World
"}, 117 | } 118 | for _, test := range tests { 119 | b = new(bytes.Buffer) 120 | err = r.RenderWithChroma(b, test.in, test.cbd) 121 | assert.NoError(t, err, "Should not fail") 122 | assert.Equal(t, test.out, b.String()) 123 | } 124 | } 125 | 126 | func TestRender(t *testing.T) { 127 | md := "```go\npackage main\n\nfunc main() {\n}\n```" 128 | r := NewRenderer() 129 | bg := r.Style.Get(chroma.Background).Background.String() 130 | 131 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 132 | assert.Contains(t, string(h), r.Style.Get(chroma.NameFunction).Colour.String()) 133 | assert.Contains(t, string(h), bg) 134 | assert.Contains(t, string(h), "") 155 | assert.Contains(t, string(h), ".chroma") 156 | assert.Contains(t, string(h), "") 157 | } 158 | 159 | func TestRenderer_ChromaCSS(t *testing.T) { 160 | r := NewRenderer() 161 | var w bytes.Buffer 162 | err := r.ChromaCSS(&w) 163 | require.NoError(t, err) 164 | assert.Contains(t, w.String(), ".chroma") 165 | 166 | } 167 | 168 | func ExampleNewRenderer() { 169 | // Complex example on how to initialize the renderer 170 | md := "```go\npackage main\n\nfunc main() {\n}\n```" 171 | 172 | r := NewRenderer( 173 | Extend(bf.NewHTMLRenderer(bf.HTMLRendererParameters{ 174 | Flags: bf.CommonHTMLFlags, 175 | })), 176 | WithoutAutodetect(), 177 | ChromaStyle(styles.Get("github")), 178 | ChromaOptions(html.WithLineNumbers(true)), 179 | ) 180 | 181 | h := bf.Run([]byte(md), bf.WithRenderer(r)) 182 | fmt.Println(string(h)) 183 | } 184 | --------------------------------------------------------------------------------