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

HttpMethod option #7

Merged
merged 6 commits into from
May 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ plugins:
* `Enablebpmn` - Enable BPMN, default: True
* `EnableExcalidraw` - Enable Excalidraw, default: True
* `EnableMermaid` - Enable Mermaid, default: True
* `HttpMethod` - Http method to use (`GET` or `POST`), default: `GET` (Note: you have to enable `DownloadImages` if you want to use `POST`!)
* `DownloadImages` - Download diagrams from kroki as static assets instead of just creating kroki links, default: False
* `DownloadDir` - The asset directory to place downloaded svg images in, default: images/kroki_generated

Expand Down
73 changes: 73 additions & 0 deletions kroki/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import base64
import requests
import zlib

from functools import partial
from mkdocs.plugins import log


info = partial(log.info, f'{__name__} %s')
debug = partial(log.debug, f'{__name__} %s')
error = partial(log.error, f'{__name__} %s')


class KrokiClient():
def __init__(self, server_url, http_method):
self.server_url = server_url
self.http_method = http_method

if http_method not in ['GET', 'POST']:
error(f'HttpMethod config error: {http_method} -> using GET!')
self.http_method = 'GET'

info(f'Initialized: {self.http_method}, {self.server_url}')

def _kroki_uri(self, kroki_type):
return f'{self.server_url}/{kroki_type}/svg'

def _get_url(self, kroki_type, kroki_diagram_data):
kroki_data_param = \
base64.urlsafe_b64encode(
zlib.compress(str.encode(kroki_diagram_data), 9)).decode()

if len(kroki_data_param) >= 4096:
debug(f'Length of encoded diagram is {len(kroki_data_param)}. '
'Kroki may not be able to read the data completely!')

kroki_uri = self._kroki_uri(kroki_type)
return f'{kroki_uri}/{kroki_data_param}'

def get_url(self, kroki_type, kroki_diagram_data):
debug(f'get_url: {kroki_type}')

if self.http_method != 'GET':
error(f'HTTP method is {self.http_method}. Config error!')
return None

return self._get_url(kroki_type, kroki_diagram_data)

def get_image_data(self, kroki_type, kroki_diagram_data):
try:
if self.http_method == 'GET':
url = self._get_url(kroki_type, kroki_diagram_data)

debug(f'get_image_data [GET {url[:50]}..]')
r = requests.get(url)
else: # POST
url = self._kroki_uri(kroki_type)

debug(f'get_image_data [POST {url}]')

r = requests.post(url, json={
"diagram_source": kroki_diagram_data
})

debug(f'get_image_data [Response: {r}]')

if r.status_code == requests.codes.ok:
return r.text
else:
error(f'Could not retrive image data, got: {r}')

except Exception as e:
error(e)
53 changes: 53 additions & 0 deletions kroki/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class KrokiDiagramTypes():
kroki_base = (
"bytefield",
"ditaa",
"erd",
"graphviz",
"nomnoml",
"plantuml",
"c4plantuml",
"svgbob",
"vega",
"vegalite",
"wavedrom",
"pikchr",
"umlet",
)

kroki_blockdiag = (
"blockdiag",
"seqdiag",
"actdiag",
"nwdiag",
"packetdiag",
"rackdiag",
)

kroki_bpmn = (
"bpmn",
)

kroki_excalidraw = (
"excalidraw",
)

kroki_mermaid = (
"mermaid",
)

def __init__(self, blockdiag_enabled, bpmn_enabled, excalidraw_enabled, mermaid_enabled):
self.diagram_types = self.kroki_base

if blockdiag_enabled:
self.diagram_types += self.kroki_blockdiag
if bpmn_enabled:
self.diagram_types += self.kroki_bpmn
if excalidraw_enabled:
self.diagram_types += self.kroki_excalidraw
if mermaid_enabled:
self.diagram_types += self.kroki_mermaid

def get_block_regex(self, fence_prefix):
diagram_types_re = "|".join(self.diagram_types)
return rf'(?:```{fence_prefix})({diagram_types_re})\n(.*?)(?:```)'
160 changes: 66 additions & 94 deletions kroki/plugin.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import base64
import hashlib
import zlib
import re
import tempfile
import pathlib
import urllib.request

from functools import partial
from mkdocs.plugins import BasePlugin
from mkdocs.structure.files import File
from mkdocs import config
from mkdocs.plugins import log
from pathlib import Path
from os.path import relpath

from .config import KrokiDiagramTypes
from .client import KrokiClient


info = partial(log.info, f'{__name__} %s')
Expand All @@ -25,125 +26,96 @@ class KrokiPlugin(BasePlugin):
('Enablebpmn', config.config_options.Type(bool, default=True)),
('EnableExcalidraw', config.config_options.Type(bool, default=True)),
('EnableMermaid', config.config_options.Type(bool, default=True)),
('HttpMethod', config.config_options.Type(str, default='GET')),
('DownloadImages', config.config_options.Type(bool, default=False)),
('EmbedImages', config.config_options.Type(bool, default=False)),
('DownloadDir', config.config_options.Type(str, default='images/kroki_generated')),
('FencePrefix', config.config_options.Type(str, default='kroki-')),
)

kroki_re = ""

kroki_base = (
"bytefield",
"ditaa",
"erd",
"graphviz",
"nomnoml",
"plantuml",
"c4plantuml",
"svgbob",
"vega",
"vegalite",
"wavedrom",
"pikchr",
"umlet",
)
fence_prefix = None
diagram_types = None
kroki_client = None

kroki_blockdiag = (
"blockdiag",
"seqdiag",
"actdiag",
"nwdiag",
"packetdiag",
"rackdiag",
)
def on_config(self, config, **_kwargs):
info(f'Configuring: {self.config}')

kroki_bpmn = (
"bpmn",
)
self.diagram_types = KrokiDiagramTypes(self.config['EnableBlockDiag'],
self.config['Enablebpmn'],
self.config['EnableExcalidraw'],
self.config['EnableMermaid'])

kroki_excalidraw = (
"excalidraw",
)
self.fence_prefix = self.config['FencePrefix']

kroki_mermaid = (
"mermaid",
)
if self.config['HttpMethod'] == 'POST' and not self.config["DownloadImages"]:
error('HttpMethod: Can\'t use POST without downloading the images! '
'Falling back to GET')
self.config['HttpMethod'] = 'GET'

def on_config(self, config, **_kwargs):
diagram_types = self.kroki_base
self.kroki_client = KrokiClient(self.config['ServerURL'], self.config['HttpMethod'])

if self.config['EnableBlockDiag']:
diagram_types += self.kroki_blockdiag
if self.config['Enablebpmn']:
diagram_types += self.kroki_bpmn
if self.config['EnableExcalidraw']:
diagram_types += self.kroki_excalidraw
if self.config['EnableMermaid']:
diagram_types += self.kroki_mermaid
self._tmp_dir = tempfile.TemporaryDirectory(prefix="mkdocs_kroki_")
self._output_dir = Path(config.get("site_dir", "site"))

frence_prefix = self.config['FencePrefix']
diagram_types_re = "|".join(diagram_types)
self.kroki_re = rf'(?:```{frence_prefix})({diagram_types_re})\n(.*?)(?:```)'
self._prepare_download_dir()

self._dir = tempfile.TemporaryDirectory(prefix="mkdocs_kroki_")
self._output_dir = pathlib.Path(config.get("site_dir", "site"))

info(f'on_config: {self.config}')
return config

def _kroki_url(self, matchobj):
kroki_type = matchobj.group(1).lower()
kroki_data = base64.urlsafe_b64encode(
zlib.compress(str.encode(matchobj.group(2)), 9)
).decode()
kroki_url = f'{self.config["ServerURL"]}/{kroki_type}/svg/{kroki_data}'

return kroki_url
def _download_dir(self):
return Path(self._tmp_dir.name) / Path(self.config["DownloadDir"])

def _kroki_link(self, matchobj):
return f"![Kroki]({self._kroki_url(matchobj)})"
def _prepare_download_dir(self):
self._download_dir().mkdir(parents=True, exist_ok=True)

def _download_image(self, matchobj, target, page, files):
url = self._kroki_url(matchobj)
hash = hashlib.md5(url.encode("utf8")).hexdigest()
def _kroki_filename(self, kroki_data, page):
digest = hashlib.md5(kroki_data.encode("utf8")).hexdigest()
prefix = page.file.name.split(".")[0]
dest_path = pathlib.Path(self.config["DownloadDir"])

(target / dest_path).mkdir(parents=True, exist_ok=True)
return f'{prefix}-{digest}.svg'

filename = dest_path / f"{ prefix }-{ hash }.svg"
def _save_kroki_image_and_get_url(self, file_name, image_data, files):
filepath = self._download_dir() / file_name
with open(filepath, 'w') as file:
file.write(image_data)

debug(f'downloading {url[:50]}..')
try:
urllib.request.urlretrieve(url, target / filename)
except Exception as e:
error(f'{e}: {url[:50]}..')
return f'!!! error "Could not render!"\n\n```\n{matchobj.group(2)}\n```'
get_url = relpath(filepath, self._tmp_dir.name)

file = File(
filename, target, self._output_dir, False)
files.append(file)
mkdocs_file = File(get_url, self._tmp_dir.name, self._output_dir, False)
files.append(mkdocs_file)

pref = "/".join([".." for _ in pathlib.Path(page.file.src_path).parents][1:])
return f'/{get_url}'

return f"![Kroki](./{ pref }/{ filename })"
def _replace_kroki_block(self, match_obj, files, page):
kroki_type = match_obj.group(1).lower()
kroki_data = match_obj.group(2)

def on_page_markdown(self, markdown, files, page, **_kwargs):
pattern = re.compile(self.kroki_re, flags=re.IGNORECASE + re.DOTALL)
get_url = None
if self.config["DownloadImages"]:
image_data = self.kroki_client.get_image_data(kroki_type, kroki_data)

if image_data:
file_name = self._kroki_filename(kroki_data, page)
get_url = self._save_kroki_image_and_get_url(file_name, image_data, files)
else:
get_url = self.kroki_client.get_url(kroki_type, kroki_data)

if not self.config["DownloadImages"]:
return re.sub(pattern, self._kroki_link, markdown)
if get_url is not None:
return f'![Kroki]({get_url})'

return f'!!! error "Could not render!"\n\n```\n{kroki_data}\n```'

def on_page_markdown(self, markdown, files, page, **_kwargs):
debug(f'on_page_markdown [page: {page}]')

target_dir = pathlib.Path(self._dir.name)
kroki_regex = self.diagram_types.get_block_regex(self.fence_prefix)
pattern = re.compile(kroki_regex, flags=re.IGNORECASE + re.DOTALL)

def do_download(matchobj):
return self._download_image(
matchobj, target_dir, page, files)
def replace_kroki_block(match_obj):
return self._replace_kroki_block(match_obj, files, page)

return re.sub(pattern, do_download, markdown)
return re.sub(pattern, replace_kroki_block, markdown)

def on_post_build(self, **_kwargs):
if hasattr(self, "_dir"):
info(f'Cleaning {self._dir}')
self._dir.cleanup()
if hasattr(self, "_tmp_dir"):
info(f'Cleaning {self._tmp_dir}')
self._tmp_dir.cleanup()
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
license='MIT',
python_requires='>=3.6',
install_requires=[
'mkdocs>=1.1.2'
'mkdocs>=1.1.2',
'requests>=2.9.1'
],
classifiers=[
'Development Status :: 4 - Beta',
Expand Down