Skip to content

Commit

Permalink
Merge branch 'release-1.4.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Michele Tessaro committed Nov 24, 2018
2 parents ac86110 + 634345b commit 42227bd
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 18 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 1.4.0 (2018-11-24)

### New

* Added `width` and `height` attributes. [Michele Tessaro]

The new attributes `width` and `height` can be used to limit image size:
if the image dimension is bigger than values specified, they will be
shrinked keeping the aspect ratio.
If there is not enought space in the page fot the diagram, the image
will be reduced.

### Fix

* Fixed navigable links in inline SVG (resolves #18) [Michele Tessaro]


## 1.3.0 (2018-11-17)

### New
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@ converted into an image and inserted in the document.
Syntax:

```markdown
::uml:: [format="png|svg|txt"] [classes="class1 class2 ..."] [alt="text for alt"] [title="Text for title"]
::uml:: [format="png|svg|txt"] [classes="class1 class2 ..."] [alt="text for alt"] [title="Text for title"] width="300px" height="300px"
PlantUML script diagram
::end-uml::
```

Example:

```markdown
::uml:: format="png" classes="uml myDiagram" alt="My super diagram placeholder" title="My super diagram"
::uml:: format="png" classes="uml myDiagram" alt="My super diagram placeholder" title="My super diagram" width="300px" height="300px"
Goofy -> MickeyMouse: calls
Goofy <-- MickeyMouse: responds
::end-uml::
```

The GitLab/GitHub block syntax is also recognized. Example:

```plantuml format="png" classes="uml myDiagram" alt="My super diagram placeholder" title="My super diagram"
```plantuml format="png" classes="uml myDiagram" alt="My super diagram placeholder" title="My super diagram" width="300px" height="300px"
Goofy -> MickeyMouse: calls
Goofy <-- MickeyMouse: responds
```

Options are optional (otherwise the wouldn't be options), but if present must be specified in the order `format`, `classes`, `alt`, `title`.
Options are optional (otherwise the wouldn't be options), but if present must be specified in the order `format`, `classes`, `alt`, `title`, `width`, `height`.
The option value may be enclosed in single or double quotes.

Supported values for `format` parameter are:
Expand All @@ -41,6 +41,8 @@ Supported values for `format` parameter are:
* `svg_inline`: HTML5 `svg` tag with inline svg image source (links are navigable, can be manipulated with CSS rules)
* `txt`: plain text diagrams.

The `width` and `height` options must include a [CSS unit](https://www.w3schools.com/cssref/css_units.asp).

Installation
------------
You need to install [PlantUML][] (see the site for details) and [Graphviz][] 2.26.3 or later.
Expand Down
41 changes: 33 additions & 8 deletions plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@
import re
import base64
from subprocess import Popen, PIPE
#import logging
import logging
import markdown
from markdown.util import etree, AtomicString


#logger = logging.getLogger('MARKDOWN')
#logger.setLevel(logging.DEBUG)
logger = logging.getLogger('MARKDOWN')
logger.setLevel(logging.DEBUG)


# For details see https://pythonhosted.org/Markdown/extensions/api.html#blockparser
Expand All @@ -76,6 +76,8 @@ class PlantUMLPreprocessor(markdown.preprocessors.Preprocessor):
\s*(classes=(?P<quot1>"|')(?P<classes>[\w\s]+)(?P=quot1))?
\s*(alt=(?P<quot2>"|')(?P<alt>[\w\s"']+)(?P=quot2))?
\s*(title=(?P<quot3>"|')(?P<title>[\w\s"']+)(?P=quot3))?
\s*(width=(?P<quot4>"|')(?P<width>[\w\s"']+)(?P=quot4))?
\s*(height=(?P<quot5>"|')(?P<height>[\w\s"']+)(?P=quot5))?
\s*\n
(?P<code>.*?)(?<=\n)
::end-uml::[ ]*$
Expand All @@ -89,6 +91,8 @@ class PlantUMLPreprocessor(markdown.preprocessors.Preprocessor):
\s*(classes=(?P<quot1>"|')(?P<classes>[\w\s]+)(?P=quot1))?
\s*(alt=(?P<quot2>"|')(?P<alt>[\w\s"']+)(?P=quot2))?
\s*(title=(?P<quot3>"|')(?P<title>[\w\s"']+)(?P=quot3))?
\s*(width=(?P<quot4>"|')(?P<width>[\w\s"']+)(?P=quot4))?
\s*(height=(?P<quot5>"|')(?P<height>[\w\s"']+)(?P=quot5))?
[ ]*
}?[ ]*\n # Optional closing }
(?P<code>.*?)(?<=\n)
Expand All @@ -108,7 +112,7 @@ 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=".*?"(.*?)>')
ADAPT_SVG_REGEX = re.compile(r'^<\?xml .*?\?><svg(.*?)xmlns=".*?" (.*?)>')

def _replace_block(self, text):
# Parse configuration params
Expand All @@ -123,6 +127,8 @@ def _replace_block(self, text):
classes = m.group('classes') if m.group('classes') else self.config['classes']
alt = m.group('alt') if m.group('alt') else self.config['alt']
title = m.group('title') if m.group('title') else self.config['title']
width = m.group('width') if m.group('width') else None
height = m.group('height') if m.group('height') else None

# Extract diagram source end convert it
code = m.group('code')
Expand All @@ -135,7 +141,13 @@ def _replace_block(self, text):
code.attrib['class'] = 'text'
code.text = AtomicString(diagram.decode('UTF-8'))
else:
if img_format == 'svg':
# 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)
# 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')
Expand All @@ -145,14 +157,24 @@ def _replace_block(self, text):
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

styles = []
if width:
styles.append("max-width:"+width)
if height:
styles.append("max-height:"+height)

if styles:
style = img.attrib['style']+';' if 'style' in img.attrib and img.attrib['style'] != '' else ''
img.attrib['style'] = 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
Expand Down Expand Up @@ -200,6 +222,9 @@ def __init__(self, *args, **kwargs):
'title': ["", "Tooltip for the diagram"]
}

# Fix to make links navigable in SVG diagrams
etree.register_namespace('xlink', 'http://www.w3.org/1999/xlink')

super(PlantUMLMarkdownExtension, self).__init__(*args, **kwargs)

def extendMarkdown(self, md, md_globals):
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

here = path.abspath(path.dirname(__file__))

with open(path.join(here, "README.md"), "r") as f:
with open(path.join(here, "README.md")) as f:
long_description = f.read()

with open(path.join(here, 'requirements.txt')) as f:
Expand All @@ -15,7 +15,7 @@

setuptools.setup(
name="plantuml-markdown",
version="1.3.0",
version="1.4.0",
author="Michele Tessaro",
author_email="[email protected]",
description="A PlantUML plugin for Markdown",
Expand Down
2 changes: 1 addition & 1 deletion test/data/svg_inline_diag.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<p><ns0:svg alt="uml diagram" class="uml" title="">...</ns0:svg></p>
<p><svg alt="uml diagram" class="uml" title="">...</svg></p>
22 changes: 21 additions & 1 deletion test/markdown_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def _reset_diagram(self):
self._alt = ""
self._title = ""
self._diagram_buffer = ""
self._width = ""
self._height = ""

def _emit_diagram(self):
"""
Expand All @@ -26,7 +28,7 @@ def _emit_diagram(self):
"""
if self._diagram_buffer:
delim = re.sub(r'(\W{3,})(\w+)', r'\1{\2', self._delimiter) if self._extended_syntax else self._delimiter
args = self._format + self._class + self._alt + self._title
args = self._format + self._class + self._alt + self._title + self._width + self._height
self._buffer += delim+args+('}' if self._extended_syntax else '')
self._buffer += "\n"+self._diagram_buffer+"\n"+self._end_delimiter+"\n"

Expand Down Expand Up @@ -77,6 +79,24 @@ def format(self, fmt):
self._format = " format='%s'" % fmt
return self

def width(self, w):
"""
Define the maximum width of the diagram image.
:param w: Max width, with unit (ex: "120px")
:return: The object itself
"""
self._width = " width='%s'" % w
return self

def height(self, h):
"""
Define the maximum height of the diagram image.
:param w: Max width, with unit (ex: "120px")
:return: The object itself
"""
self._height = " height='%s'" % h
return self

def text(self, txt):
"""
Adds a new text to the markdown source.
Expand Down
58 changes: 56 additions & 2 deletions test/test_plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ 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>')
SVG_REGEX = re.compile(r'<(?:\w+:)?svg(?:( alt=".*?")|( class=".*?")|( title=".*?")|( style=".*?")|(?:.*?))+>.*</(?:\w+:)?svg>')

@classmethod
def _stripSvgData(cls, html):
Expand All @@ -48,8 +48,11 @@ def sort_attributes(groups):
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='))
style = next(iter(x for x in groups if x and x.startswith(' style=')), None)

return "<svg{}{}{}>{}</svg>".format(alt, title, classes, cls.FAKE_SVG)
style = style if style and '""' not in style else ''

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

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

Expand Down Expand Up @@ -147,6 +150,57 @@ def test_arg_format_txt(self):
self.assertEqual(self._load_file('txt_diag.html'),
self.md.convert(text))

def test_arg_width(self):
"""
Test for the correct parsing of the width argument
"""
text = self.text_builder.diagram("A --> B").width("120px").build()
self.assertEqual(
'<p><img alt="uml diagram" class="uml" src="data:image/png;base64,%s" style="max-width:120px" title="" width="100%%" /></p>' % self.FAKE_IMAGE,
self._stripImageData(self.md.convert(text)))

def test_arg_height(self):
"""
Test for the correct parsing of the width argument
"""
text = self.text_builder.diagram("A --> B").height("120px").build()
self.assertEqual(
'<p><img alt="uml diagram" class="uml" src="data:image/png;base64,%s" style="max-height:120px" title="" width="100%%" /></p>' % self.FAKE_IMAGE,
self._stripImageData(self.md.convert(text)))

def test_arg_width_and_height(self):
"""
Test for the correct parsing of the width and height arguments
"""
text = self.text_builder.diagram("A --> B").width("120px").height("120px").build()
self.assertEqual(
'<p><img alt="uml diagram" class="uml" src="data:image/png;base64,%s" style="max-width:120px;max-height:120px" title="" width="100%%" /></p>' % self.FAKE_IMAGE,
self._stripImageData(self.md.convert(text)))

def test_arg_format_width_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").width("120px").build()
self.assertEqual(self._stripSvgData('<p><svg alt="uml diagram" title="" class="uml" style="max-width:120px">...svg-body...</svg></p>'),
self._stripSvgData(self.md.convert(text)))

def test_arg_format_height_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").height("120px").build()
self.assertEqual(self._stripSvgData('<p><svg alt="uml diagram" title="" class="uml" style="max-height:120px">...svg-body...</svg></p>'),
self._stripSvgData(self.md.convert(text)))

def test_arg_format_width_and_height_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").width('120px').height("120px").build()
self.assertEqual(self._stripSvgData('<p><svg alt="uml diagram" title="" class="uml" style="max-width:120px;max-height:120px">...svg-body...</svg></p>'),
self._stripSvgData(self.md.convert(text)))

def test_multidiagram(self):
"""
Test for the definition of multiple diagrams on the same document
Expand Down

0 comments on commit 42227bd

Please sign in to comment.