From: Vladimír Vondruš Date: Sun, 14 Oct 2018 00:03:49 +0000 (+0200) Subject: m.dot: extract the reusable guts to an independent module. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=fa7b47383d23590f346f17d43f49674f724ac63b;p=blog.git m.dot: extract the reusable guts to an independent module. Will get used in the Doxygen theme, where we don't need docutils or Pelican. --- diff --git a/pelican-plugins/dot2svg.py b/pelican-plugins/dot2svg.py new file mode 100644 index 00000000..9ff78113 --- /dev/null +++ b/pelican-plugins/dot2svg.py @@ -0,0 +1,121 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018 Vladimír VondruÅ¡ +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import re +import subprocess + +_patch_src = re.compile(r"""<\?xml version="1\.0" encoding="UTF-8" standalone="no"\?> + + + +\n""") + +# Graphviz < 2.40 (Ubuntu 16.04 and older) doesn't have a linebreak between +# and +_class_src = re.compile(r"""[\n]?(?P<title>[^<]*) +<(?Pellipse|polygon|path) fill="(?P[^"]+)" stroke="[^"]+" """) + +_class_dst = r""" +{title} +<{element} """ + +_attributes_src = re.compile(r"""<(?Pellipse|polygon|polyline) fill="[^"]+" stroke="[^"]+" """) + +_attributes_dst = r"""<\g """ + +# re.compile() is called after replacing {font} in configure(). Graphviz < 2.40 +# doesn't put the fill="" attribute there +_text_src_src = ' font-family="{font}" font-size="(?P[^"]+)"( fill="[^"]+")?' + +_text_dst = ' style="font-size: {size}px;"' + +_font = '' +_font_size = 0.0 + +# The pt are actually px (16pt font is the same size as 16px), so just +# converting to rem here +def _pt2em(pt): return pt/_font_size + +def dot2svg(source): + try: + ret = subprocess.run(['dot', '-Tsvg', + '-Gfontname={}'.format(_font), + '-Nfontname={}'.format(_font), + '-Efontname={}'.format(_font), + '-Gfontsize={}'.format(_font_size), + '-Nfontsize={}'.format(_font_size), + '-Efontsize={}'.format(_font_size), + '-Gbgcolor=transparent', + ], input=source.encode('utf-8'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if ret.returncode: print(ret.stderr.decode('utf-8')) + ret.check_returncode() + except FileNotFoundError: # pragma: no cover + raise RuntimeError("dot not found") + + # First remove comments + svg = _comment_src.sub('', ret.stdout.decode('utf-8')) + + # Remove preamble and fixed size + def patch_repl(match): return _patch_dst.format( + width=_pt2em(float(match.group('width'))), + height=_pt2em(float(match.group('height'))), + viewBox=match.group('viewBox')) + svg = _patch_src.sub(patch_repl, svg) + + # Remove unnecessary IDs and attributes, replace classes for elements + def element_repl(match): + classes = ['m-' + match.group('type')] + match.group('classes').replace('-', '-').split() + # distinguish between solid and filled nodes + if match.group('type') == 'node' and match.group('fill') == 'none': + classes += ['m-flat'] + + return _class_dst.format( + classes=' '.join(classes), + title=match.group('title'), + element=match.group('element')) + svg = _class_src.sub(element_repl, svg) + + # Remove unnecessary fill and stroke attributes + svg = _attributes_src.sub(_attributes_dst, svg) + + # Remove unnecessary text attributes. Keep font size only if nondefault + def text_repl(match): + if float(match.group('size')) != _font_size: + return _text_dst.format(size=float(match.group('size'))) + return '' + svg = _text_src.sub(text_repl, svg) + + return svg + +def configure(font, font_size): + global _font, _font_size, _text_src + _font = font + _font_size = font_size + _text_src = re.compile(_text_src_src.format(font=_font)) diff --git a/pelican-plugins/m/dot.py b/pelican-plugins/m/dot.py index 164ff29b..04e2a41e 100644 --- a/pelican-plugins/m/dot.py +++ b/pelican-plugins/m/dot.py @@ -31,43 +31,7 @@ from docutils.parsers import rst from docutils.parsers.rst import directives from docutils.parsers.rst.roles import set_classes -_patch_src = re.compile(r"""<\?xml version="1\.0" encoding="UTF-8" standalone="no"\?> - - - -\n""") - -# Graphviz < 2.40 (Ubuntu 16.04 and older) doesn't have a linebreak between -# and -_class_src = re.compile(r"""[\n]?(?P<title>[^<]*) -<(?Pellipse|polygon|path) fill="(?P[^"]+)" stroke="[^"]+" """) - -_class_dst = r""" -{title} -<{element} """ - -_attributes_src = re.compile(r"""<(?Pellipse|polygon|polyline) fill="[^"]+" stroke="[^"]+" """) - -_attributes_dst = r"""<\g """ - -# re.compile() is called after replacing {font} in configure(). Graphviz < 2.40 -# doesn't put the fill="" attribute there -_text_src_src = ' font-family="{font}" font-size="(?P[^"]+)"( fill="[^"]+")?' - -_text_dst = ' style="font-size: {size}px;"' - -_font = '' -_font_size = 0.0 - -# The pt are actually px (16pt font is the same size as 16px), so just -# converting to rem here -def _pt2em(pt): return pt/_font_size +import dot2svg class Dot(rst.Directive): has_content = True @@ -79,55 +43,7 @@ class Dot(rst.Directive): def run(self, source): set_classes(self.options) - title_text = self.arguments[0] - - try: - ret = subprocess.run(['dot', '-Tsvg', - '-Gfontname={}'.format(_font), - '-Nfontname={}'.format(_font), - '-Efontname={}'.format(_font), - '-Gfontsize={}'.format(_font_size), - '-Nfontsize={}'.format(_font_size), - '-Efontsize={}'.format(_font_size), - '-Gbgcolor=transparent', - ], input=source.encode('utf-8'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if ret.returncode: print(ret.stderr.decode('utf-8')) - ret.check_returncode() - except FileNotFoundError: # pragma: no cover - raise RuntimeError("dot not found") - - # First Remove comments - svg = _comment_src.sub('', ret.stdout.decode('utf-8')) - - # Remove preamble and fixed size - def patch_repl(match): return _patch_dst.format( - width=_pt2em(float(match.group('width'))), - height=_pt2em(float(match.group('height'))), - viewBox=match.group('viewBox')) - svg = _patch_src.sub(patch_repl, svg) - - # Remove unnecessary IDs and attributes, replace classes for elements - def element_repl(match): - classes = ['m-' + match.group('type')] + match.group('classes').replace('-', '-').split() - # distinguish between solid and filled nodes - if match.group('type') == 'node' and match.group('fill') == 'none': - classes += ['m-flat'] - - return _class_dst.format( - classes=' '.join(classes), - title=match.group('title'), - element=match.group('element')) - svg = _class_src.sub(element_repl, svg) - - # Remove unnecessary fill and stroke attributes - svg = _attributes_src.sub(_attributes_dst, svg) - - # Remove unnecessary text attributes. Keep font size only if nondefault - def text_repl(match): - if float(match.group('size')) != _font_size: - return _text_dst.format(size=float(match.group('size'))) - return '' - svg = _text_src.sub(text_repl, svg) + svg = dot2svg.dot2svg(source) container = nodes.container(**self.options) container['classes'] = ['m-graph'] + container['classes'] @@ -152,10 +68,9 @@ class StrictGraph(Dot): return Dot.run(self, 'strict graph "{}" {{\n{}}}'.format(self.arguments[0], '\n'.join(self.content))) def configure(pelicanobj): - global _font, _font_size, _text_src - _font = pelicanobj.settings.get('M_DOT_FONT', 'Source Sans Pro') - _font_size = pelicanobj.settings.get('M_DOT_FONT_SIZE', 16.0) - _text_src = re.compile(_text_src_src.format(font=_font)) + dot2svg.configure( + pelicanobj.settings.get('M_DOT_FONT', 'Source Sans Pro'), + pelicanobj.settings.get('M_DOT_FONT_SIZE', 16.0)) def register(): pelican.signals.initialized.connect(configure)