forked from mikitex70/plantuml-markdown
-
Notifications
You must be signed in to change notification settings - Fork 0
/
plantuml.py
177 lines (143 loc) · 6.88 KB
/
plantuml.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env python
"""
[PlantUML][] Extension for [Python-Markdown][]
==============================================
This plugin implements a block extension which can be used to specify a [PlantUML][] diagram which will be
converted into an image and inserted in the document.
Syntax:
::uml:: [format="png|svg"] [classes="class1 class2 ..."] [alt="text for alt"]
PlantUML script diagram
::end-uml::
Example:
::uml:: format="png" classes="uml myDiagram" alt="My super diagram"
Goofy -> MickeyMouse: calls
Goofy <-- MickeyMouse: responds
::end-uml::
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.
Installation
------------
You need to install [PlantUML][] (see the site for details) and [Graphviz][] 2.26.3 or later.
The plugin expects a program `plantuml` in the classpath. If not installed by your package
manager, you can create a shell script and place it somewhere in the classpath. For example,
save te following into `/usr/local/bin/plantuml` (supposing [PlantUML][] installed into
`/opt/plantuml`):
#!/bin/bash
java -jar /opt/plantuml/plantuml.jar ${@}
For [Gentoo Linux][Gentoo] there is an ebuild at http://gpo.zugaina.org/dev-util/plantuml/RDep: you can download
the ebuild and the `files` subfolder or you can add the `zugaina` repository with [layman][]
(reccomended).
[Python-Markdown]: http://pythonhosted.org/Markdown/
[PlantUML]: http://plantuml.sourceforge.net/
[Graphviz]: http://www.graphviz.org
[Gentoo]: http://www.gentoo.org
[layman]: http://wiki.gentoo.org/wiki/Layman
"""
import os
import re
import tempfile
from subprocess import Popen, PIPE
from zlib import adler32
import logging
import markdown
from markdown.util import etree
logger = logging.getLogger('MARKDOWN')
# For details see https://pythonhosted.org/Markdown/extensions/api.html#blockparser
class PlantUMLBlockProcessor(markdown.blockprocessors.BlockProcessor):
# Regular expression inspired by the codehilite Markdown plugin
RE = re.compile(r'''::uml::
\s*(format=(?P<quot>"|')(?P<format>\w+)(?P=quot))?
\s*(classes=(?P<quot1>"|')(?P<classes>[\w\s]+)(?P=quot1))?
\s*(alt=(?P<quot2>"|')(?P<alt>[\w\s"']+)(?P=quot2))?
''', re.VERBOSE)
# Regular expression for identify end of UML script
RE_END = re.compile(r'::end-uml::\s*$')
def test(self, parent, block):
return self.RE.search(block)
def run(self, parent, blocks):
block = blocks.pop(0)
text = block
# Parse configuration params
m = self.RE.search(block)
imgformat = m.group('format') if m.group('format') else self.config['format']
classes = m.group('classes') if m.group('classes') else self.config['classes']
alt = m.group('alt') if m.group('alt') else self.config['alt']
# Read blocks until end marker found
while blocks and not self.RE_END.search(block):
block = blocks.pop(0)
text += '\n' + block
else:
if not blocks:
raise RuntimeError("UML block not closed")
# Remove block header and footer
text = re.sub(self.RE, "", re.sub(self.RE_END, "", text))
path = os.path.abspath(self.config['outpath'])
if not os.path.exists(path):
os.makedirs(path)
# Generate image from PlantUML script
imageurl = self.config['siteurl']+self.generate_uml_image(path, text, imgformat)
# Create image tag and append to the document
etree.SubElement(parent, "img", src=imageurl, alt=alt, classes=classes)
@staticmethod
def generate_uml_image(self, path, plantuml_code, imgformat):
plantuml_code = plantuml_code.encode('utf8')
tf = tempfile.NamedTemporaryFile(delete=False)
tf.write('@startuml\n'.encode('utf8'))
tf.write(plantuml_code)
tf.write('\n@enduml'.encode('utf8'))
tf.flush()
if imgformat == 'png':
imgext = ".png"
outopt = "-tpng"
elif imgformat == 'svg':
imgext = ".svg"
outopt = "-tsvg"
else:
logger.error("Bad uml image format '"+imgformat+"', using png")
imgext = ".png"
outopt = "-tpng"
# make a name
name = tf.name+imgext
# build cmd line
cmdline = ['plantuml', '-o', path, outopt, tf.name]
try:
p = Popen(cmdline, stdout=PIPE, stderr=PIPE)
out, err = p.communicate()
except Exception as exc:
raise Exception('Failed to run plantuml: %s' % exc)
else:
if p.returncode == 0:
# diagram was correctly generated, we can remove the temporary file
os.remove(tf.name)
# make sure output path exists
if not os.path.exists(path):
os.makedirs(path)
# renaming output image using an hash code, just to not pullate
# output directory with a growing number of images
name = os.path.join(path, os.path.basename(name))
newname = os.path.join(path, "%08x" % (adler32(plantuml_code) & 0xffffffff))+imgext
if not os.path.exists(newname):
os.remove(newname)
os.rename(name, newname)
return 'images/' + os.path.basename(newname)
else:
# the temporary file is still available as aid understanding errors
raise RuntimeError('Error in "uml" directive: %s' % err)
# For details see https://pythonhosted.org/Markdown/extensions/api.html#extendmarkdown
class PlantUMLMarkdownExtension(markdown.Extension):
# For details see https://pythonhosted.org/Markdown/extensions/api.html#configsettings
def __init__(self, *args, **kwargs):
self.config = {
'classes': ["uml", "Space separated list of classes for the generated image. Defaults to 'uml'."],
'alt': ["uml diagram", "Text to show when image is not available. Defaults to 'uml diagram'"],
'format': ["png", "Format of image to generate (png or svg). Defaults to 'png'."],
'outpath': ["images", "Directory where to put generated images. Defaults to 'images'."],
'siteurl': ["", "URL of document, used as a prefix for the image diagram. Defaults to empty string."]
}
super(PlantUMLMarkdownExtension, self).__init__(*args, **kwargs)
def extendMarkdown(self, md, md_globals):
blockprocessor = PlantUMLBlockProcessor(md.parser)
blockprocessor.config = self.getConfigs()
md.parser.blockprocessors.add('plantuml', blockprocessor, '>code')
def makeExtension(*args, **kwargs):
return PlantUMLMarkdownExtension(*args, **kwargs)