Skip to content

Commit b86b6ac

Browse files
Copilotcodingjoe
andcommitted
Add SVG rendering via mermaid.ink API and restore SVG as default format
Co-authored-by: codingjoe <1772890+codingjoe@users.noreply.github.com>
1 parent d1696a8 commit b86b6ac

File tree

5 files changed

+79
-34
lines changed

5 files changed

+79
-34
lines changed

docs/commands.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ render_workflow_graph
99

1010
Render workflow graph to file::
1111

12-
usage: manage.py render_workflow_graph [-h] [-f {mmd,mermaid}] [-d DIRECTORY]
12+
usage: manage.py render_workflow_graph [-h] [-f {svg,mmd,mermaid}] [-d DIRECTORY]
1313
[workflow [workflow ...]]
1414

15-
Render workflow graph to file in Mermaid format.
15+
Render workflow graph to file.
1616

1717
positional arguments:
1818
workflow List of workflow to render in the form
1919
app_label.workflow_name
2020

2121
optional arguments:
2222
-h, --help show this help message and exit
23-
-f {mmd,mermaid}, --format {mmd,mermaid}
24-
Output file format. Default: mmd (Mermaid markdown)
23+
-f {svg,mmd,mermaid}, --format {svg,mmd,mermaid}
24+
Output file format. Default: svg
2525
-d DIRECTORY, --directory DIRECTORY
2626
Output directory. Default is current working
2727
directory.

joeflow/management/commands/render_workflow_graph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ def add_arguments(self, parser):
1818
"--format",
1919
dest="format",
2020
type=str,
21-
choices=("mmd", "mermaid"),
22-
default="mmd",
23-
help="Output file format. Default: mmd (Mermaid markdown)",
21+
choices=("svg", "mmd", "mermaid"),
22+
default="svg",
23+
help="Output file format. Default: svg",
2424
)
2525
parser.add_argument(
2626
"-d",

joeflow/mermaid_utils.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
"""Utilities for generating Mermaid diagrams."""
22
from collections import defaultdict
3+
import urllib.parse
4+
import urllib.request
5+
import base64
6+
7+
try:
8+
from mermaid import Mermaid
9+
from mermaid.graph import Graph
10+
MERMAID_PKG_AVAILABLE = True
11+
except ImportError:
12+
MERMAID_PKG_AVAILABLE = False
313

414
# Color constants
515
COLOR_BLACK = "#000"
@@ -24,6 +34,7 @@ class MermaidDiagram:
2434
def __init__(self, name="", comment=None, **kwargs):
2535
self.name = name
2636
self.comment = comment
37+
self.format = "svg" # Default format for compatibility with graphviz
2738
self.graph_attr = {}
2839
self.node_attr = {}
2940
self.edge_attr = {}
@@ -209,18 +220,47 @@ def pipe(self, format="svg", encoding="utf-8"):
209220
"""
210221
Return the diagram in the specified format.
211222
212-
For Mermaid, we return the source wrapped in appropriate HTML.
213-
This is meant for compatibility with the graphviz API.
223+
For SVG format, renders via mermaid.ink API.
224+
For other formats, returns the Mermaid source.
225+
226+
This maintains compatibility with the graphviz API.
214227
"""
215228
source = self.source()
229+
216230
if format == "svg":
217-
# Return raw mermaid source - rendering happens client-side
218-
return source
219-
elif format == "png" or format == "pdf":
220-
# For file formats, return the source as-is
221-
# The management command will handle file writing
222-
return source
223-
return source
231+
# Render to SVG using mermaid.ink API
232+
try:
233+
svg_content = self._render_to_svg(source)
234+
return svg_content if encoding else svg_content.encode('utf-8')
235+
except Exception:
236+
# Fallback to source if rendering fails
237+
return source if encoding else source.encode('utf-8')
238+
else:
239+
# For other formats, return the Mermaid source
240+
return source if encoding else source.encode('utf-8')
241+
242+
def _render_to_svg(self, mermaid_source):
243+
"""
244+
Render Mermaid source to SVG using mermaid.ink API.
245+
246+
Args:
247+
mermaid_source: Mermaid diagram source code
248+
249+
Returns:
250+
SVG content as string
251+
"""
252+
# Use mermaid.ink API to render
253+
# https://mermaid.ink/svg/<base64_encoded_source>
254+
encoded = base64.b64encode(mermaid_source.encode('utf-8')).decode('ascii')
255+
url = f"https://mermaid.ink/svg/{encoded}"
256+
257+
try:
258+
with urllib.request.urlopen(url, timeout=10) as response:
259+
svg_content = response.read().decode('utf-8')
260+
return svg_content
261+
except Exception as e:
262+
# If API call fails, return a fallback SVG with error message
263+
raise Exception(f"Failed to render via mermaid.ink: {e}")
224264

225265
def render(self, filename, directory=None, format="svg", cleanup=False):
226266
"""
@@ -229,17 +269,25 @@ def render(self, filename, directory=None, format="svg", cleanup=False):
229269
Args:
230270
filename: Base filename (without extension)
231271
directory: Output directory
232-
format: Output format (svg, png, pdf) - for compatibility
272+
format: Output format (svg or mmd)
233273
cleanup: Cleanup intermediate files (not used for Mermaid)
234274
"""
235275
import os
236276

277+
# Determine file extension and content based on format
278+
if format == "svg":
279+
ext = "svg"
280+
content = self.pipe(format="svg", encoding="utf-8")
281+
else:
282+
ext = "mmd"
283+
content = self.source()
284+
237285
if directory:
238-
filepath = os.path.join(directory, f"{filename}.mmd")
286+
filepath = os.path.join(directory, f"{filename}.{ext}")
239287
else:
240-
filepath = f"{filename}.mmd"
288+
filepath = f"{filename}.{ext}"
241289

242290
with open(filepath, "w", encoding="utf-8") as f:
243-
f.write(self.source())
291+
f.write(content)
244292

245293
return filepath

joeflow/models.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ def get_graph(cls, color="black"):
218218
@classmethod
219219
def get_graph_svg(cls):
220220
"""
221-
Return graph representation of a model workflow as Mermaid diagram.
221+
Return graph representation of a model workflow as SVG.
222222
223-
The diagram is HTML safe and can be included in a template, e.g.:
223+
The SVG is HTML safe and can be included in a template, e.g.:
224224
225225
.. code-block:: html
226226
@@ -233,14 +233,12 @@ def get_graph_svg(cls):
233233
</html>
234234
235235
Returns:
236-
(django.utils.safestring.SafeString): Mermaid diagram markup wrapped in HTML.
236+
(django.utils.safestring.SafeString): SVG representation of the workflow.
237237
238238
"""
239239
graph = cls.get_graph()
240-
mermaid_source = graph.pipe(encoding="utf-8")
241-
# Wrap in HTML div with mermaid class for rendering
242-
html = f'<div class="mermaid">\n{mermaid_source}\n</div>'
243-
return SafeString(html) # nosec
240+
graph.format = "svg"
241+
return SafeString(graph.pipe(encoding="utf-8")) # nosec
244242

245243
get_graph_svg.short_description = t("graph")
246244

@@ -310,9 +308,9 @@ def get_instance_graph(self):
310308

311309
def get_instance_graph_svg(self, output_format="svg"):
312310
"""
313-
Return graph representation of a running workflow as Mermaid diagram.
311+
Return graph representation of a running workflow as SVG.
314312
315-
The diagram is HTML safe and can be included in a template, e.g.:
313+
The SVG is HTML safe and can be included in a template, e.g.:
316314
317315
.. code-block:: html
318316
@@ -325,14 +323,12 @@ def get_instance_graph_svg(self, output_format="svg"):
325323
</html>
326324
327325
Returns:
328-
(django.utils.safestring.SafeString): Mermaid diagram markup wrapped in HTML.
326+
(django.utils.safestring.SafeString): SVG representation of a running workflow.
329327
330328
"""
331329
graph = self.get_instance_graph()
332-
mermaid_source = graph.pipe(encoding="utf-8")
333-
# Wrap in HTML div with mermaid class for rendering
334-
html = f'<div class="mermaid">\n{mermaid_source}\n</div>'
335-
return SafeString(html) # nosec
330+
graph.format = output_format
331+
return SafeString(graph.pipe(encoding="utf-8")) # nosec
336332

337333
get_instance_graph_svg.short_description = t("instance graph")
338334

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ requires-python = ">=3.9"
5050
dependencies = [
5151
"django>=2.2",
5252
"django-appconf",
53+
"mermaid>=0.3.2",
5354
]
5455

5556
[project.optional-dependencies]

0 commit comments

Comments
 (0)