Skip to content

Commit

Permalink
Add option to include graphviz output as inline svg
Browse files Browse the repository at this point in the history
With graphviz_output_format == 'inline_svg', graphviz output is included as
HTML svg element. This fixes SVG hyperlinks and makes HTML document stylesheets
apply to SVG contents.

closes sphinx-doc#967
  • Loading branch information
borman committed Aug 24, 2015
1 parent 78ac972 commit e303fd6
Showing 1 changed file with 63 additions and 33 deletions.
96 changes: 63 additions & 33 deletions sphinx/ext/graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class GraphvizError(SphinxError):
category = 'Graphviz error'


class GraphvizUnavailableError(GraphvizError):
pass


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

Expand Down Expand Up @@ -138,6 +142,38 @@ def run(self):
return [node]


def run_dot(dot_args, code):
"""Execute graphviz dot subprocess."""
# graphviz expects UTF-8 by default
if isinstance(code, text_type):
code = code.encode('utf-8')

try:
p = Popen(dot_args, stdout=PIPE, stdin=PIPE, stderr=PIPE)
except OSError as err:
if err.errno != ENOENT: # No such file or directory
raise
raise GraphvizUnavailableError('dot command %r cannot be run '
'(needed for graphviz output), '
'check the graphviz_dot setting'
% dot_args)
try:
# Graphviz may close standard input when an error occurs,
# resulting in a broken pipe on communicate()
stdout, stderr = p.communicate(code)
except (OSError, IOError) as err:
if err.errno not in (EPIPE, EINVAL):
raise
# in this case, read the standard output and standard error streams
# directly, to get the error message(s)
stdout, stderr = p.stdout.read(), p.stderr.read()
p.wait()
if p.returncode != 0:
raise GraphvizError('dot exited with error:\n[stderr]\n%s\n'
'[stdout]\n%s' % (stderr, stdout))
return stdout, stderr


def render_dot(self, code, options, format, prefix='graphviz'):
"""Render graphviz code into a PNG or PDF output file."""
hashkey = (code + str(options) +
Expand All @@ -157,54 +193,46 @@ def render_dot(self, code, options, format, prefix='graphviz'):

ensuredir(path.dirname(outfn))

# graphviz expects UTF-8 by default
if isinstance(code, text_type):
code = code.encode('utf-8')

dot_args = [self.builder.config.graphviz_dot]
dot_args.extend(self.builder.config.graphviz_dot_args)
dot_args.extend(options)
dot_args.extend(['-T' + format, '-o' + outfn])
if format == 'png':
dot_args.extend(['-Tcmapx', '-o%s.map' % outfn])
try:
p = Popen(dot_args, stdout=PIPE, stdin=PIPE, stderr=PIPE)
except OSError as err:
if err.errno != ENOENT: # No such file or directory
raise
self.builder.warn('dot command %r cannot be run (needed for graphviz '
'output), check the graphviz_dot setting' %
self.builder.config.graphviz_dot)
self.builder._graphviz_warned_dot = True
return None, None
try:
# Graphviz may close standard input when an error occurs,
# resulting in a broken pipe on communicate()
stdout, stderr = p.communicate(code)
except (OSError, IOError) as err:
if err.errno not in (EPIPE, EINVAL):
raise
# in this case, read the standard output and standard error streams
# directly, to get the error message(s)
stdout, stderr = p.stdout.read(), p.stderr.read()
p.wait()
if p.returncode != 0:
raise GraphvizError('dot exited with error:\n[stderr]\n%s\n'
'[stdout]\n%s' % (stderr, stdout))
stdout, stderr = run_dot(dot_args, code)
if not path.isfile(outfn):
raise GraphvizError('dot did not produce an output file:\n[stderr]\n%s\n'
'[stdout]\n%s' % (stderr, stdout))
return relfn, outfn


def render_dot_inline_svg(self, code, options):
"""Render graphviz code into inline SVG code."""
dot_args = [self.builder.config.graphviz_dot]
dot_args.extend(self.builder.config.graphviz_dot_args)
dot_args.extend(options)
dot_args.append('-Tsvg')

stdout, _ = run_dot(dot_args, code)
return stdout.decode('utf-8')


def render_dot_html(self, node, code, options, prefix='graphviz',
imgcls=None, alt=None):
format = self.builder.config.graphviz_output_format
graphviz_unavailable = False
try:
if format not in ('png', 'svg'):
if format not in ('png', 'svg', 'inline_svg'):
raise GraphvizError("graphviz_output_format must be one of 'png', "
"'svg', but is %r" % format)
fname, outfn = render_dot(self, code, options, format, prefix)
"'svg', 'inline_svg', but is %r" % format)
if format == 'inline_svg':
inline_svg = render_dot_inline_svg(self, code, options)
else:
fname, outfn = render_dot(self, code, options, format, prefix)
except GraphvizUnavailableError as exc:
self.builder.warn(str(exc))
self.builder._graphviz_warned_dot = True
graphviz_unavailable = True
except GraphvizError as exc:
self.builder.warn('dot code %r: ' % code + str(exc))
raise nodes.SkipNode
Expand All @@ -216,9 +244,11 @@ def render_dot_html(self, node, code, options, prefix='graphviz',
wrapper = 'p'

self.body.append(self.starttag(node, wrapper, CLASS='graphviz'))
if fname is None:
if graphviz_unavailable:
self.body.append(self.encode(code))
else:
elif format == 'inline_svg':
self.body.append(inline_svg)
else: # format in ('png, 'svg')
if alt is None:
alt = node.get('alt', self.encode(code).strip())
imgcss = imgcls and 'class="%s"' % imgcls or ''
Expand Down

0 comments on commit e303fd6

Please sign in to comment.