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('&#45;', '-').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