From: Glenn Hutchings Date: Thu, 14 Apr 2016 19:38:24 +0000 (+0100) Subject: Use blockdiag to create the Heidi box diagrams. X-Git-Url: https://jxself.org/git/?p=ibg.git;a=commitdiff_plain;h=d243892ad9717d0d1cbb0dbc941b229b24a7f09e Use blockdiag to create the Heidi box diagrams. --- diff --git a/README.md b/README.md index e90ea93..eb05a28 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,14 @@ Introduction This repository contains the sources for the [Inform Beginner's Guide](http://www.firthworks.com/roger/IBG.html), henceforth known as the IBG. This version is in a new source format that's -more contributor-friendly (see below). It's a work-in-progress; see the -[issue tracker](https://github.com/i6/ibg/issues) for how it's going. +more version-control and contributor-friendly: +[Sphinx](http://sphinx-doc.org), a documentation build tool written in +[Python](http://python.org). With Sphinx, the source files are in a very +readable format called +[reStructuredText](https://en.wikipedia.org/wiki/ReStructuredText). +This version of the IBG is a work-in-progress; see the +[issue tracker](https://github.com/i6/ibg/issues) for how it's going. Initially it will be a straight conversion of the 3rd edition. After that, the 4th edition will be prepared. The rationale for a 4th edition is: @@ -25,13 +30,19 @@ the 4th edition will be prepared. The rationale for a 4th edition is: Building from source -------------------- -The new source format is [Sphinx](http://sphinx-doc.org), a documentation -build tool written in [Python](http://python.org), which you will need to -install if you want to build the document. You can find the complete -installation instructions -[here](http://www.sphinx-doc.org/en/stable/install.html). +As well as these document sources, you will need: + +* Sphinx. You can find the complete installation instructions + [here](http://www.sphinx-doc.org/en/stable/install.html). + +* Sphinx makes use of [Blockdiag](https://pypi.python.org/pypi/blockdiag) + for some of the diagrams; you'll need that too. + +If you have `pip`, this command will be all you need: + + pip install sphinx blockdiag -After you have Sphinx installed, you can build the HTML version of the +After you have everything installed, you can build the HTML version of the guide from a command prompt, like this: make html diff --git a/chapters/04.rst b/chapters/04.rst index 8c6a7ed..d9949bb 100644 --- a/chapters/04.rst +++ b/chapters/04.rst @@ -284,45 +284,57 @@ This causes another change in the relationships. The bird is now a child of the player (and *not* of the forest), and the player is both a parent (of the bird) and a child (of the forest). -In this diagram, we show how the object relationships change during the -course of the game. The straight lines represent parent--child -relationships, with the parent object at the top of the line, and the child -object at the bottom. +Here we show how the object relationships change during the course of the +game. The straight lines represent parent--child relationships, with the +parent object at the top of the line, and the child object at the bottom. -.. list-table:: - :widths: 1 3 5 +1. At the start of the game: - * - 1. - - At the start of the game: - - .. image:: /images/heidiobj1.* + .. blockdiag:: /figures/heidiobj1.diag + :align: center + :scale: 80% - * - 2. - - The player types: ``GO EAST`` - - .. image:: /images/heidiobj2.* +2. The player types: ``GO EAST`` - * - 3. - - The player types: ``TAKE THE BIRD`` - - .. image:: /images/heidiobj3.* + .. blockdiag:: /figures/heidiobj2.diag + :align: center + :scale: 80% - * - 4. - - The player types: ``GO NORTHEAST`` - - .. image:: /images/heidiobj4.* +3. The player types: ``TAKE THE BIRD`` - * - 5. - - The player types: ``PUT BIRD IN NEST`` - - .. image:: /images/heidiobj5.* + .. blockdiag:: /figures/heidiobj3.diag + :align: center + :scale: 80% - * - 6. - - The player types: ``TAKE NEST`` - - .. image:: /images/heidiobj6.* +4. The player types: ``GO NORTHEAST`` - * - 7. - - The player types: ``UP`` - - .. image:: /images/heidiobj7.* + .. blockdiag:: /figures/heidiobj4.diag + :align: center + :scale: 80% - * - 8. - - The player types: ``PUT NEST ON BRANCH`` - - .. image:: /images/heidiobj8.* +5. The player types: ``PUT BIRD IN NEST`` + + .. blockdiag:: /figures/heidiobj5.diag + :align: center + :scale: 80% + +6. The player types: ``TAKE NEST`` + + .. blockdiag:: /figures/heidiobj6.diag + :align: center + :scale: 80% + +7. The player types: ``UP`` + + .. blockdiag:: /figures/heidiobj7.diag + :align: center + :scale: 80% + +8. The player types: ``PUT NEST ON BRANCH`` + + .. blockdiag:: /figures/heidiobj8.diag + :align: center + :scale: 80% In this short example, we've taken a lot of time and space to spell out exactly how the objects relationship patterns -- generally known as the diff --git a/conf.py b/conf.py index 402eb1c..7546c75 100644 --- a/conf.py +++ b/conf.py @@ -20,9 +20,9 @@ import codecs # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('tools')) +sys.path.insert(0, ".") -from transcript import TranscriptLexer +from tools.transcript import TranscriptLexer # Setup function. def setup(app): @@ -38,6 +38,7 @@ def setup(app): # ones. extensions = [ 'sphinx.ext.todo', + 'tools.blockdiag', ] # Add any paths that contain templates here, relative to this directory. @@ -331,3 +332,30 @@ texinfo_documents = [ # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False + + +# -- Options for block diagrams ------------------------------------------- + +# The paths to truetype fonts. blockdiag_fontpath option accepts both +# single path string and list of paths. +blockdiag_fontpath = [] + +# The path to fontmap definitions. +blockdiag_fontmap = "" + +# Render diagrams in antialias mode or not. +blockdiag_antialias = True + +# Render diagrams as transparency or not. +blockdiag_transparency = True + +# The output image format at generating HTML docs ("PNG" or "SVG"). +blockdiag_html_image_format = "SVG" + +# The output image format at generating PDF docs (through LaTeX). ("PNG" or +# "PDF"). When a value of PDF is specified, you can get clear diagram +# images. In which case, reportlab library is required. +blockdiag_latex_image_format = "PNG" + +# Enable debug mode of blockdiag. +blockdiag_debug = False diff --git a/figures/heidiobj1.diag b/figures/heidiobj1.diag new file mode 100644 index 0000000..3e18718 --- /dev/null +++ b/figures/heidiobj1.diag @@ -0,0 +1,23 @@ +// 1. At the start of the game + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC -> P; + F -> B; + C -> N, T; + TT -> TB; +} diff --git a/figures/heidiobj2.diag b/figures/heidiobj2.diag new file mode 100644 index 0000000..8564db5 --- /dev/null +++ b/figures/heidiobj2.diag @@ -0,0 +1,23 @@ +// 2. The player types: ``GO EAST`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F -> P, B; + C -> N, T; + TT -> TB; +} diff --git a/figures/heidiobj3.diag b/figures/heidiobj3.diag new file mode 100644 index 0000000..c7c41f9 --- /dev/null +++ b/figures/heidiobj3.diag @@ -0,0 +1,23 @@ +// 3. The player types: ``TAKE THE BIRD`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F -> P -> B; + C -> N, T; + TT -> TB; +} diff --git a/figures/heidiobj4.diag b/figures/heidiobj4.diag new file mode 100644 index 0000000..a4dbdf8 --- /dev/null +++ b/figures/heidiobj4.diag @@ -0,0 +1,24 @@ +// 4. The player types: ``GO NORTHEAST`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F; + C -> P -> B; + C -> N, T; + TT -> TB; +} diff --git a/figures/heidiobj5.diag b/figures/heidiobj5.diag new file mode 100644 index 0000000..c6be0d8 --- /dev/null +++ b/figures/heidiobj5.diag @@ -0,0 +1,24 @@ +// 5. The player types: ``PUT BIRD IN NEST`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F; + C -> N -> B; + C -> P, T; + TT -> TB; +} diff --git a/figures/heidiobj6.diag b/figures/heidiobj6.diag new file mode 100644 index 0000000..a1d915a --- /dev/null +++ b/figures/heidiobj6.diag @@ -0,0 +1,24 @@ +// 6. The player types: ``TAKE NEST`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F; + C -> P -> N -> B; + C -> T; + TT -> TB; +} diff --git a/figures/heidiobj7.diag b/figures/heidiobj7.diag new file mode 100644 index 0000000..74628f0 --- /dev/null +++ b/figures/heidiobj7.diag @@ -0,0 +1,24 @@ +// 7. The player types: ``UP`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F; + C -> T; + TT -> P -> N -> B; + TT -> TB; +} diff --git a/figures/heidiobj8.diag b/figures/heidiobj8.diag new file mode 100644 index 0000000..edd792b --- /dev/null +++ b/figures/heidiobj8.diag @@ -0,0 +1,24 @@ +// 8. The player types: ``PUT NEST ON BRANCH`` + +blockdiag { + default_shape = roundedbox; + orientation = portrait; + node_width = 80; + + BC [label = "before_\ncottage"]; + F [label = "forest"]; + C [label = "clearing"]; + TT [label = "top_of_\ntree"]; + + P [label = "player"]; + B [label = "bird"]; + N [label = "nest"]; + T [label = "tree"]; + TB [label = "branch"]; + + BC; + F; + C -> T; + TT -> P; + TT -> TB -> N -> B; +} diff --git a/images/heidiobj1.png b/images/heidiobj1.png deleted file mode 100644 index 6caa0e3..0000000 Binary files a/images/heidiobj1.png and /dev/null differ diff --git a/images/heidiobj2.png b/images/heidiobj2.png deleted file mode 100644 index 255a32a..0000000 Binary files a/images/heidiobj2.png and /dev/null differ diff --git a/images/heidiobj3.png b/images/heidiobj3.png deleted file mode 100644 index bfd73a5..0000000 Binary files a/images/heidiobj3.png and /dev/null differ diff --git a/images/heidiobj4.png b/images/heidiobj4.png deleted file mode 100644 index 082d70d..0000000 Binary files a/images/heidiobj4.png and /dev/null differ diff --git a/images/heidiobj5.png b/images/heidiobj5.png deleted file mode 100644 index aef4113..0000000 Binary files a/images/heidiobj5.png and /dev/null differ diff --git a/images/heidiobj6.png b/images/heidiobj6.png deleted file mode 100644 index 985fe15..0000000 Binary files a/images/heidiobj6.png and /dev/null differ diff --git a/images/heidiobj7.png b/images/heidiobj7.png deleted file mode 100644 index 46ab2b7..0000000 Binary files a/images/heidiobj7.png and /dev/null differ diff --git a/images/heidiobj8.png b/images/heidiobj8.png deleted file mode 100644 index c4ade78..0000000 Binary files a/images/heidiobj8.png and /dev/null differ diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/blockdiag.py b/tools/blockdiag.py new file mode 100644 index 0000000..1aca0cf --- /dev/null +++ b/tools/blockdiag.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +""" + blockdiag.sphinx_ext + ~~~~~~~~~~~~~~~~~~~~ + + Allow blockdiag-formatted diagrams to be included in Sphinx-generated + documents inline. + + :copyright: Copyright 2010 by Takeshi Komiya. + :license: BSDL. +""" + +from __future__ import absolute_import + +import os +import re +import posixpath +import traceback +import pkg_resources +from collections import namedtuple +from docutils import nodes +from sphinx import addnodes +from sphinx.util.osutil import ensuredir + +import blockdiag.utils.rst.nodes +import blockdiag.utils.rst.directives +from blockdiag.utils.bootstrap import detectfont, Application +from blockdiag.utils.compat import u, string_types +from blockdiag.utils.fontmap import FontMap +from blockdiag.utils.rst.directives import with_blockdiag + +# fontconfig; it will be initialized on `builder-inited` event. +fontmap = None + + +class blockdiag_node(blockdiag.utils.rst.nodes.blockdiag): + def to_drawer(self, image_format, builder, **kwargs): + if 'filename' in kwargs: + filename = kwargs.pop('filename') + else: + filename = self.get_abspath(image_format, builder) + + antialias = builder.config.blockdiag_antialias + transparency = builder.config.blockdiag_transparency + image = super(blockdiag_node, self).to_drawer(image_format, filename, fontmap, + antialias=antialias, transparency=transparency, + **kwargs) + for node in image.diagram.traverse_nodes(): + node.href = resolve_reference(builder, node.href) + + return image + + def get_relpath(self, image_format, builder): + options = dict(antialias=builder.config.blockdiag_antialias, + fontpath=builder.config.blockdiag_fontpath, + fontmap=builder.config.blockdiag_fontmap, + format=image_format, + transparency=builder.config.blockdiag_transparency) + if hasattr(builder, 'imgpath'): # Sphinx (<= 1.2.x) or HTML writer + outputdir = builder.imgpath + else: + outputdir = '' + return posixpath.join(outputdir, self.get_path(**options)) + + def get_abspath(self, image_format, builder): + options = dict(antialias=builder.config.blockdiag_antialias, + fontpath=builder.config.blockdiag_fontpath, + fontmap=builder.config.blockdiag_fontmap, + format=image_format, + transparency=builder.config.blockdiag_transparency) + + if hasattr(builder, 'imagedir'): # Sphinx (>= 1.3.x) + outputdir = os.path.join(builder.outdir, builder.imagedir) + elif hasattr(builder, 'imgpath'): # Sphinx (<= 1.2.x) and HTML writer + outputdir = os.path.join(builder.outdir, '_images') + else: + outputdir = builder.outdir + path = os.path.join(outputdir, self.get_path(**options)) + ensuredir(os.path.dirname(path)) + + return path + + +class Blockdiag(blockdiag.utils.rst.directives.BlockdiagDirective): + node_class = blockdiag_node + + def node2image(self, node, diagram): + return node + + +def resolve_reference(builder, href): + if href is None: + return None + + pattern = re.compile(u("^:ref:`(.+?)`"), re.UNICODE) + matched = pattern.search(href) + if matched is None: + return href + elif not hasattr(builder, 'current_docname'): # ex. latex builder + return matched.group(1) + else: + refid = matched.group(1) + domain = builder.env.domains['std'] + node = addnodes.pending_xref(refexplicit=False) + xref = domain.resolve_xref(builder.env, builder.current_docname, builder, + 'ref', refid, node, node) + if xref: + if 'refid' in xref: + return "#" + xref['refid'] + else: + return xref['refuri'] + else: + builder.warn('undefined label: %s' % refid) + return None + + +def html_render_svg(self, node): + image = node.to_drawer('SVG', self.builder, filename=None, nodoctype=True) + image.draw() + + if 'align' in node['options']: + align = node['options']['align'] + self.body.append('
' % (align, align)) + self.context.append('
\n') + else: + self.body.append('
') + self.context.append('
\n') + + # reftarget + for node_id in node['ids']: + self.body.append('' % node_id) + + # resize image + size = image.pagesize().resize(**node['options']) + self.body.append(image.save(size)) + self.context.append('') + + +def html_render_clickablemap(self, image, width_ratio, height_ratio): + href_nodes = [node for node in image.nodes if node.href] + if not href_nodes: + return + + self.body.append('' % id(image)) + for node in href_nodes: + x1, y1, x2, y2 = image.metrics.cell(node) + + x1 *= width_ratio + x2 *= width_ratio + y1 *= height_ratio + y2 *= height_ratio + areatag = '' % (x1, y1, x2, y2, node.href) + self.body.append(areatag) + + self.body.append('') + + +def html_render_png(self, node): + image = node.to_drawer('PNG', self.builder) + if not os.path.isfile(image.filename): + image.draw() + image.save() + + # align + if 'align' in node['options']: + align = node['options']['align'] + self.body.append('
' % (align, align)) + self.context.append('
\n') + else: + self.body.append('
') + self.context.append('
') + + # link to original image + relpath = node.get_relpath('PNG', self.builder) + if 'width' in node['options'] or 'height' in node['options'] or 'scale' in node['options']: + self.body.append('' % relpath) + self.context.append('') + else: + self.context.append('') + + # tag + original_size = image.pagesize() + resized = original_size.resize(**node['options']) + img_attr = dict(src=relpath, + width=resized.width, + height=resized.height) + + if any(node.href for node in image.nodes): + img_attr['usemap'] = "#map_%d" % id(image) + + width_ratio = float(resized.width) / original_size.width + height_ratio = float(resized.height) / original_size.height + html_render_clickablemap(self, image, width_ratio, height_ratio) + + if 'alt' in node['options']: + img_attr['alt'] = node['options']['alt'] + + self.body.append(self.starttag(node, 'img', '', empty=True, **img_attr)) + + +@with_blockdiag +def html_visit_blockdiag(self, node): + try: + image_format = get_image_format_for(self.builder) + if image_format.upper() == 'SVG': + html_render_svg(self, node) + else: + html_render_png(self, node) + except UnicodeEncodeError: + if self.builder.config.blockdiag_debug: + traceback.print_exc() + + msg = ("blockdiag error: UnicodeEncodeError caught " + "(check your font settings)") + self.builder.warn(msg) + raise nodes.SkipNode + except Exception as exc: + if self.builder.config.blockdiag_debug: + traceback.print_exc() + + self.builder.warn('dot code %r: %s' % (node['code'], str(exc))) + raise nodes.SkipNode + + +def html_depart_blockdiag(self, node): + self.body.append(self.context.pop()) + self.body.append(self.context.pop()) + + +def get_image_format_for(builder): + if builder.format in ('html', 'slides'): + image_format = builder.config.blockdiag_html_image_format.upper() + elif builder.format == 'latex': + if builder.config.blockdiag_tex_image_format: + image_format = builder.config.blockdiag_tex_image_format.upper() + else: + image_format = builder.config.blockdiag_latex_image_format.upper() + else: + image_format = 'PNG' + + if image_format.upper() not in ('PNG', 'PDF', 'SVG'): + raise ValueError('unknown format: %s' % image_format) + + if image_format.upper() == 'PDF': + try: + import reportlab # NOQA: importing test + except ImportError: + raise ImportError('Could not output PDF format. Install reportlab.') + + return image_format + + +def on_builder_inited(self): + # show deprecated message + if self.builder.config.blockdiag_tex_image_format: + self.builder.warn('blockdiag_tex_image_format is deprecated. Use blockdiag_latex_image_format.') + + # initialize fontmap + global fontmap + + try: + fontmappath = self.builder.config.blockdiag_fontmap + fontmap = FontMap(fontmappath) + except: + fontmap = FontMap(None) + + try: + fontpath = self.builder.config.blockdiag_fontpath + if isinstance(fontpath, string_types): + fontpath = [fontpath] + + if fontpath: + config = namedtuple('Config', 'font')(fontpath) + fontpath = detectfont(config) + fontmap.set_default_font(fontpath) + except: + pass + + +def on_doctree_resolved(self, doctree, docname): + if self.builder.format in ('html', 'slides'): + return + + try: + image_format = get_image_format_for(self.builder) + except Exception as exc: + if self.builder.config.blockdiag_debug: + traceback.print_exc() + + self.builder.warn('blockdiag error: %s' % exc) + for node in doctree.traverse(blockdiag_node): + node.parent.remove(node) + + return + + for node in doctree.traverse(blockdiag_node): + try: + with Application(): + relfn = node.get_relpath(image_format, self.builder) + image = node.to_drawer(image_format, self.builder) + if not os.path.isfile(image.filename): + image.draw() + image.save() + + image = nodes.image(uri=relfn, candidates={'*': relfn}, **node['options']) + node.parent.replace(node, image) + except Exception as exc: + if self.builder.config.blockdiag_debug: + traceback.print_exc() + + self.builder.warn('dot code %r: %s' % (node['code'], str(exc))) + node.parent.remove(node) + + +def setup(app): + app.add_node(blockdiag_node, + html=(html_visit_blockdiag, html_depart_blockdiag)) + app.add_directive('blockdiag', Blockdiag) + app.add_config_value('blockdiag_fontpath', None, 'html') + app.add_config_value('blockdiag_fontmap', None, 'html') + app.add_config_value('blockdiag_antialias', False, 'html') + app.add_config_value('blockdiag_transparency', True, 'html') + app.add_config_value('blockdiag_debug', False, 'html') + app.add_config_value('blockdiag_html_image_format', 'PNG', 'html') + app.add_config_value('blockdiag_tex_image_format', None, 'html') # backward compatibility for 1.3.1 + app.add_config_value('blockdiag_latex_image_format', 'PNG', 'html') + app.connect("builder-inited", on_builder_inited) + app.connect("doctree-resolved", on_doctree_resolved) + + return { + 'version': pkg_resources.require('blockdiag')[0].version, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + }