usfmtc 0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
usfmtc/railroad.py ADDED
@@ -0,0 +1,1320 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import division, unicode_literals
3
+ import sys
4
+ import math as Math
5
+
6
+ if sys.version_info >= (3, ):
7
+ unicode = str
8
+
9
+ # Display constants
10
+ DEBUG = False # if true, writes some debug information into attributes
11
+ VS = 8 # minimum vertical separation between things. For a 3px stroke, must be at least 4
12
+ AR = 10 # radius of arcs
13
+ DIAGRAM_CLASS = 'railroad-diagram' # class to put on the root <svg>
14
+ STROKE_ODD_PIXEL_LENGTH = True # is the stroke width an odd (1px, 3px, etc) pixel length?
15
+ INTERNAL_ALIGNMENT = 'center' # how to align items when they have extra space. left/right/center
16
+ CHAR_WIDTH = 8.5 # width of each monospace character. play until you find the right value for your font
17
+ COMMENT_CHAR_WIDTH = 7 # comments are in smaller text by default
18
+
19
+
20
+ def e(text):
21
+ import re
22
+ return re.sub(r"[*_\`\[\]<&]", lambda c: "&#{0};".format(ord(c.group(0))), unicode(text))
23
+
24
+ def determineGaps(outer, inner):
25
+ diff = outer - inner
26
+ if diff < 0:
27
+ diff = 0
28
+ if INTERNAL_ALIGNMENT == 'left':
29
+ return 0, diff
30
+ elif INTERNAL_ALIGNMENT == 'right':
31
+ return diff, 0
32
+ else:
33
+ return diff/2, diff/2
34
+
35
+ def doubleenumerate(seq):
36
+ length = len(list(seq))
37
+ for i,item in enumerate(seq):
38
+ yield i, i-length, item
39
+
40
+ def addDebug(el):
41
+ if not DEBUG:
42
+ return
43
+ el.attrs['data-x'] = "{0} w:{1} h:{2}/{3}/{4}".format(type(el).__name__, el.width, el.up, el.height, el.down)
44
+
45
+
46
+
47
+ class DiagramItem(object):
48
+ def __init__(self, name, attrs=None, text=None):
49
+ self.name = name
50
+ # up = distance it projects above the entry line
51
+ # height = distance between the entry/exit lines
52
+ # down = distance it projects below the exit line
53
+ self.height = 0
54
+ self.attrs = attrs or {}
55
+ self.children = [text] if text else []
56
+ self.needsSpace = False
57
+
58
+ def format(self, x, y, width):
59
+ raise NotImplementedError # Virtual
60
+
61
+ def addTo(self, parent):
62
+ parent.children.append(self)
63
+ return self
64
+
65
+ def writeSvg(self, write):
66
+ write(u'<{0}'.format(self.name))
67
+ for name, value in sorted(self.attrs.items()):
68
+ write(u' {0}="{1}"'.format(name, e(value)))
69
+ write(u'>')
70
+ if self.name in ["g", "svg"]:
71
+ write(u'\n')
72
+ for child in self.children:
73
+ if isinstance(child, DiagramItem):
74
+ child.writeSvg(write)
75
+ else:
76
+ write(e(child))
77
+ write(u'</{0}>'.format(self.name))
78
+
79
+ def walk(self, cb):
80
+ cb(self)
81
+
82
+ def __eq__(self, other):
83
+ return type(self) == type(other) and self.__dict__ == other.__dict__
84
+
85
+ def __ne__(self, other):
86
+ return not (self == other)
87
+
88
+
89
+ class DiagramMultiContainer(DiagramItem):
90
+ def __init__(self, name, items, attrs=None, text=None, **kw):
91
+ DiagramItem.__init__(self, name, attrs, text)
92
+ self.items = [wrapString(item) for item in items if item is not None]
93
+ for k, v in kw.items():
94
+ setattr(self, k, v)
95
+
96
+ def append(self, item):
97
+ self.items.append(item)
98
+
99
+ def walk(self, cb):
100
+ cb(self)
101
+ for item in self.items:
102
+ item.walk(cb)
103
+
104
+
105
+ class Path(DiagramItem):
106
+ def __init__(self, x, y):
107
+ self.x = x
108
+ self.y = y
109
+ DiagramItem.__init__(self, 'path', {'d': 'M %s %s' % (x, y)})
110
+
111
+ def m(self, x, y):
112
+ self.attrs['d'] += ' m {0} {1}'.format(x,y)
113
+ return self
114
+
115
+ def l(self, x, y):
116
+ self.attrs['d'] += ' l {0} {1}'.format(x,y)
117
+ return self
118
+
119
+ def h(self, val):
120
+ self.attrs['d'] += ' h {0}'.format(val)
121
+ return self
122
+
123
+ def right(self, val):
124
+ return self.h(max(0, val))
125
+
126
+ def left(self, val):
127
+ return self.h(-max(0, val))
128
+
129
+ def v(self, val):
130
+ self.attrs['d'] += ' v {0}'.format(val)
131
+ return self
132
+
133
+ def down(self, val):
134
+ return self.v(max(0, val))
135
+
136
+ def up(self, val):
137
+ return self.v(-max(0, val))
138
+
139
+ def arc_8(self, start, dir):
140
+ # 1/8 of a circle
141
+ arc = AR
142
+ s2 = 1/Math.sqrt(2) * arc
143
+ s2inv = (arc - s2)
144
+ path = " a {0} {0} 0 0 {1} ".format(arc, "1" if dir == 'cw' else "0")
145
+ sd = start+dir
146
+ if sd == 'ncw':
147
+ offset = [s2, s2inv]
148
+ elif sd == 'necw':
149
+ offset = [s2inv, s2]
150
+ elif sd == 'ecw':
151
+ offset = [-s2inv, s2]
152
+ elif sd == 'secw':
153
+ offset = [-s2, s2inv]
154
+ elif sd == 'scw':
155
+ offset = [-s2, -s2inv]
156
+ elif sd == 'swcw':
157
+ offset = [-s2inv, -s2]
158
+ elif sd == 'wcw':
159
+ offset = [s2inv, -s2]
160
+ elif sd == 'nwcw':
161
+ offset = [s2, -s2inv]
162
+ elif sd == 'nccw':
163
+ offset = [-s2, s2inv]
164
+ elif sd == 'nwccw':
165
+ offset = [-s2inv, s2]
166
+ elif sd == 'wccw':
167
+ offset = [s2inv, s2]
168
+ elif sd == 'swccw':
169
+ offset = [s2, s2inv]
170
+ elif sd == 'sccw':
171
+ offset = [s2, -s2inv]
172
+ elif sd == 'seccw':
173
+ offset = [s2inv, -s2]
174
+ elif sd == 'eccw':
175
+ offset = [-s2inv, -s2]
176
+ elif sd == 'neccw':
177
+ offset = [-s2, -s2inv]
178
+
179
+ path += " ".join(str(x) for x in offset)
180
+ self.attrs['d'] += path
181
+ return self
182
+
183
+ def arc(self, sweep):
184
+ x = AR
185
+ y = AR
186
+ if sweep[0] == 'e' or sweep[1] == 'w':
187
+ x *= -1
188
+ if sweep[0] == 's' or sweep[1] == 'n':
189
+ y *= -1
190
+ cw = 1 if sweep == 'ne' or sweep == 'es' or sweep == 'sw' or sweep == 'wn' else 0
191
+ self.attrs['d'] += ' a {0} {0} 0 0 {1} {2} {3}'.format(AR, cw, x, y)
192
+ #self.attrs['d'] += ' l {0} {1}'.format(x, y)
193
+ return self
194
+
195
+
196
+ def format(self):
197
+ self.attrs['d'] += ' h .5'
198
+ return self
199
+
200
+ def __repr__(self):
201
+ return 'Path(%r, %r)' % (self.x, self.y)
202
+
203
+
204
+ def wrapString(value):
205
+ return value if isinstance(value, DiagramItem) else Terminal(value)
206
+
207
+
208
+ DEFAULT_STYLE = '''\
209
+ svg.railroad-diagram {
210
+ background-color:hsl(30,20%,95%);
211
+ }
212
+ svg.railroad-diagram path {
213
+ stroke-width:3;
214
+ stroke:black;
215
+ fill:rgba(0,0,0,0);
216
+ }
217
+ svg.railroad-diagram text {
218
+ font:bold 14px monospace;
219
+ text-anchor:middle;
220
+ }
221
+ svg.railroad-diagram text.label{
222
+ text-anchor:start;
223
+ }
224
+ svg.railroad-diagram text.comment{
225
+ font:italic 12px monospace;
226
+ }
227
+ svg.railroad-diagram rect{
228
+ stroke-width:3;
229
+ stroke:black;
230
+ fill:hsl(120,100%,90%);
231
+ }
232
+ svg.railroad-diagram rect.group-box {
233
+ stroke: gray;
234
+ stroke-dasharray: 10 5;
235
+ fill: none;
236
+ }
237
+ '''
238
+
239
+
240
+ class Style(DiagramItem):
241
+ def __init__(self, css):
242
+ self.name = 'style'
243
+ self.css = css
244
+ self.height = 0
245
+ self.width = 0
246
+ self.needsSpace = False
247
+
248
+ def __repr__(self):
249
+ return 'Style(%r)' % self.css
250
+
251
+ def format(self, x, y, width):
252
+ return self
253
+
254
+ def writeSvg(self, write):
255
+ # Write included stylesheet as CDATA. See https:#developer.mozilla.org/en-US/docs/Web/SVG/Element/style
256
+ cdata = u'/* <![CDATA[ */\n{css}\n/* ]]> */\n'.format(css=self.css)
257
+ write(u'<style>{cdata}</style>'.format(cdata=cdata))
258
+
259
+
260
+ class Diagram(DiagramMultiContainer):
261
+ def __init__(self, *items, **kwargs):
262
+ # Accepts a type=[simple|complex] kwarg
263
+ DiagramMultiContainer.__init__(
264
+ self,
265
+ 'svg',
266
+ items,
267
+ {'class': DIAGRAM_CLASS, 'xmlns': "http://www.w3.org/2000/svg",
268
+ 'xmlns:xlink': "http://www.w3.org/1999/xlink"}
269
+ )
270
+ self.type = kwargs.get("type", "simple")
271
+ if items and not isinstance(items[0], Start):
272
+ self.items.insert(0, Start(self.type))
273
+ if items and not isinstance(items[-1], End):
274
+ self.items.append(End(self.type))
275
+ self.css = kwargs.get("css", DEFAULT_STYLE)
276
+ if self.css:
277
+ self.items.insert(0, Style(self.css))
278
+ self.up = 0
279
+ self.down = 0
280
+ self.height = 0
281
+ self.width = 0
282
+ for item in self.items:
283
+ if isinstance(item, Style):
284
+ continue
285
+ self.width += item.width + (20 if item.needsSpace else 0)
286
+ self.up = max(self.up, item.up - self.height)
287
+ self.height += item.height
288
+ self.down = max(self.down - item.height, item.down)
289
+ if self.items[0].needsSpace:
290
+ self.width -= 10
291
+ if self.items[-1].needsSpace:
292
+ self.width -= 10
293
+ self.formatted = False
294
+
295
+ def __repr__(self):
296
+ if self.css:
297
+ items = ', '.join(map(repr, self.items[2:-1]))
298
+ else:
299
+ items = ', '.join(map(repr, self.items[1:-1]))
300
+ pieces = [] if not items else [items]
301
+ if self.css != DEFAULT_STYLE:
302
+ pieces.append('css=%r' % self.css)
303
+ if self.type != 'simple':
304
+ pieces.append('type=%r' % self.type)
305
+ return 'Diagram(%s)' % ', '.join(pieces)
306
+
307
+ def format(self, paddingTop=20, paddingRight=None, paddingBottom=None, paddingLeft=None):
308
+ if paddingRight is None:
309
+ paddingRight = paddingTop
310
+ if paddingBottom is None:
311
+ paddingBottom = paddingTop
312
+ if paddingLeft is None:
313
+ paddingLeft = paddingRight
314
+ x = paddingLeft
315
+ y = paddingTop + self.up
316
+ g = DiagramItem('g')
317
+ if STROKE_ODD_PIXEL_LENGTH:
318
+ g.attrs['transform'] = 'translate(.5 .5)'
319
+ for item in self.items:
320
+ if item.needsSpace:
321
+ Path(x, y).h(10).addTo(g)
322
+ x += 10
323
+ item.format(x, y, item.width).addTo(g)
324
+ x += item.width
325
+ y += item.height
326
+ if item.needsSpace:
327
+ Path(x, y).h(10).addTo(g)
328
+ x += 10
329
+ self.attrs['width'] = self.width + paddingLeft + paddingRight
330
+ self.attrs['height'] = self.up + self.height + self.down + paddingTop + paddingBottom
331
+ self.attrs['viewBox'] = "0 0 {width} {height}".format(**self.attrs)
332
+ g.addTo(self)
333
+ self.formatted = True
334
+ return self
335
+
336
+
337
+ def writeSvg(self, write):
338
+ if not self.formatted:
339
+ self.format()
340
+ return DiagramItem.writeSvg(self, write)
341
+
342
+ def parseCSSGrammar(self, text):
343
+ token_patterns = {
344
+ 'keyword': r"[\w-]+\(?",
345
+ 'type': r"<[\w-]+(\(\))?>",
346
+ 'char': r"[/,()]",
347
+ 'literal': r"'(.)'",
348
+ 'openbracket': r"\[",
349
+ 'closebracket': r"\]",
350
+ 'closebracketbang': r"\]!",
351
+ 'bar': r"\|",
352
+ 'doublebar': r"\|\|",
353
+ 'doubleand': r"&&",
354
+ 'multstar': r"\*",
355
+ 'multplus': r"\+",
356
+ 'multhash': r"#",
357
+ 'multnum1': r"{\s*(\d+)\s*}",
358
+ 'multnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}",
359
+ 'multhashnum1': r"#{\s*(\d+)\s*}",
360
+ 'multhashnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}"
361
+ }
362
+
363
+
364
+ class Sequence(DiagramMultiContainer):
365
+ def __init__(self, *items, **kw):
366
+ DiagramMultiContainer.__init__(self, 'g', items, **kw)
367
+ self.needsSpace = True
368
+ self.up = 0
369
+ self.down = 0
370
+ self.height = 0
371
+ self.width = 0
372
+ for item in self.items:
373
+ self.width += item.width + (20 if item.needsSpace else 0)
374
+ self.up = max(self.up, item.up - self.height)
375
+ self.height += item.height
376
+ self.down = max(self.down - item.height, item.down)
377
+ if self.items[0].needsSpace:
378
+ self.width -= 10
379
+ if self.items[-1].needsSpace:
380
+ self.width -= 10
381
+ addDebug(self)
382
+
383
+ def __repr__(self):
384
+ items = ', '.join(map(repr, self.items))
385
+ return 'Sequence(%s)' % items
386
+
387
+ def format(self, x, y, width):
388
+ leftGap, rightGap = determineGaps(width, self.width)
389
+ Path(x, y).h(leftGap).addTo(self)
390
+ Path(x+leftGap+self.width, y+self.height).h(rightGap).addTo(self)
391
+ x += leftGap
392
+ for i,item in enumerate(self.items):
393
+ if item.needsSpace and i > 0:
394
+ Path(x, y).h(10).addTo(self)
395
+ x += 10
396
+ item.format(x, y, item.width).addTo(self)
397
+ x += item.width
398
+ y += item.height
399
+ if item.needsSpace and i < len(self.items)-1:
400
+ Path(x, y).h(10).addTo(self)
401
+ x += 10
402
+ return self
403
+
404
+
405
+ class Stack(DiagramMultiContainer):
406
+ def __init__(self, *items):
407
+ DiagramMultiContainer.__init__(self, 'g', items)
408
+ self.needsSpace = True
409
+ self.width = max(item.width + (20 if item.needsSpace else 0) for item in self.items)
410
+ # pretty sure that space calc is totes wrong
411
+ if len(self.items) > 1:
412
+ self.width += AR*2
413
+ self.up = self.items[0].up
414
+ self.down = self.items[-1].down
415
+ self.height = 0
416
+ last = len(self.items) - 1
417
+ for i,item in enumerate(self.items):
418
+ self.height += item.height
419
+ if i > 0:
420
+ self.height += max(AR*2, item.up + VS)
421
+ if i < last:
422
+ self.height += max(AR*2, item.down + VS)
423
+ addDebug(self)
424
+
425
+ def __repr__(self):
426
+ items = ', '.join(repr(item) for item in self.items)
427
+ return 'Stack(%s)' % items
428
+
429
+ def format(self, x, y, width):
430
+ leftGap, rightGap = determineGaps(width, self.width)
431
+ Path(x, y).h(leftGap).addTo(self)
432
+ x += leftGap
433
+ xInitial = x
434
+ if len(self.items) > 1:
435
+ Path(x, y).h(AR).addTo(self)
436
+ x += AR
437
+ innerWidth = self.width - AR*2
438
+ else:
439
+ innerWidth = self.width
440
+ for i,item in enumerate(self.items):
441
+ item.format(x, y, innerWidth).addTo(self)
442
+ x += innerWidth
443
+ y += item.height
444
+ if i != len(self.items)-1:
445
+ (Path(x,y)
446
+ .arc('ne').down(max(0, item.down + VS - AR*2))
447
+ .arc('es').left(innerWidth)
448
+ .arc('nw').down(max(0, self.items[i+1].up + VS - AR*2))
449
+ .arc('ws').addTo(self))
450
+ y += max(item.down + VS, AR*2) + max(self.items[i+1].up + VS, AR*2)
451
+ x = xInitial + AR
452
+ if len(self.items) > 1:
453
+ Path(x, y).h(AR).addTo(self)
454
+ x += AR
455
+ Path(x, y).h(rightGap).addTo(self)
456
+ return self
457
+
458
+
459
+ class OptionalSequence(DiagramMultiContainer):
460
+ def __new__(cls, *items, **kw):
461
+ if len(items) <= 1:
462
+ return Sequence(*items, **kw)
463
+ else:
464
+ return super(OptionalSequence, cls).__new__(cls)
465
+
466
+ def __init__(self, *items, **kw):
467
+ DiagramMultiContainer.__init__(self, 'g', items)
468
+ self.inline = kw.get('inline', False)
469
+ self.needsSpace = False
470
+ self.width = 0
471
+ self.up = 0
472
+ self.height = sum(item.height for item in self.items)
473
+ self.down = self.items[0].down
474
+ heightSoFar = 0
475
+ for i,item in enumerate(self.items):
476
+ self.up = max(self.up, max((0 if self.inline else AR * 2), item.up + VS) - heightSoFar)
477
+ heightSoFar += item.height
478
+ if i > 0:
479
+ self.down = max(self.height + self.down, heightSoFar + max((0 if self.inline else AR*2), item.down + VS)) - self.height
480
+ itemWidth = item.width + (10 if item.needsSpace else 0)
481
+ if i == 0:
482
+ self.width += AR + max(itemWidth, AR)
483
+ else:
484
+ self.width += AR*2 + max(itemWidth, AR) + AR
485
+ if self.inlines:
486
+ self.width += AR * 2
487
+ addDebug(self)
488
+
489
+ def __repr__(self):
490
+ items = ', '.join(repr(item) for item in self.items)
491
+ return 'OptionalSequence(%s)' % items
492
+
493
+ def format(self, x, y, width):
494
+ leftGap, rightGap = determineGaps(width, self.width)
495
+ Path(x, y).right(leftGap).addTo(self)
496
+ Path(x + leftGap + self.width, y + self.height).right(rightGap).addTo(self)
497
+ x += leftGap
498
+ upperLineY = y - self.up
499
+ last = len(self.items) - 1
500
+ for i,item in enumerate(self.items):
501
+ itemSpace = 10 if item.needsSpace else 0
502
+ itemWidth = item.width + itemSpace
503
+ if i == 0:
504
+ if not self.inline:
505
+ # Upper skip
506
+ (Path(x,y)
507
+ .arc('se')
508
+ .up(y - upperLineY - AR*2)
509
+ .arc('wn')
510
+ .right(itemWidth - AR)
511
+ .arc('ne')
512
+ .down(y + item.height - upperLineY - AR*2)
513
+ .arc('ws')
514
+ .addTo(self))
515
+ else:
516
+ DiagramItem('path', attrs={
517
+ "d": "M {x} {y} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z".format(x=x+30, y=y-10),
518
+ "class": "diagram-text"
519
+ }).addTo(text)
520
+ DiagramItem('text', text="?", attrs={
521
+ "x": x + 15,
522
+ "y": y + 4,
523
+ "class": "diagram-text"
524
+ }).addTo(text)
525
+
526
+ # Straight line
527
+ (Path(x, y)
528
+ .right(itemSpace + AR)
529
+ .addTo(self))
530
+ item.format(x + itemSpace + AR, y, item.width).addTo(self)
531
+ x += itemWidth + AR
532
+ y += item.height
533
+ elif i < last:
534
+ if not self.inline:
535
+ # Upper skip
536
+ (Path(x, upperLineY)
537
+ .right(AR*2 + max(itemWidth, AR) + AR)
538
+ .arc('ne')
539
+ .down(y - upperLineY + item.height - AR*2)
540
+ .arc('ws')
541
+ .addTo(self))
542
+ # Straight line
543
+ (Path(x,y)
544
+ .right(AR*2)
545
+ .addTo(self))
546
+ item.format(x + AR*2, y, item.width).addTo(self)
547
+ (Path(x + item.width + AR*2, y + item.height)
548
+ .right(itemSpace + AR)
549
+ .addTo(self))
550
+ if not self.inline:
551
+ # Lower skip
552
+ (Path(x,y)
553
+ .arc('ne')
554
+ .down(item.height + max(item.down + VS, AR*2) - AR*2)
555
+ .arc('ws')
556
+ .right(itemWidth - AR)
557
+ .arc('se')
558
+ .up(item.down + VS - AR*2)
559
+ .arc('wn')
560
+ .addTo(self))
561
+ x += AR*2 + max(itemWidth, AR) + AR
562
+ y += item.height
563
+ else:
564
+ # Straight line
565
+ (Path(x, y)
566
+ .right(AR*2)
567
+ .addTo(self))
568
+ item.format(x + AR*2, y, item.width).addTo(self)
569
+ (Path(x + AR*2 + item.width, y + item.height)
570
+ .right(itemSpace + AR)
571
+ .addTo(self))
572
+ if not self.inline:
573
+ # Lower skip
574
+ (Path(x,y)
575
+ .arc('ne')
576
+ .down(item.height + max(item.down + VS, AR*2) - AR*2)
577
+ .arc('ws')
578
+ .right(itemWidth - AR)
579
+ .arc('se')
580
+ .up(item.down + VS - AR*2)
581
+ .arc('wn')
582
+ .addTo(self))
583
+ return self
584
+
585
+ class AlternatingSequence(DiagramMultiContainer):
586
+ def __new__(cls, *items):
587
+ if len(items) == 2:
588
+ return super(AlternatingSequence, cls).__new__(cls)
589
+ else:
590
+ raise Exception("AlternatingSequence takes exactly two arguments got " + len(items))
591
+
592
+ def __init__(self, *items):
593
+ DiagramMultiContainer.__init__(self, 'g', items)
594
+ self.needsSpace = False
595
+
596
+ arc = AR
597
+ vert = VS
598
+ first = self.items[0]
599
+ second = self.items[1]
600
+
601
+ arcX = 1 / Math.sqrt(2) * arc * 2
602
+ arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
603
+ crossY = max(arc, vert)
604
+ crossX = (crossY - arcY) + arcX
605
+
606
+ firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + first.down)
607
+ self.up = firstOut + first.height + first.up
608
+
609
+ secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + second.up)
610
+ self.down = secondIn + second.height + second.down
611
+
612
+ self.height = 0
613
+
614
+ firstWidth = (20 if first.needsSpace else 0) + first.width
615
+ secondWidth = (20 if second.needsSpace else 0) + second.width
616
+ self.width = 2*arc + max(firstWidth, crossX, secondWidth) + 2*arc
617
+ addDebug(self)
618
+
619
+ def __repr__(self):
620
+ items = ', '.join(repr(item) for item in self.items)
621
+ return 'AlternatingSequence(%s)' % items
622
+
623
+ def format(self, x, y, width):
624
+ arc = AR
625
+ gaps = determineGaps(width, self.width)
626
+ Path(x,y).right(gaps[0]).addTo(self)
627
+ x += gaps[0]
628
+ Path(x+self.width, y).right(gaps[1]).addTo(self)
629
+ # bounding box
630
+ # Path(x+gaps[0], y).up(self.up).right(self.width).down(self.up+self.down).left(self.width).up(self.down).addTo(self)
631
+ first = self.items[0]
632
+ second = self.items[1]
633
+
634
+ # top
635
+ firstIn = self.up - first.up
636
+ firstOut = self.up - first.up - first.height
637
+ Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(self)
638
+ first.format(x + 2*arc, y - firstIn, self.width - 4*arc).addTo(self)
639
+ Path(x + self.width - 2*arc, y - firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(self)
640
+
641
+ # bottom
642
+ secondIn = self.down - second.down - second.height
643
+ secondOut = self.down - second.down
644
+ Path(x,y).arc('ne').down(secondIn - 2*arc).arc('ws').addTo(self)
645
+ second.format(x + 2*arc, y + secondIn, self.width - 4*arc).addTo(self)
646
+ Path(x + self.width - 2*arc, y + secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(self)
647
+
648
+ # crossover
649
+ arcX = 1 / Math.sqrt(2) * arc * 2
650
+ arcY = (1 - 1 / Math.sqrt(2)) * arc * 2
651
+ crossY = max(arc, VS)
652
+ crossX = (crossY - arcY) + arcX
653
+ crossBar = (self.width - 4*arc - crossX)/2
654
+ (Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar)
655
+ .arc_8('n', 'cw').l(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw')
656
+ .right(crossBar).arc('ne').addTo(self))
657
+ (Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar)
658
+ .arc_8('s', 'ccw').l(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw')
659
+ .right(crossBar).arc('se').addTo(self))
660
+
661
+ return self
662
+
663
+
664
+ class Choice(DiagramMultiContainer):
665
+ def __init__(self, default, *items, **kw):
666
+ DiagramMultiContainer.__init__(self, 'g', items, **kw)
667
+ assert default < len(items)
668
+ self.default = default
669
+ self.width = AR * 4 + max(item.width for item in self.items)
670
+ self.up = self.items[0].up
671
+ self.down = self.items[-1].down
672
+ self.height = self.items[default].height
673
+ for i, item in enumerate(self.items):
674
+ if i in [default-1, default+1]:
675
+ arcs = AR*2
676
+ else:
677
+ arcs = AR
678
+ if i < default:
679
+ self.up += max(arcs, item.height + item.down + VS + self.items[i+1].up)
680
+ elif i == default:
681
+ self.down += arcs
682
+ self.up += arcs
683
+ else:
684
+ self.down += max(arcs, item.up + VS + self.items[i-1].down + self.items[i-1].height)
685
+ self.down -= self.items[default].height # already counted in self.height
686
+ self.down += self.items[-1].height
687
+ self.needsSpace = True
688
+ addDebug(self)
689
+
690
+ def __repr__(self):
691
+ items = ', '.join(repr(item) for item in self.items)
692
+ return 'Choice(%r, %s)' % (self.default, items)
693
+
694
+ def format(self, x, y, width):
695
+ leftGap, rightGap = determineGaps(width, self.width)
696
+ noclose = getattr(self, 'noclose', 0)
697
+ # noclose: 1 = no entry above curve, 2 = no exit curve, 4 = no entry below curve?
698
+
699
+ # Hook up the two sides if self is narrower than its stated width.
700
+ Path(x, y).h(leftGap).addTo(self)
701
+ if (noclose & 2) == 0:
702
+ Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
703
+ x += leftGap
704
+
705
+ innerWidth = self.width - AR * 4
706
+ default = self.items[self.default]
707
+
708
+ # Do the elements that curve above
709
+ above = self.items[:self.default][::-1]
710
+ if above:
711
+ distanceFromY = max(
712
+ AR * 2,
713
+ default.up
714
+ + VS
715
+ + above[0].down
716
+ + above[0].height)
717
+ for i,ni,item in doubleenumerate(above):
718
+ Path(x, y).arc('se').up(distanceFromY - AR * 2).arc('wn').addTo(self)
719
+ item.format(x + AR * 2, y - distanceFromY,
720
+ innerWidth if (noclose & 1) == 0 else 0).addTo(self)
721
+ if (noclose & 1) == 0:
722
+ Path(x + AR * 2 + innerWidth, y - distanceFromY + item.height).arc('ne') \
723
+ .down(distanceFromY - item.height + default.height - AR*2).arc('ws').addTo(self)
724
+ if ni < -1:
725
+ distanceFromY += max(
726
+ AR,
727
+ item.up
728
+ + VS
729
+ + above[i+1].down
730
+ + above[i+1].height)
731
+
732
+ # Do the straight-line path.
733
+ Path(x, y).right(AR * 2).addTo(self)
734
+ self.items[self.default].format(x + AR * 2, y,
735
+ innerWidth if (noclose & 2) == 0 else 0).addTo(self)
736
+ if (noclose & 2) == 0:
737
+ Path(x + AR * 2 + innerWidth, y+self.height).right(AR * 2).addTo(self)
738
+
739
+ # Do the elements that curve below
740
+ below = self.items[self.default + 1:]
741
+ if below:
742
+ distanceFromY = max(
743
+ AR * 2,
744
+ default.height
745
+ + default.down
746
+ + VS
747
+ + below[0].up)
748
+ for i, item in enumerate(below):
749
+ Path(x, y).arc('ne').down(distanceFromY - AR * 2).arc('ws').addTo(self)
750
+ item.format(x + AR * 2, y + distanceFromY,
751
+ innerWidth if (noclose & 4) == 0 else 0).addTo(self)
752
+ if (noclose & 4) == 0:
753
+ Path(x + AR * 2 + innerWidth, y + distanceFromY + item.height).arc('se') \
754
+ .up(distanceFromY - AR * 2 + item.height - default.height).arc('wn').addTo(self)
755
+ distanceFromY += max(
756
+ AR,
757
+ item.height
758
+ + item.down
759
+ + VS
760
+ + (below[i + 1].up if i+1 < len(below) else 0))
761
+ return self
762
+
763
+
764
+ class MultipleChoice(DiagramMultiContainer):
765
+ def __init__(self, default, type, *items):
766
+ DiagramMultiContainer.__init__(self, 'g', items)
767
+ assert 0 <= default < len(items)
768
+ assert type in ["any", "all"]
769
+ self.default = default
770
+ self.type = type
771
+ self.needsSpace = True
772
+ self.innerWidth = max(item.width for item in self.items)
773
+ self.width = 30 + AR + self.innerWidth + AR + 20
774
+ self.up = self.items[0].up
775
+ self.down = self.items[-1].down
776
+ self.height = self.items[default].height
777
+ for i, item in enumerate(self.items):
778
+ if i in [default-1, default+1]:
779
+ minimum = 10 + AR
780
+ else:
781
+ minimum = AR
782
+ if i < default:
783
+ self.up += max(minimum, item.height + item.down + VS + self.items[i+1].up)
784
+ elif i == default:
785
+ continue
786
+ else:
787
+ self.down += max(minimum, item.up + VS + self.items[i-1].down + self.items[i-1].height)
788
+ self.down -= self.items[default].height # already counted in self.height
789
+ addDebug(self)
790
+
791
+ def __repr__(self):
792
+ items = ', '.join(map(repr, self.items))
793
+ return 'MultipleChoice(%r, %r, %s)' % (self.default, self.type, items)
794
+
795
+ def format(self, x, y, width):
796
+ leftGap, rightGap = determineGaps(width, self.width)
797
+
798
+ # Hook up the two sides if self is narrower than its stated width.
799
+ Path(x, y).h(leftGap).addTo(self)
800
+ Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
801
+ x += leftGap
802
+
803
+ default = self.items[self.default]
804
+
805
+ # Do the elements that curve above
806
+ above = self.items[:self.default][::-1]
807
+ if above:
808
+ distanceFromY = max(
809
+ 10 + AR,
810
+ default.up
811
+ + VS
812
+ + above[0].down
813
+ + above[0].height)
814
+ for i,ni,item in doubleenumerate(above):
815
+ (Path(x + 30, y)
816
+ .up(distanceFromY - AR)
817
+ .arc('wn')
818
+ .addTo(self))
819
+ item.format(x + 30 + AR, y - distanceFromY, self.innerWidth).addTo(self)
820
+ (Path(x + 30 + AR + self.innerWidth, y - distanceFromY + item.height)
821
+ .arc('ne')
822
+ .down(distanceFromY - item.height + default.height - AR - 10)
823
+ .addTo(self))
824
+ if ni < -1:
825
+ distanceFromY += max(
826
+ AR,
827
+ item.up
828
+ + VS
829
+ + above[i+1].down
830
+ + above[i+1].height)
831
+
832
+ # Do the straight-line path.
833
+ Path(x + 30, y).right(AR).addTo(self)
834
+ self.items[self.default].format(x + 30 + AR, y, self.innerWidth).addTo(self)
835
+ Path(x + 30 + AR + self.innerWidth, y + self.height).right(AR).addTo(self)
836
+
837
+ # Do the elements that curve below
838
+ below = self.items[self.default + 1:]
839
+ if below:
840
+ distanceFromY = max(
841
+ 10 + AR,
842
+ default.height
843
+ + default.down
844
+ + VS
845
+ + below[0].up)
846
+ for i, item in enumerate(below):
847
+ (Path(x+30, y)
848
+ .down(distanceFromY - AR)
849
+ .arc('ws')
850
+ .addTo(self))
851
+ item.format(x + 30 + AR, y + distanceFromY, self.innerWidth).addTo(self)
852
+ (Path(x + 30 + AR + self.innerWidth, y + distanceFromY + item.height)
853
+ .arc('se')
854
+ .up(distanceFromY - AR + item.height - default.height - 10)
855
+ .addTo(self))
856
+ distanceFromY += max(
857
+ AR,
858
+ item.height
859
+ + item.down
860
+ + VS
861
+ + (below[i + 1].up if i+1 < len(below) else 0))
862
+ text = DiagramItem('g', attrs={"class": "diagram-text"}).addTo(self)
863
+ DiagramItem('title', text="take one or more branches, once each, in any order" if self.type=="any" else "take all branches, once each, in any order").addTo(text)
864
+ DiagramItem('path', attrs={
865
+ "d": "M {x} {y} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z".format(x=x+30, y=y-10),
866
+ "class": "diagram-text"
867
+ }).addTo(text)
868
+ DiagramItem('text', text="1+" if self.type=="any" else "all", attrs={
869
+ "x": x + 15,
870
+ "y": y + 4,
871
+ "class": "diagram-text"
872
+ }).addTo(text)
873
+ DiagramItem('path', attrs={
874
+ "d": "M {x} {y} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z".format(x=x+self.width-20, y=y-10),
875
+ "class": "diagram-text"
876
+ }).addTo(text)
877
+ DiagramItem('text', text=u"↺", attrs={
878
+ "x": x + self.width - 10,
879
+ "y": y + 4,
880
+ "class": "diagram-arrow"
881
+ }).addTo(text)
882
+ return self
883
+
884
+
885
+ class HorizontalChoice(DiagramMultiContainer):
886
+ def __new__(cls, *items, **kw):
887
+ if len(items) <= 1:
888
+ return Sequence(*items)
889
+ else:
890
+ return super(HorizontalChoice, cls).__new__(cls)
891
+
892
+ def __init__(self, *items, **kw):
893
+ DiagramMultiContainer.__init__(self, 'g', items, **kw)
894
+ allButLast = self.items[:-1]
895
+ middles = self.items[1:-1]
896
+ first = self.items[0]
897
+ last = self.items[-1]
898
+ self.needsSpace = False
899
+
900
+ self.width = (AR # starting track
901
+ + AR*2 * (len(self.items)-1) # inbetween tracks
902
+ + sum(x.width + (20 if x.needsSpace else 0) for x in self.items) #items
903
+ + (AR if last.height > 0 else 0) # needs space to curve up
904
+ + AR) #ending track
905
+
906
+ # Always exits at entrance height
907
+ self.height = 0
908
+
909
+ # All but the last have a track running above them
910
+ self._upperTrack = max(
911
+ AR*2,
912
+ VS,
913
+ max(x.up for x in allButLast) + VS
914
+ )
915
+ self.up = max(self._upperTrack, last.up)
916
+
917
+ # All but the first have a track running below them
918
+ # Last either straight-lines or curves up, so has different calculation
919
+ self._lowerTrack = max(
920
+ VS,
921
+ max(x.height+max(x.down+VS, AR*2) for x in middles) if middles else 0,
922
+ last.height + last.down + VS
923
+ )
924
+ if first.height < self._lowerTrack:
925
+ # Make sure there's at least 2*AR room between first exit and lower track
926
+ self._lowerTrack = max(self._lowerTrack, first.height + AR*2)
927
+ self.down = max(self._lowerTrack, first.height + first.down)
928
+
929
+ addDebug(self)
930
+
931
+ def format(self, x, y, width):
932
+ # Hook up the two sides if self is narrower than its stated width.
933
+ leftGap, rightGap = determineGaps(width, self.width)
934
+ noclose = getattr(self, 'noclose', 0) != 0
935
+ Path(x, y).h(leftGap).addTo(self)
936
+ Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self)
937
+ x += leftGap
938
+
939
+ first = self.items[0]
940
+ last = self.items[-1]
941
+
942
+ # upper track
943
+ upperSpan = (sum(x.width+(20 if x.needsSpace and not noclose else 0) for x in self.items[:-1])
944
+ + (len(self.items) - 2) * AR*2
945
+ - AR)
946
+ (Path(x,y)
947
+ .arc('se')
948
+ .up(self._upperTrack - AR*2)
949
+ .arc('wn')
950
+ .h(upperSpan)
951
+ .addTo(self))
952
+
953
+ # lower track
954
+ if not noclose:
955
+ lowerSpan = (sum(x.width+(20 if x.needsSpace else 0) for x in self.items[1:])
956
+ + (len(self.items) - 2) * AR*2
957
+ + (AR if last.height > 0 else 0)
958
+ - AR)
959
+ lowerStart = x + AR + first.width+(20 if first.needsSpace else 0) + AR*2
960
+ (Path(lowerStart, y+self._lowerTrack)
961
+ .h(lowerSpan)
962
+ .arc('se')
963
+ .up(self._lowerTrack - AR*2)
964
+ .arc('wn')
965
+ .addTo(self))
966
+
967
+ # Items
968
+ for [i, item] in enumerate(self.items):
969
+ # input track
970
+ if i == 0:
971
+ (Path(x,y)
972
+ .h(AR)
973
+ .addTo(self))
974
+ x += AR
975
+ else:
976
+ (Path(x, y - self._upperTrack)
977
+ .arc('ne')
978
+ .v(self._upperTrack - AR*2)
979
+ .arc('ws')
980
+ .addTo(self))
981
+ x += AR*2
982
+
983
+ # item
984
+ itemWidth = item.width + (20 if item.needsSpace and not noclose else 0)
985
+ item.format(x, y, itemWidth).addTo(self)
986
+ x += itemWidth
987
+
988
+ # output track
989
+ if not noclose:
990
+ if i == len(self.items)-1:
991
+ if item.height == 0:
992
+ (Path(x,y)
993
+ .h(AR)
994
+ .addTo(self))
995
+ else:
996
+ (Path(x,y+item.height)
997
+ .arc('se')
998
+ .addTo(self))
999
+ elif i == 0 and item.height > self._lowerTrack:
1000
+ # Needs to arc up to meet the lower track, not down.
1001
+ if item.height - self._lowerTrack >= AR*2:
1002
+ (Path(x, y+item.height)
1003
+ .arc('se')
1004
+ .v(self._lowerTrack - item.height + AR*2)
1005
+ .arc('wn')
1006
+ .addTo(self))
1007
+ else:
1008
+ # Not enough space to fit two arcs
1009
+ # so just bail and draw a straight line for now.
1010
+ (Path(x, y+item.height)
1011
+ .l(AR*2, self._lowerTrack - item.height)
1012
+ .addTo(self))
1013
+ else:
1014
+ (Path(x, y+item.height)
1015
+ .arc('ne')
1016
+ .v(self._lowerTrack - item.height - AR*2)
1017
+ .arc('ws')
1018
+ .addTo(self))
1019
+ return self
1020
+
1021
+
1022
+ def Optional(item, skip=False):
1023
+ return Choice(0 if skip else 1, Skip(), item)
1024
+
1025
+
1026
+ class OneOrMore(DiagramItem):
1027
+ def __init__(self, item=None, repeat=None):
1028
+ DiagramItem.__init__(self, 'g')
1029
+ if item is not None:
1030
+ self.item = wrapString(item)
1031
+ repeat = repeat or Skip()
1032
+ self.rep = wrapString(repeat)
1033
+ self.width = max(self.item.width, self.rep.width) + AR * 2
1034
+ self.height = self.item.height
1035
+ self.up = self.item.up
1036
+ self.down = max(
1037
+ AR * 2,
1038
+ self.item.down + VS + self.rep.up + self.rep.height + self.rep.down)
1039
+ self.needsSpace = True
1040
+ addDebug(self)
1041
+
1042
+ def append(self, item):
1043
+ self.item = wrapString(item)
1044
+
1045
+ def format(self, x, y, width):
1046
+ leftGap, rightGap = determineGaps(width, self.width)
1047
+
1048
+ # Hook up the two sides if self is narrower than its stated width.
1049
+ Path(x, y).h(leftGap).addTo(self)
1050
+ Path(x + leftGap + self.width, y +self.height).h(rightGap).addTo(self)
1051
+ x += leftGap
1052
+
1053
+ # Draw item
1054
+ Path(x, y).right(AR).addTo(self)
1055
+ self.item.format(x + AR, y, self.width - AR * 2).addTo(self)
1056
+ Path(x + self.width - AR, y + self.height).right(AR).addTo(self)
1057
+
1058
+ # Draw repeat arc
1059
+ distanceFromY = max(AR*2, self.item.height + self.item.down + VS + self.rep.up)
1060
+ Path(x + AR, y).arc('nw').down(distanceFromY - AR * 2) \
1061
+ .arc('ws').addTo(self)
1062
+ self.rep.format(x + AR, y + distanceFromY, self.width - AR*2).addTo(self)
1063
+ Path(x + self.width - AR, y + distanceFromY + self.rep.height).arc('se') \
1064
+ .up(distanceFromY - AR * 2 + self.rep.height - self.item.height).arc('en').addTo(self)
1065
+
1066
+ return self
1067
+
1068
+ def walk(self, cb):
1069
+ cb(self)
1070
+ self.item.walk(cb)
1071
+ self.rep.walk(cb)
1072
+
1073
+ def __repr__(self):
1074
+ return 'OneOrMore(%r, repeat=%r)' % (self.item, self.rep)
1075
+
1076
+
1077
+ def ZeroOrMore(item=None, repeat=None, skip=False):
1078
+ result = Optional(OneOrMore(item, repeat), skip)
1079
+ return result
1080
+
1081
+
1082
+ class Group(DiagramItem):
1083
+ def __init__(self, item, label):
1084
+ DiagramItem.__init__(self, 'g')
1085
+ self.item = wrapString(item)
1086
+ if isinstance(label, DiagramItem):
1087
+ self.label = label
1088
+ elif label:
1089
+ self.label = Comment(label)
1090
+ else:
1091
+ self.label = None
1092
+
1093
+ self.width = max(
1094
+ self.item.width + (20 if self.item.needsSpace else 0),
1095
+ self.label.width if self.label else 0,
1096
+ AR*2)
1097
+ self.height = self.item.height
1098
+ self.boxUp = max(self.item.up + VS, AR)
1099
+ self.up = self.boxUp
1100
+ if self.label:
1101
+ self.up += self.label.up + self.label.height + self.label.down
1102
+ self.down = max(self.item.down + VS, AR)
1103
+ self.needsSpace = True
1104
+ addDebug(self)
1105
+
1106
+ def format(self, x, y, width):
1107
+ leftGap, rightGap = determineGaps(width, self.width)
1108
+ Path(x,y).h(leftGap).addTo(self)
1109
+ Path(x+leftGap+self.width,y+self.height).h(rightGap).addTo(self)
1110
+ x += leftGap
1111
+
1112
+ DiagramItem('rect', {
1113
+ "x":x,
1114
+ "y":y-self.boxUp,
1115
+ "width":self.width,
1116
+ "height":self.boxUp + self.height + self.down,
1117
+ "rx": AR,
1118
+ "ry": AR,
1119
+ 'class':'group-box',
1120
+ }).addTo(self)
1121
+
1122
+ self.item.format(x,y,self.width).addTo(self)
1123
+ if self.label:
1124
+ self.label.format(
1125
+ x,
1126
+ y-(self.boxUp+self.label.down+self.label.height),
1127
+ self.label.width).addTo(self)
1128
+
1129
+ return self
1130
+
1131
+ def walk(self, cb):
1132
+ cb(self)
1133
+ self.item.walk(cb)
1134
+ self.label.walk(cb)
1135
+
1136
+
1137
+ class Start(DiagramItem):
1138
+ def __init__(self, type="simple", label=None):
1139
+ DiagramItem.__init__(self, 'g')
1140
+ if label:
1141
+ self.width = max(20, len(label) * CHAR_WIDTH + 10)
1142
+ else:
1143
+ self.width = 20
1144
+ self.up = 10
1145
+ self.down = 10
1146
+ self.type = type
1147
+ self.label = label
1148
+ addDebug(self)
1149
+
1150
+ def format(self, x, y, _width):
1151
+ path = Path(x, y-10)
1152
+ if self.type == "complex":
1153
+ path.down(20).m(0, -10).right(self.width).addTo(self)
1154
+ else:
1155
+ path.down(20).m(10, -20).down(20).m(-10, -10).right(self.width).addTo(self)
1156
+ if self.label:
1157
+ DiagramItem('text', attrs={"x":x, "y":y-15, "style":"text-anchor:start"}, text=self.label).addTo(self)
1158
+ return self
1159
+
1160
+ def __repr__(self):
1161
+ return 'Start(type=%r, label=%r)' % (self.type, self.label)
1162
+
1163
+
1164
+ class End(DiagramItem):
1165
+ def __init__(self, type="simple"):
1166
+ DiagramItem.__init__(self, 'path')
1167
+ self.width = 20
1168
+ self.up = 10
1169
+ self.down = 10
1170
+ self.type = type
1171
+ addDebug(self)
1172
+
1173
+ def format(self, x, y, _width):
1174
+ if self.type == "simple":
1175
+ self.attrs['d'] = 'M {0} {1} h 20 m -10 -10 v 20 m 10 -20 v 20'.format(x, y)
1176
+ elif self.type == "complex":
1177
+ self.attrs['d'] = 'M {0} {1} h 20 m 0 -10 v 20'.format(x, y)
1178
+ return self
1179
+
1180
+ def __repr__(self):
1181
+ return 'End(type=%r)' % self.type
1182
+
1183
+
1184
+ class Terminal(DiagramItem):
1185
+ def __init__(self, text, href=None, title=None, cls=""):
1186
+ DiagramItem.__init__(self, 'g', {'class': ' '.join(['terminal', cls])})
1187
+ self.text = text
1188
+ self.href = href
1189
+ self.title = title
1190
+ self.cls = cls
1191
+ self.width = len(text) * CHAR_WIDTH + 20
1192
+ self.up = 11
1193
+ self.down = 11
1194
+ self.needsSpace = True
1195
+ addDebug(self)
1196
+
1197
+ def __repr__(self):
1198
+ return 'Terminal(%r, href=%r, title=%r)' % (self.text, self.href, self.title)
1199
+
1200
+ def format(self, x, y, width):
1201
+ leftGap, rightGap = determineGaps(width, self.width)
1202
+
1203
+ # Hook up the two sides if self is narrower than its stated width.
1204
+ Path(x, y).h(leftGap).addTo(self)
1205
+ Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
1206
+
1207
+ DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
1208
+ 'height': self.up + self.down, 'rx': 10, 'ry': 10}).addTo(self)
1209
+ text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 4}, self.text)
1210
+ if self.href is not None:
1211
+ a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self)
1212
+ #text.addTo(a)
1213
+ else:
1214
+ text.addTo(self)
1215
+ if self.title is not None:
1216
+ DiagramItem('title', {}, self.title).addTo(self)
1217
+ return self
1218
+
1219
+
1220
+ class NonTerminal(DiagramItem):
1221
+ def __init__(self, text, href=None, title=None, cls=""):
1222
+ DiagramItem.__init__(self, 'g', {'class': ' '.join(['non-terminal', cls])})
1223
+ self.text = text
1224
+ self.href = href
1225
+ self.title = title
1226
+ self.cls = cls
1227
+ self.width = len(text) * CHAR_WIDTH + 20
1228
+ self.up = 11
1229
+ self.down = 11
1230
+ self.needsSpace = True
1231
+ addDebug(self)
1232
+
1233
+ def __repr__(self):
1234
+ return 'NonTerminal(%r, href=%r, title=%r)' % (self.text, self.href, self.title)
1235
+
1236
+ def format(self, x, y, width):
1237
+ leftGap, rightGap = determineGaps(width, self.width)
1238
+
1239
+ # Hook up the two sides if self is narrower than its stated width.
1240
+ Path(x, y).h(leftGap).addTo(self)
1241
+ Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
1242
+
1243
+ DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width,
1244
+ 'height': self.up + self.down}).addTo(self)
1245
+ text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 4}, self.text)
1246
+ if self.href is not None:
1247
+ a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self)
1248
+ text.addTo(a)
1249
+ else:
1250
+ text.addTo(self)
1251
+ if self.title is not None:
1252
+ DiagramItem('title', {}, self.title).addTo(self)
1253
+ return self
1254
+
1255
+
1256
+ class Comment(DiagramItem):
1257
+ def __init__(self, text, href=None, title=None, cls=""):
1258
+ DiagramItem.__init__(self, 'g', {'class': ' '.join(['non-terminal', cls])})
1259
+ self.text = text
1260
+ self.href = href
1261
+ self.title = title
1262
+ self.cls = cls
1263
+ self.width = len(text) * COMMENT_CHAR_WIDTH + 10
1264
+ self.up = 8
1265
+ self.down = 8
1266
+ self.needsSpace = True
1267
+ addDebug(self)
1268
+
1269
+ def __repr__(self):
1270
+ return 'Comment(%r, href=%r, title=%r)' % (self.text, self.href, self.title)
1271
+
1272
+ def format(self, x, y, width):
1273
+ leftGap, rightGap = determineGaps(width, self.width)
1274
+
1275
+ # Hook up the two sides if self is narrower than its stated width.
1276
+ Path(x, y).h(leftGap).addTo(self)
1277
+ Path(x + leftGap + self.width, y).h(rightGap).addTo(self)
1278
+
1279
+ text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 5, 'class': 'comment'}, self.text)
1280
+ if self.href is not None:
1281
+ a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self)
1282
+ text.addTo(a)
1283
+ else:
1284
+ text.addTo(self)
1285
+ if self.title is not None:
1286
+ DiagramItem('title', {}, self.title).addTo(self)
1287
+ return self
1288
+
1289
+
1290
+ class Skip(DiagramItem):
1291
+ def __init__(self):
1292
+ DiagramItem.__init__(self, 'g')
1293
+ self.width = 0
1294
+ self.up = 0
1295
+ self.down = 0
1296
+ addDebug(self)
1297
+
1298
+ def format(self, x, y, width):
1299
+ Path(x, y).right(width).addTo(self)
1300
+ return self
1301
+
1302
+ def __repr__(self):
1303
+ return 'Skip()'
1304
+
1305
+
1306
+ if __name__ == '__main__':
1307
+ def add(name, diagram):
1308
+ sys.stdout.write('<h1>{0}</h1>\n'.format(e(name)))
1309
+ diagram.writeSvg(sys.stdout.write)
1310
+ sys.stdout.write('\n')
1311
+
1312
+ import sys
1313
+ sys.stdout.write("<!doctype html><title>Test</title><body>")
1314
+ exec(open('test.py').read())
1315
+ sys.stdout.write('''
1316
+ <style>
1317
+ .blue text { fill: blue; }
1318
+ </style>
1319
+ ''')
1320
+ sys.stdout.write('</body></html>')