Fix up action list in Appendix A.
[ibg.git] / tools / blockdiag.py
1 # -*- coding: utf-8 -*-
2 """
3     blockdiag.sphinx_ext
4     ~~~~~~~~~~~~~~~~~~~~
5
6     Allow blockdiag-formatted diagrams to be included in Sphinx-generated
7     documents inline.
8
9     :copyright: Copyright 2010 by Takeshi Komiya.
10     :license: BSDL.
11 """
12
13 from __future__ import absolute_import
14
15 import os
16 import re
17 import posixpath
18 import traceback
19 import pkg_resources
20 from collections import namedtuple
21 from docutils import nodes
22 from sphinx import addnodes
23 from sphinx.util.osutil import ensuredir
24
25 import blockdiag.utils.rst.nodes
26 import blockdiag.utils.rst.directives
27 from blockdiag.utils.bootstrap import detectfont, Application
28 from blockdiag.utils.compat import u, string_types
29 from blockdiag.utils.fontmap import FontMap
30 from blockdiag.utils.rst.directives import with_blockdiag
31
32 # fontconfig; it will be initialized on `builder-inited` event.
33 fontmap = None
34
35
36 class blockdiag_node(blockdiag.utils.rst.nodes.blockdiag):
37     def to_drawer(self, image_format, builder, **kwargs):
38         if 'filename' in kwargs:
39             filename = kwargs.pop('filename')
40         else:
41             filename = self.get_abspath(image_format, builder)
42
43         antialias = builder.config.blockdiag_antialias
44         transparency = builder.config.blockdiag_transparency
45         image = super(blockdiag_node, self).to_drawer(image_format, filename, fontmap,
46                                                       antialias=antialias, transparency=transparency,
47                                                       **kwargs)
48         for node in image.diagram.traverse_nodes():
49             node.href = resolve_reference(builder, node.href)
50
51         return image
52
53     def get_relpath(self, image_format, builder):
54         options = dict(antialias=builder.config.blockdiag_antialias,
55                        fontpath=builder.config.blockdiag_fontpath,
56                        fontmap=builder.config.blockdiag_fontmap,
57                        format=image_format,
58                        transparency=builder.config.blockdiag_transparency)
59         if hasattr(builder, 'imgpath'):  # Sphinx (<= 1.2.x) or HTML writer
60             outputdir = builder.imgpath
61         else:
62             outputdir = ''
63         return posixpath.join(outputdir, self.get_path(**options))
64
65     def get_abspath(self, image_format, builder):
66         options = dict(antialias=builder.config.blockdiag_antialias,
67                        fontpath=builder.config.blockdiag_fontpath,
68                        fontmap=builder.config.blockdiag_fontmap,
69                        format=image_format,
70                        transparency=builder.config.blockdiag_transparency)
71
72         if hasattr(builder, 'imagedir'):  # Sphinx (>= 1.3.x)
73             outputdir = os.path.join(builder.outdir, builder.imagedir)
74         elif hasattr(builder, 'imgpath'):  # Sphinx (<= 1.2.x) and HTML writer
75             outputdir = os.path.join(builder.outdir, '_images')
76         else:
77             outputdir = builder.outdir
78         path = os.path.join(outputdir, self.get_path(**options))
79         ensuredir(os.path.dirname(path))
80
81         return path
82
83
84 class Blockdiag(blockdiag.utils.rst.directives.BlockdiagDirective):
85     node_class = blockdiag_node
86
87     def node2image(self, node, diagram):
88         return node
89
90
91 def resolve_reference(builder, href):
92     if href is None:
93         return None
94
95     pattern = re.compile(u("^:ref:`(.+?)`"), re.UNICODE)
96     matched = pattern.search(href)
97     if matched is None:
98         return href
99     elif not hasattr(builder, 'current_docname'):  # ex. latex builder
100         return matched.group(1)
101     else:
102         refid = matched.group(1)
103         domain = builder.env.domains['std']
104         node = addnodes.pending_xref(refexplicit=False)
105         xref = domain.resolve_xref(builder.env, builder.current_docname, builder,
106                                    'ref', refid, node, node)
107         if xref:
108             if 'refid' in xref:
109                 return "#" + xref['refid']
110             else:
111                 return xref['refuri']
112         else:
113             builder.warn('undefined label: %s' % refid)
114             return None
115
116
117 def html_render_svg(self, node):
118     image = node.to_drawer('SVG', self.builder, filename=None, nodoctype=True)
119     image.draw()
120
121     if 'align' in node['options']:
122         align = node['options']['align']
123         self.body.append('<div align="%s" class="align-%s">' % (align, align))
124         self.context.append('</div>\n')
125     else:
126         self.body.append('<div>')
127         self.context.append('</div>\n')
128
129     # reftarget
130     for node_id in node['ids']:
131         self.body.append('<span id="%s"></span>' % node_id)
132
133     # resize image
134     size = image.pagesize().resize(**node['options'])
135     self.body.append(image.save(size))
136     self.context.append('')
137
138
139 def html_render_clickablemap(self, image, width_ratio, height_ratio):
140     href_nodes = [node for node in image.nodes if node.href]
141     if not href_nodes:
142         return
143
144     self.body.append('<map name="map_%d">' % id(image))
145     for node in href_nodes:
146         x1, y1, x2, y2 = image.metrics.cell(node)
147
148         x1 *= width_ratio
149         x2 *= width_ratio
150         y1 *= height_ratio
151         y2 *= height_ratio
152         areatag = '<area shape="rect" coords="%s,%s,%s,%s" href="%s">' % (x1, y1, x2, y2, node.href)
153         self.body.append(areatag)
154
155     self.body.append('</map>')
156
157
158 def html_render_png(self, node):
159     image = node.to_drawer('PNG', self.builder)
160     if not os.path.isfile(image.filename):
161         image.draw()
162         image.save()
163
164     # align
165     if 'align' in node['options']:
166         align = node['options']['align']
167         self.body.append('<div align="%s" class="align-%s">' % (align, align))
168         self.context.append('</div>\n')
169     else:
170         self.body.append('<div>')
171         self.context.append('</div>')
172
173     # link to original image
174     relpath = node.get_relpath('PNG', self.builder)
175     if 'width' in node['options'] or 'height' in node['options'] or 'scale' in node['options']:
176         self.body.append('<a class="reference internal image-reference" href="%s">' % relpath)
177         self.context.append('</a>')
178     else:
179         self.context.append('')
180
181     # <img> tag
182     original_size = image.pagesize()
183     resized = original_size.resize(**node['options'])
184     img_attr = dict(src=relpath,
185                     width=resized.width,
186                     height=resized.height)
187
188     if any(node.href for node in image.nodes):
189         img_attr['usemap'] = "#map_%d" % id(image)
190
191         width_ratio = float(resized.width) / original_size.width
192         height_ratio = float(resized.height) / original_size.height
193         html_render_clickablemap(self, image, width_ratio, height_ratio)
194
195     if 'alt' in node['options']:
196         img_attr['alt'] = node['options']['alt']
197
198     self.body.append(self.starttag(node, 'img', '', empty=True, **img_attr))
199
200
201 @with_blockdiag
202 def html_visit_blockdiag(self, node):
203     try:
204         image_format = get_image_format_for(self.builder)
205         if image_format.upper() == 'SVG':
206             html_render_svg(self, node)
207         else:
208             html_render_png(self, node)
209     except UnicodeEncodeError:
210         if self.builder.config.blockdiag_debug:
211             traceback.print_exc()
212
213         msg = ("blockdiag error: UnicodeEncodeError caught "
214                "(check your font settings)")
215         self.builder.warn(msg)
216         raise nodes.SkipNode
217     except Exception as exc:
218         if self.builder.config.blockdiag_debug:
219             traceback.print_exc()
220
221         self.builder.warn('dot code %r: %s' % (node['code'], str(exc)))
222         raise nodes.SkipNode
223
224
225 def html_depart_blockdiag(self, node):
226     self.body.append(self.context.pop())
227     self.body.append(self.context.pop())
228
229
230 def get_image_format_for(builder):
231     if builder.format in ('html', 'slides'):
232         image_format = builder.config.blockdiag_html_image_format.upper()
233     elif builder.format == 'latex':
234         if builder.config.blockdiag_tex_image_format:
235             image_format = builder.config.blockdiag_tex_image_format.upper()
236         else:
237             image_format = builder.config.blockdiag_latex_image_format.upper()
238     else:
239         image_format = 'PNG'
240
241     if image_format.upper() not in ('PNG', 'PDF', 'SVG'):
242         raise ValueError('unknown format: %s' % image_format)
243
244     if image_format.upper() == 'PDF':
245         try:
246             import reportlab  # NOQA: importing test
247         except ImportError:
248             raise ImportError('Could not output PDF format. Install reportlab.')
249
250     return image_format
251
252
253 def on_builder_inited(self):
254     # show deprecated message
255     if self.builder.config.blockdiag_tex_image_format:
256         self.builder.warn('blockdiag_tex_image_format is deprecated. Use blockdiag_latex_image_format.')
257
258     # initialize fontmap
259     global fontmap
260
261     try:
262         fontmappath = self.builder.config.blockdiag_fontmap
263         fontmap = FontMap(fontmappath)
264     except:
265         fontmap = FontMap(None)
266
267     try:
268         fontpath = self.builder.config.blockdiag_fontpath
269         if isinstance(fontpath, string_types):
270             fontpath = [fontpath]
271
272         if fontpath:
273             config = namedtuple('Config', 'font')(fontpath)
274             fontpath = detectfont(config)
275             fontmap.set_default_font(fontpath)
276     except:
277         pass
278
279
280 def on_doctree_resolved(self, doctree, docname):
281     if self.builder.format in ('html', 'slides'):
282         return
283
284     try:
285         image_format = get_image_format_for(self.builder)
286     except Exception as exc:
287         if self.builder.config.blockdiag_debug:
288             traceback.print_exc()
289
290         self.builder.warn('blockdiag error: %s' % exc)
291         for node in doctree.traverse(blockdiag_node):
292             node.parent.remove(node)
293
294         return
295
296     for node in doctree.traverse(blockdiag_node):
297         try:
298             with Application():
299                 relfn = node.get_relpath(image_format, self.builder)
300                 image = node.to_drawer(image_format, self.builder)
301                 if not os.path.isfile(image.filename):
302                     image.draw()
303                     image.save()
304
305                 image = nodes.image(uri=relfn, candidates={'*': relfn}, **node['options'])
306                 node.parent.replace(node, image)
307         except Exception as exc:
308             if self.builder.config.blockdiag_debug:
309                 traceback.print_exc()
310
311             self.builder.warn('dot code %r: %s' % (node['code'], str(exc)))
312             node.parent.remove(node)
313
314
315 def setup(app):
316     app.add_node(blockdiag_node,
317                  html=(html_visit_blockdiag, html_depart_blockdiag))
318     app.add_directive('blockdiag', Blockdiag)
319     app.add_config_value('blockdiag_fontpath', None, 'html')
320     app.add_config_value('blockdiag_fontmap', None, 'html')
321     app.add_config_value('blockdiag_antialias', False, 'html')
322     app.add_config_value('blockdiag_transparency', True, 'html')
323     app.add_config_value('blockdiag_debug', False, 'html')
324     app.add_config_value('blockdiag_html_image_format', 'PNG', 'html')
325     app.add_config_value('blockdiag_tex_image_format', None, 'html')  # backward compatibility for 1.3.1
326     app.add_config_value('blockdiag_latex_image_format', 'PNG', 'html')
327     app.connect("builder-inited", on_builder_inited)
328     app.connect("doctree-resolved", on_doctree_resolved)
329
330     return {
331         'version': pkg_resources.require('blockdiag')[0].version,
332         'parallel_read_safe': True,
333         'parallel_write_safe': True,
334     }