Irrespectively of graphviz version.
Fixes https://github.com/jrfonseca/xdot.py/issues/92
... | ... |
@@ -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] |
As suggested by notEvil.
Fixes https://github.com/jrfonseca/xdot.py/issues/94
... | ... |
@@ -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) |
- 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
... | ... |
@@ -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) |
It was missing as pointed out by sunziping2016.
Fixes https://github.com/jrfonseca/xdot.py/issues/74
... | ... |
@@ -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): |
https://github.com/jrfonseca/xdot.py/pull/68
... | ... |
@@ -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",") |
In particular, don't lose edges to/from subgraphs.
Fixes https://github.com/jrfonseca/xdot.py/issues/65
... | ... |
@@ -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) |
Fixes https://github.com/jrfonseca/xdot.py/issues/39
... | ... |
@@ -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: |
... | ... |
@@ -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: |
Now all dependencies on GUI library are in xdot.ui
... | ... |
@@ -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 |
... | ... |
@@ -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",") |
... | ... |
@@ -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 |
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 |