├── .changes ├── header.tpl.md ├── unreleased │ ├── .gitkeep │ ├── Added-20250120-145706.yaml │ └── Changed-20231224-153904.yaml ├── v0.1.0.md ├── v0.1.1.md ├── v0.2.0.md ├── v0.3.0.md ├── v0.4.0.md └── v0.5.0.md ├── .changie.yaml ├── .codecov.yml ├── .github └── workflows │ ├── ci.yml │ ├── doc.yml │ └── stitchmd.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ast.go ├── ast_test.go ├── cli.go ├── cli_test.go ├── client_render.go ├── client_render_test.go ├── demo ├── Makefile ├── go.mod ├── go.sum ├── main.go └── static │ ├── .gitignore │ └── index.html ├── design └── 28-render-without-mmdc.adoc ├── doc.go ├── doc ├── README.md ├── install.md ├── intro.md ├── license.md ├── render-mode.md ├── render-server.md └── usage.md ├── example_test.go ├── extend.go ├── extend_test.go ├── go.mod ├── go.sum ├── integration_test.go ├── internal ├── exectest │ ├── cmd.go │ └── cmd_test.go ├── svgtest │ └── norm.go └── testdata │ ├── .gitattributes │ └── mermaid-10.6.0.js ├── mermaidcdp ├── compiler.go ├── compiler_test.go ├── ctx.go ├── ctx_post_go121.go ├── ctx_pre_go121.go ├── ctx_test.go ├── doc.go ├── download.go ├── download_test.go ├── extras.js └── testdata │ ├── mermaid.js │ └── render.yaml ├── mise.lock ├── mise.oldstable.toml ├── mise.toml ├── package-lock.json ├── package.json ├── rendermode.go ├── rendermode_string.go ├── rendermode_test.go ├── renovate.json ├── server_render.go ├── server_render_test.go ├── testdata ├── client.yaml ├── mermaid.js ├── server_cdp.yaml └── server_cli.yaml ├── transform.go ├── transform_test.go └── utils_for_test.go /.changes/header.tpl.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), 6 | and is generated by [Changie](https://github.com/miniscruff/changie). 7 | -------------------------------------------------------------------------------- /.changes/unreleased/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhinav/goldmark-mermaid/1a309286c5dbf3e331d63ebdcf16ff49513bf232/.changes/unreleased/.gitkeep -------------------------------------------------------------------------------- /.changes/unreleased/Added-20250120-145706.yaml: -------------------------------------------------------------------------------- 1 | kind: Added 2 | body: 'mermaidcdp: Support disabling Chrome sandbox.' 3 | time: 2025-01-20T14:57:06.212379-08:00 4 | -------------------------------------------------------------------------------- /.changes/unreleased/Changed-20231224-153904.yaml: -------------------------------------------------------------------------------- 1 | kind: Changed 2 | body: Relicense to BSD3. 3 | time: 2023-12-24T15:39:04.336148-08:00 4 | -------------------------------------------------------------------------------- /.changes/v0.1.0.md: -------------------------------------------------------------------------------- 1 | ## v0.1.0 - 2021-04-12 2 | - Initial release. 3 | -------------------------------------------------------------------------------- /.changes/v0.1.1.md: -------------------------------------------------------------------------------- 1 | ## v0.1.1 - 2021-11-03 2 | ### Fixed 3 | 4 | - Fix handling of multiple mermaid blocks. 5 | -------------------------------------------------------------------------------- /.changes/v0.2.0.md: -------------------------------------------------------------------------------- 1 | ## v0.2.0 - 2022-11-04 2 | ### Added 3 | - ServerRenderer with support for rendering Mermaid diagrams 4 | into inline SVGs server-side. 5 | This is picked automatically if an 'mmdc' executable is found on PATH. 6 | - Support opting out of the MermaidJS ``) 77 | } else { 78 | _, _ = w.WriteString("") 79 | } 80 | 81 | return ast.WalkContinue, nil 82 | } 83 | -------------------------------------------------------------------------------- /client_render_test.go: -------------------------------------------------------------------------------- 1 | package mermaid 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/yuin/goldmark/renderer" 10 | "github.com/yuin/goldmark/text" 11 | "github.com/yuin/goldmark/util" 12 | ) 13 | 14 | func TestRenderer_Block(t *testing.T) { 15 | t.Parallel() 16 | 17 | tests := []struct { 18 | desc string 19 | give string 20 | 21 | tag string // ContainerTag option 22 | 23 | want string 24 | }{ 25 | { 26 | desc: "empty", 27 | give: "", 28 | want: `
`,
 29 | 		},
 30 | 		{
 31 | 			desc: "graph",
 32 | 			give: "graph TD;",
 33 | 			want: `
graph TD;
`, 34 | }, 35 | { 36 | desc: "newlines", 37 | give: unlines("foo", "bar"), 38 | want: `
foo` + "\nbar" + "\n
", 39 | }, 40 | { 41 | desc: "escaping", 42 | give: "A -> B", 43 | want: `
A -> B
`, 44 | }, 45 | { 46 | desc: "custom container tag", 47 | give: "graph TD;", 48 | tag: "div", 49 | want: `
graph TD;
`, 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | tt := tt 55 | t.Run(tt.desc, func(t *testing.T) { 56 | t.Parallel() 57 | 58 | r := buildNodeRenderer(&ClientRenderer{ 59 | ContainerTag: tt.tag, 60 | }) 61 | 62 | reader := text.NewReader([]byte(tt.give)) 63 | give := blockFromReader(reader) 64 | 65 | var buff bytes.Buffer 66 | assert.NoError(t, r.Render(&buff, reader.Source(), give), "Render") 67 | assert.Equal(t, tt.want, buff.String()) 68 | }) 69 | } 70 | } 71 | 72 | func TestRenderer_ContainerTag_arbitraryTagInjection(t *testing.T) { 73 | t.Parallel() 74 | 75 | r := buildNodeRenderer(&ClientRenderer{ 76 | ContainerTag: "pre>", _defaultMermaidJS), 100 | }, 101 | { 102 | desc: "explicit mermaid.js", 103 | mermaidJS: "mermaid.js", 104 | want: ``, 105 | }, 106 | } 107 | 108 | for _, tt := range tests { 109 | tt := tt 110 | t.Run(tt.desc, func(t *testing.T) { 111 | t.Parallel() 112 | 113 | r := buildNodeRenderer(&ClientRenderer{ 114 | MermaidURL: tt.mermaidJS, 115 | }) 116 | 117 | var buff bytes.Buffer 118 | assert.NoError(t, 119 | r.Render(&buff, nil /* src */, &ScriptBlock{})) 120 | assert.Equal(t, tt.want, buff.String()) 121 | }) 122 | } 123 | } 124 | 125 | func buildNodeRenderer(r renderer.NodeRenderer) renderer.Renderer { 126 | return renderer.NewRenderer( 127 | renderer.WithNodeRenderers( 128 | util.Prioritized(r, 100), 129 | ), 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | OUT = static 2 | 3 | .PHONY: all 4 | all: $(OUT)/wasm_exec.js $(OUT)/main.wasm 5 | 6 | $(OUT)/wasm_exec.js: 7 | @mkdir -p $(OUT) 8 | cp "$(shell go env GOROOT)/lib/wasm/wasm_exec.js" $@ 9 | 10 | $(OUT)/main.wasm: $(wildcard *.go) 11 | @mkdir -p $(OUT) 12 | GOOS=js GOARCH=wasm go build -o $@ 13 | -------------------------------------------------------------------------------- /demo/go.mod: -------------------------------------------------------------------------------- 1 | module go.abhg.dev/goldmark/mermaid/demo 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | replace go.abhg.dev/goldmark/mermaid => ../ 8 | 9 | require ( 10 | github.com/yuin/goldmark v1.7.12 11 | go.abhg.dev/goldmark/mermaid v0.5.0 12 | ) 13 | -------------------------------------------------------------------------------- /demo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/chromedp/cdproto v0.0.0-20250527225801-8f9bc3ce9e31 h1:c+8rK0AM8pcVXbwg5rio9VL3zCiUn2klFGGgZfamPZ4= 2 | github.com/chromedp/cdproto v0.0.0-20250527225801-8f9bc3ce9e31/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= 3 | github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk= 4 | github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A= 5 | github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= 6 | github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= 10 | github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= 11 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 12 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 13 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 14 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 15 | github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= 16 | github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= 22 | github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 23 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 24 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /demo/main.go: -------------------------------------------------------------------------------- 1 | // demo implements a WASM module that can be used to format markdown 2 | // with the goldmark-mermaid extension. 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "syscall/js" 8 | 9 | "github.com/yuin/goldmark" 10 | "go.abhg.dev/goldmark/mermaid" 11 | ) 12 | 13 | func main() { 14 | js.Global().Set("formatMarkdown", js.FuncOf(func(this js.Value, args []js.Value) any { 15 | var req request 16 | req.Decode(args[0]) 17 | 18 | return formatMarkdown(&req) 19 | })) 20 | select {} 21 | } 22 | 23 | type request struct { 24 | Markdown string 25 | ContainerTag string 26 | } 27 | 28 | func (r *request) Decode(v js.Value) { 29 | r.Markdown = v.Get("markdown").String() 30 | r.ContainerTag = v.Get("containerTag").String() 31 | } 32 | 33 | func formatMarkdown(r *request) any { 34 | input := r.Markdown 35 | md := goldmark.New( 36 | goldmark.WithExtensions( 37 | &mermaid.Extender{ 38 | RenderMode: mermaid.RenderModeClient, 39 | NoScript: true, 40 | ContainerTag: r.ContainerTag, 41 | }, 42 | ), 43 | ) 44 | 45 | var buf bytes.Buffer 46 | if err := md.Convert([]byte(input), &buf); err != nil { 47 | return err.Error() 48 | } 49 | return buf.String() 50 | } 51 | -------------------------------------------------------------------------------- /demo/static/.gitignore: -------------------------------------------------------------------------------- 1 | /wasm_exec.js 2 | /main.wasm 3 | -------------------------------------------------------------------------------- /demo/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | goldmark-mermaid 7 | 8 | 14 | 45 | 46 | 47 |
48 |

goldmark-mermaid

49 |
50 | 51 |
52 |
53 |

Input

54 | 62 | 63 | 64 | 68 |
69 | 70 |
71 |

Output

72 |
73 |
74 |
75 | 76 | 77 | 96 | 97 | -------------------------------------------------------------------------------- /design/28-render-without-mmdc.adoc: -------------------------------------------------------------------------------- 1 | = Rendering Mermaid diagrams with Chrome DevTools Protocol 2 | 2023-11-03 3 | :toc: preamble 4 | :source-language: go 5 | 6 | Abstract:: 7 | This documents a design for adding support to goldmark-mermaid 8 | for rendering Mermaid diagrams server-side 9 | using the Chrome DevTools Protocol (CDP). 10 | Issue:: 11 | https://github.com/abhinav/goldmark-mermaid/issues/28[#28] 12 | 13 | == Background 14 | 15 | goldmark-mermaid supports two forms of rendering: 16 | client-side and server-side. 17 | Server-side rendering is implemented by 18 | shelling out to the Mermaid CLI (`mmdc`). 19 | 20 | It is possible to implement this functionality without the Mermaid CLI 21 | with use of the Chrome DevTools Protocol (CDP). 22 | CDP expects Chrome, Chromium, or another compatible browser on the system. 23 | It spawns a headless version of the browser, and drives it with an RPC API. 24 | 25 | `mmdc` relies on Puppeteer, which uses similar technology. 26 | So if `mmdc` works on a system, so will CDP. 27 | 28 | === Existing API 29 | 30 | goldmark-mermaid currently offers the following APIs 31 | with regards to server-side rendering. 32 | 33 | ---- 34 | // MMDC builds an exec.Cmd that will run 35 | // the Mermaid CLI with the provided arguments. 36 | type MMDC struct { 37 | Command(...string) *exec.Cmd 38 | } 39 | 40 | type CLI struct{ /* .. */ } 41 | 42 | var _ MMDC = (*CLI)(nil) 43 | 44 | // ServerRenderer renders Goldmark diagrams server-side, 45 | // replacing them with SVGs in the final output. 46 | // 47 | // It is a goldmark.NodeRenderer. 48 | type ServerRenderer struct { 49 | MMDC MMDC 50 | Theme string 51 | // ... 52 | } 53 | 54 | // Extender extends a goldmark.Markdown, 55 | // installing the ServerRenderer if appropriate. 56 | type Extender struct { 57 | RenderMode RenderMode 58 | MMDC MMDC 59 | Theme Theme 60 | // ... 61 | } 62 | 63 | type RenderMode int 64 | 65 | const ( 66 | // RenderModeServer indicates that we're rendering diagrams 67 | // server-side. 68 | RenderModeServer RenderMode = // ... 69 | // ... 70 | ) 71 | ---- 72 | 73 | === MermaidJS API 74 | 75 | To use CDP to render Mermaid diagrams, 76 | we'll be delegating the rendering to a JavaScript interpreter 77 | running inside the headless Chrome process. 78 | 79 | The following bits of the MermaidJS API are relevant: 80 | 81 | [,typescript] 82 | ---- 83 | mermaid.initialize({ 84 | startOnLoad: bool, // we want this to be false 85 | theme: string, 86 | }) 87 | 88 | mermaid.render( 89 | name: string, 90 | src: string, 91 | ): Promise<{svg: string}> 92 | ---- 93 | 94 | > **NOTE**: Theme is specified once at initialization. 95 | 96 | == Constraints 97 | 98 | The following base constraints make sense for CDP-based rendering. 99 | 100 | * We should spawn a long-running browser process 101 | that can be re-used between render invocations. 102 | * The dependency on the CDP library should be optional. 103 | This will keep the dependency footprint small 104 | for users of client-side rendering and Mermaid CLI-based rendering. 105 | 106 | The dependency constraint means that 107 | the root goldmark/mermaid package should not import the CDP library directly. 108 | The functionality should be implemented in an independent sub-package 109 | that exports a type satisfying an interface defined in the outer package. 110 | 111 | == Design 112 | 113 | `ServerRenderer` will provide the injection point 114 | where the CLI-based and CDP-based renderers will be plugged in. 115 | 116 | The following interface will be defined for that purpose. 117 | 118 | ---- 119 | type CompileRequest struct { 120 | Source string 121 | } 122 | 123 | type CompileRequest struct { 124 | SVG string 125 | } 126 | 127 | type Compiler interface { 128 | Compile(context.Context, *CompileRequest) (*CompileResponse, error) 129 | } 130 | ---- 131 | 132 | Two implementations will be provided: 133 | 134 | * a CLI-based version in the root mermaid package 135 | * a CDP-based version in a `mermaidcdp` sub-package 136 | 137 | ---- 138 | // go.abhg.dev/goldmark/mermaid 139 | type CLICompiler struct{ /* ... */ } 140 | 141 | func (*CLICompiler) Compile(context.Context, *CompileRequest) (*CompileResponse, error) 142 | 143 | // go.abhg.dev/goldmark/mermaid/mermaidcdp 144 | type Compiler struct{ /* ... */ } 145 | 146 | func (*Compiler) Compile(context.Context, *CompileRequest) (*CompileResponse, error) 147 | ---- 148 | 149 | These will plug into the `ServerRenderer` type under a new `Compiler` field. 150 | This field will replace 151 | the existing `MMDC` and `Theme` fields of `ServerRenderer`. 152 | 153 | [,diff] 154 | ---- 155 | type ServerRenderer struct { 156 | - MMDC MMDC 157 | - Theme string 158 | + Compiler Compiler 159 | // ... 160 | } 161 | ---- 162 | 163 | For convenience, if the `ServerRenderer` was chosen 164 | and a `Compiler` was not provided, the CLI-based compiler will be used. 165 | 166 | === CLI-based rendering 167 | 168 | The `CLICompiler` will borrow fields that were previously on `ServerRenderer`: 169 | `MMDC` and `Theme`. 170 | 171 | For clarity, the `MMDC` field and type will be renamed to `CLI` -- 172 | making its association with the CLI-based compiler explicit. 173 | We can also take this opportunity to use `CommandContext` on the interface, 174 | instead of plain `Command`. 175 | 176 | ---- 177 | type CLI interface { 178 | CommandContext(context.Context, args ...string) *exec.Cmd 179 | } 180 | 181 | type CLICompiler struct { 182 | CLI CLI 183 | Theme string 184 | } 185 | ---- 186 | 187 | The name `CLI` was previously taken by the default implementation of `MMDC`. 188 | This will be made private to make room for the new `CLI` type, 189 | and the name `MMDC` will be re-used for a constructor function. 190 | 191 | [,diff] 192 | ---- 193 | -type CLI struct{ /* ... */ } 194 | +type mmdcCLI struct{ /* ... */ } 195 | 196 | +func MMDC(path string) CLI 197 | ---- 198 | 199 | In short, 200 | 201 | [cols="1a,1a,3a"] 202 | |==== 203 | | Before | After | Purpose 204 | 205 | | `MMDC` | `CLI` | Interface to build an `exec.Cmd` to run the Mermaid CLI 206 | | `CLI` | `mmdcCLI` | Default implementation of the interface 207 | | n/a | `MMDC` | Constructor function for the default implementation 208 | |==== 209 | 210 | === CDP-based rendering 211 | 212 | The CDP-based renderer will be implemented in a new `mermaidcdp` package 213 | that will export a `Compiler` struct. 214 | This will be built with a `New` function that accepts a `Config` struct. 215 | 216 | ---- 217 | package mermaidcdp 218 | 219 | type Compiler struct { 220 | // ... 221 | } 222 | 223 | var _ mermaid.Compiler = (*Compiler)(nil) 224 | 225 | func New(*Config) (*Compiler, error) 226 | ---- 227 | 228 | It'll have a `Close` method that will clean up the browser process. 229 | Until this is called, the `Compiler` may be re-used across render invocations. 230 | 231 | ---- 232 | func (*Compiler) Close() error 233 | ---- 234 | 235 | The `Compiler` needs the following inputs: 236 | 237 | * a copy of the MermaidJS source code to evaluate in the browser 238 | * the theme to use for rendering (for `mermaid.initialize`) 239 | 240 | Therefore, the `Config` struct will look as follows. 241 | 242 | ---- 243 | type Config struct { 244 | JSSource string 245 | Theme string 246 | } 247 | ---- 248 | 249 | === MermaidJS source code 250 | 251 | As mentioned previously, the CDP-based renderer 252 | needs a copy of the MermaidJS source code. 253 | The recommended approach will be for users to download it 254 | and embed it into their program with `go:embed`. 255 | 256 | As a convenience, we'll provide a function to download it on demand from a CDN. 257 | 258 | ---- 259 | package mermaidcdp 260 | 261 | func DownloadJSSource(..., version string) (string, error) 262 | ---- 263 | 264 | === Extender 265 | 266 | The `mermaid.Extender` is intended to be the most convenient way 267 | of installing this functionality into a `goldmark.Mermaid`. 268 | Therefore, its `CLI` (née `MMDC`) and `Theme` fields will be retained -- 269 | unlike `ServerRenderer` type. 270 | 271 | A new `Compiler` field will be added to the `Extender` type 272 | to allow users to plug in the CDP-based compiler. 273 | 274 | [,diff] 275 | ---- 276 | type Extender struct { 277 | - MMDC MMDC 278 | + CLI CLI 279 | Theme string 280 | + Compiler Compiler 281 | // ... 282 | } 283 | ---- 284 | 285 | == Example 286 | 287 | Putting it all together, example usage will look as follows. 288 | 289 | .Using the CLI compiler 290 | ---- 291 | &mermaid.Extender{ 292 | Compiler: &mermaid.CLICompiler{ 293 | Theme: "default", 294 | }, 295 | } 296 | ---- 297 | 298 | .Using the CDP compiler 299 | ---- 300 | //go:embed mermaid.min.js 301 | var mermaidJS string 302 | 303 | comp, err := mermaidcdp.New(&mermaidcdp.Config{ 304 | MermaidJS: mermaidJS, 305 | Theme: "default", 306 | }) 307 | if err != nil { 308 | // ... 309 | } 310 | defer comp.Close() 311 | 312 | &mermaid.Extender{ 313 | Compiler: comp, 314 | } 315 | ---- 316 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package mermaid adds support for Mermaid diagrams to the Goldmark Markdown 2 | // parser. 3 | package mermaid 4 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # goldmark-mermaid 2 | 3 | - [Introduction](intro.md) 4 | - [Installation](install.md) 5 | - [Usage](usage.md) 6 | - Rendering 7 | - [Rendering modes](render-mode.md) 8 | - [Server-side rendering](render-server.md) 9 | - [License](license.md) 10 | -------------------------------------------------------------------------------- /doc/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Install the latest version of the library with Go modules. 4 | 5 | ```bash 6 | go get go.abhg.dev/goldmark/mermaid@latest 7 | ``` 8 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/go.abhg.dev/goldmark/mermaid.svg)](https://pkg.go.dev/go.abhg.dev/goldmark/mermaid) 4 | [![CI](https://github.com/abhinav/goldmark-mermaid/actions/workflows/ci.yml/badge.svg)](https://github.com/abhinav/goldmark-mermaid/actions/workflows/ci.yml) 5 | [![codecov](https://codecov.io/gh/abhinav/goldmark-mermaid/branch/main/graph/badge.svg?token=W98KYF8SPE)](https://codecov.io/gh/abhinav/goldmark-mermaid) 6 | 7 | goldmark-mermaid is an extension for the [goldmark] Markdown parser that adds 8 | support for [Mermaid] diagrams. 9 | 10 | [goldmark]: http://github.com/yuin/goldmark 11 | [Mermaid]: https://mermaid-js.github.io/mermaid/ 12 | 13 | **Demo**: 14 | A web-based demonstration of the extension is available at 15 | . 16 | 17 | ## Features 18 | 19 | - Pluggable components 20 | - Supports client-side rendering by injecting JavaScript 21 | - Supports server-side rendering with the MermaidJS CLI or with your browser 22 | -------------------------------------------------------------------------------- /doc/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | This software is made available under the BSD3 license. 4 | -------------------------------------------------------------------------------- /doc/render-mode.md: -------------------------------------------------------------------------------- 1 | # Rendering methods 2 | 3 | goldmark-mermaid supports two rendering modes: 4 | 5 | - **Client-side**: 6 | Diagrams are rendered in-browser by injecting MermaidJS 7 | into the generated HTML. 8 | - **Server-side**: 9 | Diagrams are rendered at the time the HTML is generated. 10 | The browser receives only the final SVGs. 11 | 12 | You can pick between these by setting the `RenderMode` field. 13 | 14 | ```go 15 | goldmark.New( 16 | goldmark.WithExtensions( 17 | &mermaid.Extender{ 18 | RenderMode: mermaid.RenderModeServer, // or RenderModeClient 19 | }, 20 | ), 21 | // ... 22 | ).Convert(src, out) 23 | ``` 24 | 25 | A third automatic mode is provided as a convenience. 26 | It automatically picks between client-side and server-side rendering 27 | based on other configurations and system functionality. 28 | This mode is the default. 29 | -------------------------------------------------------------------------------- /doc/render-server.md: -------------------------------------------------------------------------------- 1 | # Server-side rendering 2 | 3 | goldmark-mermaid offers two options for server-side rendering: 4 | 5 | - **CLI-based rendering** 6 | invokes the MermaidJS CLI (`mmdc`) on your system to render diagrams 7 | - **CDP-based rendering** 8 | uses Chrome DevTools Protocol to drive a headless browser on your system, 9 | and uses it to render diagrams 10 | 11 | 12 | ## Rendering with the MermaidJS CLI 13 | 14 | If server-side rendering is chosen, by default, the CLI-based renderer is used. 15 | You can request it explicitly 16 | by supplying a `CLICompiler` to `mermaid.Extender` or `mermaid.ServerRenderer`. 17 | 18 | ```go 19 | &mermaid.Extender{ 20 | Compiler: &mermaid.CLICompiler{ 21 | Theme: "neutral", 22 | }, 23 | } 24 | ``` 25 | 26 | By default, the `CLICompiler` will search for `mmdc` on your `$PATH`. 27 | Specify an alternative path with the `CLI` field: 28 | 29 | ```go 30 | &mermaid.CLICompiler{ 31 | CLI: mermaid.MMDC(pathToMMDC), 32 | } 33 | ``` 34 | 35 | ## Rendering with Chrome DevTools Protocol 36 | 37 | 38 | 39 | If you have a Chromium-like browser installed on your system 40 | goldmark-mermaid can spin up a long-running headless process of it, 41 | and use that to render MermaidJS diagrams. 42 | 43 | To use this, first download a minified copy of the MermaidJS source code. 44 | You can get it from . 45 | Embed this into your program with `go:embed`. 46 | 47 | ```go 48 | import _ "embed" // needed for go:embed 49 | 50 | //go:embed mermaid.min.js 51 | var mermaidJSSource string 52 | ``` 53 | 54 | Next, import `go.abhg.dev/goldmark/mermaid/mermaidcdp`, 55 | and set up a `mermaidcdp.Compiler`. 56 | 57 | ```go 58 | compiler, err := mermaidcdp.New(&mermaidcdp.Config{ 59 | JSSource: mermaidJSSource, 60 | }) 61 | if err != nil { 62 | panic(err) 63 | } 64 | defer compiler.Close() // Don't forget this! 65 | ``` 66 | 67 | Plug this compiler into the `mermaid.Extender` that 68 | you install into your Goldmark Markdown object, 69 | and use the Markdown object like usual. 70 | 71 | ```go 72 | md := goldmark.New( 73 | goldmark.WithExtensions( 74 | // ... 75 | &mermaid.Extender{ 76 | Compiler: compiler, 77 | }, 78 | ), 79 | // ... 80 | ) 81 | 82 | md.Convert(...) 83 | ``` 84 | -------------------------------------------------------------------------------- /doc/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | To use goldmark-mermaid, import the `mermaid` package. 4 | 5 | ```go 6 | import "go.abhg.dev/goldmark/mermaid" 7 | ``` 8 | 9 | Then include the `mermaid.Extender` in the list of extensions you build your 10 | [`goldmark.Markdown`] with. 11 | 12 | [`goldmark.Markdown`]: https://pkg.go.dev/github.com/yuin/goldmark#Markdown 13 | 14 | ```go 15 | goldmark.New( 16 | goldmark.WithExtensions( 17 | // ... 18 | &mermaid.Extender{}, 19 | ), 20 | // ... 21 | ).Convert(src, out) 22 | ``` 23 | 24 | The package supports Mermaid diagrams inside fenced code blocks with the language `mermaid`. For example, 25 | 26 |
27 | ```mermaid
28 | graph TD;
29 |     A-->B;
30 |     A-->C;
31 |     B-->D;
32 |     C-->D;
33 | ```
34 | 
35 | 36 | When you render the Markdown as HTML, these will be rendered into diagrams. 37 | 38 | You can also render diagrams server-side if you have a Chromium-like browser 39 | installed. See [Rendering with CDP](render-server.md#render-cdp) for details. 40 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package mermaid_test 2 | 3 | import ( 4 | "github.com/yuin/goldmark" 5 | "go.abhg.dev/goldmark/mermaid" 6 | ) 7 | 8 | func ExampleExtender() { 9 | goldmark.New( 10 | // ... 11 | goldmark.WithExtensions( 12 | &mermaid.Extender{ 13 | RenderMode: mermaid.RenderModeServer, 14 | }, 15 | // ... 16 | ), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /extend.go: -------------------------------------------------------------------------------- 1 | package mermaid 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | 7 | "github.com/yuin/goldmark" 8 | "github.com/yuin/goldmark/parser" 9 | "github.com/yuin/goldmark/renderer" 10 | "github.com/yuin/goldmark/util" 11 | ) 12 | 13 | // Extender adds support for Mermaid diagrams to a Goldmark Markdown parser. 14 | // 15 | // Use it by installing it to the goldmark.Markdown object upon creation. 16 | type Extender struct { 17 | // RenderMode specifies which renderer the Extender should install. 18 | // 19 | // Defaults to AutoRenderMode, picking renderers 20 | // based on the availability of the Mermaid CLI. 21 | RenderMode RenderMode 22 | 23 | // Compiler specifies how to compile Mermaid diagrams server-side. 24 | // 25 | // If specified, and render mode is not set to client-side, 26 | // this will be used to render diagrams. 27 | Compiler Compiler 28 | 29 | // CLI specifies how to invoke the Mermaid CLI 30 | // to compile Mermaid diagrams server-side. 31 | // 32 | // If specified, and render mode is not set to client-side, 33 | // this will be used to render diagrams. 34 | // 35 | // If both CLI and Compiler are specified, Compiler takes precedence. 36 | CLI CLI 37 | 38 | // URL of Mermaid Javascript to be included in the page 39 | // for client-side rendering. 40 | // 41 | // Ignored if NoScript is true or if we're rendering diagrams server-side. 42 | // 43 | // Defaults to the latest version available on cdn.jsdelivr.net. 44 | MermaidURL string 45 | 46 | // HTML tag to use for the container element for diagrams. 47 | // 48 | // Defaults to "pre" for client-side rendering, 49 | // and "div" for server-side rendering. 50 | ContainerTag string 51 | 52 | // If true, don't add a 20 | 21 | - desc: noscript/single block 22 | noscript: true 23 | give: | 24 | Single mermaid block. 25 | 26 | ```mermaid 27 | graph TD; 28 | A-->B; 29 | A-->C; 30 | B-->D; 31 | C-->D; 32 | ``` 33 | want: | 34 |

Single mermaid block.

35 |
graph TD;
 36 |         A-->B;
 37 |         A-->C;
 38 |         B-->D;
 39 |         C-->D;
 40 |     
41 | 42 | - desc: unmarked block 43 | give: | 44 | Leaves unmarked blocks alone. 45 | 46 | ``` 47 | graph TD; 48 | A-->B; 49 | A-->C; 50 | B-->D; 51 | C-->D; 52 | ``` 53 | want: | 54 |

Leaves unmarked blocks alone.

55 |
graph TD;
 56 |         A-->B;
 57 |         A-->C;
 58 |         B-->D;
 59 |         C-->D;
 60 |     
61 | 62 | - desc: ignore others 63 | give: | 64 | Does not change other languages. 65 | 66 | ```javascript 67 | console.log("hello") 68 | ``` 69 | want: | 70 |

Does not change other languages.

71 |
console.log("hello")
 72 |     
73 | 74 | - desc: multiple blocks 75 | give: | 76 | Supports multiple Mermaid blocks. (#3) 77 | 78 | ```mermaid 79 | graph TD; 80 | A-->B; 81 | A-->C; 82 | B-->D; 83 | C-->D; 84 | ``` 85 | 86 | ```mermaid 87 | graph TD; 88 | A-->B; 89 | A-->C; 90 | B-->D; 91 | C-->D; 92 | ``` 93 | want: | 94 |

Supports multiple Mermaid blocks. (#3)

95 |
graph TD;
 96 |         A-->B;
 97 |         A-->C;
 98 |         B-->D;
 99 |         C-->D;
100 |     
graph TD;
101 |         A-->B;
102 |         A-->C;
103 |         B-->D;
104 |         C-->D;
105 |     
106 | 107 | 108 | - desc: noscript/multiple blocks 109 | noscript: true 110 | give: | 111 | Supports multiple Mermaid blocks. (#3) 112 | 113 | ```mermaid 114 | graph TD; 115 | A-->B; 116 | A-->C; 117 | B-->D; 118 | C-->D; 119 | ``` 120 | 121 | ```mermaid 122 | graph TD; 123 | A-->B; 124 | A-->C; 125 | B-->D; 126 | C-->D; 127 | ``` 128 | want: | 129 |

Supports multiple Mermaid blocks. (#3)

130 |
graph TD;
131 |         A-->B;
132 |         A-->C;
133 |         B-->D;
134 |         C-->D;
135 |     
graph TD;
136 |         A-->B;
137 |         A-->C;
138 |         B-->D;
139 |         C-->D;
140 |     
141 | 142 | - desc: container tag 143 | containerTag: div 144 | give: | 145 | Transforms mermaid blocks. 146 | 147 | ```mermaid 148 | graph TD; 149 | A-->B; 150 | A-->C; 151 | B-->D; 152 | C-->D; 153 | ``` 154 | want: | 155 |

Transforms mermaid blocks.

156 |
graph TD; 157 | A-->B; 158 | A-->C; 159 | B-->D; 160 | C-->D; 161 |
162 | 163 | -------------------------------------------------------------------------------- /testdata/mermaid.js: -------------------------------------------------------------------------------- 1 | ../internal/testdata/mermaid-10.6.0.js -------------------------------------------------------------------------------- /testdata/server_cdp.yaml: -------------------------------------------------------------------------------- 1 | - desc: single block 2 | give: | 3 | Transforms mermaid blocks. 4 | 5 | ```mermaid 6 | graph TD; 7 | A-->B; 8 | ``` 9 | want: | 10 |

Transforms mermaid blocks.

11 |
A
B
12 | -------------------------------------------------------------------------------- /testdata/server_cli.yaml: -------------------------------------------------------------------------------- 1 | - desc: single block 2 | give: | 3 | Transforms mermaid blocks. 4 | 5 | ```mermaid 6 | graph TD; 7 | A-->B; 8 | ``` 9 | want: | 10 |

Transforms mermaid blocks.

11 |

A

B

12 | 13 | - desc: container tag 14 | containerTag: pre 15 | give: | 16 | Transforms mermaid blocks. 17 | 18 | ```mermaid 19 | graph TD; 20 | A-->B; 21 | ``` 22 | want: | 23 |

Transforms mermaid blocks.

24 |

A

B

25 | -------------------------------------------------------------------------------- /transform.go: -------------------------------------------------------------------------------- 1 | package mermaid 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/yuin/goldmark/ast" 7 | "github.com/yuin/goldmark/parser" 8 | "github.com/yuin/goldmark/text" 9 | ) 10 | 11 | // Transformer transforms a Goldmark Markdown AST with support for Mermaid 12 | // diagrams. It makes the following transformations: 13 | // 14 | // - replace mermaid code blocks with mermaid.Block nodes 15 | // - add a mermaid.ScriptBlock node if the document uses Mermaid 16 | // and one does not already exist 17 | type Transformer struct { 18 | // Don't add a ScriptBlock to the end of the page 19 | // even if the page doesn't already have one. 20 | NoScript bool 21 | } 22 | 23 | var _mermaid = []byte("mermaid") 24 | 25 | // Transform transforms the provided Markdown AST. 26 | func (t *Transformer) Transform(doc *ast.Document, reader text.Reader, _ parser.Context) { 27 | var ( 28 | hasScript bool 29 | mermaidBlocks []*ast.FencedCodeBlock 30 | ) 31 | 32 | // Collect all blocks to be replaced without modifying the tree. 33 | _ = ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) { 34 | if !enter { 35 | return ast.WalkContinue, nil 36 | } 37 | 38 | // For multiple transforms. 39 | if _, ok := node.(*ScriptBlock); ok { 40 | hasScript = true 41 | return ast.WalkContinue, nil 42 | } 43 | 44 | cb, ok := node.(*ast.FencedCodeBlock) 45 | if !ok { 46 | return ast.WalkContinue, nil 47 | } 48 | 49 | lang := cb.Language(reader.Source()) 50 | if !bytes.Equal(lang, _mermaid) { 51 | return ast.WalkContinue, nil 52 | } 53 | 54 | mermaidBlocks = append(mermaidBlocks, cb) 55 | return ast.WalkContinue, nil 56 | }) 57 | 58 | // Nothing to do. 59 | if len(mermaidBlocks) == 0 { 60 | return 61 | } 62 | 63 | for _, cb := range mermaidBlocks { 64 | b := new(Block) 65 | b.SetLines(cb.Lines()) 66 | 67 | parent := cb.Parent() 68 | if parent != nil { 69 | parent.ReplaceChild(parent, cb, b) 70 | } 71 | } 72 | 73 | if !hasScript && !t.NoScript { 74 | doc.AppendChild(doc, &ScriptBlock{}) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /transform_test.go: -------------------------------------------------------------------------------- 1 | package mermaid 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "github.com/yuin/goldmark" 10 | "github.com/yuin/goldmark/ast" 11 | "github.com/yuin/goldmark/parser" 12 | "github.com/yuin/goldmark/text" 13 | "github.com/yuin/goldmark/util" 14 | ) 15 | 16 | func TestTransformer(t *testing.T) { 17 | t.Parallel() 18 | 19 | tests := []struct { 20 | desc string 21 | give string 22 | noScript bool 23 | wantBodies []string 24 | wantScript bool 25 | }{ 26 | { 27 | desc: "empty", 28 | give: "", 29 | }, 30 | { 31 | desc: "mermaid", 32 | give: unlines( 33 | "```mermaid", 34 | "foo", 35 | "```", 36 | ), 37 | wantBodies: []string{"foo\n"}, 38 | wantScript: true, 39 | }, 40 | { 41 | desc: "mermaid and not", 42 | give: unlines( 43 | "Foo", 44 | "", 45 | "```mermaid", 46 | "foo", 47 | "```", 48 | "", 49 | "Bar", 50 | "", 51 | "```go", 52 | "bar", 53 | "", 54 | "Baz", 55 | "", 56 | ), 57 | wantBodies: []string{"foo\n"}, 58 | wantScript: true, 59 | }, 60 | { 61 | desc: "noscript", 62 | noScript: true, 63 | give: unlines( 64 | "```mermaid", 65 | "foo", 66 | "```", 67 | ), 68 | wantBodies: []string{"foo\n"}, 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | tt := tt 74 | t.Run(tt.desc, func(t *testing.T) { 75 | t.Parallel() 76 | 77 | p := goldmark.New().Parser() 78 | p.AddOptions( 79 | parser.WithASTTransformers( 80 | util.Prioritized(&Transformer{ 81 | NoScript: tt.noScript, 82 | }, 100), 83 | ), 84 | ) 85 | 86 | src := []byte(tt.give) 87 | got := p.Parse(text.NewReader(src)) 88 | 89 | var ( 90 | gotBodies []string 91 | gotScript int 92 | ) 93 | err := ast.Walk(got, func(node ast.Node, enter bool) (ast.WalkStatus, error) { 94 | if !enter { 95 | return ast.WalkContinue, nil 96 | } 97 | 98 | switch n := node.(type) { 99 | case *Block: 100 | var buff bytes.Buffer 101 | lines := n.Lines() 102 | for i := 0; i < lines.Len(); i++ { 103 | line := lines.At(i) 104 | buff.Write(line.Value(src)) 105 | } 106 | 107 | gotBodies = append(gotBodies, buff.String()) 108 | 109 | case *ScriptBlock: 110 | gotScript++ 111 | } 112 | 113 | return ast.WalkContinue, nil 114 | }) 115 | require.NoError(t, err) 116 | assert.Equal(t, tt.wantBodies, gotBodies) 117 | if tt.wantScript { 118 | assert.Equal(t, 1, gotScript) 119 | } else { 120 | assert.Zero(t, gotScript) 121 | } 122 | }) 123 | } 124 | } 125 | 126 | func TestTransformer_RepeatedTransformations(t *testing.T) { 127 | t.Parallel() 128 | 129 | src := []byte(unlines( 130 | "```mermaid", 131 | "foo", 132 | "```", 133 | )) 134 | r := text.NewReader(src) 135 | 136 | pctx := parser.NewContext() 137 | doc := goldmark.New().Parser(). 138 | Parse(r, parser.WithContext(pctx)).(*ast.Document) 139 | 140 | var trans Transformer 141 | for i := 0; i < 10; i++ { 142 | trans.Transform(doc, r, pctx) 143 | } 144 | 145 | var scriptCount int 146 | err := ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) { 147 | if _, ok := node.(*ScriptBlock); ok && enter { 148 | scriptCount++ 149 | } 150 | return ast.WalkContinue, nil 151 | }) 152 | require.NoError(t, err) 153 | assert.Equal(t, 1, scriptCount) 154 | } 155 | -------------------------------------------------------------------------------- /utils_for_test.go: -------------------------------------------------------------------------------- 1 | package mermaid 2 | 3 | import "strings" 4 | 5 | // unlines returns the string formed by joining the provided strings after 6 | // appending a newline to each. 7 | func unlines(lines ...string) string { 8 | return strings.Join(lines, "\n") + "\n" 9 | } 10 | --------------------------------------------------------------------------------