diff --git a/assets/packs/mermaid/main.css b/assets/packs/mermaid/main.css
new file mode 100644
index 00000000..e5716737
--- /dev/null
+++ b/assets/packs/mermaid/main.css
@@ -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;
+}
diff --git a/assets/packs/mermaid/main.js b/assets/packs/mermaid/main.js
index 724759d8..8b1e62b5 100644
--- a/assets/packs/mermaid/main.js
+++ b/assets/packs/mermaid/main.js
@@ -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(/
/gi, "
");
+
+ 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 = ``;
+ 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");
});
}
diff --git a/lib/assets/mermaid/build/main.css b/lib/assets/mermaid/build/main.css
new file mode 100644
index 00000000..fd9f7b79
--- /dev/null
+++ b/lib/assets/mermaid/build/main.css
@@ -0,0 +1 @@
+.container{display:flex;align-items:flex-start;gap:8px;width:-moz-max-content;width:max-content}.container .download-btn{cursor:pointer;padding:.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:-moz-max-content;width:max-content}.caption{margin-top:8px;font-size:.875rem;color:#61758a}
diff --git a/lib/assets/mermaid/build/main.js b/lib/assets/mermaid/build/main.js
index 6f7b20a1..774098fa 100644
--- a/lib/assets/mermaid/build/main.js
+++ b/lib/assets/mermaid/build/main.js
@@ -1 +1 @@
-import{J as d,b as p,c as a,sb as r}from"./chunk-ZRV56LX2.js";import{f as i}from"./chunk-JGKZPFWO.js";var g=i(p(),1),h=i(a(),1);var v=i(d(),1);r.initialize({startOnLoad:!1});function L(t,m){function o(){r.render("graph1",m).then(({svg:n,bindFunctions:e})=>{t.root.innerHTML=n,e&&e(t.root),t.root.querySelector("svg").removeAttribute("height")})}window.innerWidth===0?window.addEventListener("resize",()=>o(),{once:!0}):o()}export{L as init};
+import{J as w,b as f,c as h,sb as r}from"./chunk-ZRV56LX2.js";import{f as d}from"./chunk-JGKZPFWO.js";var b=d(f(),1),C=d(h(),1);var H=d(w(),1);r.initialize({startOnLoad:!1});function R(a,{diagram:p,caption:l,download:s}){a.importCSS("main.css");function c(){r.render("diagram",p).then(({svg:i,bindFunctions:m})=>{i=i.replace(/
/gi,"
");let e=document.createElement("div");e.classList.add("container"),a.root.appendChild(e);let o=document.createElement("figure");if(o.classList.add("figure"),o.innerHTML=i,e.appendChild(o),l){let t=document.createElement("figcaption");t.classList.add("caption"),t.textContent=l,o.appendChild(t)}if(s){let t=document.createElement("button");t.classList.add("download-btn"),t.title="Download",t.innerHTML='',e.appendChild(t),t.addEventListener("click",v=>{let g=URL.createObjectURL(new Blob([i],{type:"image/svg+xml"})),n=document.createElement("a");n.style.display="none",n.href=g,n.download="diagram.svg",e.appendChild(n),n.click(),e.removeChild(n)})}m&&m(a.root),o.querySelector("svg").removeAttribute("height")})}window.innerWidth===0?window.addEventListener("resize",()=>c(),{once:!0}):c()}export{R as init};
diff --git a/lib/kino/mermaid.ex b/lib/kino/mermaid.ex
index 0c26f168..0390a904 100644
--- a/lib/kino/mermaid.ex
+++ b/lib/kino/mermaid.ex
@@ -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.
@@ -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
diff --git a/lib/kino/process.ex b/lib/kino/process.ex
index b5d7cb50..21b3be21 100644
--- a/lib/kino/process.ex
+++ b/lib/kino/process.ex
@@ -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:
@@ -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 """
@@ -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:
@@ -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 """
@@ -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
@@ -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
diff --git a/lib/kino/shorts.ex b/lib/kino/shorts.ex
index 95d1dad8..666aecb1 100644
--- a/lib/kino/shorts.ex
+++ b/lib/kino/shorts.ex
@@ -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`.
@@ -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.
diff --git a/test/kino/process_test.exs b/test/kino/process_test.exs
index ca81eb85..0fdd48b9 100644
--- a/test/kino/process_test.exs
+++ b/test/kino/process_test.exs
@@ -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