|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +""" |
| 3 | + sphinxcontrib.aafig |
| 4 | + ~~~~~~~~~~~~~~~~~~~ |
| 5 | +
|
| 6 | + Allow embeded ASCII art to be rendered as nice looking images |
| 7 | + using the aafigure reStructuredText extension. |
| 8 | +
|
| 9 | + See the README file for details. |
| 10 | +
|
| 11 | + :author: Leandro Lucarella <[email protected]> |
| 12 | + :license: BOLA, see LICENSE for details |
| 13 | +""" |
| 14 | + |
| 15 | +import posixpath |
| 16 | +from os import path |
| 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.directives import images, nonnegative_int, flag |
| 24 | + |
| 25 | +from sphinx.errors import SphinxError |
| 26 | +from sphinx.util import ensuredir, relative_uri |
| 27 | +from sphinx.util.compat import Directive |
| 28 | + |
| 29 | +try: |
| 30 | + import aafigure |
| 31 | +except ImportError: |
| 32 | + aafigure = None |
| 33 | + |
| 34 | + |
| 35 | +DEFAULT_FORMATS = dict(html='svg', latex='pdf', text=None) |
| 36 | + |
| 37 | + |
| 38 | +def merge_dict(dst, src): |
| 39 | + for (k, v) in src.items(): |
| 40 | + if k not in dst: |
| 41 | + dst[k] = v |
| 42 | + return dst |
| 43 | + |
| 44 | + |
| 45 | +def get_basename(text, options, prefix='aafig'): |
| 46 | + options = options.copy() |
| 47 | + if 'format' in options: |
| 48 | + del options['format'] |
| 49 | + hashkey = text.encode('utf-8') + str(options) |
| 50 | + id = sha(hashkey).hexdigest() |
| 51 | + return '%s-%s' % (prefix, id) |
| 52 | + |
| 53 | + |
| 54 | +class AafigError(SphinxError): |
| 55 | + category = 'aafig error' |
| 56 | + |
| 57 | + |
| 58 | +class AafigDirective(images.Image): |
| 59 | + """ |
| 60 | + Directive to insert an ASCII art figure to be rendered by aafigure. |
| 61 | + """ |
| 62 | + has_content = True |
| 63 | + required_arguments = 0 |
| 64 | + own_option_spec = dict( |
| 65 | + line_width = float, |
| 66 | + background = str, |
| 67 | + foreground = str, |
| 68 | + fill = str, |
| 69 | + aspect = nonnegative_int, |
| 70 | + textual = flag, |
| 71 | + proportional = flag, |
| 72 | + ) |
| 73 | + option_spec = images.Image.option_spec.copy() |
| 74 | + option_spec.update(own_option_spec) |
| 75 | + |
| 76 | + def run(self): |
| 77 | + aafig_options = dict() |
| 78 | + image_attrs = dict() |
| 79 | + own_options_keys = self.own_option_spec.keys() + ['scale'] |
| 80 | + for (k, v) in self.options.items(): |
| 81 | + if k in own_options_keys: |
| 82 | + # convert flags to booleans |
| 83 | + if v is None: |
| 84 | + v = True |
| 85 | + # convert percentage to float |
| 86 | + if k == 'scale' or k == 'aspect': |
| 87 | + v = float(v) / 100.0 |
| 88 | + aafig_options[k] = v |
| 89 | + del self.options[k] |
| 90 | + self.arguments = [''] |
| 91 | + (image_node,) = images.Image.run(self) |
| 92 | + if isinstance(image_node, nodes.system_message): |
| 93 | + return [image_node] |
| 94 | + text = '\n'.join(self.content) |
| 95 | + image_node.aafig = dict(options = aafig_options, text = text) |
| 96 | + return [image_node] |
| 97 | + |
| 98 | + |
| 99 | +def render_aafig_images(app, doctree): |
| 100 | + format_map = app.builder.config.aafig_format |
| 101 | + merge_dict(format_map, DEFAULT_FORMATS) |
| 102 | + if aafigure is None: |
| 103 | + app.builder.warn('aafigure module not installed, ASCII art images ' |
| 104 | + 'will be redered as literal text') |
| 105 | + for img in doctree.traverse(nodes.image): |
| 106 | + if not hasattr(img, 'aafig'): |
| 107 | + continue |
| 108 | + if aafigure is None: |
| 109 | + img.replace_self(nodes.literal_block(text, text)) |
| 110 | + continue |
| 111 | + options = img.aafig['options'] |
| 112 | + text = img.aafig['text'] |
| 113 | + format = app.builder.format |
| 114 | + merge_dict(options, app.builder.config.aafig_default_options) |
| 115 | + if format in format_map: |
| 116 | + options['format'] = format_map[format] |
| 117 | + else: |
| 118 | + app.builder.warn('unsupported builder format "%s", please ' |
| 119 | + 'add a custom entry in aafig_format config option ' |
| 120 | + 'for this builder' % format) |
| 121 | + img.replace_self(nodes.literal_block(text, text)) |
| 122 | + continue |
| 123 | + if options['format'] is None: |
| 124 | + img.replace_self(nodes.literal_block(text, text)) |
| 125 | + continue |
| 126 | + try: |
| 127 | + fname, outfn, id, extra = render_aafigure(app, text, options) |
| 128 | + except AafigError, exc: |
| 129 | + app.builder.warn('aafigure error: ' + str(exc)) |
| 130 | + img.replace_self(nodes.literal_block(text, text)) |
| 131 | + continue |
| 132 | + img['uri'] = fname |
| 133 | + # FIXME: find some way to avoid this hack in aafigure |
| 134 | + if extra: |
| 135 | + (width, height) = [x.split('"')[1] for x in extra.split()] |
| 136 | + if not img.has_key('width'): |
| 137 | + img['width'] = width |
| 138 | + if not img.has_key('height'): |
| 139 | + img['height'] = height |
| 140 | + |
| 141 | + |
| 142 | +def render_aafigure(app, text, options): |
| 143 | + """ |
| 144 | + Render an ASCII art figure into the requested format output file. |
| 145 | + """ |
| 146 | + |
| 147 | + if aafigure is None: |
| 148 | + raise AafigError('aafigure module not installed') |
| 149 | + |
| 150 | + fname = get_basename(text, options) |
| 151 | + fname = '%s.%s' % (get_basename(text, options), options['format']) |
| 152 | + if app.builder.format == 'html': |
| 153 | + # HTML |
| 154 | + imgpath = relative_uri(app.builder.env.docname, '_images') |
| 155 | + relfn = posixpath.join(imgpath, fname) |
| 156 | + outfn = path.join(app.builder.outdir, '_images', fname) |
| 157 | + else: |
| 158 | + # Non-HTML |
| 159 | + if app.builder.format != 'latex': |
| 160 | + app.builder.warn('aafig: the builder format %s is not officially ' |
| 161 | + 'supported, aafigure images could not work. Please report ' |
| 162 | + 'problems and working builder to avoid this warning in ' |
| 163 | + 'the future' % app.builder.format) |
| 164 | + relfn = fname |
| 165 | + outfn = path.join(app.builder.outdir, fname) |
| 166 | + metadata_fname = '%s.aafig' % outfn |
| 167 | + |
| 168 | + try: |
| 169 | + if path.isfile(outfn): |
| 170 | + extra = None |
| 171 | + if options['format'].lower() == 'svg': |
| 172 | + f = None |
| 173 | + try: |
| 174 | + try: |
| 175 | + f = file(metadata_fname, 'r') |
| 176 | + extra = f.read() |
| 177 | + except: |
| 178 | + raise AafigError() |
| 179 | + finally: |
| 180 | + if f is not None: |
| 181 | + f.close() |
| 182 | + return relfn, outfn, id, extra |
| 183 | + except AafigError: |
| 184 | + pass |
| 185 | + |
| 186 | + ensuredir(path.dirname(outfn)) |
| 187 | + |
| 188 | + try: |
| 189 | + (visitor, output) = aafigure.render(text, outfn, options) |
| 190 | + output.close() |
| 191 | + except aafigure.UnsupportedFormatError, e: |
| 192 | + raise AafigError(str(e)) |
| 193 | + |
| 194 | + extra = None |
| 195 | + if options['format'].lower() == 'svg': |
| 196 | + extra = visitor.get_size_attrs() |
| 197 | + f = file(metadata_fname, 'w') |
| 198 | + f.write(extra) |
| 199 | + f.close() |
| 200 | + |
| 201 | + return relfn, outfn, id, extra |
| 202 | + |
| 203 | + |
| 204 | +def setup(app): |
| 205 | + app.add_directive('aafig', AafigDirective) |
| 206 | + app.connect('doctree-read', render_aafig_images) |
| 207 | + app.add_config_value('aafig_format', DEFAULT_FORMATS, 'html') |
| 208 | + app.add_config_value('aafig_default_options', dict(), 'html') |
| 209 | + |
| 210 | + |
| 211 | +# vim: set expandtab shiftwidth=4 softtabstop=4 : |
0 commit comments