1# 2# This file is part of m.css. 3# 4# Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz> 5# 6# Permission is hereby granted, free of charge, to any person obtaining a 7# copy of this software and associated documentation files (the "Software"), 8# to deal in the Software without restriction, including without limitation 9# the rights to use, copy, modify, merge, publish, distribute, sublicense, 10# and/or sell copies of the Software, and to permit persons to whom the 11# Software is furnished to do so, subject to the following conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22# DEALINGS IN THE SOFTWARE. 23# 24 25import re 26import subprocess 27 28_patch_src = re.compile(r"""<\?xml version="1\.0" encoding="UTF-8" standalone="no"\?> 29<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1\.1//EN" 30 "http://www\.w3\.org/Graphics/SVG/1\.1/DTD/svg11\.dtd"> 31<svg width="(?P<width>\d+)pt" height="(?P<height>\d+)pt" 32 viewBox="(?P<viewBox>[^"]+)" xmlns="http://www\.w3\.org/2000/svg" xmlns:xlink="http://www\.w3\.org/1999/xlink"> 33<g id="graph0" class="graph" """) 34 35_patch_dst = r"""<svg{attribs} style="width: {width:.3f}rem; height: {height:.3f}rem;" viewBox="{viewBox}"> 36<g """ 37_patch_custom_size_dst = r"""<svg{attribs} style="{size}" viewBox="\g<viewBox>"> 38<g """ 39 40_comment_src = re.compile(r"""<!--[^-]+-->\n""") 41 42# Graphviz < 2.40 (Ubuntu 16.04 and older) doesn't have a linebreak between <g> 43# and <title> 44_class_src = re.compile(r"""<g id="(edge|node)\d+" class="(?P<type>edge|node)(?P<classes>[^"]*)">[\n]?<title>(?P<title>[^<]*)</title> 45<(?P<element>ellipse|polygon|path|text)( fill="(?P<fill>[^"]+)" stroke="[^"]+")? """) 46 47_class_dst = r"""<g class="{classes}"> 48<title>{title}</title> 49<{element} """ 50 51_attributes_src = re.compile(r"""<(?P<element>ellipse|polygon|polyline) fill="[^"]+" stroke="[^"]+" """) 52 53_attributes_dst = r"""<\g<element> """ 54 55# re.compile() is called after replacing {font} in configure(). Graphviz < 2.40 56# doesn't put the fill="" attribute there 57_text_src_src = ' font-family="{font}" font-size="(?P<size>[^"]+)"( fill="[^"]+")?' 58 59_text_dst = ' style="font-size: {size}px;"' 60 61_font = '' 62_font_size = 0.0 63 64# The pt are actually px (16pt font is the same size as 16px), so just 65# converting to rem here 66def _pt2em(pt): return pt/_font_size 67 68def dot2svg(source, size=None, attribs=''): 69 try: 70 ret = subprocess.run(['dot', '-Tsvg', 71 '-Gfontname={}'.format(_font), 72 '-Nfontname={}'.format(_font), 73 '-Efontname={}'.format(_font), 74 '-Gfontsize={}'.format(_font_size), 75 '-Nfontsize={}'.format(_font_size), 76 '-Efontsize={}'.format(_font_size), 77 '-Gbgcolor=transparent', 78 ], input=source.encode('utf-8'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 79 if ret.returncode: print(ret.stderr.decode('utf-8')) 80 ret.check_returncode() 81 except FileNotFoundError: # pragma: no cover 82 raise RuntimeError("dot not found") 83 84 # First remove comments 85 svg = _comment_src.sub('', ret.stdout.decode('utf-8')) 86 87 # Remove preamble and fixed size 88 if size: 89 svg = _patch_src.sub(_patch_custom_size_dst.format(attribs=attribs, size=size), svg) 90 else: 91 def patch_repl(match): return _patch_dst.format( 92 attribs=attribs, 93 width=_pt2em(float(match.group('width'))), 94 height=_pt2em(float(match.group('height'))), 95 viewBox=match.group('viewBox')) 96 svg = _patch_src.sub(patch_repl, svg) 97 98 # Remove unnecessary IDs and attributes, replace classes for elements 99 def element_repl(match): 100 classes = ['m-' + match.group('type')] + match.group('classes').replace('-', '-').split() 101 # distinguish between solid and filled nodes 102 if ((match.group('type') == 'node' and match.group('fill') == 'none') or 103 # a plaintext node is also flat 104 match.group('element') == 'text' 105 ): 106 classes += ['m-flat'] 107 108 return _class_dst.format( 109 classes=' '.join(classes), 110 title=match.group('title'), 111 element=match.group('element')) 112 svg = _class_src.sub(element_repl, svg) 113 114 # Remove unnecessary fill and stroke attributes 115 svg = _attributes_src.sub(_attributes_dst, svg) 116 117 # Remove unnecessary text attributes. Keep font size only if nondefault 118 def text_repl(match): 119 if float(match.group('size')) != _font_size: 120 return _text_dst.format(size=float(match.group('size'))) 121 return '' 122 svg = _text_src.sub(text_repl, svg) 123 124 return svg 125 126def configure(font, font_size): 127 global _font, _font_size, _text_src 128 _font = font 129 _font_size = font_size 130 _text_src = re.compile(_text_src_src.format(font=_font)) 131