├── requirements.txt ├── try-statement.png ├── with-statement.png ├── README.md ├── .gitattributes ├── LICENSE ├── main.py ├── .gitignore ├── with-statement.svg ├── try-statement.svg └── railroad.py /requirements.txt: -------------------------------------------------------------------------------- 1 | CairoSVG==2.4.2 -------------------------------------------------------------------------------- /try-statement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonybaloney/python-railroads/HEAD/try-statement.png -------------------------------------------------------------------------------- /with-statement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonybaloney/python-railroads/HEAD/with-statement.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python grammar railroad diagrams 2 | ================================ 3 | 4 | Requirements: 5 | - `cairo`, e.g. `brew install cairo` 6 | 7 | Run: 8 | ```console 9 | $ python main.py 10 | ``` 11 | 12 | # The try statement 13 | 14 | ![Try Statement](try-statement.svg) 15 | 16 | # The with statement 17 | 18 | ![With Statement](with-statement.svg) 19 | 20 | Credit 21 | ------ 22 | 23 | railroad.py is adapted from https://github.com/tabatkins/railroad-diagrams.git -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020, Anthony Shaw 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import cairosvg 2 | from railroad import * 3 | 4 | TRY_STATEMENT = Diagram( 5 | Terminal('try', 'skip'), 6 | Terminal(':'), 7 | NonTerminal('suite'), 8 | Choice(0, 9 | Sequence( 10 | OneOrMore( 11 | Sequence( 12 | Terminal('except'), 13 | Optional( 14 | Sequence( 15 | NonTerminal('test'), 16 | Optional( 17 | Sequence( 18 | Terminal('as'), 19 | NonTerminal('NAME') 20 | ) 21 | ) 22 | ) 23 | ) 24 | ), Arrow('<') 25 | ), 26 | Optional( 27 | Sequence( 28 | Terminal('else'), 29 | Terminal(':'), 30 | NonTerminal('suite') 31 | ) 32 | ), 33 | Optional( 34 | Sequence( 35 | Terminal('finally'), 36 | Terminal(':'), 37 | NonTerminal('suite') 38 | ) 39 | ), 40 | ), 41 | Sequence( 42 | Terminal('finally'), 43 | Terminal(':'), 44 | NonTerminal('suite') 45 | ) 46 | ) 47 | ) 48 | 49 | WITH_STATEMENT = Diagram( 50 | Terminal('with', 'skip'), 51 | NonTerminal('test'), 52 | Optional( 53 | Sequence( 54 | Terminal('as'), 55 | NonTerminal('expr') 56 | )), 57 | Optional(OneOrMore(Sequence( 58 | Terminal(', as'), 59 | NonTerminal('expr') 60 | ), Arrow('<'))), 61 | Terminal(':'), 62 | NonTerminal('suite') 63 | ) 64 | 65 | statements = { 66 | 'try-statement': TRY_STATEMENT, 67 | 'with-statement': WITH_STATEMENT 68 | } 69 | 70 | 71 | def main(): 72 | for key, value in statements.items(): 73 | with open('{0}.svg'.format(key), 'w') as out_svg: 74 | diagram = value 75 | diagram.writeSvg(out_svg.write) 76 | cairosvg.svg2png(url='{0}.svg'.format(key), write_to='{0}.png'.format(key), output_width=3000, dpi=300) 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | MANIFEST 149 | 150 | # Installer logs 151 | pip-log.txt 152 | 153 | # Unit test / coverage reports 154 | .coverage 155 | .tox 156 | 157 | #Translations 158 | *.mo 159 | 160 | #Mr Developer 161 | .mr.developer.cfg 162 | 163 | # Mac crap 164 | .DS_Store 165 | 166 | testpy.html 167 | /.venv/ 168 | /.idea/ 169 | -------------------------------------------------------------------------------- /with-statement.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | withwith 36 | test 37 | 38 | 39 | 40 | as 41 | expr 42 | 43 | 44 | 45 | 46 | , as 47 | expr 48 | < 49 | : 50 | suite -------------------------------------------------------------------------------- /try-statement.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | trytry 36 | : 37 | suite 38 | 39 | 40 | 41 | 42 | except 43 | 44 | 45 | 46 | test 47 | 48 | 49 | 50 | as 51 | NAME 52 | < 53 | 54 | 55 | 56 | else 57 | : 58 | suite 59 | 60 | 61 | 62 | finally 63 | : 64 | suite 65 | 66 | finally 67 | : 68 | suite -------------------------------------------------------------------------------- /railroad.py: -------------------------------------------------------------------------------- 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 14 | STROKE_ODD_PIXEL_LENGTH = False # 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 INTERNAL_ALIGNMENT == 'left': 27 | return 0, diff 28 | elif INTERNAL_ALIGNMENT == 'right': 29 | return diff, 0 30 | else: 31 | return diff/2, diff/2 32 | 33 | def doubleenumerate(seq): 34 | length = len(list(seq)) 35 | for i,item in enumerate(seq): 36 | yield i, i-length, item 37 | 38 | def addDebug(el): 39 | if not DEBUG: 40 | return 41 | el.attrs['data-x'] = "{0} w:{1} h:{2}/{3}/{4}".format(type(el).__name__, el.width, el.up, el.height, el.down) 42 | 43 | 44 | 45 | class DiagramItem(object): 46 | def __init__(self, name, attrs=None, text=None): 47 | self.name = name 48 | # up = distance it projects above the entry line 49 | # height = distance between the entry/exit lines 50 | # down = distance it projects below the exit line 51 | self.height = 0 52 | self.attrs = attrs or {} 53 | self.children = [text] if text else [] 54 | self.needsSpace = False 55 | 56 | def format(self, x, y, width): 57 | raise NotImplementedError # Virtual 58 | 59 | def addTo(self, parent): 60 | parent.children.append(self) 61 | return self 62 | 63 | def writeSvg(self, write): 64 | write(u'<{0}'.format(self.name)) 65 | for name, value in sorted(self.attrs.items()): 66 | write(u' {0}="{1}"'.format(name, e(value))) 67 | write(u'>') 68 | if self.name in ["g", "svg"]: 69 | write(u'\n') 70 | for child in self.children: 71 | if isinstance(child, DiagramItem): 72 | child.writeSvg(write) 73 | else: 74 | write(e(child)) 75 | write(u''.format(self.name)) 76 | 77 | def __eq__(self, other): 78 | return type(self) == type(other) and self.__dict__ == other.__dict__ 79 | 80 | def __ne__(self, other): 81 | return not (self == other) 82 | 83 | 84 | class Path(DiagramItem): 85 | def __init__(self, x, y): 86 | self.x = x 87 | self.y = y 88 | DiagramItem.__init__(self, 'path', {'d': 'M%s %s' % (x, y)}) 89 | 90 | def m(self, x, y): 91 | self.attrs['d'] += 'm{0} {1}'.format(x,y) 92 | return self 93 | 94 | def l(self, x, y): 95 | self.attrs['d'] += 'l{0} {1}'.format(x,y) 96 | return self 97 | 98 | def h(self, val): 99 | self.attrs['d'] += 'h{0}'.format(val) 100 | return self 101 | 102 | def right(self, val): 103 | return self.h(max(0, val)) 104 | 105 | def left(self, val): 106 | return self.h(-max(0, val)) 107 | 108 | def v(self, val): 109 | self.attrs['d'] += 'v{0}'.format(val) 110 | return self 111 | 112 | def down(self, val): 113 | return self.v(max(0, val)) 114 | 115 | def up(self, val): 116 | return self.v(-max(0, val)) 117 | 118 | def arc_8(self, start, dir): 119 | # 1/8 of a circle 120 | arc = AR 121 | s2 = 1/Math.sqrt(2) * arc 122 | s2inv = (arc - s2) 123 | path = "a {0} {0} 0 0 {1} ".format(arc, "1" if dir == 'cw' else "0") 124 | sd = start+dir 125 | if sd == 'ncw': 126 | offset = [s2, s2inv] 127 | elif sd == 'necw': 128 | offset = [s2inv, s2] 129 | elif sd == 'ecw': 130 | offset = [-s2inv, s2] 131 | elif sd == 'secw': 132 | offset = [-s2, s2inv] 133 | elif sd == 'scw': 134 | offset = [-s2, -s2inv] 135 | elif sd == 'swcw': 136 | offset = [-s2inv, -s2] 137 | elif sd == 'wcw': 138 | offset = [s2inv, -s2] 139 | elif sd == 'nwcw': 140 | offset = [s2, -s2inv] 141 | elif sd == 'nccw': 142 | offset = [-s2, s2inv] 143 | elif sd == 'nwccw': 144 | offset = [-s2inv, s2] 145 | elif sd == 'wccw': 146 | offset = [s2inv, s2] 147 | elif sd == 'swccw': 148 | offset = [s2, s2inv] 149 | elif sd == 'sccw': 150 | offset = [s2, -s2inv] 151 | elif sd == 'seccw': 152 | offset = [s2inv, -s2] 153 | elif sd == 'eccw': 154 | offset = [-s2inv, -s2] 155 | elif sd == 'neccw': 156 | offset = [-s2, -s2inv] 157 | 158 | path += " ".join(str(x) for x in offset) 159 | self.attrs['d'] += path 160 | return self 161 | 162 | def arc(self, sweep): 163 | x = AR 164 | y = AR 165 | if sweep[0] == 'e' or sweep[1] == 'w': 166 | x *= -1 167 | if sweep[0] == 's' or sweep[1] == 'n': 168 | y *= -1 169 | cw = 1 if sweep == 'ne' or sweep == 'es' or sweep == 'sw' or sweep == 'wn' else 0 170 | self.attrs['d'] += 'a{0} {0} 0 0 {1} {2} {3}'.format(AR, cw, x, y) 171 | return self 172 | 173 | 174 | def format(self): 175 | self.attrs['d'] += 'h.5' 176 | return self 177 | 178 | def __repr__(self): 179 | return 'Path(%r, %r)' % (self.x, self.y) 180 | 181 | 182 | def wrapString(value): 183 | return value if isinstance(value, DiagramItem) else Terminal(value) 184 | 185 | 186 | DEFAULT_STYLE = '''\ 187 | svg.railroad-diagram { 188 | background-color: white; 189 | } 190 | svg.railroad-diagram path { 191 | stroke-width:2; 192 | stroke:black; 193 | fill: rgba(255, 255, 255, 1.0); 194 | } 195 | svg.railroad-diagram text { 196 | font:bold 14px monospace; 197 | text-anchor:middle; 198 | } 199 | svg.railroad-diagram text.arrow { 200 | font:bold 24px monospace; 201 | text-anchor:middle; 202 | } 203 | svg.railroad-diagram text.label{ 204 | text-anchor:start; 205 | } 206 | svg.railroad-diagram text.comment{ 207 | font:italic 12px monospace; 208 | } 209 | svg.railroad-diagram rect{ 210 | stroke-width:2; 211 | stroke:black; 212 | fill:white; 213 | } 214 | ''' 215 | 216 | 217 | class Style(DiagramItem): 218 | def __init__(self, css): 219 | self.name = 'style' 220 | self.css = css 221 | self.height = 0 222 | self.width = 0 223 | self.needsSpace = False 224 | 225 | def __repr__(self): 226 | return 'Style(%r)' % css 227 | 228 | def format(self, x, y, width): 229 | return self 230 | 231 | def writeSvg(self, write): 232 | # Write included stylesheet as CDATA. See https:#developer.mozilla.org/en-US/docs/Web/SVG/Element/style 233 | cdata = u'/* */\n{css}\n/* */\n'.format(css=self.css) 234 | write(u''.format(cdata=cdata)) 235 | 236 | 237 | class Diagram(DiagramItem): 238 | def __init__(self, *items, **kwargs): 239 | # Accepts a type=[simple|complex] kwarg 240 | DiagramItem.__init__(self, 'svg', { 241 | 'class': DIAGRAM_CLASS, 242 | 'xmlns':"http://www.w3.org/2000/svg", 243 | 'xmlns:xlink':"http://www.w3.org/1999/xlink"}) 244 | self.type = kwargs.get("type", "simple") 245 | self.items = [wrapString(item) for item in items] 246 | if items and not isinstance(items[0], Start): 247 | self.items.insert(0, Start(self.type)) 248 | if items and not isinstance(items[-1], End): 249 | self.items.append(End(self.type)) 250 | self.css = kwargs.get("css", DEFAULT_STYLE) 251 | if self.css: 252 | self.items.insert(0, Style(self.css)) 253 | self.up = 0 254 | self.down = 0 255 | self.height = 0 256 | self.width = 0 257 | for item in self.items: 258 | if isinstance(item, Style): 259 | continue 260 | self.width += item.width + (20 if item.needsSpace else 0) 261 | self.up = max(self.up, item.up - self.height) 262 | self.height += item.height 263 | self.down = max(self.down - item.height, item.down) 264 | if self.items[0].needsSpace: 265 | self.width -= 10 266 | if self.items[-1].needsSpace: 267 | self.width -= 10 268 | self.formatted = False 269 | 270 | def __repr__(self): 271 | if self.css: 272 | items = ', '.join(map(repr, self.items[2:-1])) 273 | else: 274 | items = ', '.join(map(repr, self.items[1:-1])) 275 | pieces = [] if not items else [items] 276 | if self.css != DEFAULT_STYLE: 277 | pieces.append('css=%r' % self.css) 278 | if self.type != 'simple': 279 | pieces.append('type=%r' % self.type) 280 | return 'Diagram(%s)' % ', '.join(pieces) 281 | 282 | def format(self, paddingTop=20, paddingRight=None, paddingBottom=None, paddingLeft=None): 283 | if paddingRight is None: 284 | paddingRight = paddingTop 285 | if paddingBottom is None: 286 | paddingBottom = paddingTop 287 | if paddingLeft is None: 288 | paddingLeft = paddingRight 289 | x = paddingLeft 290 | y = paddingTop + self.up 291 | g = DiagramItem('g') 292 | if STROKE_ODD_PIXEL_LENGTH: 293 | g.attrs['transform'] = 'translate(.5 .5)' 294 | for item in self.items: 295 | if item.needsSpace: 296 | Path(x, y).h(10).addTo(g) 297 | x += 10 298 | item.format(x, y, item.width).addTo(g) 299 | x += item.width 300 | y += item.height 301 | if item.needsSpace: 302 | Path(x, y).h(10).addTo(g) 303 | x += 10 304 | self.attrs['width'] = self.width + paddingLeft + paddingRight 305 | self.attrs['height'] = self.up + self.height + self.down + paddingTop + paddingBottom 306 | self.attrs['viewBox'] = "0 0 {width} {height}".format(**self.attrs) 307 | g.addTo(self) 308 | self.formatted = True 309 | return self 310 | 311 | 312 | def writeSvg(self, write): 313 | if not self.formatted: 314 | self.format() 315 | return DiagramItem.writeSvg(self, write) 316 | 317 | def parseCSSGrammar(self, text): 318 | token_patterns = { 319 | 'keyword': r"[\w-]+\(?", 320 | 'type': r"<[\w-]+(\(\))?>", 321 | 'char': r"[/,()]", 322 | 'literal': r"'(.)'", 323 | 'openbracket': r"\[", 324 | 'closebracket': r"\]", 325 | 'closebracketbang': r"\]!", 326 | 'bar': r"\|", 327 | 'doublebar': r"\|\|", 328 | 'doubleand': r"&&", 329 | 'multstar': r"\*", 330 | 'multplus': r"\+", 331 | 'multhash': r"#", 332 | 'multnum1': r"{\s*(\d+)\s*}", 333 | 'multnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}", 334 | 'multhashnum1': r"#{\s*(\d+)\s*}", 335 | 'multhashnum2': r"{\s*(\d+)\s*,\s*(\d*)\s*}" 336 | } 337 | 338 | 339 | class Sequence(DiagramItem): 340 | def __init__(self, *items): 341 | DiagramItem.__init__(self, 'g') 342 | self.items = [wrapString(item) for item in items] 343 | self.needsSpace = True 344 | self.up = 0 345 | self.down = 0 346 | self.height = 0 347 | self.width = 0 348 | for item in self.items: 349 | self.width += item.width + (20 if item.needsSpace else 0) 350 | self.up = max(self.up, item.up - self.height) 351 | self.height += item.height 352 | self.down = max(self.down - item.height, item.down) 353 | if self.items[0].needsSpace: 354 | self.width -= 10 355 | if self.items[-1].needsSpace: 356 | self.width -= 10 357 | addDebug(self) 358 | 359 | def __repr__(self): 360 | items = ', '.join(map(repr, self.items)) 361 | return 'Sequence(%s)' % items 362 | 363 | def format(self, x, y, width): 364 | leftGap, rightGap = determineGaps(width, self.width) 365 | Path(x, y).h(leftGap).addTo(self) 366 | Path(x+leftGap+self.width, y+self.height).h(rightGap).addTo(self) 367 | x += leftGap 368 | for i,item in enumerate(self.items): 369 | if item.needsSpace and i > 0: 370 | Path(x, y).h(10).addTo(self) 371 | x += 10 372 | item.format(x, y, item.width).addTo(self) 373 | x += item.width 374 | y += item.height 375 | if item.needsSpace and i < len(self.items)-1: 376 | Path(x, y).h(10).addTo(self) 377 | x += 10 378 | return self 379 | 380 | 381 | class Stack(DiagramItem): 382 | def __init__(self, *items): 383 | DiagramItem.__init__(self, 'g') 384 | self.items = [wrapString(item) for item in items] 385 | self.needsSpace = True 386 | self.width = max(item.width + (20 if item.needsSpace else 0) for item in self.items) 387 | # pretty sure that space calc is totes wrong 388 | if len(self.items) > 1: 389 | self.width += AR*2 390 | self.up = self.items[0].up 391 | self.down = self.items[-1].down 392 | self.height = 0 393 | last = len(self.items) - 1 394 | for i,item in enumerate(self.items): 395 | self.height += item.height 396 | if i > 0: 397 | self.height += max(AR*2, item.up + VS) 398 | if i < last: 399 | self.height += max(AR*2, item.down + VS) 400 | addDebug(self) 401 | 402 | def __repr__(self): 403 | items = ', '.join(repr(item) for item in self.items) 404 | return 'Stack(%s)' % items 405 | 406 | def format(self, x, y, width): 407 | leftGap, rightGap = determineGaps(width, self.width) 408 | Path(x, y).h(leftGap).addTo(self) 409 | x += leftGap 410 | xInitial = x 411 | if len(self.items) > 1: 412 | Path(x, y).h(AR).addTo(self) 413 | x += AR 414 | innerWidth = self.width - AR*2 415 | else: 416 | innerWidth = self.width 417 | for i,item in enumerate(self.items): 418 | item.format(x, y, innerWidth).addTo(self) 419 | x += innerWidth 420 | y += item.height 421 | if i != len(self.items)-1: 422 | (Path(x,y) 423 | .arc('ne').down(max(0, item.down + VS - AR*2)) 424 | .arc('es').left(innerWidth) 425 | .arc('nw').down(max(0, self.items[i+1].up + VS - AR*2)) 426 | .arc('ws').addTo(self)) 427 | y += max(item.down + VS, AR*2) + max(self.items[i+1].up + VS, AR*2) 428 | x = xInitial + AR 429 | if len(self.items) > 1: 430 | Path(x, y).h(AR).addTo(self) 431 | x += AR 432 | Path(x, y).h(rightGap).addTo(self) 433 | return self 434 | 435 | 436 | class OptionalSequence(DiagramItem): 437 | def __new__(cls, *items): 438 | if len(items) <= 1: 439 | return Sequence(*items) 440 | else: 441 | return super(OptionalSequence, cls).__new__(cls) 442 | 443 | def __init__(self, *items): 444 | DiagramItem.__init__(self, 'g') 445 | self.items = [wrapString(item) for item in items] 446 | self.needsSpace = False 447 | self.width = 0 448 | self.up = 0 449 | self.height = sum(item.height for item in self.items) 450 | self.down = self.items[0].down 451 | heightSoFar = 0 452 | for i,item in enumerate(self.items): 453 | self.up = max(self.up, max(AR * 2, item.up + VS) - heightSoFar) 454 | heightSoFar += item.height 455 | if i > 0: 456 | self.down = max(self.height + self.down, heightSoFar + max(AR*2, item.down + VS)) - self.height 457 | itemWidth = item.width + (20 if item.needsSpace else 0) 458 | if i == 0: 459 | self.width += AR + max(itemWidth, AR) 460 | else: 461 | self.width += AR*2 + max(itemWidth, AR) + AR 462 | addDebug(self) 463 | 464 | def __repr__(self): 465 | items = ', '.join(repr(item) for item in self.items) 466 | return 'OptionalSequence(%s)' % items 467 | 468 | def format(self, x, y, width): 469 | leftGap, rightGap = determineGaps(width, self.width) 470 | Path(x, y).right(leftGap).addTo(self) 471 | Path(x + leftGap + self.width, y + self.height).right(rightGap).addTo(self) 472 | x += leftGap 473 | upperLineY = y - self.up 474 | last = len(self.items) - 1 475 | for i,item in enumerate(self.items): 476 | itemSpace = 10 if item.needsSpace else 0 477 | itemWidth = item.width + itemSpace 478 | if i == 0: 479 | # Upper skip 480 | (Path(x,y) 481 | .arc('se') 482 | .up(y - upperLineY - AR*2) 483 | .arc('wn') 484 | .right(itemWidth - AR) 485 | .arc('ne') 486 | .down(y + item.height - upperLineY - AR*2) 487 | .arc('ws') 488 | .addTo(self)) 489 | # Straight line 490 | (Path(x, y) 491 | .right(itemSpace + AR) 492 | .addTo(self)) 493 | item.format(x + itemSpace + AR, y, item.width).addTo(self) 494 | x += itemWidth + AR 495 | y += item.height 496 | elif i < last: 497 | # Upper skip 498 | (Path(x, upperLineY) 499 | .right(AR*2 + max(itemWidth, AR) + AR) 500 | .arc('ne') 501 | .down(y - upperLineY + item.height - AR*2) 502 | .arc('ws') 503 | .addTo(self)) 504 | # Straight line 505 | (Path(x,y) 506 | .right(AR*2) 507 | .addTo(self)) 508 | item.format(x + AR*2, y, item.width).addTo(self) 509 | (Path(x + item.width + AR*2, y + item.height) 510 | .right(itemSpace + AR) 511 | .addTo(self)) 512 | # Lower skip 513 | (Path(x,y) 514 | .arc('ne') 515 | .down(item.height + max(item.down + VS, AR*2) - AR*2) 516 | .arc('ws') 517 | .right(itemWidth - AR) 518 | .arc('se') 519 | .up(item.down + VS - AR*2) 520 | .arc('wn') 521 | .addTo(self)) 522 | x += AR*2 + max(itemWidth, AR) + AR 523 | y += item.height 524 | else: 525 | # Straight line 526 | (Path(x, y) 527 | .right(AR*2) 528 | .addTo(self)) 529 | item.format(x + AR*2, y, item.width).addTo(self) 530 | (Path(x + AR*2 + item.width, y + item.height) 531 | .right(itemSpace + AR) 532 | .addTo(self)) 533 | # Lower skip 534 | (Path(x,y) 535 | .arc('ne') 536 | .down(item.height + max(item.down + VS, AR*2) - AR*2) 537 | .arc('ws') 538 | .right(itemWidth - AR) 539 | .arc('se') 540 | .up(item.down + VS - AR*2) 541 | .arc('wn') 542 | .addTo(self)) 543 | return self 544 | 545 | class AlternatingSequence(DiagramItem): 546 | def __new__(cls, *items): 547 | if len(items) == 2: 548 | return super(AlternatingSequence, cls).__new__(cls) 549 | else: 550 | raise Exception("AlternatingSequence takes exactly two arguments got " + len(items)) 551 | 552 | def __init__(self, *items): 553 | DiagramItem.__init__(self, 'g') 554 | self.items = [wrapString(item) for item in items] 555 | self.needsSpace = False 556 | 557 | arc = AR 558 | vert = VS 559 | first = self.items[0] 560 | second = self.items[1] 561 | 562 | arcX = 1 / Math.sqrt(2) * arc * 2 563 | arcY = (1 - 1 / Math.sqrt(2)) * arc * 2 564 | crossY = max(arc, vert) 565 | crossX = (crossY - arcY) + arcX 566 | 567 | firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + first.down) 568 | self.up = firstOut + first.height + first.up 569 | 570 | secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + second.up) 571 | self.down = secondIn + second.height + second.down 572 | 573 | self.height = 0 574 | 575 | firstWidth = (20 if first.needsSpace else 0) + first.width 576 | secondWidth = (20 if second.needsSpace else 0) + second.width 577 | self.width = 2*arc + max(firstWidth, crossX, secondWidth) + 2*arc 578 | addDebug(self) 579 | 580 | def __repr__(self): 581 | items = ', '.join(repr(item) for item in self.items) 582 | return 'AlternatingSequence(%s)' % items 583 | 584 | def format(self, x, y, width): 585 | arc = AR 586 | gaps = determineGaps(width, self.width) 587 | Path(x,y).right(gaps[0]).addTo(self) 588 | x += gaps[0] 589 | Path(x+self.width, y).right(gaps[1]).addTo(self) 590 | # bounding box 591 | # Path(x+gaps[0], y).up(self.up).right(self.width).down(self.up+self.down).left(self.width).up(self.down).addTo(self) 592 | first = self.items[0] 593 | second = self.items[1] 594 | 595 | # top 596 | firstIn = self.up - first.up 597 | firstOut = self.up - first.up - first.height 598 | Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(self) 599 | first.format(x + 2*arc, y - firstIn, self.width - 4*arc).addTo(self) 600 | Path(x + self.width - 2*arc, y - firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(self) 601 | 602 | # bottom 603 | secondIn = self.down - second.down - second.height 604 | secondOut = self.down - second.down 605 | Path(x,y).arc('ne').down(secondIn - 2*arc).arc('ws').addTo(self) 606 | second.format(x + 2*arc, y + secondIn, self.width - 4*arc).addTo(self) 607 | Path(x + self.width - 2*arc, y + secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(self) 608 | 609 | # crossover 610 | arcX = 1 / Math.sqrt(2) * arc * 2 611 | arcY = (1 - 1 / Math.sqrt(2)) * arc * 2 612 | crossY = max(arc, VS) 613 | crossX = (crossY - arcY) + arcX 614 | crossBar = (self.width - 4*arc - crossX)/2 615 | (Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar) 616 | .arc_8('n', 'cw').l(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw') 617 | .right(crossBar).arc('ne').addTo(self)) 618 | (Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar) 619 | .arc_8('s', 'ccw').l(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw') 620 | .right(crossBar).arc('se').addTo(self)) 621 | 622 | return self 623 | 624 | 625 | class Choice(DiagramItem): 626 | def __init__(self, default, *items): 627 | DiagramItem.__init__(self, 'g') 628 | assert default < len(items) 629 | self.default = default 630 | self.items = [wrapString(item) for item in items] 631 | self.width = AR * 4 + max(item.width for item in self.items) 632 | self.up = self.items[0].up 633 | self.down = self.items[-1].down 634 | self.height = self.items[default].height 635 | for i, item in enumerate(self.items): 636 | if i in [default-1, default+1]: 637 | arcs = AR*2 638 | else: 639 | arcs = AR 640 | if i < default: 641 | self.up += max(arcs, item.height + item.down + VS + self.items[i+1].up) 642 | elif i == default: 643 | continue 644 | else: 645 | self.down += max(arcs, item.up + VS + self.items[i-1].down + self.items[i-1].height) 646 | self.down -= self.items[default].height # already counted in self.height 647 | addDebug(self) 648 | 649 | def __repr__(self): 650 | items = ', '.join(repr(item) for item in self.items) 651 | return 'Choice(%r, %s)' % (self.default, items) 652 | 653 | def format(self, x, y, width): 654 | leftGap, rightGap = determineGaps(width, self.width) 655 | 656 | # Hook up the two sides if self is narrower than its stated width. 657 | Path(x, y).h(leftGap).addTo(self) 658 | Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self) 659 | x += leftGap 660 | 661 | innerWidth = self.width - AR * 4 662 | default = self.items[self.default] 663 | 664 | # Do the elements that curve above 665 | above = self.items[:self.default][::-1] 666 | if above: 667 | distanceFromY = max( 668 | AR * 2, 669 | default.up 670 | + VS 671 | + above[0].down 672 | + above[0].height) 673 | for i,ni,item in doubleenumerate(above): 674 | Path(x, y).arc('se').up(distanceFromY - AR * 2).arc('wn').addTo(self) 675 | item.format(x + AR * 2, y - distanceFromY, innerWidth).addTo(self) 676 | Path(x + AR * 2 + innerWidth, y - distanceFromY + item.height).arc('ne') \ 677 | .down(distanceFromY - item.height + default.height - AR*2).arc('ws').addTo(self) 678 | if ni < -1: 679 | distanceFromY += max( 680 | AR, 681 | item.up 682 | + VS 683 | + above[i+1].down 684 | + above[i+1].height) 685 | 686 | # Do the straight-line path. 687 | Path(x, y).right(AR * 2).addTo(self) 688 | self.items[self.default].format(x + AR * 2, y, innerWidth).addTo(self) 689 | Path(x + AR * 2 + innerWidth, y+self.height).right(AR * 2).addTo(self) 690 | 691 | # Do the elements that curve below 692 | below = self.items[self.default + 1:] 693 | if below: 694 | distanceFromY = max( 695 | AR * 2, 696 | default.height 697 | + default.down 698 | + VS 699 | + below[0].up) 700 | for i, item in enumerate(below): 701 | Path(x, y).arc('ne').down(distanceFromY - AR * 2).arc('ws').addTo(self) 702 | item.format(x + AR * 2, y + distanceFromY, innerWidth).addTo(self) 703 | Path(x + AR * 2 + innerWidth, y + distanceFromY + item.height).arc('se') \ 704 | .up(distanceFromY - AR * 2 + item.height - default.height).arc('wn').addTo(self) 705 | distanceFromY += max( 706 | AR, 707 | item.height 708 | + item.down 709 | + VS 710 | + (below[i + 1].up if i+1 < len(below) else 0)) 711 | return self 712 | 713 | 714 | class MultipleChoice(DiagramItem): 715 | def __init__(self, default, type, *items): 716 | DiagramItem.__init__(self, 'g') 717 | assert 0 <= default < len(items) 718 | assert type in ["any", "all"] 719 | self.default = default 720 | self.type = type 721 | self.needsSpace = True 722 | self.items = [wrapString(item) for item in items] 723 | self.innerWidth = max(item.width for item in self.items) 724 | self.width = 30 + AR + self.innerWidth + AR + 20 725 | self.up = self.items[0].up 726 | self.down = self.items[-1].down 727 | self.height = self.items[default].height 728 | for i, item in enumerate(self.items): 729 | if i in [default-1, default+1]: 730 | minimum = 10 + AR 731 | else: 732 | minimum = AR 733 | if i < default: 734 | self.up += max(minimum, item.height + item.down + VS + self.items[i+1].up) 735 | elif i == default: 736 | continue 737 | else: 738 | self.down += max(minimum, item.up + VS + self.items[i-1].down + self.items[i-1].height) 739 | self.down -= self.items[default].height # already counted in self.height 740 | addDebug(self) 741 | 742 | def __repr__(self): 743 | items = ', '.join(map(repr, self.items)) 744 | return 'MultipleChoice(%r, %r, %s)' % (self.default, self.type, items) 745 | 746 | def format(self, x, y, width): 747 | leftGap, rightGap = determineGaps(width, self.width) 748 | 749 | # Hook up the two sides if self is narrower than its stated width. 750 | Path(x, y).h(leftGap).addTo(self) 751 | Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self) 752 | x += leftGap 753 | 754 | default = self.items[self.default] 755 | 756 | # Do the elements that curve above 757 | above = self.items[:self.default][::-1] 758 | if above: 759 | distanceFromY = max( 760 | 10 + AR, 761 | default.up 762 | + VS 763 | + above[0].down 764 | + above[0].height) 765 | for i,ni,item in doubleenumerate(above): 766 | (Path(x + 30, y) 767 | .up(distanceFromY - AR) 768 | .arc('wn') 769 | .addTo(self)) 770 | item.format(x + 30 + AR, y - distanceFromY, self.innerWidth).addTo(self) 771 | (Path(x + 30 + AR + self.innerWidth, y - distanceFromY + item.height) 772 | .arc('ne') 773 | .down(distanceFromY - item.height + default.height - AR - 10) 774 | .addTo(self)) 775 | if ni < -1: 776 | distanceFromY += max( 777 | AR, 778 | item.up 779 | + VS 780 | + above[i+1].down 781 | + above[i+1].height) 782 | 783 | # Do the straight-line path. 784 | Path(x + 30, y).right(AR).addTo(self) 785 | self.items[self.default].format(x + 30 + AR, y, self.innerWidth).addTo(self) 786 | Path(x + 30 + AR + self.innerWidth, y + self.height).right(AR).addTo(self) 787 | 788 | # Do the elements that curve below 789 | below = self.items[self.default + 1:] 790 | if below: 791 | distanceFromY = max( 792 | 10 + AR, 793 | default.height 794 | + default.down 795 | + VS 796 | + below[0].up) 797 | for i, item in enumerate(below): 798 | (Path(x+30, y) 799 | .down(distanceFromY - AR) 800 | .arc('ws') 801 | .addTo(self)) 802 | item.format(x + 30 + AR, y + distanceFromY, self.innerWidth).addTo(self) 803 | (Path(x + 30 + AR + self.innerWidth, y + distanceFromY + item.height) 804 | .arc('se') 805 | .up(distanceFromY - AR + item.height - default.height - 10) 806 | .addTo(self)) 807 | distanceFromY += max( 808 | AR, 809 | item.height 810 | + item.down 811 | + VS 812 | + (below[i + 1].up if i+1 < len(below) else 0)) 813 | text = DiagramItem('g', attrs={"class": "diagram-text"}).addTo(self) 814 | 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) 815 | DiagramItem('path', attrs={ 816 | "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), 817 | "class": "diagram-text" 818 | }).addTo(text) 819 | DiagramItem('text', text="1+" if self.type=="any" else "all", attrs={ 820 | "x": x + 15, 821 | "y": y + 4, 822 | "class": "diagram-text" 823 | }).addTo(text) 824 | DiagramItem('path', attrs={ 825 | "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), 826 | "class": "diagram-text" 827 | }).addTo(text) 828 | DiagramItem('text', text=u"↺", attrs={ 829 | "x": x + self.width - 10, 830 | "y": y + 4, 831 | "class": "diagram-arrow" 832 | }).addTo(text) 833 | return self 834 | 835 | 836 | class HorizontalChoice(DiagramItem): 837 | def __new__(cls, *items): 838 | if len(items) <= 1: 839 | return Sequence(*items) 840 | else: 841 | return super(HorizontalChoice, cls).__new__(cls) 842 | 843 | def __init__(self, *items): 844 | DiagramItem.__init__(self, 'g') 845 | self.items = [wrapString(item) for item in items] 846 | allButLast = self.items[:-1] 847 | middles = self.items[1:-1] 848 | first = self.items[0] 849 | last = self.items[-1] 850 | self.needsSpace = False 851 | 852 | self.width = (AR # starting track 853 | + AR*2 * (len(self.items)-1) # inbetween tracks 854 | + sum(x.width + (20 if x.needsSpace else 0) for x in self.items) #items 855 | + (AR if last.height > 0 else 0) # needs space to curve up 856 | + AR) #ending track 857 | 858 | # Always exits at entrance height 859 | self.height = 0 860 | 861 | # All but the last have a track running above them 862 | self._upperTrack = max( 863 | AR*2, 864 | VS, 865 | max(x.up for x in allButLast) + VS 866 | ) 867 | self.up = max(self._upperTrack, last.up) 868 | 869 | # All but the first have a track running below them 870 | # Last either straight-lines or curves up, so has different calculation 871 | self._lowerTrack = max( 872 | VS, 873 | max(x.height+max(x.down+VS, AR*2) for x in middles) if middles else 0, 874 | last.height + last.down + VS 875 | ) 876 | if first.height < self._lowerTrack: 877 | # Make sure there's at least 2*AR room between first exit and lower track 878 | self._lowerTrack = max(self._lowerTrack, first.height + AR*2) 879 | self.down = max(self._lowerTrack, first.height + first.down) 880 | 881 | addDebug(self) 882 | 883 | def format(self, x, y, width): 884 | # Hook up the two sides if self is narrower than its stated width. 885 | leftGap, rightGap = determineGaps(width, self.width) 886 | Path(x, y).h(leftGap).addTo(self) 887 | Path(x + leftGap + self.width, y + self.height).h(rightGap).addTo(self) 888 | x += leftGap 889 | 890 | first = self.items[0] 891 | last = self.items[-1] 892 | 893 | # upper track 894 | upperSpan = (sum(x.width+(20 if x.needsSpace else 0) for x in self.items[:-1]) 895 | + (len(self.items) - 2) * AR*2 896 | - AR) 897 | (Path(x,y) 898 | .arc('se') 899 | .up(self._upperTrack - AR*2) 900 | .arc('wn') 901 | .h(upperSpan) 902 | .addTo(self)) 903 | 904 | # lower track 905 | lowerSpan = (sum(x.width+(20 if x.needsSpace else 0) for x in self.items[1:]) 906 | + (len(self.items) - 2) * AR*2 907 | + (AR if last.height > 0 else 0) 908 | - AR) 909 | lowerStart = x + AR + first.width+(20 if first.needsSpace else 0) + AR*2 910 | (Path(lowerStart, y+self._lowerTrack) 911 | .h(lowerSpan) 912 | .arc('se') 913 | .up(self._lowerTrack - AR*2) 914 | .arc('wn') 915 | .addTo(self)) 916 | 917 | # Items 918 | for [i, item] in enumerate(self.items): 919 | # input track 920 | if i == 0: 921 | (Path(x,y) 922 | .h(AR) 923 | .addTo(self)) 924 | x += AR 925 | else: 926 | (Path(x, y - self._upperTrack) 927 | .arc('ne') 928 | .v(self._upperTrack - AR*2) 929 | .arc('ws') 930 | .addTo(self)) 931 | x += AR*2 932 | 933 | # item 934 | itemWidth = item.width + (20 if item.needsSpace else 0) 935 | item.format(x, y, itemWidth).addTo(self) 936 | x += itemWidth 937 | 938 | # output track 939 | if i == len(self.items)-1: 940 | if item.height == 0: 941 | (Path(x,y) 942 | .h(AR) 943 | .addTo(self)) 944 | else: 945 | (Path(x,y+item.height) 946 | .arc('se') 947 | .addTo(self)) 948 | elif i == 0 and item.height > self._lowerTrack: 949 | # Needs to arc up to meet the lower track, not down. 950 | if item.height - self._lowerTrack >= AR*2: 951 | (Path(x, y+item.height) 952 | .arc('se') 953 | .v(self._lowerTrack - item.height + AR*2) 954 | .arc('wn') 955 | .addTo(self)) 956 | else: 957 | # Not enough space to fit two arcs 958 | # so just bail and draw a straight line for now. 959 | (Path(x, y+item.height) 960 | .l(AR*2, self._lowerTrack - item.height) 961 | .addTo(self)) 962 | else: 963 | (Path(x, y+item.height) 964 | .arc('ne') 965 | .v(self._lowerTrack - item.height - AR*2) 966 | .arc('ws') 967 | .addTo(self)) 968 | return self 969 | 970 | 971 | def Optional(item, skip=False): 972 | return Choice(0 if skip else 1, Skip(), item) 973 | 974 | 975 | class OneOrMore(DiagramItem): 976 | def __init__(self, item, repeat=None): 977 | DiagramItem.__init__(self, 'g') 978 | repeat = repeat or Skip() 979 | self.item = wrapString(item) 980 | self.rep = wrapString(repeat) 981 | self.width = max(self.item.width, self.rep.width) + AR * 2 982 | self.height = self.item.height 983 | self.up = self.item.up 984 | self.down = max( 985 | AR * 2, 986 | self.item.down + VS + self.rep.up + self.rep.height + self.rep.down) 987 | self.needsSpace = True 988 | addDebug(self) 989 | 990 | def format(self, x, y, width): 991 | leftGap, rightGap = determineGaps(width, self.width) 992 | 993 | # Hook up the two sides if self is narrower than its stated width. 994 | Path(x, y).h(leftGap).addTo(self) 995 | Path(x + leftGap + self.width, y +self.height).h(rightGap).addTo(self) 996 | x += leftGap 997 | 998 | # Draw item 999 | Path(x, y).right(AR).addTo(self) 1000 | self.item.format(x + AR, y, self.width - AR * 2).addTo(self) 1001 | Path(x + self.width - AR, y + self.height).right(AR).addTo(self) 1002 | 1003 | # Draw repeat arc 1004 | distanceFromY = max(AR*2, self.item.height + self.item.down + VS + self.rep.up) 1005 | Path(x + AR, y).arc('nw').down(distanceFromY - AR * 2) \ 1006 | .arc('ws').addTo(self) 1007 | self.rep.format(x + AR, y + distanceFromY, self.width - AR*2).addTo(self) 1008 | Path(x + self.width - AR, y + distanceFromY + self.rep.height).arc('se') \ 1009 | .up(distanceFromY - AR * 2 + self.rep.height - self.item.height).arc('en').addTo(self) 1010 | 1011 | return self 1012 | 1013 | def __repr__(self): 1014 | return 'OneOrMore(%r, repeat=%r)' % (self.item, self.rep) 1015 | 1016 | 1017 | def ZeroOrMore(item, repeat=None, skip=False): 1018 | result = Optional(OneOrMore(item, repeat), skip) 1019 | return result 1020 | 1021 | 1022 | class Start(DiagramItem): 1023 | def __init__(self, type="simple", label=None): 1024 | DiagramItem.__init__(self, 'g') 1025 | if label: 1026 | self.width = max(20, len(label) * CHAR_WIDTH + 10) 1027 | else: 1028 | self.width = 20 1029 | self.up = 10 1030 | self.down = 10 1031 | self.type = type 1032 | self.label = label 1033 | addDebug(self) 1034 | 1035 | def format(self, x, y, _width): 1036 | path = Path(x, y-10) 1037 | if self.type == "complex": 1038 | path.down(20).m(0, -10).right(self.width).addTo(self) 1039 | else: 1040 | path.down(20).m(10, -20).down(20).m(-10, -10).right(self.width).addTo(self) 1041 | if self.label: 1042 | DiagramItem('text', attrs={"x":x, "y":y-15, "style":"text-anchor:start"}, text=self.label).addTo(self) 1043 | return self 1044 | 1045 | def __repr__(self): 1046 | return 'Start(type=%r, label=%r)' % (self.type, self.label) 1047 | 1048 | 1049 | class End(DiagramItem): 1050 | def __init__(self, type="simple"): 1051 | DiagramItem.__init__(self, 'path') 1052 | self.width = 20 1053 | self.up = 10 1054 | self.down = 10 1055 | self.type = type 1056 | addDebug(self) 1057 | 1058 | def format(self, x, y, _width): 1059 | if self.type == "simple": 1060 | self.attrs['d'] = 'M {0} {1} h 20 m -10 -10 v 20 m 10 -20 v 20'.format(x, y) 1061 | elif self.type == "complex": 1062 | self.attrs['d'] = 'M {0} {1} h 20 m 0 -10 v 20'.format(x, y) 1063 | return self 1064 | 1065 | def __repr__(self): 1066 | return 'End(type=%r)' % self.type 1067 | 1068 | 1069 | class Arrow(DiagramItem): 1070 | def __init__(self, text, ): 1071 | DiagramItem.__init__(self, 'g', {'class': 'arrow'}) 1072 | self.text = text 1073 | self.width = len(text) * CHAR_WIDTH + 20 1074 | self.up = 11 1075 | self.down = 11 1076 | self.needsSpace = True 1077 | addDebug(self) 1078 | 1079 | def __repr__(self): 1080 | return 'Arrow(%r)' % (self.text) 1081 | 1082 | def format(self, x, y, width): 1083 | leftGap, rightGap = determineGaps(width, self.width) 1084 | # Hook up the two sides if self is narrower than its stated width. 1085 | Path(x, y).h(leftGap + self.width).addTo(self) 1086 | Path(x + leftGap + self.width, y).h(rightGap).addTo(self) 1087 | text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 8, 'class': 'arrow'}, self.text) 1088 | text.addTo(self) 1089 | return self 1090 | 1091 | 1092 | class Terminal(DiagramItem): 1093 | def __init__(self, text, href=None, title=None): 1094 | DiagramItem.__init__(self, 'g', {'class': 'terminal'}) 1095 | self.text = text 1096 | self.href = href 1097 | self.title = title 1098 | self.width = len(text) * CHAR_WIDTH + 20 1099 | self.up = 11 1100 | self.down = 11 1101 | self.needsSpace = True 1102 | addDebug(self) 1103 | 1104 | def __repr__(self): 1105 | return 'Terminal(%r, href=%r, title=%r)' % (self.text, self.href, self.title) 1106 | 1107 | def format(self, x, y, width): 1108 | leftGap, rightGap = determineGaps(width, self.width) 1109 | 1110 | # Hook up the two sides if self is narrower than its stated width. 1111 | Path(x, y).h(leftGap).addTo(self) 1112 | Path(x + leftGap + self.width, y).h(rightGap).addTo(self) 1113 | 1114 | DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width, 1115 | 'height': self.up + self.down, 'rx': 10, 'ry': 10}).addTo(self) 1116 | text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 4}, self.text) 1117 | if self.href is not None: 1118 | a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self) 1119 | text.addTo(a) 1120 | else: 1121 | text.addTo(self) 1122 | if self.title is not None: 1123 | DiagramItem('title', {}, self.title).addTo(self) 1124 | return self 1125 | 1126 | 1127 | class NonTerminal(DiagramItem): 1128 | def __init__(self, text, href=None, title=None): 1129 | DiagramItem.__init__(self, 'g', {'class': 'non-terminal'}) 1130 | self.text = text 1131 | self.href = href 1132 | self.title = title 1133 | self.width = len(text) * CHAR_WIDTH + 20 1134 | self.up = 11 1135 | self.down = 11 1136 | self.needsSpace = True 1137 | addDebug(self) 1138 | 1139 | def __repr__(self): 1140 | return 'NonTerminal(%r, href=%r, title=%r)' % (self.text, self.href, self.title) 1141 | 1142 | def format(self, x, y, width): 1143 | leftGap, rightGap = determineGaps(width, self.width) 1144 | 1145 | # Hook up the two sides if self is narrower than its stated width. 1146 | Path(x, y).h(leftGap).addTo(self) 1147 | Path(x + leftGap + self.width, y).h(rightGap).addTo(self) 1148 | 1149 | DiagramItem('rect', {'x': x + leftGap, 'y': y - 11, 'width': self.width, 1150 | 'height': self.up + self.down}).addTo(self) 1151 | text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 4}, self.text) 1152 | if self.href is not None: 1153 | a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self) 1154 | text.addTo(a) 1155 | else: 1156 | text.addTo(self) 1157 | if self.title is not None: 1158 | DiagramItem('title', {}, self.title).addTo(self) 1159 | return self 1160 | 1161 | 1162 | class Comment(DiagramItem): 1163 | def __init__(self, text, href=None, title=None): 1164 | DiagramItem.__init__(self, 'g') 1165 | self.text = text 1166 | self.href = href 1167 | self.title = title 1168 | self.width = len(text) * COMMENT_CHAR_WIDTH + 10 1169 | self.up = 11 1170 | self.down = 11 1171 | self.needsSpace = True 1172 | addDebug(self) 1173 | 1174 | def __repr__(self): 1175 | return 'Comment(%r, href=%r, title=%r)' % (self.text, self.href, self.title) 1176 | 1177 | def format(self, x, y, width): 1178 | leftGap, rightGap = determineGaps(width, self.width) 1179 | 1180 | # Hook up the two sides if self is narrower than its stated width. 1181 | Path(x, y).h(leftGap).addTo(self) 1182 | Path(x + leftGap + self.width, y).h(rightGap).addTo(self) 1183 | 1184 | text = DiagramItem('text', {'x': x + leftGap + self.width / 2, 'y': y + 5, 'class': 'comment'}, self.text) 1185 | if self.href is not None: 1186 | a = DiagramItem('a', {'xlink:href':self.href}, text).addTo(self) 1187 | text.addTo(a) 1188 | else: 1189 | text.addTo(self) 1190 | if self.title is not None: 1191 | DiagramItem('title', {}, self.title).addTo(self) 1192 | return self 1193 | 1194 | 1195 | class Skip(DiagramItem): 1196 | def __init__(self): 1197 | DiagramItem.__init__(self, 'g') 1198 | self.width = 0 1199 | self.up = 0 1200 | self.down = 0 1201 | addDebug(self) 1202 | 1203 | def format(self, x, y, width): 1204 | Path(x, y).right(width).addTo(self) 1205 | return self 1206 | 1207 | def __repr__(self): 1208 | return 'Skip()' 1209 | --------------------------------------------------------------------------------