Browse code

Handle xdot backslashes correctly.

Irrespectively of graphviz version.

Fixes https://github.com/jrfonseca/xdot.py/issues/92

Jose Fonseca authored on 28/09/2021 12:19:49
Showing 1 changed files
... ...
@@ -14,8 +14,11 @@
14 14
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 15
 #
16 16
 import colorsys
17
+import re
17 18
 import sys
18 19
 
20
+from distutils.version import LooseVersion
21
+
19 22
 from .lexer import ParseError, DotLexer
20 23
 
21 24
 from ..ui.colors import lookup_color
... ...
@@ -85,7 +88,14 @@ class XDotAttrParser:
85 88
     - http://www.graphviz.org/doc/info/output.html#d:xdot
86 89
     """
87 90
 
88
-    def __init__(self, parser, buf):
91
+    def __init__(self, parser, buf, broken_backslashes):
92
+
93
+        # `\` should be escaped as `\\`, but older versions of graphviz xdot
94
+        # output failed to properly escape it.  See also
95
+        # https://github.com/jrfonseca/xdot.py/issues/92
96
+        if not broken_backslashes:
97
+            buf = re.sub(br'\\(.)', br'\1', buf)
98
+
89 99
         self.parser = parser
90 100
         self.buf = buf
91 101
         self.pos = 0
... ...
@@ -427,10 +437,16 @@ class XDotParser(DotParser):
427 437
 
428 438
     XDOTVERSION = '1.7'
429 439
 
430
-    def __init__(self, xdotcode):
440
+    def __init__(self, xdotcode, graphviz_version=None):
431 441
         lexer = DotLexer(buf=xdotcode)
432 442
         DotParser.__init__(self, lexer)
433 443
 
444
+        # https://github.com/jrfonseca/xdot.py/issues/92
445
+        self.broken_backslashes = False
446
+        if graphviz_version is not None and \
447
+                LooseVersion(graphviz_version) < LooseVersion("2.46.0"):
448
+            self.broken_backslashes = True
449
+
434 450
         self.nodes = []
435 451
         self.edges = []
436 452
         self.shapes = []
... ...
@@ -480,7 +496,7 @@ class XDotParser(DotParser):
480 496
 
481 497
         for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
482 498
             if attr in attrs:
483
-                parser = XDotAttrParser(self, attrs[attr])
499
+                parser = XDotAttrParser(self, attrs[attr], self.broken_backslashes)
484 500
                 self.shapes.extend(parser.parse())
485 501
 
486 502
     def handle_node(self, id, attrs):
... ...
@@ -502,7 +518,7 @@ class XDotParser(DotParser):
502 518
         shapes = []
503 519
         for attr in ("_draw_", "_ldraw_"):
504 520
             if attr in attrs:
505
-                parser = XDotAttrParser(self, attrs[attr])
521
+                parser = XDotAttrParser(self, attrs[attr], self.broken_backslashes)
506 522
                 shapes.extend(parser.parse())
507 523
         try:
508 524
             url = attrs['URL']
... ...
@@ -525,7 +541,7 @@ class XDotParser(DotParser):
525 541
         shapes = []
526 542
         for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
527 543
             if attr in attrs:
528
-                parser = XDotAttrParser(self, attrs[attr])
544
+                parser = XDotAttrParser(self, attrs[attr], self.broken_backslashes)
529 545
                 shapes.extend(parser.parse())
530 546
         if shapes:
531 547
             src = self.node_by_name[src_id]
Browse code

Ignore empty nodes, when a proper node already exists.

As suggested by notEvil.

Fixes https://github.com/jrfonseca/xdot.py/issues/94

Jose Fonseca authored on 04/09/2021 09:14:04
Showing 1 changed files
... ...
@@ -488,11 +488,12 @@ class XDotParser(DotParser):
488 488
             pos = attrs['pos']
489 489
         except KeyError:
490 490
             # Node without pos attribute, most likely a subgraph.  We need to
491
-            # create a Node object nevertheless, so that any edges to/from it
492
-            # don't get lost.
493
-            # TODO: Extract the position from subgraph > graph > bb attribute.
494
-            node = elements.Node(id, 0.0, 0.0, 0.0, 0.0, [], None, None)
495
-            self.node_by_name[id] = node
491
+            # create a Node object nevertheless, when one doesn't exist
492
+            # already, so that any edges to/from it don't get lost.
493
+            if id not in self.node_by_name:
494
+                # TODO: Extract the position from subgraph > graph > bb attribute.
495
+                node = elements.Node(id, 0.0, 0.0, 0.0, 0.0, [], None, None)
496
+                self.node_by_name[id] = node
496 497
             return
497 498
 
498 499
         x, y = self.parse_node_pos(pos)
Browse code

Add support for tooltips.

- added support for tooltips
- fixed issue related to residual tooltip windows
- added tooltip window resize to label size

See https://github.com/jrfonseca/xdot.py/pull/81

Fixes https://github.com/jrfonseca/xdot.py/issues/71

notEvil authored on 06/07/2020 09:29:39 • Jose Fonseca committed on 20/07/2020 11:07:52
Showing 1 changed files
... ...
@@ -491,7 +491,7 @@ class XDotParser(DotParser):
491 491
             # create a Node object nevertheless, so that any edges to/from it
492 492
             # don't get lost.
493 493
             # TODO: Extract the position from subgraph > graph > bb attribute.
494
-            node = elements.Node(id, 0.0, 0.0, 0.0, 0.0, [], None)
494
+            node = elements.Node(id, 0.0, 0.0, 0.0, 0.0, [], None, None)
495 495
             self.node_by_name[id] = node
496 496
             return
497 497
 
... ...
@@ -509,7 +509,7 @@ class XDotParser(DotParser):
509 509
             url = None
510 510
         else:
511 511
             url = url.decode('utf-8')
512
-        node = elements.Node(id, x, y, w, h, shapes, url)
512
+        node = elements.Node(id, x, y, w, h, shapes, url, attrs.get("tooltip"))
513 513
         self.node_by_name[id] = node
514 514
         if shapes:
515 515
             self.nodes.append(node)
... ...
@@ -529,7 +529,7 @@ class XDotParser(DotParser):
529 529
         if shapes:
530 530
             src = self.node_by_name[src_id]
531 531
             dst = self.node_by_name[dst_id]
532
-            self.edges.append(elements.Edge(src, dst, points, shapes))
532
+            self.edges.append(elements.Edge(src, dst, points, shapes, attrs.get("tooltip")))
533 533
 
534 534
     def parse(self):
535 535
         DotParser.parse(self)
Browse code

Add consume method call.

It was missing as pointed out by sunziping2016.

Fixes https://github.com/jrfonseca/xdot.py/issues/74

Jose Fonseca authored on 04/01/2020 11:44:23
Showing 1 changed files
... ...
@@ -354,6 +354,7 @@ class DotParser(Parser):
354 354
                 self.consume()
355 355
                 node_ids = [id, self.parse_node_id()]
356 356
                 while self.lookahead.type == EDGE_OP:
357
+                    self.consume()
357 358
                     node_ids.append(self.parse_node_id())
358 359
                 attrs = self.parse_attrs()
359 360
                 for i in range(0, len(node_ids) - 1):
Browse code

Support "outputorder" attribute when drawing.

https://github.com/jrfonseca/xdot.py/pull/68

Tal Ben-Nun authored on 10/03/2019 16:55:23 • Jose Fonseca committed on 05/04/2019 12:56:33
Showing 1 changed files
... ...
@@ -437,6 +437,7 @@ class XDotParser(DotParser):
437 437
         self.top_graph = True
438 438
         self.width = 0
439 439
         self.height = 0
440
+        self.outputorder = 'breadthfirst'
440 441
 
441 442
     def handle_graph(self, attrs):
442 443
         if self.top_graph:
... ...
@@ -450,6 +451,12 @@ class XDotParser(DotParser):
450 451
                     sys.stderr.write('warning: xdot version %s, but supported is %s\n' %
451 452
                                      (xdotversion, self.XDOTVERSION))
452 453
 
454
+            # Parse output order
455
+            try:
456
+                self.outputorder = attrs['outputorder'].decode('utf-8')
457
+            except KeyError:
458
+                pass
459
+
453 460
             # Parse bounding box
454 461
             try:
455 462
                 bb = attrs['bb']
... ...
@@ -526,7 +533,7 @@ class XDotParser(DotParser):
526 533
     def parse(self):
527 534
         DotParser.parse(self)
528 535
         return elements.Graph(self.width, self.height, self.shapes,
529
-                              self.nodes, self.edges)
536
+                              self.nodes, self.edges, self.outputorder)
530 537
 
531 538
     def parse_node_pos(self, pos):
532 539
         x, y = pos.split(b",")
Browse code

Treat subgraphs as nodes.

In particular, don't lose edges to/from subgraphs.

Fixes https://github.com/jrfonseca/xdot.py/issues/65

Jose Fonseca authored on 28/12/2018 11:02:45
Showing 1 changed files
... ...
@@ -325,6 +325,8 @@ class DotParser(Parser):
325 325
             if self.lookahead.type == ID:
326 326
                 id = self.lookahead.text
327 327
                 self.consume()
328
+                # A subgraph is also a node.
329
+                self.handle_node(id, {})
328 330
         if self.lookahead.type == LCURLY:
329 331
             self.consume()
330 332
             while self.lookahead.type != RCURLY:
... ...
@@ -477,6 +479,12 @@ class XDotParser(DotParser):
477 479
         try:
478 480
             pos = attrs['pos']
479 481
         except KeyError:
482
+            # Node without pos attribute, most likely a subgraph.  We need to
483
+            # create a Node object nevertheless, so that any edges to/from it
484
+            # don't get lost.
485
+            # TODO: Extract the position from subgraph > graph > bb attribute.
486
+            node = elements.Node(id, 0.0, 0.0, 0.0, 0.0, [], None)
487
+            self.node_by_name[id] = node
480 488
             return
481 489
 
482 490
         x, y = self.parse_node_pos(pos)
Browse code

Don't decode missing URLs.

Fixes https://github.com/jrfonseca/xdot.py/issues/39

Jose Fonseca authored on 15/11/2016 15:59:43
Showing 1 changed files
... ...
@@ -487,7 +487,12 @@ class XDotParser(DotParser):
487 487
             if attr in attrs:
488 488
                 parser = XDotAttrParser(self, attrs[attr])
489 489
                 shapes.extend(parser.parse())
490
-        url = attrs.get('URL', None).decode('utf-8')
490
+        try:
491
+            url = attrs['URL']
492
+        except KeyError:
493
+            url = None
494
+        else:
495
+            url = url.decode('utf-8')
491 496
         node = elements.Node(id, x, y, w, h, shapes, url)
492 497
         self.node_by_name[id] = node
493 498
         if shapes:
Browse code

Fix emiting clicked signal with url as bytes.

Marek Rusinowski authored on 03/10/2016 18:45:11 • José Fonseca committed on 04/10/2016 07:15:36
Showing 1 changed files
... ...
@@ -487,7 +487,7 @@ class XDotParser(DotParser):
487 487
             if attr in attrs:
488 488
                 parser = XDotAttrParser(self, attrs[attr])
489 489
                 shapes.extend(parser.parse())
490
-        url = attrs.get('URL', None)
490
+        url = attrs.get('URL', None).decode('utf-8')
491 491
         node = elements.Node(id, x, y, w, h, shapes, url)
492 492
         self.node_by_name[id] = node
493 493
         if shapes:
Browse code

Fix British spelling of "colour"

Peter Hill authored on 02/07/2016 15:58:34 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -18,7 +18,7 @@ import sys
18 18
 
19 19
 from .lexer import ParseError, DotLexer
20 20
 
21
-from ..ui.colours import lookup_color
21
+from ..ui.colors import lookup_color
22 22
 from ..ui.pen import Pen
23 23
 from ..ui import elements
24 24
 
Browse code

Move lookup_color into ui.colours

Now all dependencies on GUI library are in xdot.ui

Peter Hill authored on 02/07/2016 15:34:17 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -16,15 +16,9 @@
16 16
 import colorsys
17 17
 import sys
18 18
 
19
-import gi
20
-gi.require_version('Gtk', '3.0')
21
-gi.require_version('PangoCairo', '1.0')
22
-
23
-from gi.repository import Gdk
24
-
25 19
 from .lexer import ParseError, DotLexer
26 20
 
27
-from ..ui.colours import brewer_colors
21
+from ..ui.colours import lookup_color
28 22
 from ..ui.pen import Pen
29 23
 from ..ui import elements
30 24
 
... ...
@@ -166,36 +160,7 @@ class XDotAttrParser:
166 160
             sys.stderr.write('warning: color gradients not supported yet\n')
167 161
             return None
168 162
         else:
169
-            return self.lookup_color(c)
170
-
171
-    def lookup_color(self, c):
172
-        try:
173
-            color = Gdk.color_parse(c)
174
-        except ValueError:
175
-            pass
176
-        else:
177
-            s = 1.0/65535.0
178
-            r = color.red*s
179
-            g = color.green*s
180
-            b = color.blue*s
181
-            a = 1.0
182
-            return r, g, b, a
183
-
184
-        try:
185
-            dummy, scheme, index = c.split('/')
186
-            r, g, b = brewer_colors[scheme][int(index)]
187
-        except (ValueError, KeyError):
188
-            pass
189
-        else:
190
-            s = 1.0/255.0
191
-            r = r*s
192
-            g = g*s
193
-            b = b*s
194
-            a = 1.0
195
-            return r, g, b, a
196
-
197
-        sys.stderr.write("warning: unknown color '%s'\n" % c)
198
-        return None
163
+            return lookup_color(c)
199 164
 
200 165
     def parse(self):
201 166
         s = self
Browse code

Make internal imports more explicit

Peter Hill authored on 02/07/2016 10:30:50 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -25,12 +25,8 @@ from gi.repository import Gdk
25 25
 from .lexer import ParseError, DotLexer
26 26
 
27 27
 from ..ui.colours import brewer_colors
28
-from ..ui.pen import (Pen, BOLD, ITALIC, UNDERLINE, SUPERSCRIPT,
29
-                      SUBSCRIPT, STRIKE_THROUGH, OVERLINE)
30
-from ..ui.elements import (TextShape, ImageShape, EllipseShape,
31
-                           PolygonShape, LineShape, BezierShape,
32
-                           CompoundShape, Url, Jump, Element,
33
-                           Node, Edge, Graph)
28
+from ..ui.pen import Pen
29
+from ..ui import elements
34 30
 
35 31
 
36 32
 EOF = -1
... ...
@@ -298,42 +294,42 @@ class XDotAttrParser:
298 294
         self.pen.fontname = name
299 295
 
300 296
     def handle_font_characteristics(self, flags):
301
-        self.pen.bold = bool(flags & BOLD)
302
-        self.pen.italic = bool(flags & ITALIC)
303
-        self.pen.underline = bool(flags & UNDERLINE)
304
-        self.pen.superscript = bool(flags & SUPERSCRIPT)
305
-        self.pen.subscript = bool(flags & SUBSCRIPT)
306
-        self.pen.strikethrough = bool(flags & STRIKE_THROUGH)
307
-        self.pen.overline = bool(flags & OVERLINE)
297
+        self.pen.bold = bool(flags & Pen.BOLD)
298
+        self.pen.italic = bool(flags & Pen.ITALIC)
299
+        self.pen.underline = bool(flags & Pen.UNDERLINE)
300
+        self.pen.superscript = bool(flags & Pen.SUPERSCRIPT)
301
+        self.pen.subscript = bool(flags & Pen.SUBSCRIPT)
302
+        self.pen.strikethrough = bool(flags & Pen.STRIKE_THROUGH)
303
+        self.pen.overline = bool(flags & Pen.OVERLINE)
308 304
         if self.pen.overline:
309 305
             sys.stderr.write('warning: overlined text not supported yet\n')
310 306
 
311 307
     def handle_text(self, x, y, j, w, t):
312
-        self.shapes.append(TextShape(self.pen, x, y, j, w, t))
308
+        self.shapes.append(elements.TextShape(self.pen, x, y, j, w, t))
313 309
 
314 310
     def handle_ellipse(self, x0, y0, w, h, filled=False):
315 311
         if filled:
316 312
             # xdot uses this to mean "draw a filled shape with an outline"
317
-            self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
318
-        self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
313
+            self.shapes.append(elements.EllipseShape(self.pen, x0, y0, w, h, filled=True))
314
+        self.shapes.append(elements.EllipseShape(self.pen, x0, y0, w, h))
319 315
 
320 316
     def handle_image(self, x0, y0, w, h, path):
321
-        self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
317
+        self.shapes.append(elements.ImageShape(self.pen, x0, y0, w, h, path))
322 318
 
323 319
     def handle_line(self, points):
324
-        self.shapes.append(LineShape(self.pen, points))
320
+        self.shapes.append(elements.LineShape(self.pen, points))
325 321
 
326 322
     def handle_bezier(self, points, filled=False):
327 323
         if filled:
328 324
             # xdot uses this to mean "draw a filled shape with an outline"
329
-            self.shapes.append(BezierShape(self.pen, points, filled=True))
330
-        self.shapes.append(BezierShape(self.pen, points))
325
+            self.shapes.append(elements.BezierShape(self.pen, points, filled=True))
326
+        self.shapes.append(elements.BezierShape(self.pen, points))
331 327
 
332 328
     def handle_polygon(self, points, filled=False):
333 329
         if filled:
334 330
             # xdot uses this to mean "draw a filled shape with an outline"
335
-            self.shapes.append(PolygonShape(self.pen, points, filled=True))
336
-        self.shapes.append(PolygonShape(self.pen, points))
331
+            self.shapes.append(elements.PolygonShape(self.pen, points, filled=True))
332
+        self.shapes.append(elements.PolygonShape(self.pen, points))
337 333
 
338 334
 
339 335
 class DotParser(Parser):
... ...
@@ -527,7 +523,7 @@ class XDotParser(DotParser):
527 523
                 parser = XDotAttrParser(self, attrs[attr])
528 524
                 shapes.extend(parser.parse())
529 525
         url = attrs.get('URL', None)
530
-        node = Node(id, x, y, w, h, shapes, url)
526
+        node = elements.Node(id, x, y, w, h, shapes, url)
531 527
         self.node_by_name[id] = node
532 528
         if shapes:
533 529
             self.nodes.append(node)
... ...
@@ -547,11 +543,12 @@ class XDotParser(DotParser):
547 543
         if shapes:
548 544
             src = self.node_by_name[src_id]
549 545
             dst = self.node_by_name[dst_id]
550
-            self.edges.append(Edge(src, dst, points, shapes))
546
+            self.edges.append(elements.Edge(src, dst, points, shapes))
551 547
 
552 548
     def parse(self):
553 549
         DotParser.parse(self)
554
-        return Graph(self.width, self.height, self.shapes, self.nodes, self.edges)
550
+        return elements.Graph(self.width, self.height, self.shapes,
551
+                              self.nodes, self.edges)
555 552
 
556 553
     def parse_node_pos(self, pos):
557 554
         x, y = pos.split(b",")
Browse code

Fix most flake8 errors

Peter Hill authored on 02/07/2016 10:27:27 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -68,19 +68,19 @@ class Parser:
68 68
     def match(self, type):
69 69
         if self.lookahead.type != type:
70 70
             raise ParseError(
71
-                msg = 'unexpected token %r' % self.lookahead.text,
72
-                filename = self.lexer.filename,
73
-                line = self.lookahead.line,
74
-                col = self.lookahead.col)
71
+                msg='unexpected token {}'.format(self.lookahead.text),
72
+                filename=self.lexer.filename,
73
+                line=self.lookahead.line,
74
+                col=self.lookahead.col)
75 75
 
76 76
     def skip(self, type):
77 77
         while self.lookahead.type != type:
78 78
             if self.lookahead.type == EOF:
79 79
                 raise ParseError(
80
-                   msg = 'unexpected end of file',
81
-                   filename = self.lexer.filename,
82
-                   line = self.lookahead.line,
83
-                   col = self.lookahead.col)
80
+                    msg='unexpected end of file',
81
+                    filename=self.lexer.filename,
82
+                    line=self.lookahead.line,
83
+                    col=self.lookahead.col)
84 84
             self.consume()
85 85
 
86 86
     def consume(self):
... ...
@@ -115,7 +115,7 @@ class XDotAttrParser:
115 115
         return res
116 116
 
117 117
     def skip_space(self):
118
-        while self.pos < len(self.buf) and self.buf[self.pos : self.pos + 1].isspace():
118
+        while self.pos < len(self.buf) and self.buf[self.pos:self.pos+1].isspace():
119 119
             self.pos += 1
120 120
 
121 121
     def read_int(self):
... ...
@@ -439,7 +439,8 @@ class DotParser(Parser):
439 439
         else:
440 440
             port = None
441 441
             compass_pt = None
442
-        # XXX: we don't really care about port and compass point values when parsing xdot
442
+            # XXX: we don't really care about port and compass point
443
+            # values when parsing xdot
443 444
         return node_id
444 445
 
445 446
     def parse_id(self):
... ...
@@ -463,7 +464,7 @@ class XDotParser(DotParser):
463 464
     XDOTVERSION = '1.7'
464 465
 
465 466
     def __init__(self, xdotcode):
466
-        lexer = DotLexer(buf = xdotcode)
467
+        lexer = DotLexer(buf=xdotcode)
467 468
         DotParser.__init__(self, lexer)
468 469
 
469 470
         self.nodes = []
... ...
@@ -483,7 +484,8 @@ class XDotParser(DotParser):
483 484
                 pass
484 485
             else:
485 486
                 if float(xdotversion) > float(self.XDOTVERSION):
486
-                    sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
487
+                    sys.stderr.write('warning: xdot version %s, but supported is %s\n' %
488
+                                     (xdotversion, self.XDOTVERSION))
487 489
 
488 490
             # Parse bounding box
489 491
             try:
... ...
@@ -500,7 +502,7 @@ class XDotParser(DotParser):
500 502
                 self.yscale = -1.0
501 503
                 # FIXME: scale from points to pixels
502 504
 
503
-                self.width  = max(xmax - xmin, 1)
505
+                self.width = max(xmax - xmin, 1)
504 506
                 self.height = max(ymax - ymin, 1)
505 507
 
506 508
                 self.top_graph = False
Browse code

Cleaner splitting into separate modules

Peter Hill authored on 02/07/2016 09:45:05 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,575 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import colorsys
17
+import sys
18
+
19
+import gi
20
+gi.require_version('Gtk', '3.0')
21
+gi.require_version('PangoCairo', '1.0')
22
+
23
+from gi.repository import Gdk
24
+
25
+from .lexer import ParseError, DotLexer
26
+
27
+from ..ui.colours import brewer_colors
28
+from ..ui.pen import (Pen, BOLD, ITALIC, UNDERLINE, SUPERSCRIPT,
29
+                      SUBSCRIPT, STRIKE_THROUGH, OVERLINE)
30
+from ..ui.elements import (TextShape, ImageShape, EllipseShape,
31
+                           PolygonShape, LineShape, BezierShape,
32
+                           CompoundShape, Url, Jump, Element,
33
+                           Node, Edge, Graph)
34
+
35
+
36
+EOF = -1
37
+SKIP = -2
38
+
39
+ID = 0
40
+STR_ID = 1
41
+HTML_ID = 2
42
+EDGE_OP = 3
43
+
44
+LSQUARE = 4
45
+RSQUARE = 5
46
+LCURLY = 6
47
+RCURLY = 7
48
+COMMA = 8
49
+COLON = 9
50
+SEMI = 10
51
+EQUAL = 11
52
+PLUS = 12
53
+
54
+STRICT = 13
55
+GRAPH = 14
56
+DIGRAPH = 15
57
+NODE = 16
58
+EDGE = 17
59
+SUBGRAPH = 18
60
+
61
+
62
+class Parser:
63
+
64
+    def __init__(self, lexer):
65
+        self.lexer = lexer
66
+        self.lookahead = next(self.lexer)
67
+
68
+    def match(self, type):
69
+        if self.lookahead.type != type:
70
+            raise ParseError(
71
+                msg = 'unexpected token %r' % self.lookahead.text,
72
+                filename = self.lexer.filename,
73
+                line = self.lookahead.line,
74
+                col = self.lookahead.col)
75
+
76
+    def skip(self, type):
77
+        while self.lookahead.type != type:
78
+            if self.lookahead.type == EOF:
79
+                raise ParseError(
80
+                   msg = 'unexpected end of file',
81
+                   filename = self.lexer.filename,
82
+                   line = self.lookahead.line,
83
+                   col = self.lookahead.col)
84
+            self.consume()
85
+
86
+    def consume(self):
87
+        token = self.lookahead
88
+        self.lookahead = next(self.lexer)
89
+        return token
90
+
91
+
92
+class XDotAttrParser:
93
+    """Parser for xdot drawing attributes.
94
+    See also:
95
+    - http://www.graphviz.org/doc/info/output.html#d:xdot
96
+    """
97
+
98
+    def __init__(self, parser, buf):
99
+        self.parser = parser
100
+        self.buf = buf
101
+        self.pos = 0
102
+
103
+        self.pen = Pen()
104
+        self.shapes = []
105
+
106
+    def __bool__(self):
107
+        return self.pos < len(self.buf)
108
+
109
+    def read_code(self):
110
+        pos = self.buf.find(b" ", self.pos)
111
+        res = self.buf[self.pos:pos]
112
+        self.pos = pos + 1
113
+        self.skip_space()
114
+        res = res.decode('utf-8')
115
+        return res
116
+
117
+    def skip_space(self):
118
+        while self.pos < len(self.buf) and self.buf[self.pos : self.pos + 1].isspace():
119
+            self.pos += 1
120
+
121
+    def read_int(self):
122
+        return int(self.read_code())
123
+
124
+    def read_float(self):
125
+        return float(self.read_code())
126
+
127
+    def read_point(self):
128
+        x = self.read_float()
129
+        y = self.read_float()
130
+        return self.transform(x, y)
131
+
132
+    def read_text(self):
133
+        num = self.read_int()
134
+        pos = self.buf.find(b"-", self.pos) + 1
135
+        self.pos = pos + num
136
+        res = self.buf[pos:self.pos]
137
+        self.skip_space()
138
+        res = res.decode('utf-8')
139
+        return res
140
+
141
+    def read_polygon(self):
142
+        n = self.read_int()
143
+        p = []
144
+        for i in range(n):
145
+            x, y = self.read_point()
146
+            p.append((x, y))
147
+        return p
148
+
149
+    def read_color(self):
150
+        # See http://www.graphviz.org/doc/info/attrs.html#k:color
151
+        c = self.read_text()
152
+        c1 = c[:1]
153
+        if c1 == '#':
154
+            hex2float = lambda h: float(int(h, 16)/255.0)
155
+            r = hex2float(c[1:3])
156
+            g = hex2float(c[3:5])
157
+            b = hex2float(c[5:7])
158
+            try:
159
+                a = hex2float(c[7:9])
160
+            except (IndexError, ValueError):
161
+                a = 1.0
162
+            return r, g, b, a
163
+        elif c1.isdigit() or c1 == ".":
164
+            # "H,S,V" or "H S V" or "H, S, V" or any other variation
165
+            h, s, v = map(float, c.replace(",", " ").split())
166
+            r, g, b = colorsys.hsv_to_rgb(h, s, v)
167
+            a = 1.0
168
+            return r, g, b, a
169
+        elif c1 == "[" or c1 == "(":
170
+            sys.stderr.write('warning: color gradients not supported yet\n')
171
+            return None
172
+        else:
173
+            return self.lookup_color(c)
174
+
175
+    def lookup_color(self, c):
176
+        try:
177
+            color = Gdk.color_parse(c)
178
+        except ValueError:
179
+            pass
180
+        else:
181
+            s = 1.0/65535.0
182
+            r = color.red*s
183
+            g = color.green*s
184
+            b = color.blue*s
185
+            a = 1.0
186
+            return r, g, b, a
187
+
188
+        try:
189
+            dummy, scheme, index = c.split('/')
190
+            r, g, b = brewer_colors[scheme][int(index)]
191
+        except (ValueError, KeyError):
192
+            pass
193
+        else:
194
+            s = 1.0/255.0
195
+            r = r*s
196
+            g = g*s
197
+            b = b*s
198
+            a = 1.0
199
+            return r, g, b, a
200
+
201
+        sys.stderr.write("warning: unknown color '%s'\n" % c)
202
+        return None
203
+
204
+    def parse(self):
205
+        s = self
206
+
207
+        while s:
208
+            op = s.read_code()
209
+            if op == "c":
210
+                color = s.read_color()
211
+                if color is not None:
212
+                    self.handle_color(color, filled=False)
213
+            elif op == "C":
214
+                color = s.read_color()
215
+                if color is not None:
216
+                    self.handle_color(color, filled=True)
217
+            elif op == "S":
218
+                # http://www.graphviz.org/doc/info/attrs.html#k:style
219
+                style = s.read_text()
220
+                if style.startswith("setlinewidth("):
221
+                    lw = style.split("(")[1].split(")")[0]
222
+                    lw = float(lw)
223
+                    self.handle_linewidth(lw)
224
+                elif style in ("solid", "dashed", "dotted"):
225
+                    self.handle_linestyle(style)
226
+            elif op == "F":
227
+                size = s.read_float()
228
+                name = s.read_text()
229
+                self.handle_font(size, name)
230
+            elif op == "T":
231
+                x, y = s.read_point()
232
+                j = s.read_int()
233
+                w = s.read_float()
234
+                t = s.read_text()
235
+                self.handle_text(x, y, j, w, t)
236
+            elif op == "t":
237
+                f = s.read_int()
238
+                self.handle_font_characteristics(f)
239
+            elif op == "E":
240
+                x0, y0 = s.read_point()
241
+                w = s.read_float()
242
+                h = s.read_float()
243
+                self.handle_ellipse(x0, y0, w, h, filled=True)
244
+            elif op == "e":
245
+                x0, y0 = s.read_point()
246
+                w = s.read_float()
247
+                h = s.read_float()
248
+                self.handle_ellipse(x0, y0, w, h, filled=False)
249
+            elif op == "L":
250
+                points = self.read_polygon()
251
+                self.handle_line(points)
252
+            elif op == "B":
253
+                points = self.read_polygon()
254
+                self.handle_bezier(points, filled=False)
255
+            elif op == "b":
256
+                points = self.read_polygon()
257
+                self.handle_bezier(points, filled=True)
258
+            elif op == "P":
259
+                points = self.read_polygon()
260
+                self.handle_polygon(points, filled=True)
261
+            elif op == "p":
262
+                points = self.read_polygon()
263
+                self.handle_polygon(points, filled=False)
264
+            elif op == "I":
265
+                x0, y0 = s.read_point()
266
+                w = s.read_float()
267
+                h = s.read_float()
268
+                path = s.read_text()
269
+                self.handle_image(x0, y0, w, h, path)
270
+            else:
271
+                sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
272
+                sys.exit(1)
273
+
274
+        return self.shapes
275
+
276
+    def transform(self, x, y):
277
+        return self.parser.transform(x, y)
278
+
279
+    def handle_color(self, color, filled=False):
280
+        if filled:
281
+            self.pen.fillcolor = color
282
+        else:
283
+            self.pen.color = color
284
+
285
+    def handle_linewidth(self, linewidth):
286
+        self.pen.linewidth = linewidth
287
+
288
+    def handle_linestyle(self, style):
289
+        if style == "solid":
290
+            self.pen.dash = ()
291
+        elif style == "dashed":
292
+            self.pen.dash = (6, )       # 6pt on, 6pt off
293
+        elif style == "dotted":
294
+            self.pen.dash = (2, 4)       # 2pt on, 4pt off
295
+
296
+    def handle_font(self, size, name):
297
+        self.pen.fontsize = size
298
+        self.pen.fontname = name
299
+
300
+    def handle_font_characteristics(self, flags):
301
+        self.pen.bold = bool(flags & BOLD)
302
+        self.pen.italic = bool(flags & ITALIC)
303
+        self.pen.underline = bool(flags & UNDERLINE)
304
+        self.pen.superscript = bool(flags & SUPERSCRIPT)
305
+        self.pen.subscript = bool(flags & SUBSCRIPT)
306
+        self.pen.strikethrough = bool(flags & STRIKE_THROUGH)
307
+        self.pen.overline = bool(flags & OVERLINE)
308
+        if self.pen.overline:
309
+            sys.stderr.write('warning: overlined text not supported yet\n')
310
+
311
+    def handle_text(self, x, y, j, w, t):
312
+        self.shapes.append(TextShape(self.pen, x, y, j, w, t))
313
+
314
+    def handle_ellipse(self, x0, y0, w, h, filled=False):
315
+        if filled:
316
+            # xdot uses this to mean "draw a filled shape with an outline"
317
+            self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
318
+        self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
319
+
320
+    def handle_image(self, x0, y0, w, h, path):
321
+        self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
322
+
323
+    def handle_line(self, points):
324
+        self.shapes.append(LineShape(self.pen, points))
325
+
326
+    def handle_bezier(self, points, filled=False):
327
+        if filled:
328
+            # xdot uses this to mean "draw a filled shape with an outline"
329
+            self.shapes.append(BezierShape(self.pen, points, filled=True))
330
+        self.shapes.append(BezierShape(self.pen, points))
331
+
332
+    def handle_polygon(self, points, filled=False):
333
+        if filled:
334
+            # xdot uses this to mean "draw a filled shape with an outline"
335
+            self.shapes.append(PolygonShape(self.pen, points, filled=True))
336
+        self.shapes.append(PolygonShape(self.pen, points))
337
+
338
+
339
+class DotParser(Parser):
340
+
341
+    def __init__(self, lexer):
342
+        Parser.__init__(self, lexer)
343
+        self.graph_attrs = {}
344
+        self.node_attrs = {}
345
+        self.edge_attrs = {}
346
+
347
+    def parse(self):
348
+        self.parse_graph()
349
+        self.match(EOF)
350
+
351
+    def parse_graph(self):
352
+        if self.lookahead.type == STRICT:
353
+            self.consume()
354
+        self.skip(LCURLY)
355
+        self.consume()
356
+        while self.lookahead.type != RCURLY:
357
+            self.parse_stmt()
358
+        self.consume()
359
+
360
+    def parse_subgraph(self):
361
+        id = None
362
+        if self.lookahead.type == SUBGRAPH:
363
+            self.consume()
364
+            if self.lookahead.type == ID:
365
+                id = self.lookahead.text
366
+                self.consume()
367
+        if self.lookahead.type == LCURLY:
368
+            self.consume()
369
+            while self.lookahead.type != RCURLY:
370
+                self.parse_stmt()
371
+            self.consume()
372
+        return id
373
+
374
+    def parse_stmt(self):
375
+        if self.lookahead.type == GRAPH:
376
+            self.consume()
377
+            attrs = self.parse_attrs()
378
+            self.graph_attrs.update(attrs)
379
+            self.handle_graph(attrs)
380
+        elif self.lookahead.type == NODE:
381
+            self.consume()
382
+            self.node_attrs.update(self.parse_attrs())
383
+        elif self.lookahead.type == EDGE:
384
+            self.consume()
385
+            self.edge_attrs.update(self.parse_attrs())
386
+        elif self.lookahead.type in (SUBGRAPH, LCURLY):
387
+            self.parse_subgraph()
388
+        else:
389
+            id = self.parse_node_id()
390
+            if self.lookahead.type == EDGE_OP:
391
+                self.consume()
392
+                node_ids = [id, self.parse_node_id()]
393
+                while self.lookahead.type == EDGE_OP:
394
+                    node_ids.append(self.parse_node_id())
395
+                attrs = self.parse_attrs()
396
+                for i in range(0, len(node_ids) - 1):
397
+                    self.handle_edge(node_ids[i], node_ids[i + 1], attrs)
398
+            elif self.lookahead.type == EQUAL:
399
+                self.consume()
400
+                self.parse_id()
401
+            else:
402
+                attrs = self.parse_attrs()
403
+                self.handle_node(id, attrs)
404
+        if self.lookahead.type == SEMI:
405
+            self.consume()
406
+
407
+    def parse_attrs(self):
408
+        attrs = {}
409
+        while self.lookahead.type == LSQUARE:
410
+            self.consume()
411
+            while self.lookahead.type != RSQUARE:
412
+                name, value = self.parse_attr()
413
+                name = name.decode('utf-8')
414
+                attrs[name] = value
415
+                if self.lookahead.type == COMMA:
416
+                    self.consume()
417
+            self.consume()
418
+        return attrs
419
+
420
+    def parse_attr(self):
421
+        name = self.parse_id()
422
+        if self.lookahead.type == EQUAL:
423
+            self.consume()
424
+            value = self.parse_id()
425
+        else:
426
+            value = b'true'
427
+        return name, value
428
+
429
+    def parse_node_id(self):
430
+        node_id = self.parse_id()
431
+        if self.lookahead.type == COLON:
432
+            self.consume()
433
+            port = self.parse_id()
434
+            if self.lookahead.type == COLON:
435
+                self.consume()
436
+                compass_pt = self.parse_id()
437
+            else:
438
+                compass_pt = None
439
+        else:
440
+            port = None
441
+            compass_pt = None
442
+        # XXX: we don't really care about port and compass point values when parsing xdot
443
+        return node_id
444
+
445
+    def parse_id(self):
446
+        self.match(ID)
447
+        id = self.lookahead.text
448
+        self.consume()
449
+        return id
450
+
451
+    def handle_graph(self, attrs):
452
+        pass
453
+
454
+    def handle_node(self, id, attrs):
455
+        pass
456
+
457
+    def handle_edge(self, src_id, dst_id, attrs):
458
+        pass
459
+
460
+
461
+class XDotParser(DotParser):
462
+
463
+    XDOTVERSION = '1.7'
464
+
465
+    def __init__(self, xdotcode):
466
+        lexer = DotLexer(buf = xdotcode)
467
+        DotParser.__init__(self, lexer)
468
+
469
+        self.nodes = []
470
+        self.edges = []
471
+        self.shapes = []
472
+        self.node_by_name = {}
473
+        self.top_graph = True
474
+        self.width = 0
475
+        self.height = 0
476
+
477
+    def handle_graph(self, attrs):
478
+        if self.top_graph:
479
+            # Check xdot version
480
+            try:
481
+                xdotversion = attrs['xdotversion']
482
+            except KeyError:
483
+                pass
484
+            else:
485
+                if float(xdotversion) > float(self.XDOTVERSION):
486
+                    sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
487
+
488
+            # Parse bounding box
489
+            try:
490
+                bb = attrs['bb']
491
+            except KeyError:
492
+                return
493
+
494
+            if bb:
495
+                xmin, ymin, xmax, ymax = map(float, bb.split(b","))
496
+
497
+                self.xoffset = -xmin
498
+                self.yoffset = -ymax
499
+                self.xscale = 1.0
500
+                self.yscale = -1.0
501
+                # FIXME: scale from points to pixels
502
+
503
+                self.width  = max(xmax - xmin, 1)
504
+                self.height = max(ymax - ymin, 1)
505
+
506
+                self.top_graph = False
507
+
508
+        for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
509
+            if attr in attrs:
510
+                parser = XDotAttrParser(self, attrs[attr])
511
+                self.shapes.extend(parser.parse())
512
+
513
+    def handle_node(self, id, attrs):
514
+        try:
515
+            pos = attrs['pos']
516
+        except KeyError:
517
+            return
518
+
519
+        x, y = self.parse_node_pos(pos)
520
+        w = float(attrs.get('width', 0))*72
521
+        h = float(attrs.get('height', 0))*72
522
+        shapes = []
523
+        for attr in ("_draw_", "_ldraw_"):
524
+            if attr in attrs:
525
+                parser = XDotAttrParser(self, attrs[attr])
526
+                shapes.extend(parser.parse())
527
+        url = attrs.get('URL', None)
528
+        node = Node(id, x, y, w, h, shapes, url)
529
+        self.node_by_name[id] = node
530
+        if shapes:
531
+            self.nodes.append(node)
532
+
533
+    def handle_edge(self, src_id, dst_id, attrs):
534
+        try:
535
+            pos = attrs['pos']
536
+        except KeyError:
537
+            return
538
+
539
+        points = self.parse_edge_pos(pos)
540
+        shapes = []
541
+        for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
542
+            if attr in attrs:
543
+                parser = XDotAttrParser(self, attrs[attr])
544
+                shapes.extend(parser.parse())
545
+        if shapes:
546
+            src = self.node_by_name[src_id]
547
+            dst = self.node_by_name[dst_id]
548
+            self.edges.append(Edge(src, dst, points, shapes))
549
+
550
+    def parse(self):
551
+        DotParser.parse(self)
552
+        return Graph(self.width, self.height, self.shapes, self.nodes, self.edges)
553
+
554
+    def parse_node_pos(self, pos):
555
+        x, y = pos.split(b",")
556
+        return self.transform(float(x), float(y))
557
+
558
+    def parse_edge_pos(self, pos):
559
+        points = []
560
+        for entry in pos.split(b' '):
561
+            fields = entry.split(b',')
562
+            try:
563
+                x, y = fields
564
+            except ValueError:
565
+                # TODO: handle start/end points
566
+                continue
567
+            else:
568
+                points.append(self.transform(float(x), float(y)))
569
+        return points
570
+
571
+    def transform(self, x, y):
572
+        # XXX: this is not the right place for this code
573
+        x = (x + self.xoffset)*self.xscale
574
+        y = (y + self.yoffset)*self.yscale
575
+        return x, y