File size: 7,539 Bytes
ab4488b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import codecs
import itertools
import re

import six

from anytree import PreOrderIter

_RE_ESC = re.compile(r'["\\]')


class MermaidExporter:

    """
    Mermaid Exporter.

    Args:
        node (Node): start node.

    Keyword Args:
        graph: Mermaid graph type.

        name: Mermaid graph name.

        options: list of options added to the graph.

        indent (int): number of spaces for indent.

        nodenamefunc: Function to extract node name from `node` object.
                      The function shall accept one `node` object as
                      argument and return the name of it.
                      Returns a unique identifier by default.

        nodefunc: Function to decorate a node with attributes.
                      The function shall accept one `node` object as
                      argument and return the attributes.
                      Returns ``[{node.name}]`` and creates therefore a
                      rectangular node by default.

        edgefunc: Function to decorate a edge with attributes.
                  The function shall accept two `node` objects as
                  argument. The first the node and the second the child
                  and return edge.
                  Returns ``-->`` by default.


        filter_: Function to filter nodes to include in export.
                 The function shall accept one `node` object as
                 argument and return True if it should be included,
                 or False if it should not be included.

        stop: stop iteration at `node` if `stop` function returns `True` for `node`.

        maxlevel (int): Limit export to this number of levels.

    >>> from anytree import Node
    >>> root = Node("root")
    >>> s0 = Node("sub0", parent=root, edge=2)
    >>> s0b = Node("sub0B", parent=s0, foo=4, edge=109)
    >>> s0a = Node("sub0A", parent=s0, edge="")
    >>> s1 = Node("sub1", parent=root, edge="")
    >>> s1a = Node("sub1A", parent=s1, edge=7)
    >>> s1b = Node("sub1B", parent=s1, edge=8)
    >>> s1c = Node("sub1C", parent=s1, edge=22)
    >>> s1ca = Node("sub1Ca", parent=s1c, edge=42)

    A top-down graph:

    >>> from anytree.exporter import MermaidExporter
    >>> for line in MermaidExporter(root):
    ...     print(line)
    graph TD
    N0["root"]
    N1["sub0"]
    N2["sub0B"]
    N3["sub0A"]
    N4["sub1"]
    N5["sub1A"]
    N6["sub1B"]
    N7["sub1C"]
    N8["sub1Ca"]
    N0-->N1
    N0-->N4
    N1-->N2
    N1-->N3
    N4-->N5
    N4-->N6
    N4-->N7
    N7-->N8

    A customized graph with round boxes and named arrows:

    >>> def nodefunc(node):
    ...     return '("%s")' % (node.name)
    >>> def edgefunc(node, child):
    ...     return f"--{child.edge}-->"
    >>> options = [
    ...     "%% just an example comment",
    ...     "%% could be an option too",
    ... ]
    >>> for line in MermaidExporter(root, options=options, nodefunc=nodefunc, edgefunc=edgefunc):
    ...     print(line)
    graph TD
    %% just an example comment
    %% could be an option too
    N0("root")
    N1("sub0")
    N2("sub0B")
    N3("sub0A")
    N4("sub1")
    N5("sub1A")
    N6("sub1B")
    N7("sub1C")
    N8("sub1Ca")
    N0--2-->N1
    N0---->N4
    N1--109-->N2
    N1---->N3
    N4--7-->N5
    N4--8-->N6
    N4--22-->N7
    N7--42-->N8
    """

    def __init__(
        self,
        node,
        graph="graph",
        name="TD",
        options=None,
        indent=0,
        nodenamefunc=None,
        nodefunc=None,
        edgefunc=None,
        filter_=None,
        stop=None,
        maxlevel=None,
    ):
        self.node = node
        self.graph = graph
        self.name = name
        self.options = options
        self.indent = indent
        self.nodenamefunc = nodenamefunc
        self.nodefunc = nodefunc
        self.edgefunc = edgefunc
        self.filter_ = filter_
        self.stop = stop
        self.maxlevel = maxlevel
        self.__node_ids = {}
        self.__node_counter = itertools.count()

    def __iter__(self):
        # prepare
        indent = " " * self.indent
        nodenamefunc = self.nodenamefunc or self._default_nodenamefunc
        nodefunc = self.nodefunc or self._default_nodefunc
        edgefunc = self.edgefunc or self._default_edgefunc
        filter_ = self.filter_ or (lambda node: True)
        stop = self.stop or (lambda node: False)
        return self.__iter(indent, nodenamefunc, nodefunc, edgefunc, filter_, stop)

    # pylint: disable=arguments-differ
    def _default_nodenamefunc(self, node):
        node_id = id(node)
        try:
            num = self.__node_ids[node_id]
        except KeyError:
            num = self.__node_ids[node_id] = next(self.__node_counter)
        return "N%d" % (num,)

    @staticmethod
    def _default_nodefunc(node):
        # pylint: disable=W0613
        return '["%s"]' % (MermaidExporter.esc(node.name),)

    @staticmethod
    def _default_edgefunc(node, child):
        # pylint: disable=W0613
        return "-->"

    def __iter(self, indent, nodenamefunc, nodefunc, edgefunc, filter_, stop):
        yield "{self.graph} {self.name}".format(self=self)
        for option in self.__iter_options(indent):
            yield option
        for node in self.__iter_nodes(indent, nodenamefunc, nodefunc, filter_, stop):
            yield node
        for edge in self.__iter_edges(indent, nodenamefunc, edgefunc, filter_, stop):
            yield edge

    def __iter_options(self, indent):
        options = self.options
        if options:
            for option in options:
                yield "%s%s" % (indent, option)

    def __iter_nodes(self, indent, nodenamefunc, nodefunc, filter_, stop):
        for node in PreOrderIter(self.node, filter_=filter_, stop=stop, maxlevel=self.maxlevel):
            nodename = nodenamefunc(node)
            node = nodefunc(node)
            yield "%s%s%s" % (indent, nodename, node)

    def __iter_edges(self, indent, nodenamefunc, edgefunc, filter_, stop):
        maxlevel = self.maxlevel - 1 if self.maxlevel else None
        for node in PreOrderIter(self.node, filter_=filter_, stop=stop, maxlevel=maxlevel):
            nodename = nodenamefunc(node)
            for child in node.children:
                if filter_(child) and not stop(child):
                    childname = nodenamefunc(child)
                    edge = edgefunc(node, child)
                    yield "%s%s%s%s" % (
                        indent,
                        nodename,
                        edge,
                        childname,
                    )

    def to_file(self, filename):
        """
        Write graph to `filename`.

        >>> from anytree import Node
        >>> root = Node("root")
        >>> s0 = Node("sub0", parent=root)
        >>> s0b = Node("sub0B", parent=s0)
        >>> s0a = Node("sub0A", parent=s0)
        >>> s1 = Node("sub1", parent=root)
        >>> s1a = Node("sub1A", parent=s1)
        >>> s1b = Node("sub1B", parent=s1)
        >>> s1c = Node("sub1C", parent=s1)
        >>> s1ca = Node("sub1Ca", parent=s1c)

        >>> from anytree.exporter import MermaidExporter
        >>> MermaidExporter(root).to_file("tree.md")
        """
        with codecs.open(filename, "w", "utf-8") as file:
            file.write("```mermaid\n")
            for line in self:
                file.write("%s\n" % line)
            file.write("```")

    @staticmethod
    def esc(value):
        """Escape Strings."""
        return _RE_ESC.sub(lambda m: r"\%s" % m.group(0), six.text_type(value))