|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +""" |
| 3 | + sphinx.ext.ditaa |
| 4 | + ~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 5 | + Allow ditaa-formatted graphs to by included in Sphinx-generated |
| 6 | + documents inline. |
| 7 | + :copyright: Copyright 2017 by Yongping Guo |
| 8 | + :license: BSD, see LICENSE for details. |
| 9 | +""" |
| 10 | + |
| 11 | +import re, os |
| 12 | +import codecs |
| 13 | +import posixpath |
| 14 | +from os import path |
| 15 | +from math import ceil |
| 16 | +from subprocess import Popen, PIPE |
| 17 | +try: |
| 18 | + from hashlib import sha1 as sha |
| 19 | +except ImportError: |
| 20 | + from sha import sha |
| 21 | + |
| 22 | +from docutils import nodes |
| 23 | +from docutils.parsers.rst import directives |
| 24 | + |
| 25 | +from sphinx.errors import SphinxError |
| 26 | +from sphinx.util.osutil import ensuredir, ENOENT, EPIPE |
| 27 | +from sphinx.util import relative_uri |
| 28 | +#from sphinx.util.compat import Directive |
| 29 | +from docutils.parsers.rst import Directive |
| 30 | + |
| 31 | +mapname_re = re.compile(r'<map id="(.*?)"') |
| 32 | +svg_dim_re = re.compile(r'<svg\swidth="(\d+)pt"\sheight="(\d+)pt"', re.M) |
| 33 | + |
| 34 | +class DitaaError(SphinxError): |
| 35 | + category = 'Ditaa error' |
| 36 | + |
| 37 | + |
| 38 | +class ditaa(nodes.General, nodes.Element): |
| 39 | + pass |
| 40 | + |
| 41 | +class Ditaa(directives.images.Image): |
| 42 | + """ |
| 43 | + Directive to insert ditaa markup. |
| 44 | + """ |
| 45 | + has_content = True |
| 46 | + required_arguments = 0 |
| 47 | + optional_arguments = 0 |
| 48 | + final_argument_whitespace = False |
| 49 | + option_spec = { |
| 50 | + # image parameter |
| 51 | + 'name': directives.unchanged, |
| 52 | + 'class': directives.unchanged, |
| 53 | + 'alt': directives.unchanged, |
| 54 | + 'title': directives.unchanged, |
| 55 | + 'height': directives.unchanged, |
| 56 | + 'width': directives.unchanged, |
| 57 | + 'scale': directives.unchanged, |
| 58 | + 'align': directives.unchanged, |
| 59 | + 'target': directives.unchanged, |
| 60 | + 'inline': directives.unchanged, |
| 61 | + # ditaa parameter |
| 62 | + '--no-antialias': directives.flag, |
| 63 | + '--background': directives.unchanged, |
| 64 | + '--no-antialias': directives.flag, |
| 65 | + '--no-separation': directives.flag, |
| 66 | + '--encoding': directives.unchanged, |
| 67 | + '--html': directives.flag, |
| 68 | + '--overwrite': directives.flag, |
| 69 | + '--round-corners': directives.flag, |
| 70 | + '--no-shadows': directives.flag, |
| 71 | + '--scale': directives.unchanged, |
| 72 | + '--transparent': directives.flag, |
| 73 | + '--tabs': directives.unchanged, |
| 74 | + '--fixed-slope': directives.flag, |
| 75 | + } |
| 76 | + |
| 77 | + def run(self): |
| 78 | + if self.arguments: |
| 79 | + print self.arguments |
| 80 | + document = self.state.document |
| 81 | + if self.content: |
| 82 | + return [document.reporter.warning( |
| 83 | + 'Ditaa directive cannot have both content and ' |
| 84 | + 'a filename argument', line=self.lineno)] |
| 85 | + env = self.state.document.settings.env |
| 86 | + rel_filename, filename = env.relfn2path(self.arguments[0]) |
| 87 | + env.note_dependency(rel_filename) |
| 88 | + try: |
| 89 | + fp = codecs.open(filename, 'r', 'utf-8') |
| 90 | + try: |
| 91 | + dotcode = fp.read() |
| 92 | + finally: |
| 93 | + fp.close() |
| 94 | + except (IOError, OSError): |
| 95 | + return [document.reporter.warning( |
| 96 | + 'External Ditaa file %r not found or reading ' |
| 97 | + 'it failed' % filename, line=self.lineno)] |
| 98 | + else: |
| 99 | + dotcode = '\n'.join(self.content) |
| 100 | + if not dotcode.strip(): |
| 101 | + return [self.state_machine.reporter.warning( |
| 102 | + 'Ignoring "ditaa" directive without content.', |
| 103 | + line=self.lineno)] |
| 104 | + node = ditaa() |
| 105 | + node['code'] = dotcode |
| 106 | + node['options'] = [] |
| 107 | + node['img_options'] = {} |
| 108 | + for k,v in self.options.items(): |
| 109 | + if k[:2] == '--': |
| 110 | + node['options'].append(k) |
| 111 | + if v is not None: |
| 112 | + node['options'].append(v) |
| 113 | + else: |
| 114 | + node['img_options'][k] = v |
| 115 | + #if v is not None: |
| 116 | + # node['options'].append("%s" %(v)) |
| 117 | + |
| 118 | + return [node] |
| 119 | + |
| 120 | +def render_ditaa(app, code, options, format, prefix='ditaa'): |
| 121 | + """Render ditaa code into a PNG output file.""" |
| 122 | + hashkey = code.encode('utf-8') + str(options) + \ |
| 123 | + str(app.builder.config.ditaa) + \ |
| 124 | + str(app.builder.config.ditaa_args) |
| 125 | + infname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), "ditaa") |
| 126 | + outfname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), "png") |
| 127 | + |
| 128 | + rel_imgpath = (format == "html") and relative_uri(app.builder.env.docname, app.builder.imagedir) or '' |
| 129 | + infullfn = path.join(app.builder.outdir, app.builder.imagedir, infname) |
| 130 | + outrelfn = posixpath.join(relative_uri(app.builder.env.docname, app.builder.imagedir), outfname) |
| 131 | + outfullfn = path.join(app.builder.outdir, app.builder.imagedir, outfname) |
| 132 | + #inrelfn = posixpath.join(relative_uri(app.builder.env.docname, app.builder.imagedir), infname) |
| 133 | + |
| 134 | + if path.isfile(outfullfn): |
| 135 | + return outrelfn, outfullfn |
| 136 | + |
| 137 | + ensuredir(path.dirname(outfullfn)) |
| 138 | + # ditaa expects UTF-8 by default |
| 139 | + if isinstance(code, unicode): code = code.encode('utf-8') |
| 140 | + |
| 141 | + ditaa_args = [app.builder.config.ditaa] |
| 142 | + ditaa_args.extend(app.builder.config.ditaa_args) |
| 143 | + ditaa_args.extend(options) |
| 144 | + ditaa_args.extend( [infname, outfname] ) # use relative path |
| 145 | + f = open(infullfn, 'w') |
| 146 | + f.write(code.encode('utf-8')) |
| 147 | + f.close() |
| 148 | + currpath = os.getcwd() |
| 149 | + os.chdir(path.join(app.builder.outdir, app.builder.imagedir)) |
| 150 | + |
| 151 | + try: |
| 152 | + if app.builder.config.ditaa_log_enable: |
| 153 | + print "rending %s" %(outfullfn) |
| 154 | + #app.builder.warn(ditaa_args) |
| 155 | + p = Popen(ditaa_args, stdout=PIPE, stdin=PIPE, stderr=PIPE) |
| 156 | + except OSError, err: |
| 157 | + if err.errno != ENOENT: # No such file or directory |
| 158 | + raise |
| 159 | + app.builder.warn('ditaa command %r cannot be run (needed for ditaa ' |
| 160 | + 'output), check the ditaa setting' % |
| 161 | + app.builder.config.ditaa) |
| 162 | + app.builder._ditaa_warned_dot = True |
| 163 | + os.chdir(currpath) |
| 164 | + return None, None |
| 165 | + |
| 166 | + os.chdir(currpath) |
| 167 | + wentWrong = False |
| 168 | + try: |
| 169 | + # Ditaa may close standard input when an error occurs, |
| 170 | + # resulting in a broken pipe on communicate() |
| 171 | + stdout, stderr = p.communicate(code) |
| 172 | + except OSError, err: |
| 173 | + if err.errno != EPIPE: |
| 174 | + raise |
| 175 | + wentWrong = True |
| 176 | + except IOError, err: |
| 177 | + if err.errno != EINVAL: |
| 178 | + raise |
| 179 | + wentWrong = True |
| 180 | + if wentWrong: |
| 181 | + # in this case, read the standard output and standard error streams |
| 182 | + # directly, to get the error message(s) |
| 183 | + stdout, stderr = p.stdout.read(), p.stderr.read() |
| 184 | + p.wait() |
| 185 | + if p.returncode != 0: |
| 186 | + raise DitaaError('ditaa exited with error:\n[stderr]\n%s\n' |
| 187 | + '[stdout]\n%s' % (stderr, stdout)) |
| 188 | + return outrelfn, outfullfn |
| 189 | + |
| 190 | +def on_doctree_resolved(app, doctree): |
| 191 | + #print "app.builder.env.docname: ", app.builder.env.docname |
| 192 | + for node in doctree.traverse(ditaa): |
| 193 | + try: |
| 194 | + # Generate the output png files |
| 195 | + relfn, outfn = render_ditaa(app, node['code'], node['options'], app.builder.format, "ditaa") |
| 196 | + image = nodes.image(uri=relfn, candidates={'*': outfn}, **node['img_options']) |
| 197 | + #for (k, v) in options.items(): |
| 198 | + # image[k] = v |
| 199 | + node.parent.replace(node, image) |
| 200 | + except DitaaError, exc: |
| 201 | + node.parent.remove(node) |
| 202 | + raise nodes.SkipNode |
| 203 | + |
| 204 | +def setup(app): |
| 205 | + app.add_node(ditaa, html=(None, None), |
| 206 | + #latex=(latex_visit_ditaa, None), |
| 207 | + ) |
| 208 | + app.add_directive('ditaa', Ditaa) |
| 209 | + app.add_config_value('ditaa', 'ditaa', 'html') |
| 210 | + app.add_config_value('ditaa_args', [], 'html') |
| 211 | + app.add_config_value('ditaa_log_enable', True, 'html') |
| 212 | + app.connect('doctree-read', on_doctree_resolved) |
0 commit comments