Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
mikitex70 committed Oct 5, 2022
2 parents 0f21494 + fc736d6 commit 7b1692a
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 86 deletions.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
# Changelog


## 3.7.0 (2022-10-05)

### New

* Exposed error messages from kroki (refs #75) [Michele Tessaro]

Error messages from Kroki server are rendered as text in the output.
This is to overcome the problem that Kroki does not render errors as
images as PluntUML does.

* Added kroki as rendering server (refs #75) [Michele Tessaro]

With the plugin configuration `kroki_Server` is now possible to use a
Kroki server fore remote rendering.
Image maps are not supported by Kroki.

* Added option to disable image maps (refs #74) [Michele Tessaro]

### Changes

* Regenerated changelog. [Michele Tessaro]


## 3.6.3 (2022-08-01)

### Fix

* Fixed yaml renderingwith remote server (fixes #72) [Michele Tessaro]

* Removed unused `plantuml` import. [Michele Tessaro]

* Doc: fix typos. [Kian-Meng Ang]
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ The plugin has several configuration option:
* `fallback_to_get`: Fallback to `GET` if `POST` fails. Defaults to True
* `format`: format of image to generate (`png`, `svg`, `svg_object`, `svg_inline` or `txt`). Defaults to `png` (See example section above for further explanations of the values for `format`)
* `http_method`: Http Method for server - `GET` or `POST`. "Defaults to `GET`
* `image_maps`: generate image maps if format is `png` and the diagram has hyperlinks; `true`, `on`, `yes` or `1` activates image maps, everything else disables it. Defaults to `true`
* `kroki_server`: Kroki server url, as alternative to `server` for remote rendering (image maps mus be disabled manually). Defaults to `''`, use PlantUML server if defined
* `priority`: extension priority. Higher values means the extension is applied sooner than others. Defaults to `30`
* `puml_notheme_cmdlist`: theme will not be set if listed commands present. Default list is `['version', 'listfonts', 'stdlib', 'license']`. **If modifying please copy the default list provided and append**
* `server`: PlantUML server url, for remote rendering. Defaults to `''`, use local command
Expand Down
189 changes: 106 additions & 83 deletions plantuml_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
import zlib
import string
from subprocess import Popen, PIPE
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple
from zlib import adler32

import logging
Expand Down Expand Up @@ -186,105 +186,113 @@ def _replace_block(self, text):
code += m.group('code')

# Extract diagram source end convert it (if not external)
diagram = self._render_diagram(code, requested_format, base_dir)
self_closed = True # tags are always self closing
map_tag = ''

if img_format == 'txt':
# logger.debug(diagram)
img = etree.Element('pre')
code = etree.SubElement(img, 'code')
code.attrib['class'] = 'text'
code.text = AtomicString(diagram.decode('UTF-8'))
diagram, err = self._render_diagram(code, requested_format, base_dir)

if err:
# there is an error message: create a nice tag to show it
diag_tag = f'<div style="color: red">{err}</div>'
else:
# These are images
if img_format == 'svg_inline':
data = self.ADAPT_SVG_REGEX.sub('<svg \\1\\2>', diagram.decode('UTF-8'))
img = etree.fromstring(data.encode('UTF-8'))
# remove width and height in style attribute
img.attrib['style'] = re.sub(r'\b(?:width|height):\d+px;', '', img.attrib['style'])
elif img_format == 'svg':
# Firefox handles only base64 encoded SVGs
data = 'data:image/svg+xml;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
img = etree.Element('img')
img.attrib['src'] = data
elif img_format == 'svg_object':
# Firefox handles only base64 encoded SVGs
data = 'data:image/svg+xml;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
img = etree.Element('object')
img.attrib['data'] = data
self_closed = False # object tag must be explicitly closed
else: # png format, explicitly set or as a default when format is not recognized
data = 'data:image/png;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
img = etree.Element('img')
img.attrib['src'] = data
# Check for hyperlinks
map_data = self._render_diagram(code, 'map', base_dir).decode("utf-8")
if map_data.startswith('<map '):
# There are hyperlinks, add the image map
unique_id = str(uuid.uuid4())
map = etree.fromstring(map_data)
map.attrib['id'] = unique_id
map.attrib['name'] = unique_id
map_tag = etree.tostring(map, short_empty_elements=self_closed).decode()
img.attrib['usemap'] = '#' + unique_id

styles = []
if 'style' in img.attrib and img.attrib['style'] != '':
styles.append(re.sub(r';$', '', img.attrib['style']))
if width:
styles.append("max-width:"+width)
if height:
styles.append("max-height:"+height)

if styles:
img.attrib['style'] = ";".join(styles)
img.attrib['width'] = '100%'
if 'height' in img.attrib:
img.attrib.pop('height')

img.attrib['class'] = classes
img.attrib['alt'] = alt
img.attrib['title'] = title

diag_tag = etree.tostring(img, short_empty_elements=self_closed).decode()
diag_tag = diag_tag + map_tag
self_closed = True # tags are always self-closing
map_tag = ''

if img_format == 'txt':
# logger.debug(diagram)
img = etree.Element('pre')
code = etree.SubElement(img, 'code')
code.attrib['class'] = 'text'
code.text = AtomicString(diagram.decode('UTF-8'))
else:
# These are images
if img_format == 'svg_inline':
data = self.ADAPT_SVG_REGEX.sub('<svg \\1\\2>', diagram.decode('UTF-8'))
img = etree.fromstring(data.encode('UTF-8'))
# remove width and height in style attribute
img.attrib['style'] = re.sub(r'\b(?:width|height):\d+px;', '', img.attrib['style'])
elif img_format == 'svg':
# Firefox handles only base64 encoded SVGs
data = 'data:image/svg+xml;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
img = etree.Element('img')
img.attrib['src'] = data
elif img_format == 'svg_object':
# Firefox handles only base64 encoded SVGs
data = 'data:image/svg+xml;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
img = etree.Element('object')
img.attrib['data'] = data
self_closed = False # object tag must be explicitly closed
else: # png format, explicitly set or as a default when format is not recognized
data = 'data:image/png;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
img = etree.Element('img')
img.attrib['src'] = data

# check if image maps are enabled
if str(self.config['image_maps']).lower() in ['true', 'on', 'yes', '1']:
# Check for hyperlinks
map_data, err = self._render_diagram(code, 'map', base_dir)
map_data = map_data.decode("utf-8")

if map_data.startswith('<map '):
# There are hyperlinks, add the image map
unique_id = str(uuid.uuid4())
map = etree.fromstring(map_data)
map.attrib['id'] = unique_id
map.attrib['name'] = unique_id
map_tag = etree.tostring(map, short_empty_elements=self_closed).decode()
img.attrib['usemap'] = '#' + unique_id

styles = []
if 'style' in img.attrib and img.attrib['style'] != '':
styles.append(re.sub(r';$', '', img.attrib['style']))
if width:
styles.append("max-width:"+width)
if height:
styles.append("max-height:"+height)

if styles:
img.attrib['style'] = ";".join(styles)
img.attrib['width'] = '100%'
if 'height' in img.attrib:
img.attrib.pop('height')

img.attrib['class'] = classes
img.attrib['alt'] = alt
img.attrib['title'] = title

diag_tag = etree.tostring(img, short_empty_elements=self_closed).decode()
diag_tag = diag_tag + map_tag

return text[:m.start()] + m.group('indent') + diag_tag + text[m.end():], \
m.start() + len(m.group('indent')) + len(diag_tag)

def _render_diagram(self, code, requested_format, base_dir):
def _render_diagram(self, code: str, requested_format: str, base_dir: str) -> Tuple[any, Optional[str]]:
cached_diagram_file = None
diagram = None

if self.config['cachedir']:
diagram_hash = "%08x" % (adler32(code.encode('UTF-8')) & 0xffffffff)
cached_diagram_file = os.path.expanduser(
os.path.join(
self.config['cachedir'],
diagram_hash + '.' + requested_format))
os.path.join(self.config['cachedir'], diagram_hash + '.' + requested_format))

if os.path.isfile(cached_diagram_file):
with open(cached_diagram_file, 'rb') as f:
diagram = f.read()

if diagram:
# if cache found then end this function here
return diagram
return diagram, None

# if cache not found create the diagram
code = self._set_theme(code)

if self.config['server']:
diagram = self._render_remote_uml_image(code, requested_format, base_dir)
if self.config['server'] or self.config['kroki_server']:
diagram, err = self._render_remote_uml_image(code, requested_format, base_dir)
else:
diagram = self._render_local_uml_image(code, requested_format)
diagram, err = self._render_local_uml_image(code, requested_format)

if self.config['cachedir']:
if not err and self.config['cachedir']:
with open(cached_diagram_file, 'wb') as f:
f.write(diagram)

return diagram
return diagram, err

def _set_theme(self, code):
theme = self.config['theme'].strip()
Expand Down Expand Up @@ -333,36 +341,36 @@ def _set_theme(self, code):
return code

@staticmethod
def _render_local_uml_image(plantuml_code, img_format):
def _render_local_uml_image(plantuml_code: str, img_format: str) -> Tuple[any, Optional[str]]:
plantuml_code = plantuml_code.encode('utf8')
cmdline = ['plantuml', '-pipemap' if img_format == 'map' else '-p', "-t" + img_format]

try:
# On Windows run batch files through a shell so the extension can be resolved
p = Popen(cmdline, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=(os.name == 'nt'))
out, err = p.communicate(input=plantuml_code)

except Exception as exc:
raise Exception('Failed to run plantuml: %s' % exc)
else:
if p.returncode != 0:
# plantuml returns a nice image in case of syntax error so log but still return out
logger.error('Error in "uml" directive: %s' % err)

return out
return out, None

def _render_remote_uml_image(self, plantuml_code, img_format, base_dir):
def _render_remote_uml_image(self, plantuml_code: str, img_format: str, base_dir: str) -> Tuple[any, Optional[str]]:
# build the whole source diagram, executing include directives
temp_file = PlantUMLIncluder(False).readFile(plantuml_code, base_dir)
http_method = self.config['http_method'].strip()
fallback_to_get = self.config['fallback_to_get']

# Use GET if preferred, use POST with GET as fallback if POST fails
post_failed = False
server = self.config['kroki_server'] if self.config['kroki_server'] else self.config['server']

if http_method == "POST":
# image_url for POST attempt first
image_url = "%s/%s/" % (self.config['server'], img_format)
image_url = "%s/%s/" % (server, img_format)
# download manually the image to be able to continue in case of errors

with requests.post(image_url, data=temp_file, headers={"Content-Type": 'text/plain; charset=utf-8'}) as r:
Expand All @@ -372,16 +380,22 @@ def _render_remote_uml_image(self, plantuml_code, img_format, base_dir):
logger.error('Falling back to Get')
post_failed = True
if not post_failed:
return r.content
return r.content, None

if http_method == "GET" or post_failed:
image_url = self.config['server']+"/"+img_format+"/"+self._deflate_and_encode(temp_file)
if self.config['kroki_server']:
with open('/tmp/test/diag.puml', 'w') as f:
f.write(temp_file)
image_url = server + "/plantuml/" + img_format + "/" + self._compress_and_encode(temp_file)
else:
image_url = server+"/"+img_format+"/"+self._deflate_and_encode(temp_file)

with requests.get(image_url) as r:
if not r.ok:
logger.warning('WARNING in "uml" directive: remote server has returned error %d on GET' % r.status_code)
logger.warning(f'WARNING in "uml" directive: remote server has returned error %d on GET: %s' % (r.status_code, r.content.decode('utf-8')))
return None, r.content.decode('utf-8')

return r.content
return r.content, None

@staticmethod
def _deflate_and_encode(source: str) -> str:
Expand All @@ -390,6 +404,11 @@ def _deflate_and_encode(source: str) -> str:

return base64.b64encode(zlibbed_str[2:-4]).translate(b64_to_plantuml).decode('utf-8')

@staticmethod
def _compress_and_encode(source: str) -> str:
# diagram encoding for Kroki
return base64.urlsafe_b64encode(zlib.compress(source.encode('utf-8'), 9)).decode('utf-8')


class PlantUMLIncluder:

Expand All @@ -404,7 +423,7 @@ def readFile(self, plantuml_code: str, directory: str) -> str:
lines = plantuml_code.splitlines()
# Wrap the whole combined text between startuml and enduml tags as recursive processing would have removed them
# This is necessary for it to work correctly with plamtuml POST processing
return "@startuml\n" + "\n".join(self._readFileRec(lines, directory)) + "@enduml\n"
return "@startuml\n" + "\n".join(self._readFileRec(lines, directory)) + "\n@enduml\n"

# Reads the file recursively
def _readFileRec(self, lines: List[str], directory: str) -> List[str]:
Expand Down Expand Up @@ -479,7 +498,11 @@ def __init__(self, **kwargs):
'format': ["png", "Format of image to generate (png, svg or txt). Defaults to 'png'."],
'title': ["", "Tooltip for the diagram"],
'server': ["", "PlantUML server url, for remote rendering. Defaults to '', use local command."],
'kroki_server': ["", "Kroki server url, as alternative to 'server' for remote rendering (image maps must "
"be disabled manually). Defaults to '', use PlantUML server if defined"],
'cachedir': ["", "Directory for caching of diagrams. Defaults to '', no caching"],
'image_maps': ["true", "Enable generation of PNG image maps, allowing to use hyperlinks with PNG images."
"Defaults to true"],
'priority': ["30", "Extension priority. Higher values means the extension is applied sooner than others. "
"Defaults to 30"],
'base_dir': [".", "Base directory for external files inclusion"],
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

setuptools.setup(
name="plantuml-markdown",
version="3.6.3",
version="3.7.0",
author="Michele Tessaro",
author_email="[email protected]",
description="A PlantUML plugin for Markdown",
Expand Down
Loading

0 comments on commit 7b1692a

Please sign in to comment.