Skip to content
This repository has been archived by the owner on Aug 3, 2024. It is now read-only.

Commit

Permalink
fix possible issues loading plantuml
Browse files Browse the repository at this point in the history
- move plantuml extension out of sphinxcontrib to avoid clashing with
  system level sphinxcontrib module.
- upgrade to sphinxcontrib-plantuml 0.8.1 while we're at it.
  • Loading branch information
ervandew committed Jul 12, 2017
1 parent fc1cbe3 commit e4fcdef
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 55 deletions.
2 changes: 1 addition & 1 deletion doc/content/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.extlinks',
'sphinxcontrib.plantuml',
'plantuml',
'eclim.sphinx.include',
'eclim.sphinx.vimdoc',
'eclim.sphinx.rss',
Expand Down
212 changes: 171 additions & 41 deletions doc/extension/sphinxcontrib/plantuml.py → doc/extension/plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,45 @@
:copyright: Copyright 2010 by Yuya Nishihara <yuya@tcha.org>.
:license: BSD, see LICENSE for details.
"""
import errno, os, re, shlex, subprocess
try:
from hashlib import sha1
except ImportError: # Python<2.5
from sha import sha as sha1

import codecs
import errno
import hashlib
import os
import re
import shlex
import subprocess

from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.errors import SphinxError
from sphinx.util.compat import Directive
from sphinx.util.osutil import ensuredir, ENOENT
from sphinx.util.osutil import (
ensuredir,
ENOENT,
)

try:
from PIL import Image
except ImportError:
Image = None

try:
from sphinx.util.i18n import search_image_for_language
except ImportError: # Sphinx < 1.4
def search_image_for_language(filename, env):
return filename

class PlantUmlError(SphinxError):
pass

class plantuml(nodes.General, nodes.Element):
pass

def align(argument):
align_values = ('left', 'center', 'right')
return directives.choice(argument, align_values)

class UmlDirective(Directive):
"""Directive to insert PlantUML markup
Expand All @@ -37,16 +59,70 @@ class UmlDirective(Directive):
Alice <- Bob: Hi
"""
has_content = True
option_spec = {'alt': directives.unchanged}
required_arguments = 0
optional_arguments = 1
option_spec = {'alt': directives.unchanged,
'caption': directives.unchanged,
'height': directives.length_or_unitless,
'width': directives.length_or_percentage_or_unitless,
'scale': directives.percentage,
'align': align,
}

def run(self):
node = plantuml()
node['uml'] = '\n'.join(self.content)
node['alt'] = self.options.get('alt', None)
warning = self.state.document.reporter.warning
env = self.state.document.settings.env
if self.arguments and self.content:
return [warning('uml directive cannot have both content and '
'a filename argument', line=self.lineno)]
if self.arguments:
fn = search_image_for_language(self.arguments[0], env)
relfn, absfn = env.relfn2path(fn)
env.note_dependency(relfn)
try:
umlcode = _read_utf8(absfn)
except (IOError, UnicodeDecodeError) as err:
return [warning('PlantUML file "%s" cannot be read: %s'
% (fn, err), line=self.lineno)]
else:
relfn = env.doc2path(env.docname, base=None)
umlcode = '\n'.join(self.content)

node = plantuml(self.block_text, **self.options)
node['uml'] = umlcode
node['incdir'] = os.path.dirname(relfn)

# XXX maybe this should be moved to _visit_plantuml functions. it
# seems wrong to insert "figure" node by "plantuml" directive.
if 'caption' in self.options or 'align' in self.options:
node = nodes.figure('', node)
if 'align' in self.options:
node['align'] = self.options['align']
if 'caption' in self.options:
import docutils.statemachine
cnode = nodes.Element() # anonymous container for parsing
sl = docutils.statemachine.StringList([self.options['caption']],
source='')
self.state.nested_parse(sl, self.content_offset, cnode)
caption = nodes.caption(self.options['caption'], '', *cnode)
node += caption

return [node]

def _read_utf8(filename):
fp = codecs.open(filename, 'rb', 'utf-8')
try:
return fp.read()
finally:
fp.close()

def generate_name(self, node, fileformat):
key = sha1(node['uml'].encode('utf-8')).hexdigest()
h = hashlib.sha1()
# may include different file relative to doc
h.update(node['incdir'].encode('utf-8'))
h.update(b'\0')
h.update(node['uml'].encode('utf-8'))
key = h.hexdigest()
fname = 'plantuml-%s.%s' % (key, fileformat)
imgpath = getattr(self.builder, 'imgpath', None)
if imgpath:
Expand All @@ -62,16 +138,10 @@ def generate_name(self, node, fileformat):
}

def generate_plantuml_args(self, fileformat):
try:
is_string = isinstance(self.builder.config.plantuml, basestring)
except NameError:
# python 3
is_string = isinstance(self.builder.config.plantuml, str)

if is_string:
args = shlex.split(self.builder.config.plantuml)
else:
if isinstance(self.builder.config.plantuml, (tuple, list)):
args = list(self.builder.config.plantuml)
else:
args = shlex.split(self.builder.config.plantuml)
args.extend('-pipe -charset utf-8'.split())
args.extend(_ARGS_BY_FILEFORMAT[fileformat])
return args
Expand All @@ -80,29 +150,85 @@ def render_plantuml(self, node, fileformat):
refname, outfname = generate_name(self, node, fileformat)
if os.path.exists(outfname):
return refname, outfname # don't regenerate
absincdir = os.path.join(self.builder.srcdir, node['incdir'])
ensuredir(os.path.dirname(outfname))
f = open(outfname, 'wb')
try:
try:
p = subprocess.Popen(generate_plantuml_args(self, fileformat),
stdout=f, stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
stderr=subprocess.PIPE,
cwd=absincdir)
except OSError as err:
if err.errno != ENOENT:
raise
raise PlantUmlError('plantuml command %r cannot be run'
% self.builder.config.plantuml)
serr = p.communicate(node['uml'].encode('utf-8'))[1]
if p.returncode != 0:
raise PlantUmlError('error while running plantuml\n\n' + serr)
raise PlantUmlError('error while running plantuml\n\n%s' % serr)
return refname, outfname
finally:
f.close()

def _get_png_tag(self, fnames, alt):
def _get_png_tag(self, fnames, node):
refname, _outfname = fnames['png']
return ('<img src="%s" alt="%s" />\n'
% (self.encode(refname), self.encode(alt)))
alt = node.get('alt', node['uml'])

# mimic StandaloneHTMLBuilder.post_process_images(). maybe we should
# process images prior to html_vist.
scale_keys = ('scale', 'width', 'height')
if all(key not in node for key in scale_keys) or Image is None:
return ('<img src="%s" alt="%s" />\n'
% (self.encode(refname), self.encode(alt)))

# Get sizes from the rendered image (defaults)
im = Image.open(_outfname)
im.load()
(fw, fh) = im.size

# Regex to get value and units
vu = re.compile(r"(?P<value>\d+)\s*(?P<units>[a-zA-Z%]+)?")

# Width
if 'width' in node:
m = vu.match(node['width'])
if not m:
raise PlantUmlError('Invalid width')
else:
m = m.groupdict()
w = int(m['value'])
wu = m['units'] if m['units'] else 'px'
else:
w = fw
wu = 'px'

# Height
if 'height' in node:
m = vu.match(node['height'])
if not m:
raise PlantUmlError('Invalid height')
else:
m = m.groupdict()
h = int(m['value'])
hu = m['units'] if m['units'] else 'px'
else:
h = fh
hu = 'px'

# Scale
if 'scale' not in node:
node['scale'] = 100

return ('<a href="%s"><img src="%s" alt="%s" width="%s%s" height="%s%s"/>'
'</a>\n'
% (self.encode(refname),
self.encode(refname),
self.encode(alt),
self.encode(w * node['scale'] / 100),
self.encode(wu),
self.encode(h * node['scale'] / 100),
self.encode(hu)))

def _get_svg_style(fname):
f = open(fname)
Expand All @@ -122,14 +248,14 @@ def _get_svg_style(fname):
return
return m.group(1)

def _get_svg_tag(self, fnames, alt):
def _get_svg_tag(self, fnames, node):
refname, outfname = fnames['svg']
return '\n'.join([
# copy width/height style from <svg> tag, so that <object> area
# has enough space.
'<object data="%s" type="image/svg+xml" style="%s">' % (
self.encode(refname), _get_svg_style(outfname) or ''),
_get_png_tag(self, fnames, alt),
_get_png_tag(self, fnames, node),
'</object>'])

_KNOWN_HTML_FORMATS = {
Expand All @@ -154,21 +280,15 @@ def html_visit_plantuml(self, node):
raise nodes.SkipNode

self.body.append(self.starttag(node, 'p', CLASS='plantuml'))
self.body.append(gettag(self, fnames, alt=node['alt'] or node['uml']))
self.body.append(gettag(self, fnames, node))
self.body.append('</p>\n')
raise nodes.SkipNode

def _convert_eps_to_pdf(self, refname, fname):
try:
is_string = isinstance(self.builder.config.plantuml_epstopdf, basestring)
except NameError:
# python 3
is_string = isinstance(self.builder.config.plantuml_epstopdf, str)

if is_string:
args = shlex.split(self.builder.config.plantuml_epstopdf)
else:
if isinstance(self.builder.config.plantuml_epstopdf, (tuple, list)):
args = list(self.builder.config.plantuml_epstopdf)
else:
args = shlex.split(self.builder.config.plantuml_epstopdf)
args.append(fname)
try:
try:
Expand Down Expand Up @@ -210,8 +330,16 @@ def latex_visit_plantuml(self, node):
except PlantUmlError as err:
self.builder.warn(str(err))
raise nodes.SkipNode
self.body.append('\n\\includegraphics{%s}\n' % self.encode(refname))
raise nodes.SkipNode

# put node representing rendered image
img_node = nodes.image(uri=refname, **node.attributes)
img_node.delattr('uml')
if not img_node.hasattr('alt'):
img_node['alt'] = node['uml']
node.append(img_node)

def latex_depart_plantuml(self, node):
pass

def pdf_visit_plantuml(self, node):
try:
Expand All @@ -220,13 +348,13 @@ def pdf_visit_plantuml(self, node):
except PlantUmlError as err:
self.builder.warn(str(err))
raise nodes.SkipNode
rep = nodes.image(uri=outfname, alt=node['alt'] or node['uml'])
rep = nodes.image(uri=outfname, alt=node.get('alt', node['uml']))
node.parent.replace(node, rep)

def setup(app):
app.add_node(plantuml,
html=(html_visit_plantuml, None),
latex=(latex_visit_plantuml, None))
latex=(latex_visit_plantuml, latex_depart_plantuml))
app.add_directive('uml', UmlDirective)
app.add_config_value('plantuml', 'plantuml', 'html')
app.add_config_value('plantuml_output_format', 'png', 'html')
Expand All @@ -237,3 +365,5 @@ def setup(app):
if 'rst2pdf.pdfbuilder' in app.config.extensions:
from rst2pdf.pdfbuilder import PDFTranslator as translator
setattr(translator, 'visit_' + plantuml.__name__, pdf_visit_plantuml)

return {'parallel_read_safe': True}
13 changes: 0 additions & 13 deletions doc/extension/sphinxcontrib/__init__.py

This file was deleted.

0 comments on commit e4fcdef

Please sign in to comment.