Skip to content

Commit

Permalink
Merge branch 'release-1.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Michele Tessaro committed Nov 17, 2018
2 parents 48824ce + 9bd09f6 commit ac86110
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 26 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
# Changelog

## 1.3.0 (2018-11-17)

### New

* Added support for clickable SVGs (closes #17) [Michele Tessaro]

Added two new output formats:
* `svg_object`: generated an `object` tag for displaing svg images
* `svg_inline`: embedded the svg source image directly in the document

### Fix

* Fixed error when the output format is not recognized. [Michele Tessaro]


## 1.2.6 (2018-11-04)

### Changes

* Update documentation. [Michele Tessaro]

### Fix

* Fixed wrong `classes` HTML attribute (fixes #16) [Michele Tessaro]
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ The GitLab/GitHub block syntax is also recognized. Example:
Options are optional (otherwise the wouldn't be options), but if present must be specified in the order `format`, `classes`, `alt`, `title`.
The option value may be enclosed in single or double quotes.

Supported values for `format` parameter are:

* `png`: HTML `img` tag with embedded png image
* `svg`: HTML `img` tag with embedded svg image (links are not navigable)
* `svg_object`: HTML `object` tag with embedded svg image (links are navigable)
* `svg_inline`: HTML5 `svg` tag with inline svg image source (links are navigable, can be manipulated with CSS rules)
* `txt`: plain text diagrams.

Installation
------------
You need to install [PlantUML][] (see the site for details) and [Graphviz][] 2.26.3 or later.
Expand Down Expand Up @@ -84,7 +92,7 @@ To use the plugin with [Python-Markdown][] you have three choices:

You must export `PYTHONPATH` before running `markdown_py`, or you can put the definition in `~/.bashrc`.

After installed, you can use this plugin by activating it in the `markdownm_py` command. For example:
After installed, you can use this plugin by activating it in the `markdown_py` command. For example:

markdown_py -x plantuml mydoc.md > out.html

Expand Down
1 change: 1 addition & 0 deletions mdx_plantuml.py
59 changes: 37 additions & 22 deletions plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
Options are optional, but if present must be specified in the order format, classes, alt.
The option value may be enclosed in single or double quotes.
Supported values for `format` parameter are:
* `png`: HTML `img` tag with embedded png image
* `svg`: HTML `img` tag with embedded svg image (links are not navigable)
* `svg_object`: HTML `object` tag with embedded svg image (links are navigable)
* `svg_inline`: HTML5 `svg` tag with inline svg image source (links are navigable, can be manipulated with CSS rules)
* `txt`: plain text diagrams.
Installation
------------
You need to install [PlantUML][] (see the site for details) and [Graphviz][] 2.26.3 or later.
Expand All @@ -48,7 +57,7 @@
import re
import base64
from subprocess import Popen, PIPE
import logging
#import logging
import markdown
from markdown.util import etree, AtomicString

Expand Down Expand Up @@ -98,6 +107,9 @@ def run(self, lines):

return text.split('\n')

# regex for removing some parts from the plantuml generated svg
ADAPT_SVG_REGEX = re.compile(r'^<\?xml .*?\?><svg(.*?)xmlns=".*?"(.*?)>')

def _replace_block(self, text):
# Parse configuration params
m = self.FENCED_BLOCK_RE.search(text)
Expand All @@ -116,39 +128,42 @@ def _replace_block(self, text):
code = m.group('code')
diagram = self.generate_uml_image(code, img_format)

if img_format == 'png':
data = 'data:image/png;base64,{0}'.format(
base64.b64encode(diagram).decode('ascii')
)
img = etree.Element('img')
img.attrib['src' ] = data
img.attrib['class' ] = classes
img.attrib['alt' ] = alt
img.attrib['title' ] = title
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
img.attrib['class' ] = classes
img.attrib['alt' ] = alt
img.attrib['title' ] = title
elif img_format == 'txt':
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:
if 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
elif img_format == 'svg_inline':
data = self.ADAPT_SVG_REGEX.sub('<svg\\1\\2>', diagram.decode('UTF-8'))
img = etree.fromstring(data)
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

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

return text[:m.start()] + etree.tostring(img).decode() + text[m.end():], True

@staticmethod
def generate_uml_image(plantuml_code, imgformat):
if imgformat == 'png':
outopt = "-tpng"
elif imgformat == 'svg':
elif imgformat in ['svg', 'svg_object', 'svg_inline']:
outopt = "-tsvg"
elif imgformat == 'txt':
outopt = "-ttxt"
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
with open(path.join(here, "README.md"), "r") as f:
long_description = f.read()

with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f:
with open(path.join(here, 'requirements.txt')) as f:
install_requirements = f.read().splitlines()

with open(path.join(here, 'test-requirements.txt'), encoding='utf-8') as f:
with open(path.join(here, 'test-requirements.txt')) as f:
test_requirements = f.read().splitlines()

setuptools.setup(
name="plantuml-markdown",
version="1.2.6",
version="1.3.0",
author="Michele Tessaro",
author_email="[email protected]",
description="A PlantUML plugin for Markdown",
Expand Down
1 change: 1 addition & 0 deletions test/data/svg_inline_diag.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p><ns0:svg alt="uml diagram" class="uml" title="">...</ns0:svg></p>
1 change: 1 addition & 0 deletions test/data/svg_object_diag.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p><object alt="uml diagram" class="uml" data="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U2NyaXB0VHlwZT0iYXBwbGljYXRpb24vZWNtYXNjcmlwdCIgY29udGVudFN0eWxlVHlwZT0idGV4dC9jc3MiIGhlaWdodD0iMTEycHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDo3OXB4O2hlaWdodDoxMTJweDsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDc5IDExMiIgd2lkdGg9Ijc5cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzPjxmaWx0ZXIgaGVpZ2h0PSIzMDAlIiBpZD0iZjFlbHJvc3VmNnJkZHgiIHdpZHRoPSIzMDAlIiB4PSItMSIgeT0iLTEiPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImJsdXJPdXQiIHN0ZERldmlhdGlvbj0iMi4wIi8+PGZlQ29sb3JNYXRyaXggaW49ImJsdXJPdXQiIHJlc3VsdD0iYmx1ck91dDIiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAuNCAwIi8+PGZlT2Zmc2V0IGR4PSI0LjAiIGR5PSI0LjAiIGluPSJibHVyT3V0MiIgcmVzdWx0PSJibHVyT3V0MyIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImJsdXJPdXQzIiBtb2RlPSJub3JtYWwiLz48L2ZpbHRlcj48L2RlZnM+PGc+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IHN0cm9rZS1kYXNoYXJyYXk6IDUuMCw1LjA7IiB4MT0iMjEiIHgyPSIyMSIgeTE9IjM4LjA5ODYiIHkyPSI3Mi4wOTg2Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IHN0cm9rZS1kYXNoYXJyYXk6IDUuMCw1LjA7IiB4MT0iNTgiIHgyPSI1OCIgeTE9IjM4LjA5ODYiIHkyPSI3Mi4wOTg2Ii8+PHJlY3QgZmlsbD0iI0ZFRkVDRSIgZmlsdGVyPSJ1cmwoI2YxZWxyb3N1ZjZyZGR4KSIgaGVpZ2h0PSIzMC4wOTg2IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuNTsiIHdpZHRoPSIyMyIgeD0iOCIgeT0iMyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjkiIHg9IjE1IiB5PSIyMy4zNjA4Ij5BPC90ZXh0PjxyZWN0IGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmMWVscm9zdWY2cmRkeCkiIGhlaWdodD0iMzAuMDk4NiIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjU7IiB3aWR0aD0iMjMiIHg9IjgiIHk9IjcxLjA5ODYiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI5IiB4PSIxNSIgeT0iOTEuNDU5NSI+QTwvdGV4dD48cmVjdCBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjFlbHJvc3VmNnJkZHgpIiBoZWlnaHQ9IjMwLjA5ODYiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS41OyIgd2lkdGg9IjIzIiB4PSI0NSIgeT0iMyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjkiIHg9IjUyIiB5PSIyMy4zNjA4Ij5CPC90ZXh0PjxyZWN0IGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmMWVscm9zdWY2cmRkeCkiIGhlaWdodD0iMzAuMDk4NiIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjU7IiB3aWR0aD0iMjMiIHg9IjQ1IiB5PSI3MS4wOTg2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iOSIgeD0iNTIiIHk9IjkxLjQ1OTUiPkI8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI0Ni41LDUwLjA5ODYsNTYuNSw1NC4wOTg2LDQ2LjUsNTguMDk4Niw1MC41LDU0LjA5ODYiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyBzdHJva2UtZGFzaGFycmF5OiAyLjAsMi4wOyIgeDE9IjIxLjUiIHgyPSI1Mi41IiB5MT0iNTQuMDk4NiIgeTI9IjU0LjA5ODYiLz48IS0tCkBzdGFydHVtbA0KQSAtIC0+IEINCg0KQGVuZHVtbA0KClBsYW50VU1MIHZlcnNpb24gMS4yMDE4LjAyKEZyaSBNYXIgMDkgMTg6MjA6NDQgQ0VUIDIwMTgpCihHUEwgc291cmNlIGRpc3RyaWJ1dGlvbikKSmF2YSBSdW50aW1lOiBKYXZhKFRNKSBTRSBSdW50aW1lIEVudmlyb25tZW50CkpWTTogSmF2YSBIb3RTcG90KFRNKSA2NC1CaXQgU2VydmVyIFZNCkphdmEgVmVyc2lvbjogMS44LjBfMjUtYjE3Ck9wZXJhdGluZyBTeXN0ZW06IExpbnV4Ck9TIFZlcnNpb246IDQuMTQuMC1zYWJheW9uCkRlZmF1bHQgRW5jb2Rpbmc6IFVURi04Ckxhbmd1YWdlOiBpdApDb3VudHJ5OiBJVAotLT48L2c+PC9zdmc+" title="" /></p>
67 changes: 67 additions & 0 deletions test/test_plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ def _load_file(self, filename):
def _stripImageData(cls, html):
return cls.BASE64_REGEX.sub(r'\1%s' % cls.FAKE_IMAGE, html)

FAKE_SVG = '...svg-body...'
SVG_REGEX = re.compile(r'<(?:\w+:)?svg(?:( alt=".*?")|( class=".*?")|( title=".*?")|(?:.*?))+>.*</(?:\w+:)?svg>')

@classmethod
def _stripSvgData(cls, html):
"""
Simplifies SVG tags to easy comparing.
:param html: source HTML
:return: HTML code with simplified svg tags
"""
def sort_attributes(groups):
"""
Sorts attributes in a specific order.
:param groups: matched attributed groups
:return: a SVG tag string source
"""
alt = next(x for x in groups if x.startswith(' alt='))
title = next(x for x in groups if x.startswith(' title='))
classes = next(x for x in groups if x.startswith(' class='))

return "<svg{}{}{}>{}</svg>".format(alt, title, classes, cls.FAKE_SVG)

return cls.SVG_REGEX.sub(lambda x: sort_attributes(x.groups()), html)

def test_arg_title(self):
"""
Test for the correct parsing of the title argument
Expand All @@ -38,6 +62,15 @@ def test_arg_title(self):
'<p><img alt="uml diagram" class="uml" src="data:image/png;base64,%s" title="Diagram test" /></p>' % self.FAKE_IMAGE,
self._stripImageData(self.md.convert(text)))

def test_arg_title_inline_svg(self):
"""
Test for setting title attribute in inline SVG
"""
text = self.text_builder.diagram("A --> B").format("svg_inline").title("Diagram test").build()
self.assertEqual(
'<p><svg alt="uml diagram" title="Diagram test" class="uml">%s</svg></p>' % self.FAKE_SVG,
self._stripSvgData(self.md.convert(text)))

def test_arg_alt(self):
"""
Test for the correct parsing of the alt argument
Expand All @@ -47,6 +80,15 @@ def test_arg_alt(self):
'<p><img alt="Diagram test" class="uml" src="data:image/png;base64,%s" title="" /></p>' % self.FAKE_IMAGE,
self._stripImageData(self.md.convert(text)))

def test_arg_alt_inline_svg(self):
"""
Test for setting alt attribute in inline SVG
"""
text = self.text_builder.diagram("A --> B").format("svg_inline").alt("Diagram test").build()
self.assertEqual(
'<p><svg alt="Diagram test" title="" class="uml">%s</svg></p>' % self.FAKE_SVG,
self._stripSvgData(self.md.convert(text)))

def test_arg_classes(self):
"""
Test for the correct parsing of the classes argument
Expand All @@ -56,6 +98,15 @@ def test_arg_classes(self):
'<p><img alt="uml diagram" class="class1 class2" src="data:image/png;base64,%s" title="" /></p>' % self.FAKE_IMAGE,
self._stripImageData(self.md.convert(text)))

def test_arg_classes_inline_svg(self):
"""
Test for setting class attribute in inline SVG
"""
text = self.text_builder.diagram("A --> B").format("svg_inline").classes("class1 class2").build()
self.assertEqual(
'<p><svg alt="uml diagram" title="" class="class1 class2">%s</svg></p>' % self.FAKE_SVG,
self._stripSvgData(self.md.convert(text)))

def test_arg_format_png(self):
"""
Test for the correct parsing of the format argument, generating a png image
Expand All @@ -72,6 +123,22 @@ def test_arg_format_svg(self):
self.assertEqual(self._stripImageData(self._load_file('svg_diag.html')),
self._stripImageData(self.md.convert(text)))

def test_arg_format_svg_object(self):
"""
Test for the correct parsing of the format argument, generating a svg image
"""
text = self.text_builder.diagram("A --> B").format("svg_object").build()
self.assertEqual(self._stripImageData(self._load_file('svg_object_diag.html')),
self._stripImageData(self.md.convert(text)))

def test_arg_format_svg_inline(self):
"""
Test for the correct parsing of the format argument, generating a svg image
"""
text = self.text_builder.diagram("A --> B").format("svg_inline").build()
self.assertEqual(self._stripSvgData(self._load_file('svg_inline_diag.html')),
self._stripSvgData(self.md.convert(text)))

def test_arg_format_txt(self):
"""
Test for the correct parsing of the format argument, generating a txt image
Expand Down

0 comments on commit ac86110

Please sign in to comment.