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
26 changes: 26 additions & 0 deletions assets/packs/mermaid/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#contents button#download {
position: absolute;
display: none;
}

#contents:hover button#download {
display: inline;
right: 0;
}

figure {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

figure figcaption {
border-radius: 0.5rem;
background-color: rgb(240 245 249);
padding: 0.5rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
color: rgb(97 117 138);
}
60 changes: 56 additions & 4 deletions assets/packs/mermaid/main.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
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
const renderedSvg = svg.replace(/<br>/gi, "<br />");

let contents = document.createElement("div");
contents.id = "contents";
ctx.root.appendChild(contents);

let figure = document.createElement("figure");
figure.id = "figure";
figure.innerHTML = renderedSvg;
contents.appendChild(figure);

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

if (download) {
let downloadButton = document.createElement("button");
downloadButton.id = "download";
downloadButton.title = `Download ${download.title}`;
downloadButton.textContent = "⇩";
contents.prepend(downloadButton);

contents
.querySelector("#download")
.addEventListener("click", (event) => {
var downloadData = [];
downloadData.push(renderedSvg);
const downloadBlob = URL.createObjectURL(
new Blob(downloadData, { type: "image/svg+xml" })
);

const downloadLink = document.createElement("a");
downloadLink.href = downloadBlob;
downloadLink.download = download.filename;
contents.appendChild(downloadLink);

downloadLink.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window,
})
);

contents.removeChild(downloadLink);
});
}

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
49 changes: 43 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 @@ -24,11 +24,48 @@ defmodule Kino.Mermaid do

@type t :: Kino.JS.t()

@download_defaults [title: "Diagram", filename: "diagram.svg"]

@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.
Can be `nil` or a string. Defaults to `nil`.

* `:download` - whether or not to allow downloading the rendered Mermaid svg.
Defaults to `true`.

Downloads can be further customized by providing a keyword list
instead of a boolean, containing:

* `:title` - The alt text displayed for the download button.
* `:filename` - The name of the file to be downloaded.

"""
@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: false, download: true)

download =
case Keyword.fetch!(opts, :download) do
true ->
Map.new(@download_defaults)

download_opts when is_list(download_opts) ->
download_opts
|> Keyword.validate!(@download_defaults)
|> Map.new()

_ ->
false
end

caption = Keyword.fetch!(opts, :caption)

Kino.JS.new(__MODULE__, %{diagram: diagram, caption: caption, download: download},
export: fn diagram -> {"mermaid", diagram} end
)
end
end
77 changes: 59 additions & 18 deletions lib/kino/process.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ 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.
Can be `true` to use the default, falsey for none, or a string for a custom caption.
Defaults to the provided `application` name.

## Examples

To view the applications running in your instance run:
Expand Down Expand Up @@ -86,13 +90,21 @@ 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 =
opts
|> Keyword.get(:caption, true)
|> get_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 +120,10 @@ 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.
Can be `true` to use the default, falsey for none, or a string for a custom caption.
Defaults to the provided `supervisor`.

## Examples

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

edges = traverse_supervisor(supervisor_pid, opts)

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

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

@doc """
Expand Down Expand Up @@ -236,6 +260,10 @@ 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.
Can be `true` to use the default, falsey for none, or a string for a custom caption.
Defaults to the provided `trace_target`.

## Examples

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

caption =
opts
|> Keyword.get(:caption, true)
|> get_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 Expand Up @@ -806,4 +842,9 @@ defmodule Kino.Process do
defp process_info(pid, spec) do
:erpc.call(node(pid), Process, :info, [pid, spec])
end

defp get_caption(true, default), do: default
defp get_caption(false, _default), do: nil
defp get_caption(nil, _default), do: nil
defp get_caption(string, _default) when is_binary(string), do: string
end
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, options \\ []), do: Kino.Mermaid.new(diagram, options)

@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