provide markdown extension to render graphviz

this is server side, and a more standard format, and thus I like it more
than mermaid, which I've been using at work. but, I really wanted a
server-side option (see my manifesto) for drawing relationship graphs,
for D&D stuff of all things.

this adds an optional 'graphviz' feature to package installation which
consequently depends on pydot
This commit is contained in:
Brian S. Stephan 2021-06-24 09:46:26 -05:00
parent 1583e3be99
commit da055acda6
7 changed files with 113 additions and 14 deletions

View File

@ -0,0 +1 @@
"""Markdown extensions."""

View File

@ -0,0 +1,46 @@
"""Serve dot diagrams inline."""
import base64
import re
import markdown
import pydot
class InlinePydot(markdown.Extension):
"""Wrap the markdown prepcoressor."""
def extendMarkdown(self, md, md_globals):
"""Add InlinePydotPreprocessor to the Markdown instance."""
md.registerExtension(self)
md.preprocessors.add('dot_block', InlinePydotPreprocessor(md), '_begin')
class InlinePydotPreprocessor(markdown.preprocessors.Preprocessor):
"""Identify dot codeblocks and run them through pydot."""
BLOCK_RE = re.compile(r'~~~pydot:(?P<filename>[^\s]+)\n(?P<content>.*?)~~~', re.DOTALL)
def run(self, lines):
"""Match and generate diagrams from dot code blocks."""
text = '\n'.join(lines)
for match in self.BLOCK_RE.finditer(text):
filename = match.group(1)
dot_string = match.group(2)
# use pydot to turn the text into pydot
graphs = pydot.graph_from_dot_data(dot_string)
# encode the image and provide as an inline image in markdown
encoded_image = base64.b64encode(graphs[0].create_png()).decode('ascii')
data_path = f'data:image/png;base64,{encoded_image}'
inline_image = f'![{filename}]({data_path})'
# replace the image in the output markdown
text = f'{text[:match.start()]}\n{inline_image}\n{text[match.end():]}'
return text.split('\n')
def makeExtension(*args, **kwargs):
"""Provide the extension to the markdown extension loader."""
return InlinePydot(*args, **kwargs)

View File

@ -1,6 +1,7 @@
-r requirements.in
# testing runner, test reporting, packages used during testing (e.g. requests-mock), etc.
pydot
pytest
pytest-cov

View File

@ -6,7 +6,7 @@
#
appdirs==1.4.4
# via virtualenv
attrs==20.3.0
attrs==21.2.0
# via pytest
bandit==1.6.2
# via -r requirements/requirements-dev.in
@ -18,7 +18,7 @@ click==7.1.2
# pip-tools
coverage==5.5
# via pytest-cov
distlib==0.3.1
distlib==0.3.2
# via virtualenv
dlint==0.11.0
# via -r requirements/requirements-dev.in
@ -42,7 +42,7 @@ flake8-logging-format==0.6.0
# via -r requirements/requirements-dev.in
flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in
flake8==3.9.1
flake8==3.9.2
# via
# -r requirements/requirements-dev.in
# dlint
@ -55,11 +55,11 @@ flask==1.1.2
# via -r requirements/requirements.in
gitdb==4.0.7
# via gitpython
gitpython==3.1.14
gitpython==3.1.18
# via bandit
iniconfig==1.1.1
# via pytest
isort==5.8.0
isort==5.9.1
# via flake8-isort
itsdangerous==1.1.0
# via flask
@ -80,11 +80,11 @@ packaging==20.9
# bleach
# pytest
# tox
pbr==5.5.1
pbr==5.6.0
# via stevedore
pep517==0.10.0
# via pip-tools
pip-tools==6.1.0
pip-tools==6.2.0
# via -r requirements/requirements-dev.in
pluggy==0.13.1
# via
@ -96,15 +96,19 @@ py==1.10.0
# tox
pycodestyle==2.7.0
# via flake8
pydocstyle==6.0.0
pydocstyle==6.1.1
# via flake8-docstrings
pydot==1.4.2
# via -r requirements/requirements-dev.in
pyflakes==2.3.1
# via flake8
pyparsing==2.4.7
# via packaging
pytest-cov==2.11.1
# via
# packaging
# pydot
pytest-cov==2.12.1
# via -r requirements/requirements-dev.in
pytest==6.2.3
pytest==6.2.4
# via
# -r requirements/requirements-dev.in
# pytest-cov
@ -130,10 +134,11 @@ toml==0.10.2
# via
# pep517
# pytest
# pytest-cov
# tox
tox-wheel==0.6.0
# via -r requirements/requirements-dev.in
tox==3.23.0
tox==3.23.1
# via
# -r requirements/requirements-dev.in
# tox-wheel
@ -141,14 +146,16 @@ tzlocal==2.1
# via -r requirements/requirements.in
versioneer==0.19
# via -r requirements/requirements-dev.in
virtualenv==20.4.3
virtualenv==20.4.7
# via tox
webencodings==0.5.1
# via bleach
werkzeug==1.0.1
# via flask
wheel==0.36.2
# via tox-wheel
# via
# pip-tools
# tox-wheel
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -27,4 +27,7 @@ setup(
include_package_data=True,
zip_safe=False,
install_requires=extract_requires(),
extras_require={
'graphviz': ['pydot'],
},
)

View File

@ -0,0 +1,29 @@
"""Test graphviz functionality."""
import os
from incorporealcms import create_app
HERE = os.path.dirname(os.path.abspath(__file__))
def app_with_pydot():
"""Create the test app, including the pydot extension."""
return create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': ['incorporealcms.mdx.pydot']})
def test_functional_initialization():
"""Test initialization with the graphviz config."""
app = app_with_pydot()
assert app is not None
def test_graphviz_is_rendered():
"""Initialize the app with the graphviz extension and ensure it does something."""
app = app_with_pydot()
client = app.test_client()
response = client.get('/test-graphviz')
assert response.status_code == 200
assert b'~~~pydot' not in response.data
assert b'data:image/png;base64' in response.data

View File

@ -0,0 +1,12 @@
# test
test
~~~pydot:attack-plan
digraph G {
rankdir=LR
Earth
Mars
Earth -> Mars
}
~~~
more test