Skip to content

Commit bc6b3dd

Browse files
committed
Add aafig extension
1 parent 6144489 commit bc6b3dd

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

_ext/__init__.py

Whitespace-only changes.

_ext/aafig.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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 :

_ext/aafig.pyc

6.56 KB
Binary file not shown.

0 commit comments

Comments
 (0)