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 |