├── .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: `
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 |Transforms mermaid blocks.
11 | 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 | 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 | 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 | --------------------------------------------------------------------------------