# -*- coding: utf-8 -*- """ sphinx.ext.graphviz ~~~~~~~~~~~~~~~~~~~ Allow graphviz-formatted graphs to be included in Sphinx-generated documents inline. :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re import codecs import posixpath from os import path from subprocess import Popen, PIPE from hashlib import sha1 from six import text_type from docutils import nodes from docutils.parsers.rst import Directive, directives from docutils.statemachine import ViewList import sphinx from sphinx.errors import SphinxError from sphinx.locale import _, __ from sphinx.util import logging from sphinx.util.i18n import search_image_for_language from sphinx.util.osutil import ensuredir, ENOENT, EPIPE, EINVAL if False: # For type annotation from typing import Any, Dict, List, Tuple # NOQA from sphinx.application import Sphinx # NOQA logger = logging.getLogger(__name__) mapname_re = re.compile(r' nodes.figure figure_node = nodes.figure('', node) if 'align' in node: figure_node['align'] = node.attributes.pop('align') parsed = nodes.Element() directive.state.nested_parse(ViewList([caption], source=''), directive.content_offset, parsed) caption_node = nodes.caption(parsed[0].rawsource, '', *parsed[0].children) caption_node.source = parsed[0].source caption_node.line = parsed[0].line figure_node += caption_node return figure_node def align_spec(argument): # type: (Any) -> bool return directives.choice(argument, ('left', 'center', 'right')) class Graphviz(Directive): """ Directive to insert arbitrary dot markup. """ has_content = True required_arguments = 0 optional_arguments = 1 final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, 'align': align_spec, 'caption': directives.unchanged, 'graphviz_dot': directives.unchanged, 'name': directives.unchanged, } def run(self): # type: () -> List[nodes.Node] if self.arguments: document = self.state.document if self.content: return [document.reporter.warning( __('Graphviz directive cannot have both content and ' 'a filename argument'), line=self.lineno)] env = self.state.document.settings.env argument = search_image_for_language(self.arguments[0], env) rel_filename, filename = env.relfn2path(argument) env.note_dependency(rel_filename) try: with codecs.open(filename, 'r', 'utf-8') as fp: dotcode = fp.read() except (IOError, OSError): return [document.reporter.warning( __('External Graphviz file %r not found or reading ' 'it failed') % filename, line=self.lineno)] else: dotcode = '\n'.join(self.content) if not dotcode.strip(): return [self.state_machine.reporter.warning( __('Ignoring "graphviz" directive without content.'), line=self.lineno)] node = graphviz() node['code'] = dotcode node['options'] = {} if 'graphviz_dot' in self.options: node['options']['graphviz_dot'] = self.options['graphviz_dot'] if 'alt' in self.options: node['alt'] = self.options['alt'] if 'align' in self.options: node['align'] = self.options['align'] caption = self.options.get('caption') if caption: node = figure_wrapper(self, node, caption) self.add_name(node) return [node] class GraphvizSimple(Directive): """ Directive to insert arbitrary dot markup. """ has_content = True required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, 'align': align_spec, 'caption': directives.unchanged, 'graphviz_dot': directives.unchanged, 'name': directives.unchanged, } def run(self): # type: () -> List[nodes.Node] node = graphviz() node['code'] = '%s %s {\n%s\n}\n' % \ (self.name, self.arguments[0], '\n'.join(self.content)) node['options'] = {} if 'graphviz_dot' in self.options: node['options']['graphviz_dot'] = self.options['graphviz_dot'] if 'alt' in self.options: node['alt'] = self.options['alt'] if 'align' in self.options: node['align'] = self.options['align'] caption = self.options.get('caption') if caption: node = figure_wrapper(self, node, caption) self.add_name(node) return [node] def render_dot(self, code, options, format, prefix='graphviz'): # type: (nodes.NodeVisitor, unicode, Dict, unicode, unicode) -> Tuple[unicode, unicode] """Render graphviz code into a PNG or PDF output file.""" graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot) hashkey = (code + str(options) + str(graphviz_dot) + str(self.builder.config.graphviz_dot_args)).encode('utf-8') fname = '%s-%s.%s' % (prefix, sha1(hashkey).hexdigest(), format) relfn = posixpath.join(self.builder.imgpath, fname) outfn = path.join(self.builder.outdir, self.builder.imagedir, fname) if path.isfile(outfn): return relfn, outfn if (hasattr(self.builder, '_graphviz_warned_dot') and self.builder._graphviz_warned_dot.get(graphviz_dot)): return None, None ensuredir(path.dirname(outfn)) # graphviz expects UTF-8 by default if isinstance(code, text_type): code = code.encode('utf-8') dot_args = [graphviz_dot] dot_args.extend(self.builder.config.graphviz_dot_args) 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 logger.warning(__('dot command %r cannot be run (needed for graphviz ' 'output), check the graphviz_dot setting'), graphviz_dot) if not hasattr(self.builder, '_graphviz_warned_dot'): self.builder._graphviz_warned_dot = {} self.builder._graphviz_warned_dot[graphviz_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)) 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_html(self, node, code, options, prefix='graphviz', imgcls=None, alt=None): # type: (nodes.NodeVisitor, graphviz, unicode, Dict, unicode, unicode, unicode) -> Tuple[unicode, unicode] # NOQA format = self.builder.config.graphviz_output_format try: if format not in ('png', '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) except GraphvizError as exc: logger.warning('dot code %r: ' % code + str(exc)) raise nodes.SkipNode if fname is None: self.body.append(self.encode(code)) else: if alt is None: alt = node.get('alt', self.encode(code).strip()) imgcss = imgcls and 'class="%s"' % imgcls or '' if 'align' in node: self.body.append('
' % (node['align'], node['align'])) if format == 'svg': svgtag = '''

%s

\n''' % (fname, alt) self.body.append(svgtag) else: with open(outfn + '.map', 'rb') as mapfile: imgmap = mapfile.readlines() if len(imgmap) == 2: # nothing in image map (the lines are and ) self.body.append('%s\n' % (fname, alt, imgcss)) else: # has a map: get the name of the map and connect the parts mapname = mapname_re.match(imgmap[0].decode('utf-8')).group(1) # type: ignore self.body.append('%s\n' % (fname, alt, mapname, imgcss)) self.body.extend([item.decode('utf-8') for item in imgmap]) if 'align' in node: self.body.append('
\n') raise nodes.SkipNode def html_visit_graphviz(self, node): # type: (nodes.NodeVisitor, graphviz) -> None render_dot_html(self, node, node['code'], node['options']) def render_dot_latex(self, node, code, options, prefix='graphviz'): # type: (nodes.NodeVisitor, graphviz, unicode, Dict, unicode) -> None try: fname, outfn = render_dot(self, code, options, 'pdf', prefix) except GraphvizError as exc: logger.warning('dot code %r: ' % code + str(exc)) raise nodes.SkipNode is_inline = self.is_inline(node) if not is_inline: pre = '' post = '' if 'align' in node: if node['align'] == 'left': pre = '{' post = r'\hspace*{\fill}}' elif node['align'] == 'right': pre = r'{\hspace*{\fill}' post = '}' elif node['align'] == 'center': pre = r'{\hfill' post = r'\hspace*{\fill}}' self.body.append('\n%s' % pre) self.body.append(r'\includegraphics{%s}' % fname) if not is_inline: self.body.append('%s\n' % post) raise nodes.SkipNode def latex_visit_graphviz(self, node): # type: (nodes.NodeVisitor, graphviz) -> None render_dot_latex(self, node, node['code'], node['options']) def render_dot_texinfo(self, node, code, options, prefix='graphviz'): # type: (nodes.NodeVisitor, graphviz, unicode, Dict, unicode) -> None try: fname, outfn = render_dot(self, code, options, 'png', prefix) except GraphvizError as exc: logger.warning('dot code %r: ' % code + str(exc)) raise nodes.SkipNode if fname is not None: self.body.append('@image{%s,,,[graphviz],png}\n' % fname[:-4]) raise nodes.SkipNode def texinfo_visit_graphviz(self, node): # type: (nodes.NodeVisitor, graphviz) -> None render_dot_texinfo(self, node, node['code'], node['options']) def text_visit_graphviz(self, node): # type: (nodes.NodeVisitor, graphviz) -> None if 'alt' in node.attributes: self.add_text(_('[graph: %s]') % node['alt']) else: self.add_text(_('[graph]')) raise nodes.SkipNode def man_visit_graphviz(self, node): # type: (nodes.NodeVisitor, graphviz) -> None if 'alt' in node.attributes: self.body.append(_('[graph: %s]') % node['alt']) else: self.body.append(_('[graph]')) raise nodes.SkipNode def setup(app): # type: (Sphinx) -> Dict[unicode, Any] app.add_node(graphviz, html=(html_visit_graphviz, None), latex=(latex_visit_graphviz, None), texinfo=(texinfo_visit_graphviz, None), text=(text_visit_graphviz, None), man=(man_visit_graphviz, None)) app.add_directive('graphviz', Graphviz) app.add_directive('graph', GraphvizSimple) app.add_directive('digraph', GraphvizSimple) app.add_config_value('graphviz_dot', 'dot', 'html') app.add_config_value('graphviz_dot_args', [], 'html') app.add_config_value('graphviz_output_format', 'png', 'html') return {'version': sphinx.__display_version__, 'parallel_read_safe': True}