Skip to content

Commit ac86110

Browse files
author
Michele Tessaro
committed
Merge branch 'release-1.3.0'
2 parents 48824ce + 9bd09f6 commit ac86110

File tree

8 files changed

+138
-26
lines changed

8 files changed

+138
-26
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
# Changelog
22

3+
## 1.3.0 (2018-11-17)
4+
5+
### New
6+
7+
* Added support for clickable SVGs (closes #17) [Michele Tessaro]
8+
9+
Added two new output formats:
10+
* `svg_object`: generated an `object` tag for displaing svg images
11+
* `svg_inline`: embedded the svg source image directly in the document
12+
13+
### Fix
14+
15+
* Fixed error when the output format is not recognized. [Michele Tessaro]
16+
17+
318
## 1.2.6 (2018-11-04)
419

20+
### Changes
21+
22+
* Update documentation. [Michele Tessaro]
23+
524
### Fix
625

726
* Fixed wrong `classes` HTML attribute (fixes #16) [Michele Tessaro]

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ The GitLab/GitHub block syntax is also recognized. Example:
3333
Options are optional (otherwise the wouldn't be options), but if present must be specified in the order `format`, `classes`, `alt`, `title`.
3434
The option value may be enclosed in single or double quotes.
3535

36+
Supported values for `format` parameter are:
37+
38+
* `png`: HTML `img` tag with embedded png image
39+
* `svg`: HTML `img` tag with embedded svg image (links are not navigable)
40+
* `svg_object`: HTML `object` tag with embedded svg image (links are navigable)
41+
* `svg_inline`: HTML5 `svg` tag with inline svg image source (links are navigable, can be manipulated with CSS rules)
42+
* `txt`: plain text diagrams.
43+
3644
Installation
3745
------------
3846
You need to install [PlantUML][] (see the site for details) and [Graphviz][] 2.26.3 or later.
@@ -84,7 +92,7 @@ To use the plugin with [Python-Markdown][] you have three choices:
8492

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

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

8997
markdown_py -x plantuml mydoc.md > out.html
9098

mdx_plantuml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plantuml.py

plantuml.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
Options are optional, but if present must be specified in the order format, classes, alt.
2323
The option value may be enclosed in single or double quotes.
2424
25+
26+
Supported values for `format` parameter are:
27+
28+
* `png`: HTML `img` tag with embedded png image
29+
* `svg`: HTML `img` tag with embedded svg image (links are not navigable)
30+
* `svg_object`: HTML `object` tag with embedded svg image (links are navigable)
31+
* `svg_inline`: HTML5 `svg` tag with inline svg image source (links are navigable, can be manipulated with CSS rules)
32+
* `txt`: plain text diagrams.
33+
2534
Installation
2635
------------
2736
You need to install [PlantUML][] (see the site for details) and [Graphviz][] 2.26.3 or later.
@@ -48,7 +57,7 @@
4857
import re
4958
import base64
5059
from subprocess import Popen, PIPE
51-
import logging
60+
#import logging
5261
import markdown
5362
from markdown.util import etree, AtomicString
5463

@@ -98,6 +107,9 @@ def run(self, lines):
98107

99108
return text.split('\n')
100109

110+
# regex for removing some parts from the plantuml generated svg
111+
ADAPT_SVG_REGEX = re.compile(r'^<\?xml .*?\?><svg(.*?)xmlns=".*?"(.*?)>')
112+
101113
def _replace_block(self, text):
102114
# Parse configuration params
103115
m = self.FENCED_BLOCK_RE.search(text)
@@ -116,39 +128,42 @@ def _replace_block(self, text):
116128
code = m.group('code')
117129
diagram = self.generate_uml_image(code, img_format)
118130

119-
if img_format == 'png':
120-
data = 'data:image/png;base64,{0}'.format(
121-
base64.b64encode(diagram).decode('ascii')
122-
)
123-
img = etree.Element('img')
124-
img.attrib['src' ] = data
125-
img.attrib['class' ] = classes
126-
img.attrib['alt' ] = alt
127-
img.attrib['title' ] = title
128-
elif img_format == 'svg':
129-
# Firefox handles only base64 encoded SVGs
130-
data = 'data:image/svg+xml;base64,{0}'.format(
131-
base64.b64encode(diagram).decode('ascii')
132-
)
133-
img = etree.Element('img')
134-
img.attrib['src' ] = data
135-
img.attrib['class' ] = classes
136-
img.attrib['alt' ] = alt
137-
img.attrib['title' ] = title
138-
elif img_format == 'txt':
131+
if img_format == 'txt':
139132
# logger.debug(diagram)
140133
img = etree.Element('pre')
141134
code = etree.SubElement(img, 'code')
142135
code.attrib['class'] = 'text'
143136
code.text = AtomicString(diagram.decode('UTF-8'))
137+
else:
138+
if img_format == 'svg':
139+
# Firefox handles only base64 encoded SVGs
140+
data = 'data:image/svg+xml;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
141+
img = etree.Element('img')
142+
img.attrib['src'] = data
143+
elif img_format == 'svg_object':
144+
# Firefox handles only base64 encoded SVGs
145+
data = 'data:image/svg+xml;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
146+
img = etree.Element('object')
147+
img.attrib['data'] = data
148+
elif img_format == 'svg_inline':
149+
data = self.ADAPT_SVG_REGEX.sub('<svg\\1\\2>', diagram.decode('UTF-8'))
150+
img = etree.fromstring(data)
151+
else: # png format, explicitly set or as a default when format is not recognized
152+
data = 'data:image/png;base64,{0}'.format(base64.b64encode(diagram).decode('ascii'))
153+
img = etree.Element('img')
154+
img.attrib['src'] = data
155+
156+
img.attrib['class'] = classes
157+
img.attrib['alt'] = alt
158+
img.attrib['title'] = title
144159

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

147162
@staticmethod
148163
def generate_uml_image(plantuml_code, imgformat):
149164
if imgformat == 'png':
150165
outopt = "-tpng"
151-
elif imgformat == 'svg':
166+
elif imgformat in ['svg', 'svg_object', 'svg_inline']:
152167
outopt = "-tsvg"
153168
elif imgformat == 'txt':
154169
outopt = "-ttxt"

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
with open(path.join(here, "README.md"), "r") as f:
88
long_description = f.read()
99

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

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

1616
setuptools.setup(
1717
name="plantuml-markdown",
18-
version="1.2.6",
18+
version="1.3.0",
1919
author="Michele Tessaro",
2020
author_email="[email protected]",
2121
description="A PlantUML plugin for Markdown",

test/data/svg_inline_diag.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p><ns0:svg alt="uml diagram" class="uml" title="">...</ns0:svg></p>

test/data/svg_object_diag.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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>

test/test_plantuml.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ def _load_file(self, filename):
2929
def _stripImageData(cls, html):
3030
return cls.BASE64_REGEX.sub(r'\1%s' % cls.FAKE_IMAGE, html)
3131

32+
FAKE_SVG = '...svg-body...'
33+
SVG_REGEX = re.compile(r'<(?:\w+:)?svg(?:( alt=".*?")|( class=".*?")|( title=".*?")|(?:.*?))+>.*</(?:\w+:)?svg>')
34+
35+
@classmethod
36+
def _stripSvgData(cls, html):
37+
"""
38+
Simplifies SVG tags to easy comparing.
39+
:param html: source HTML
40+
:return: HTML code with simplified svg tags
41+
"""
42+
def sort_attributes(groups):
43+
"""
44+
Sorts attributes in a specific order.
45+
:param groups: matched attributed groups
46+
:return: a SVG tag string source
47+
"""
48+
alt = next(x for x in groups if x.startswith(' alt='))
49+
title = next(x for x in groups if x.startswith(' title='))
50+
classes = next(x for x in groups if x.startswith(' class='))
51+
52+
return "<svg{}{}{}>{}</svg>".format(alt, title, classes, cls.FAKE_SVG)
53+
54+
return cls.SVG_REGEX.sub(lambda x: sort_attributes(x.groups()), html)
55+
3256
def test_arg_title(self):
3357
"""
3458
Test for the correct parsing of the title argument
@@ -38,6 +62,15 @@ def test_arg_title(self):
3862
'<p><img alt="uml diagram" class="uml" src="data:image/png;base64,%s" title="Diagram test" /></p>' % self.FAKE_IMAGE,
3963
self._stripImageData(self.md.convert(text)))
4064

65+
def test_arg_title_inline_svg(self):
66+
"""
67+
Test for setting title attribute in inline SVG
68+
"""
69+
text = self.text_builder.diagram("A --> B").format("svg_inline").title("Diagram test").build()
70+
self.assertEqual(
71+
'<p><svg alt="uml diagram" title="Diagram test" class="uml">%s</svg></p>' % self.FAKE_SVG,
72+
self._stripSvgData(self.md.convert(text)))
73+
4174
def test_arg_alt(self):
4275
"""
4376
Test for the correct parsing of the alt argument
@@ -47,6 +80,15 @@ def test_arg_alt(self):
4780
'<p><img alt="Diagram test" class="uml" src="data:image/png;base64,%s" title="" /></p>' % self.FAKE_IMAGE,
4881
self._stripImageData(self.md.convert(text)))
4982

83+
def test_arg_alt_inline_svg(self):
84+
"""
85+
Test for setting alt attribute in inline SVG
86+
"""
87+
text = self.text_builder.diagram("A --> B").format("svg_inline").alt("Diagram test").build()
88+
self.assertEqual(
89+
'<p><svg alt="Diagram test" title="" class="uml">%s</svg></p>' % self.FAKE_SVG,
90+
self._stripSvgData(self.md.convert(text)))
91+
5092
def test_arg_classes(self):
5193
"""
5294
Test for the correct parsing of the classes argument
@@ -56,6 +98,15 @@ def test_arg_classes(self):
5698
'<p><img alt="uml diagram" class="class1 class2" src="data:image/png;base64,%s" title="" /></p>' % self.FAKE_IMAGE,
5799
self._stripImageData(self.md.convert(text)))
58100

101+
def test_arg_classes_inline_svg(self):
102+
"""
103+
Test for setting class attribute in inline SVG
104+
"""
105+
text = self.text_builder.diagram("A --> B").format("svg_inline").classes("class1 class2").build()
106+
self.assertEqual(
107+
'<p><svg alt="uml diagram" title="" class="class1 class2">%s</svg></p>' % self.FAKE_SVG,
108+
self._stripSvgData(self.md.convert(text)))
109+
59110
def test_arg_format_png(self):
60111
"""
61112
Test for the correct parsing of the format argument, generating a png image
@@ -72,6 +123,22 @@ def test_arg_format_svg(self):
72123
self.assertEqual(self._stripImageData(self._load_file('svg_diag.html')),
73124
self._stripImageData(self.md.convert(text)))
74125

126+
def test_arg_format_svg_object(self):
127+
"""
128+
Test for the correct parsing of the format argument, generating a svg image
129+
"""
130+
text = self.text_builder.diagram("A --> B").format("svg_object").build()
131+
self.assertEqual(self._stripImageData(self._load_file('svg_object_diag.html')),
132+
self._stripImageData(self.md.convert(text)))
133+
134+
def test_arg_format_svg_inline(self):
135+
"""
136+
Test for the correct parsing of the format argument, generating a svg image
137+
"""
138+
text = self.text_builder.diagram("A --> B").format("svg_inline").build()
139+
self.assertEqual(self._stripSvgData(self._load_file('svg_inline_diag.html')),
140+
self._stripSvgData(self.md.convert(text)))
141+
75142
def test_arg_format_txt(self):
76143
"""
77144
Test for the correct parsing of the format argument, generating a txt image

0 commit comments

Comments
 (0)