Skip to content

Commit d243493

Browse files
Add :caption and :download options to Kino.Mermaid (#477)
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
1 parent f538e63 commit d243493

File tree

8 files changed

+151
-33
lines changed

8 files changed

+151
-33
lines changed

assets/packs/mermaid/main.css

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.container {
2+
display: flex;
3+
align-items: flex-start;
4+
gap: 8px;
5+
width: max-content;
6+
}
7+
8+
.container .download-btn {
9+
cursor: pointer;
10+
padding: 0.25rem;
11+
color: #61758A;
12+
background: none;
13+
border: none;
14+
}
15+
16+
.container .download-btn:hover {
17+
color: #0D1829;
18+
}
19+
20+
.container:not(:hover) .download-btn {
21+
opacity: 0;
22+
}
23+
24+
.figure {
25+
margin: 0;
26+
display: flex;
27+
flex-direction: column;
28+
align-items: center;
29+
width: max-content;
30+
}
31+
32+
.caption {
33+
margin-top: 8px;
34+
font-size: 0.875rem;
35+
color: #61758a;
36+
}

assets/packs/mermaid/main.js

+47-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,61 @@
11
import mermaid from "mermaid";
2+
import "./main.css";
23

34
mermaid.initialize({ startOnLoad: false });
45

5-
export function init(ctx, content) {
6+
export function init(ctx, { diagram, caption, download }) {
7+
ctx.importCSS("main.css");
8+
69
function render() {
7-
mermaid.render("graph1", content).then(({ svg, bindFunctions }) => {
8-
ctx.root.innerHTML = svg;
10+
mermaid.render("diagram", diagram).then(({ svg, bindFunctions }) => {
11+
// Fix for: https://github.com/mermaid-js/mermaid/issues/1766
12+
svg = svg.replace(/<br>/gi, "<br/>");
13+
14+
let container = document.createElement("div");
15+
container.classList.add("container");
16+
ctx.root.appendChild(container);
17+
18+
const figure = document.createElement("figure");
19+
figure.classList.add("figure");
20+
figure.innerHTML = svg;
21+
container.appendChild(figure);
22+
23+
if (caption) {
24+
const figcaption = document.createElement("figcaption");
25+
figcaption.classList.add("caption");
26+
figcaption.textContent = caption;
27+
figure.appendChild(figcaption);
28+
}
29+
30+
if (download) {
31+
const downloadBtn = document.createElement("button");
32+
downloadBtn.classList.add("download-btn");
33+
downloadBtn.title = "Download";
34+
downloadBtn.innerHTML = `<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 10H18L12 16L6 10H11V3H13V10ZM4 19H20V12H22V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V12H4V19Z"></path></svg>`;
35+
container.appendChild(downloadBtn);
36+
37+
downloadBtn.addEventListener("click", (event) => {
38+
const blobURL = URL.createObjectURL(
39+
new Blob([svg], { type: "image/svg+xml" }),
40+
);
41+
42+
const a = document.createElement("a");
43+
a.style.display = "none";
44+
a.href = blobURL;
45+
a.download = "diagram.svg";
46+
47+
container.appendChild(a);
48+
a.click();
49+
container.removeChild(a);
50+
});
51+
}
952

1053
if (bindFunctions) {
1154
bindFunctions(ctx.root);
1255
}
1356

1457
// A workaround for https://github.com/mermaid-js/mermaid/issues/1758
15-
const svgEl = ctx.root.querySelector("svg");
58+
const svgEl = figure.querySelector("svg");
1659
svgEl.removeAttribute("height");
1760
});
1861
}

lib/assets/mermaid/build/main.css

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/assets/mermaid/build/main.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/kino/mermaid.ex

+20-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
defmodule Kino.Mermaid do
22
@moduledoc ~S'''
3-
A kino for rendering Mermaid graphs.
3+
A kino for rendering Mermaid diagrams.
44
55
> #### Relation to Kino.Markdown {: .info}
66
>
7-
> Mermaid graphs can also be generated dynamically with `Kino.Markdown`,
7+
> Mermaid diagrams can also be generated dynamically with `Kino.Markdown`,
88
> however the output of `Kino.Markdown` is never persisted in the
99
> notebook source. `Kino.Mermaid` doesn't have this limitation.
1010
@@ -25,10 +25,24 @@ defmodule Kino.Mermaid do
2525
@type t :: Kino.JS.t()
2626

2727
@doc """
28-
Creates a new kino displaying the given Mermaid graph.
28+
Creates a new kino displaying the given Mermaid diagram.
29+
30+
## Options
31+
32+
* `:caption` - an optional caption for the rendered diagram.
33+
34+
* `:download` - whether or not to show a button for downloading
35+
the diagram as a SVG. Defaults to `true`.
36+
2937
"""
30-
@spec new(binary()) :: t()
31-
def new(content) do
32-
Kino.JS.new(__MODULE__, content, export: fn content -> {"mermaid", content} end)
38+
@spec new(binary(), keyword()) :: t()
39+
def new(diagram, opts \\ []) do
40+
opts = Keyword.validate!(opts, caption: nil, download: true)
41+
42+
Kino.JS.new(
43+
__MODULE__,
44+
%{diagram: diagram, caption: opts[:caption], download: opts[:download]},
45+
export: fn diagram -> {"mermaid", diagram} end
46+
)
3347
end
3448
end

lib/kino/process.ex

+42-18
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ defmodule Kino.Process do
3535
* `:render_ets_tables` - determines whether ETS tables associated with the
3636
supervision tree are rendered. Defaults to `false`.
3737
38+
* `:caption` - an optional caption for the diagram. Either a custom
39+
caption as string, or `nil` to disable the default caption.
40+
3841
## Examples
3942
4043
To view the applications running in your instance run:
@@ -86,13 +89,18 @@ defmodule Kino.Process do
8689
{:dictionary, dictionary} = process_info(root_supervisor, :dictionary)
8790
[ancestor] = dictionary[:"$ancestors"]
8891

89-
Mermaid.new("""
90-
graph #{direction};
91-
application_master(#{inspect(master)}):::supervisor ---> supervisor_ancestor;
92-
supervisor_ancestor(#{inspect(ancestor)}):::supervisor ---> 0;
93-
#{edges}
94-
#{@mermaid_classdefs}
95-
""")
92+
caption = Keyword.get(opts, :caption, "Application tree for #{inspect(application)}")
93+
94+
Mermaid.new(
95+
"""
96+
graph #{direction};
97+
application_master(#{inspect(master)}):::supervisor ---> supervisor_ancestor;
98+
supervisor_ancestor(#{inspect(ancestor)}):::supervisor ---> 0;
99+
#{edges}
100+
#{@mermaid_classdefs}
101+
""",
102+
caption: caption
103+
)
96104
end
97105

98106
@doc """
@@ -108,6 +116,9 @@ defmodule Kino.Process do
108116
* `:direction` - defines the direction of the graph visual. The
109117
value can either be `:top_down` or `:left_right`. Defaults to `:top_down`.
110118
119+
* `:caption` - an optional caption for the diagram. Either a custom
120+
caption as string, or `nil` to disable the default caption.
121+
111122
## Examples
112123
113124
With a supervisor definition like so:
@@ -162,11 +173,16 @@ defmodule Kino.Process do
162173

163174
edges = traverse_supervisor(supervisor_pid, opts)
164175

165-
Mermaid.new("""
166-
graph #{direction};
167-
#{edges}
168-
#{@mermaid_classdefs}
169-
""")
176+
caption = Keyword.get(opts, :caption, "Supervisor tree for #{inspect(supervisor)}")
177+
178+
Mermaid.new(
179+
"""
180+
graph #{direction};
181+
#{edges}
182+
#{@mermaid_classdefs}
183+
""",
184+
caption: caption
185+
)
170186
end
171187

172188
@doc """
@@ -236,6 +252,9 @@ defmodule Kino.Process do
236252
is used. However, if the function returns a `String.t()`, then
237253
that will be used for the label.
238254
255+
* `:caption` - an optional caption for the diagram. Either a custom
256+
caption as string, or `nil` to disable the default caption.
257+
239258
## Examples
240259
241260
To generate a trace of all the messages occurring during the execution of the
@@ -412,13 +431,18 @@ defmodule Kino.Process do
412431
|> Enum.reverse()
413432
|> Enum.join("\n")
414433

434+
caption = Keyword.get(opts, :caption, "Messages traced from #{inspect(trace_pids)}")
435+
415436
sequence_diagram =
416-
Mermaid.new("""
417-
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
418-
sequenceDiagram
419-
#{participants}
420-
#{messages}
421-
""")
437+
Mermaid.new(
438+
"""
439+
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
440+
sequenceDiagram
441+
#{participants}
442+
#{messages}
443+
""",
444+
caption: caption
445+
)
422446

423447
{func_result, sequence_diagram}
424448
end

lib/kino/shorts.ex

+3-3
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ defmodule Kino.Shorts do
162162
def text(text), do: Kino.Text.new(text)
163163

164164
@doc ~S'''
165-
Renders Mermaid graphs.
165+
Renders Mermaid diagrams.
166166
167167
It is a wrapper around `Kino.Mermaid.new/1`.
168168
@@ -178,8 +178,8 @@ defmodule Kino.Shorts do
178178
C-->D;
179179
""")
180180
'''
181-
@spec mermaid(String.t()) :: Kino.Mermaid.t()
182-
def mermaid(mermaid), do: Kino.Mermaid.new(mermaid)
181+
@spec mermaid(String.t(), keyword()) :: Kino.Mermaid.t()
182+
def mermaid(diagram, opts \\ []), do: Kino.Mermaid.new(diagram, opts)
183183

184184
@doc """
185185
A placeholder for static outputs that can be dynamically updated.

test/kino/process_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ defmodule Kino.ProcessTest do
227227
send(Kino.JS.DataStore, {:connect, self(), %{origin: "client:#{inspect(self())}", ref: ref}})
228228
assert_receive {:connect_reply, data, %{ref: ^ref}}
229229

230-
data
230+
data.diagram
231231
end
232232

233233
defp supervision_tree_with_ets_table do

0 commit comments

Comments
 (0)