From e0c361b42220c3dd7a37121581f45061bfb77240 Mon Sep 17 00:00:00 2001 From: Michele Tessaro Date: Tue, 4 Oct 2022 18:39:29 +0200 Subject: [PATCH 1/5] new: usr: added option to disable image maps (refs #74) --- README.md | 1 + plantuml_markdown.py | 24 ++++++++++++++---------- test/test_plantuml.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cc6b5a4..483264d 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,7 @@ 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` * `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 diff --git a/plantuml_markdown.py b/plantuml_markdown.py index 66c3eec..e27a904 100644 --- a/plantuml_markdown.py +++ b/plantuml_markdown.py @@ -218,16 +218,18 @@ def _replace_block(self, text): 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('

""" % self.FAKE_IMAGE), self.UUID_REGEX.sub('"test"', self.COORDS_REGEX.sub(' coords="1,2,3,4"', self._stripImageData(self.md.convert(text))))) + def test_plantuml_map_disabled(self): + """ + Test map markup is not generated when disabled + """ + include_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') + configs = { + 'plantuml_markdown': { + 'base_dir': include_path, + 'image_maps': 'false' + } + } + self.md = markdown.Markdown(extensions=['markdown.extensions.fenced_code', + 'pymdownx.snippets', 'plantuml_markdown'], + extension_configs=configs) + + text = self.text_builder.diagram('A --> B [[https://www.google.fr]]').build() + result = self._stripImageData(self.md.convert(text)) + self.assertFalse(" Date: Tue, 4 Oct 2022 20:38:22 +0200 Subject: [PATCH 2/5] new: usr: added kroki as rendering server (refs #75) With the plugin configuration `kroki_Server` is now possible to use a Kroki server fore remote rendering. Image maps are not supported by Kroki. --- README.md | 3 ++- plantuml_markdown.py | 25 ++++++++++++++++++++----- test/test_plantuml.py | 23 ++++++++++++++++++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 483264d..ab3103c 100644 --- a/README.md +++ b/README.md @@ -212,7 +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` +* `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 not supported). 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 diff --git a/plantuml_markdown.py b/plantuml_markdown.py index e27a904..68a1d5e 100644 --- a/plantuml_markdown.py +++ b/plantuml_markdown.py @@ -219,7 +219,9 @@ def _replace_block(self, text): img = etree.Element('img') img.attrib['src'] = data - if str(self.config['image_maps']).lower() in ['true', 'on', 'yes', '1']: + # image maps are not supported by Kroki + if not self.config['kroki_server'] and \ + str(self.config['image_maps']).lower() in ['true', 'on', 'yes', '1']: # Check for hyperlinks map_data = self._render_diagram(code, 'map', base_dir).decode("utf-8") if map_data.startswith(' 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: @@ -406,7 +419,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]: @@ -481,6 +494,8 @@ 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 not " + "supported). 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"], diff --git a/test/test_plantuml.py b/test/test_plantuml.py index b983024..7f53a52 100644 --- a/test/test_plantuml.py +++ b/test/test_plantuml.py @@ -149,7 +149,7 @@ def _test_snippets(self, priority, expected): from test.markdown_builder import MarkdownBuilder from plantuml_markdown import PlantUMLPreprocessor - # mcking a method to capture the generated PlantUML source code + # mocking a method to capture the generated PlantUML source code with mock.patch.object(PlantUMLPreprocessor, '_render_diagram', return_value='testing'.encode('utf8')) as mocked_plugin: text = self.text_builder.diagram("--8<-- \"" + defs_file + "\"").build() @@ -623,3 +623,24 @@ def test_include(self): } }) self.assertEqual('
A -> B -> C
', self.md.convert(text)) + + def test_kroki(self): + """ + Test calling a kroki server for rendering + """ + with ServedBaseHTTPServerMock() as kroki_server_mock: + kroki_server_mock.responses[MethodName.GET].append( + MockHTTPResponse(status_code=200, headers={}, reason_phrase='', body=b"dummy") + ) + self.md = markdown.Markdown(extensions=['plantuml_markdown'], + extension_configs={ + 'plantuml_markdown': { + 'kroki_server': kroki_server_mock.url, + } + }) + text = self.text_builder.diagram('A -> B').format('png').build() + + self.assertEqual(self._stripImageData(self._load_file('png_diag.html')), + self._stripImageData(self.md.convert(text))) + req = kroki_server_mock.requests[MethodName.GET].pop(0) + self.assertTrue(req.path.startswith('/plantuml/png/')) From d61fd6d10911242939336d4e8f423dc340d99103 Mon Sep 17 00:00:00 2001 From: Michele Tessaro Date: Tue, 4 Oct 2022 20:41:54 +0200 Subject: [PATCH 3/5] chg: doc: regenerated changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2e24a..5100a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,25 @@ # Changelog +## development (unreleased) + +### New + +* 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] + + ## 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] From 5c1fa58f07dc54cebcb2dec3c809eca602c49a02 Mon Sep 17 00:00:00 2001 From: Michele Tessaro Date: Wed, 5 Oct 2022 20:26:40 +0200 Subject: [PATCH 4/5] new: usr: exposed error messages from kroki (refs #75) 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. --- README.md | 2 +- plantuml_markdown.py | 174 +++++++++++++++++++++--------------------- test/test_plantuml.py | 3 +- 3 files changed, 92 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index ab3103c..12708fb 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ The plugin has several configuration option: * `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 not supported). Defaults to `''`, use PlantUML server if defined +* `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 diff --git a/plantuml_markdown.py b/plantuml_markdown.py index 68a1d5e..0033832 100644 --- a/plantuml_markdown.py +++ b/plantuml_markdown.py @@ -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 @@ -186,87 +186,91 @@ 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'
{err}
' else: - # These are images - if img_format == 'svg_inline': - data = self.ADAPT_SVG_REGEX.sub('', 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 - - # image maps are not supported by Kroki - if not self.config['kroki_server'] and \ - str(self.config['image_maps']).lower() in ['true', 'on', 'yes', '1']: - # Check for hyperlinks - map_data = self._render_diagram(code, 'map', base_dir).decode("utf-8") - if map_data.startswith('', 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(' 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: @@ -274,21 +278,21 @@ def _render_diagram(self, code, requested_format, base_dir): 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'] or self.config['kroki_server']: - diagram = self._render_remote_uml_image(code, requested_format, base_dir) + 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() @@ -337,7 +341,7 @@ 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] @@ -345,7 +349,6 @@ def _render_local_uml_image(plantuml_code, img_format): # 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: @@ -353,9 +356,9 @@ def _render_local_uml_image(plantuml_code, img_format): # 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() @@ -377,7 +380,7 @@ 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: if self.config['kroki_server']: @@ -389,9 +392,10 @@ def _render_remote_uml_image(self, plantuml_code, img_format, base_dir): 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: @@ -494,8 +498,8 @@ 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 not " - "supported). Defaults to '', use PlantUML server if defined"], + '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"], diff --git a/test/test_plantuml.py b/test/test_plantuml.py index 7f53a52..2aa82c4 100644 --- a/test/test_plantuml.py +++ b/test/test_plantuml.py @@ -151,7 +151,7 @@ def _test_snippets(self, priority, expected): # mocking a method to capture the generated PlantUML source code with mock.patch.object(PlantUMLPreprocessor, '_render_diagram', - return_value='testing'.encode('utf8')) as mocked_plugin: + return_value=('testing'.encode('utf8'), None)) as mocked_plugin: text = self.text_builder.diagram("--8<-- \"" + defs_file + "\"").build() self.md.convert(text) mocked_plugin.assert_called_with(expected, 'map', '.') @@ -636,6 +636,7 @@ def test_kroki(self): extension_configs={ 'plantuml_markdown': { 'kroki_server': kroki_server_mock.url, + 'image_maps': 'no' } }) text = self.text_builder.diagram('A -> B').format('png').build() From fc736d6fef577c65de080db7fdfeea31740a2701 Mon Sep 17 00:00:00 2001 From: Michele Tessaro Date: Wed, 5 Oct 2022 20:32:56 +0200 Subject: [PATCH 5/5] chg: pkg: changed version for the new release --- CHANGELOG.md | 12 +++++++++++- setup.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5100a80..4a5b5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Changelog -## development (unreleased) +## 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 @@ -13,6 +19,10 @@ * Added option to disable image maps (refs #74) [Michele Tessaro] +### Changes + +* Regenerated changelog. [Michele Tessaro] + ## 3.6.3 (2022-08-01) diff --git a/setup.py b/setup.py index 20b9368..5c71c4f 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setuptools.setup( name="plantuml-markdown", - version="3.6.3", + version="3.7.0", author="Michele Tessaro", author_email="michele.tessaro@email.it", description="A PlantUML plugin for Markdown",