# -*- coding: utf-8 -*- """ symbolator_sphinx ~~~~~~~~~~~~~~~~~ Allow symbolator-formatted graphs to be included in Sphinx-generated documents inline. Derived from sphinx.ext.graphviz. :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE.Sphinx 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__) class SymbolatorError(SphinxError): category = 'Symbolator error' class symbolator(nodes.General, nodes.Inline, nodes.Element): '''Base class for symbolator node''' pass def figure_wrapper(directive, node, caption): # type: (Directive, nodes.Node, unicode) -> 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 Symbolator(Directive): """ Directive to insert HDL symbol. """ has_content = True required_arguments = 0 optional_arguments = 1 final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, 'align': align_spec, 'caption': directives.unchanged, 'symbolator_cmd': 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( __('Symbolator 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: symbolator_code = fp.read() except (IOError, OSError): return [document.reporter.warning( __('External Symbolator file %r not found or reading ' 'it failed') % filename, line=self.lineno)] else: symbolator_code = '\n'.join(self.content) if not symbolator_code.strip(): return [self.state_machine.reporter.warning( __('Ignoring "symbolator" directive without content.'), line=self.lineno)] node = symbolator() node['code'] = symbolator_code node['options'] = {} if 'symbolator_cmd' in self.options: node['options']['symbolator_cmd'] = self.options['symbolator_cmd'] if 'alt' in self.options: node['alt'] = self.options['alt'] if 'align' in self.options: node['align'] = self.options['align'] if 'name' in self.options: node['options']['name'] = self.options['name'] caption = self.options.get('caption') if caption: node = figure_wrapper(self, node, caption) self.add_name(node) return [node] def render_symbol(self, code, options, format, prefix='symbol'): # type: (nodes.NodeVisitor, unicode, Dict, unicode, unicode) -> Tuple[unicode, unicode] """Render symbolator code into a PNG or SVG output file.""" symbolator_cmd = options.get('symbolator_cmd', self.builder.config.symbolator_cmd) hashkey = (code + str(options) + str(symbolator_cmd) + str(self.builder.config.symbolator_cmd_args)).encode('utf-8') # Use name option if present otherwise fallback onto SHA-1 hash name = options.get('name', sha1(hashkey).hexdigest()) fname = '%s-%s.%s' % (prefix, name, 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, '_symbolator_warned_cmd') and self.builder._symbolator_warned_cmd.get(symbolator_cmd)): return None, None ensuredir(path.dirname(outfn)) # Symbolator expects UTF-8 by default if isinstance(code, text_type): code = code.encode('utf-8') cmd_args = [symbolator_cmd] cmd_args.extend(self.builder.config.symbolator_cmd_args) cmd_args.extend(['-i', '-', '-f', format, '-o', outfn]) try: p = Popen(cmd_args, stdout=PIPE, stdin=PIPE, stderr=PIPE) except OSError as err: if err.errno != ENOENT: # No such file or directory raise logger.warning('symbolator command %r cannot be run (needed for symbolator ' 'output), check the symbolator_cmd setting', symbolator_cmd) if not hasattr(self.builder, '_symbolator_warned_cmd'): self.builder._symbolator_warned_cmd = {} self.builder._symbolator_warned_cmd[symbolator_cmd] = True return None, None try: # Symbolator 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 SymbolatorError('symbolator exited with error:\n[stderr]\n%s\n' '[stdout]\n%s' % (stderr, stdout)) if not path.isfile(outfn): raise SymbolatorError('symbolator did not produce an output file:\n[stderr]\n%s\n' '[stdout]\n%s' % (stderr, stdout)) return relfn, outfn def render_symbol_html(self, node, code, options, prefix='symbol', imgcls=None, alt=None): # type: (nodes.NodeVisitor, symbolator, unicode, Dict, unicode, unicode, unicode) -> Tuple[unicode, unicode] # NOQA format = self.builder.config.symbolator_output_format try: if format not in ('png', 'svg'): raise SymbolatorError("symbolator_output_format must be one of 'png', " "'svg', but is %r" % format) fname, outfn = render_symbol(self, code, options, format, prefix) except SymbolatorError as exc: logger.warning('symbolator 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 format == 'svg': svgtag = '''<object data="%s" type="image/svg+xml"> <p class="warning">%s</p></object>\n''' % (fname, alt) self.body.append(svgtag) else: if 'align' in node: self.body.append('<div align="%s" class="align-%s">' % (node['align'], node['align'])) self.body.append('<img src="%s" alt="%s" %s/>\n' % (fname, alt, imgcss)) if 'align' in node: self.body.append('</div>\n') raise nodes.SkipNode def html_visit_symbolator(self, node): # type: (nodes.NodeVisitor, symbolator) -> None render_symbol_html(self, node, node['code'], node['options']) def render_symbol_latex(self, node, code, options, prefix='symbol'): # type: (nodes.NodeVisitor, symbolator, unicode, Dict, unicode) -> None try: fname, outfn = render_symbol(self, code, options, 'pdf', prefix) except SymbolatorError as exc: logger.warning('symbolator code %r: ' % code + str(exc)) raise nodes.SkipNode is_inline = self.is_inline(node) if is_inline: para_separator = '' else: para_separator = '\n' if fname is not None: post = None # type: unicode if not is_inline and 'align' in node: if node['align'] == 'left': self.body.append('{') post = '\\hspace*{\\fill}}' elif node['align'] == 'right': self.body.append('{\\hspace*{\\fill}') post = '}' self.body.append('%s\\includegraphics{%s}%s' % (para_separator, fname, para_separator)) if post: self.body.append(post) raise nodes.SkipNode def latex_visit_symbolator(self, node): # type: (nodes.NodeVisitor, symbolator) -> None render_symbol_latex(self, node, node['code'], node['options']) def render_symbol_texinfo(self, node, code, options, prefix='symbol'): # type: (nodes.NodeVisitor, symbolator, unicode, Dict, unicode) -> None try: fname, outfn = render_symbol(self, code, options, 'png', prefix) except SymbolatorError as exc: logger.warning('symbolator code %r: ' % code + str(exc)) raise nodes.SkipNode if fname is not None: self.body.append('@image{%s,,,[symbolator],png}\n' % fname[:-4]) raise nodes.SkipNode def texinfo_visit_symbolator(self, node): # type: (nodes.NodeVisitor, symbolator) -> None render_symbol_texinfo(self, node, node['code'], node['options']) def text_visit_symbolator(self, node): # type: (nodes.NodeVisitor, symbolator) -> None if 'alt' in node.attributes: self.add_text(_('[symbol: %s]') % node['alt']) else: self.add_text(_('[symbol]')) raise nodes.SkipNode def man_visit_symbolator(self, node): # type: (nodes.NodeVisitor, symbolator) -> None if 'alt' in node.attributes: self.body.append(_('[symbol: %s]') % node['alt']) else: self.body.append(_('[symbol]')) raise nodes.SkipNode def setup(app): # type: (Sphinx) -> Dict[unicode, Any] app.add_node(symbolator, html=(html_visit_symbolator, None), latex=(latex_visit_symbolator, None), texinfo=(texinfo_visit_symbolator, None), text=(text_visit_symbolator, None), man=(man_visit_symbolator, None)) app.add_directive('symbolator', Symbolator) app.add_config_value('symbolator_cmd', 'symbolator', 'html') app.add_config_value('symbolator_cmd_args', ['-t'], 'html') app.add_config_value('symbolator_output_format', 'svg', 'html') return {'version': '1.0', 'parallel_read_safe': True}