Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add :caption and :download options to Kino.Mermaid #477

Merged
merged 11 commits into from
Oct 15, 2024
36 changes: 36 additions & 0 deletions assets/packs/mermaid/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.container {
display: flex;
align-items: flex-start;
gap: 8px;
width: max-content;
}

.container .download-btn {
cursor: pointer;
padding: 0.25rem;
color: #61758A;
background: none;
border: none;
}

.container .download-btn:hover {
color: #0D1829;
}

.container:not(:hover) .download-btn {
opacity: 0;
}

.figure {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
width: max-content;
}

.caption {
margin-top: 8px;
font-size: 0.875rem;
color: #61758a;
}
51 changes: 47 additions & 4 deletions assets/packs/mermaid/main.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,61 @@
import mermaid from "mermaid";
import "./main.css";

mermaid.initialize({ startOnLoad: false });

export function init(ctx, content) {
export function init(ctx, { diagram, caption, download }) {
ctx.importCSS("main.css");

function render() {
mermaid.render("graph1", content).then(({ svg, bindFunctions }) => {
ctx.root.innerHTML = svg;
mermaid.render("diagram", diagram).then(({ svg, bindFunctions }) => {
// Fix for: https://github.com/mermaid-js/mermaid/issues/1766
svg = svg.replace(/<br>/gi, "<br/>");

let container = document.createElement("div");
container.classList.add("container");
ctx.root.appendChild(container);

const figure = document.createElement("figure");
figure.classList.add("figure");
figure.innerHTML = svg;
container.appendChild(figure);

if (caption) {
const figcaption = document.createElement("figcaption");
figcaption.classList.add("caption");
figcaption.textContent = caption;
figure.appendChild(figcaption);
}

if (download) {
const downloadBtn = document.createElement("button");
downloadBtn.classList.add("download-btn");
downloadBtn.title = "Download";
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>`;
container.appendChild(downloadBtn);

downloadBtn.addEventListener("click", (event) => {
const blobURL = URL.createObjectURL(
new Blob([svg], { type: "image/svg+xml" }),
);

const a = document.createElement("a");
a.style.display = "none";
a.href = blobURL;
a.download = "diagram.svg";

container.appendChild(a);
a.click();
container.removeChild(a);
});
}

if (bindFunctions) {
bindFunctions(ctx.root);
}

// A workaround for https://github.com/mermaid-js/mermaid/issues/1758
const svgEl = ctx.root.querySelector("svg");
const svgEl = figure.querySelector("svg");
svgEl.removeAttribute("height");
});
}
Expand Down
1 change: 1 addition & 0 deletions lib/assets/mermaid/build/main.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/assets/mermaid/build/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 20 additions & 6 deletions lib/kino/mermaid.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
defmodule Kino.Mermaid do
@moduledoc ~S'''
A kino for rendering Mermaid graphs.
A kino for rendering Mermaid diagrams.

> #### Relation to Kino.Markdown {: .info}
>
> Mermaid graphs can also be generated dynamically with `Kino.Markdown`,
> Mermaid diagrams can also be generated dynamically with `Kino.Markdown`,
> however the output of `Kino.Markdown` is never persisted in the
> notebook source. `Kino.Mermaid` doesn't have this limitation.

Expand All @@ -25,10 +25,24 @@ defmodule Kino.Mermaid do
@type t :: Kino.JS.t()

@doc """
Creates a new kino displaying the given Mermaid graph.
Creates a new kino displaying the given Mermaid diagram.

## Options

* `:caption` - an optional caption for the rendered diagram.

* `:download` - whether or not to show a button for downloading
the diagram as a SVG. Defaults to `true`.

"""
@spec new(binary()) :: t()
def new(content) do
Kino.JS.new(__MODULE__, content, export: fn content -> {"mermaid", content} end)
@spec new(binary(), keyword()) :: t()
def new(diagram, opts \\ []) do
opts = Keyword.validate!(opts, caption: nil, download: true)

Kino.JS.new(
__MODULE__,
%{diagram: diagram, caption: opts[:caption], download: opts[:download]},
export: fn diagram -> {"mermaid", diagram} end
)
end
end
60 changes: 42 additions & 18 deletions lib/kino/process.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ defmodule Kino.Process do
* `:render_ets_tables` - determines whether ETS tables associated with the
supervision tree are rendered. Defaults to `false`.

* `:caption` - an optional caption for the diagram. Either a custom
caption as string, or `nil` to disable the default caption.

## Examples

To view the applications running in your instance run:
Expand Down Expand Up @@ -86,13 +89,18 @@ defmodule Kino.Process do
{:dictionary, dictionary} = process_info(root_supervisor, :dictionary)
[ancestor] = dictionary[:"$ancestors"]

Mermaid.new("""
graph #{direction};
application_master(#{inspect(master)}):::supervisor ---> supervisor_ancestor;
supervisor_ancestor(#{inspect(ancestor)}):::supervisor ---> 0;
#{edges}
#{@mermaid_classdefs}
""")
caption = Keyword.get(opts, :caption, "Application tree for #{inspect(application)}")

Mermaid.new(
"""
graph #{direction};
application_master(#{inspect(master)}):::supervisor ---> supervisor_ancestor;
supervisor_ancestor(#{inspect(ancestor)}):::supervisor ---> 0;
#{edges}
#{@mermaid_classdefs}
""",
caption: caption
)
end

@doc """
Expand All @@ -108,6 +116,9 @@ defmodule Kino.Process do
* `:direction` - defines the direction of the graph visual. The
value can either be `:top_down` or `:left_right`. Defaults to `:top_down`.

* `:caption` - an optional caption for the diagram. Either a custom
caption as string, or `nil` to disable the default caption.

## Examples

With a supervisor definition like so:
Expand Down Expand Up @@ -162,11 +173,16 @@ defmodule Kino.Process do

edges = traverse_supervisor(supervisor_pid, opts)

Mermaid.new("""
graph #{direction};
#{edges}
#{@mermaid_classdefs}
""")
caption = Keyword.get(opts, :caption, "Supervisor tree for #{inspect(supervisor)}")

Mermaid.new(
"""
graph #{direction};
#{edges}
#{@mermaid_classdefs}
""",
caption: caption
)
end

@doc """
Expand Down Expand Up @@ -236,6 +252,9 @@ defmodule Kino.Process do
is used. However, if the function returns a `String.t()`, then
that will be used for the label.

* `:caption` - an optional caption for the diagram. Either a custom
caption as string, or `nil` to disable the default caption.

## Examples

To generate a trace of all the messages occurring during the execution of the
Expand Down Expand Up @@ -412,13 +431,18 @@ defmodule Kino.Process do
|> Enum.reverse()
|> Enum.join("\n")

caption = Keyword.get(opts, :caption, "Messages traced from #{inspect(trace_pids)}")

sequence_diagram =
Mermaid.new("""
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
sequenceDiagram
#{participants}
#{messages}
""")
Mermaid.new(
"""
%%{init: {'themeCSS': '.actor:last-of-type:not(:only-of-type) {dominant-baseline: hanging;}'} }%%
sequenceDiagram
#{participants}
#{messages}
""",
caption: caption
)

{func_result, sequence_diagram}
end
Expand Down
6 changes: 3 additions & 3 deletions lib/kino/shorts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ defmodule Kino.Shorts do
def text(text), do: Kino.Text.new(text)

@doc ~S'''
Renders Mermaid graphs.
Renders Mermaid diagrams.

It is a wrapper around `Kino.Mermaid.new/1`.

Expand All @@ -178,8 +178,8 @@ defmodule Kino.Shorts do
C-->D;
""")
'''
@spec mermaid(String.t()) :: Kino.Mermaid.t()
def mermaid(mermaid), do: Kino.Mermaid.new(mermaid)
@spec mermaid(String.t(), keyword()) :: Kino.Mermaid.t()
def mermaid(diagram, opts \\ []), do: Kino.Mermaid.new(diagram, opts)

@doc """
A placeholder for static outputs that can be dynamically updated.
Expand Down
2 changes: 1 addition & 1 deletion test/kino/process_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ defmodule Kino.ProcessTest do
send(Kino.JS.DataStore, {:connect, self(), %{origin: "client:#{inspect(self())}", ref: ref}})
assert_receive {:connect_reply, data, %{ref: ^ref}}

data
data.diagram
end

defp supervision_tree_with_ets_table do
Expand Down
Loading