UNPKG

@prantlf/railroad-diagrams

Version:

JavaScript library and command-line tools for drawing railroad syntax diagrams to SVG.

1 lines 127 kB
{"version":3,"file":"index.mjs","sources":["../railroad.js"],"sourcesContent":["\"use strict\";\n\n// Export function versions of all the constructors.\n// Each class will add itself to this object.\nconst funcs = {};\nexport default funcs;\n\nexport const Options = {\n\tDEBUG: false, // if true, writes some debug information into attributes\n\tVS: 8, // minimum vertical separation between things. For a 3px stroke, must be at least 4\n\tAR: 10, // radius of arcs\n\tDIAGRAM_CLASS: 'railroad-diagram', // class to put on the root <svg>\n\tSTROKE_ODD_PIXEL_LENGTH: true, // is the stroke width an odd (1px, 3px, etc) pixel length?\n\tINTERNAL_ALIGNMENT: 'center', // how to align items when they have extra space. left/right/center\n\tCHAR_WIDTH: 8.5, // width of each monospace character. play until you find the right value for your font\n\tCOMMENT_CHAR_WIDTH: 7, // comments are in smaller text by default\n};\n\nexport const defaultCSS = `\n\tsvg {\n\t\tbackground-color: hsl(30,20%,95%);\n\t}\n\tpath {\n\t\tstroke-width: 3;\n\t\tstroke: black;\n\t\tfill: rgba(0,0,0,0);\n\t}\n\ttext {\n\t\tfont: bold 14px monospace;\n\t\ttext-anchor: middle;\n\t\twhite-space: pre;\n\t}\n\ttext.diagram-text {\n\t\tfont-size: 12px;\n\t}\n\ttext.diagram-arrow {\n\t\tfont-size: 16px;\n\t}\n\ttext.label {\n\t\ttext-anchor: start;\n\t}\n\ttext.comment {\n\t\tfont: italic 12px monospace;\n\t}\n\tg.non-terminal text {\n\t\t/*font-style: italic;*/\n\t}\n\trect {\n\t\tstroke-width: 3;\n\t\tstroke: black;\n\t\tfill: hsl(120,100%,90%);\n\t}\n\trect.group-box {\n\t\tstroke: gray;\n\t\tstroke-dasharray: 10 5;\n\t\tfill: none;\n\t}\n\tpath.diagram-text {\n\t\tstroke-width: 3;\n\t\tstroke: black;\n\t\tfill: white;\n\t\tcursor: help;\n\t}\n\tg.diagram-text:hover path.diagram-text {\n\t\tfill: #eee;\n\t}`;\n\n\nexport class FakeSVG {\n\tconstructor(tagName, attrs, text) {\n\t\tif(text) this.children = text;\n\t\telse this.children = [];\n\t\tthis.tagName = tagName;\n\t\tthis.attrs = unnull(attrs, {});\n\t}\n\tformat(/*x, y, width*/) {\n\t\t// Virtual\n\t}\n\taddTo(parent) {\n\t\tif(parent instanceof FakeSVG) {\n\t\t\tparent.children.push(this);\n\t\t\treturn this;\n\t\t} else {\n\t\t\tvar svg = this.toSVG();\n\t\t\tparent.appendChild(svg);\n\t\t\treturn svg;\n\t\t}\n\t}\n\ttoSVG() {\n\t\tvar el = SVG(this.tagName, this.attrs);\n\t\tif(typeof this.children == 'string') {\n\t\t\tel.textContent = this.children;\n\t\t} else {\n\t\t\tthis.children.forEach(function(e) {\n\t\t\t\tel.appendChild(e.toSVG());\n\t\t\t});\n\t\t}\n\t\treturn el;\n\t}\n\ttoString() {\n\t\tvar str = '<' + this.tagName;\n\t\tvar group = this.tagName == \"g\" || this.tagName == \"svg\";\n\t\tfor(var attr in this.attrs) {\n\t\t\tstr += ' ' + attr + '=\"' + (this.attrs[attr]+'').replace(/&/g, '&amp;').replace(/\"/g, '&quot;') + '\"';\n\t\t}\n\t\tstr += '>';\n\t\tif(group) str += \"\\n\";\n\t\tif(typeof this.children == 'string') {\n\t\t\tstr += escapeString(this.children);\n\t\t} else {\n\t\t\tthis.children.forEach(function(e) {\n\t\t\t\tstr += e;\n\t\t\t});\n\t\t}\n\t\tstr += '</' + this.tagName + '>\\n';\n\t\treturn str;\n\t}\n\twalk(cb) {\n\t\tcb(this);\n\t}\n}\n\n\nexport class Path extends FakeSVG {\n\tconstructor(x,y) {\n\t\tsuper('path');\n\t\tthis.attrs.d = \"M\"+x+' '+y;\n\t}\n\tm(x,y) {\n\t\tthis.attrs.d += 'm'+x+' '+y;\n\t\treturn this;\n\t}\n\th(val) {\n\t\tthis.attrs.d += 'h'+val;\n\t\treturn this;\n\t}\n\tright(val) { return this.h(Math.max(0, val)); }\n\tleft(val) { return this.h(-Math.max(0, val)); }\n\tv(val) {\n\t\tthis.attrs.d += 'v'+val;\n\t\treturn this;\n\t}\n\tdown(val) { return this.v(Math.max(0, val)); }\n\tup(val) { return this.v(-Math.max(0, val)); }\n\tarc(sweep){\n\t\t// 1/4 of a circle\n\t\tvar x = Options.AR;\n\t\tvar y = Options.AR;\n\t\tif(sweep[0] == 'e' || sweep[1] == 'w') {\n\t\t\tx *= -1;\n\t\t}\n\t\tif(sweep[0] == 's' || sweep[1] == 'n') {\n\t\t\ty *= -1;\n\t\t}\n\t\tvar cw;\n\t\tif(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 'wn') {\n\t\t\tcw = 1;\n\t\t} else {\n\t\t\tcw = 0;\n\t\t}\n\t\tthis.attrs.d += \"a\"+Options.AR+\" \"+Options.AR+\" 0 0 \"+cw+' '+x+' '+y;\n\t\treturn this;\n\t}\n\tarc_8(start, dir) {\n\t\t// 1/8 of a circle\n\t\tconst arc = Options.AR;\n\t\tconst s2 = 1/Math.sqrt(2) * arc;\n\t\tconst s2inv = (arc - s2);\n\t\tlet path = \"a \" + arc + \" \" + arc + \" 0 0 \" + (dir=='cw' ? \"1\" : \"0\") + \" \";\n\t\tconst sd = start+dir;\n\t\tconst offset =\n\t\t\tsd == 'ncw' ? [s2, s2inv] :\n\t\t\tsd == 'necw' ? [s2inv, s2] :\n\t\t\tsd == 'ecw' ? [-s2inv, s2] :\n\t\t\tsd == 'secw' ? [-s2, s2inv] :\n\t\t\tsd == 'scw' ? [-s2, -s2inv] :\n\t\t\tsd == 'swcw' ? [-s2inv, -s2] :\n\t\t\tsd == 'wcw' ? [s2inv, -s2] :\n\t\t\tsd == 'nwcw' ? [s2, -s2inv] :\n\t\t\tsd == 'nccw' ? [-s2, s2inv] :\n\t\t\tsd == 'nwccw' ? [-s2inv, s2] :\n\t\t\tsd == 'wccw' ? [s2inv, s2] :\n\t\t\tsd == 'swccw' ? [s2, s2inv] :\n\t\t\tsd == 'sccw' ? [s2, -s2inv] :\n\t\t\tsd == 'seccw' ? [s2inv, -s2] :\n\t\t\tsd == 'eccw' ? [-s2inv, -s2] :\n\t\t\tsd == 'neccw' ? [-s2, -s2inv] : null\n\t\t;\n\t\tpath += offset.join(\" \");\n\t\tthis.attrs.d += path;\n\t\treturn this;\n\t}\n\tl(x, y) {\n\t\tthis.attrs.d += 'l'+x+' '+y;\n\t\treturn this;\n\t}\n\tformat() {\n\t\t// All paths in this library start/end horizontally.\n\t\t// The extra .5 ensures a minor overlap, so there's no seams in bad rasterizers.\n\t\tthis.attrs.d += 'h.5';\n\t\treturn this;\n\t}\n}\n\n\nexport class DiagramMultiContainer extends FakeSVG {\n\tconstructor(tagName, items, attrs, text) {\n\t\tsuper(tagName, attrs, text);\n\t\tthis.items = items.map(wrapString);\n\t}\n\twalk(cb) {\n\t\tcb(this);\n\t\tthis.items.forEach(x=>x.walk(cb));\n\t}\n}\n\n\nexport class Diagram extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('svg', items, {class: Options.DIAGRAM_CLASS});\n\t\tif(!(this.items[0] instanceof Start)) {\n\t\t\tthis.items.unshift(new Start());\n\t\t}\n\t\tif(!(this.items[this.items.length-1] instanceof End)) {\n\t\t\tthis.items.push(new End());\n\t\t}\n\t\tthis.up = this.down = this.height = this.width = 0;\n\t\tfor(const item of this.items) {\n\t\t\tthis.width += item.width + (item.needsSpace?20:0);\n\t\t\tthis.up = Math.max(this.up, item.up - this.height);\n\t\t\tthis.height += item.height;\n\t\t\tthis.down = Math.max(this.down - item.height, item.down);\n\t\t}\n\t\tthis.formatted = false;\n\t}\n\tformat(paddingt, paddingr, paddingb, paddingl) {\n\t\tpaddingt = unnull(paddingt, 20);\n\t\tpaddingr = unnull(paddingr, paddingt, 20);\n\t\tpaddingb = unnull(paddingb, paddingt, 20);\n\t\tpaddingl = unnull(paddingl, paddingr, 20);\n\t\tvar x = paddingl;\n\t\tvar y = paddingt;\n\t\ty += this.up;\n\t\tvar g = new FakeSVG('g', Options.STROKE_ODD_PIXEL_LENGTH ? {transform:'translate(.5 .5)'} : {});\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tif(item.needsSpace) {\n\t\t\t\tnew Path(x,y).h(10).addTo(g);\n\t\t\t\tx += 10;\n\t\t\t}\n\t\t\titem.format(x, y, item.width).addTo(g);\n\t\t\tx += item.width;\n\t\t\ty += item.height;\n\t\t\tif(item.needsSpace) {\n\t\t\t\tnew Path(x,y).h(10).addTo(g);\n\t\t\t\tx += 10;\n\t\t\t}\n\t\t}\n\t\tthis.attrs.width = this.width + paddingl + paddingr;\n\t\tthis.attrs.height = this.up + this.height + this.down + paddingt + paddingb;\n\t\tthis.attrs.viewBox = \"0 0 \" + this.attrs.width + \" \" + this.attrs.height;\n\t\tg.addTo(this);\n\t\tthis.formatted = true;\n\t\treturn this;\n\t}\n\taddTo(parent) {\n\t\tif(!parent) {\n\t\t\tvar scriptTag = document.getElementsByTagName('script');\n\t\t\tscriptTag = scriptTag[scriptTag.length - 1];\n\t\t\tparent = scriptTag.parentNode;\n\t\t}\n\t\treturn super.addTo.call(this, parent);\n\t}\n\ttoSVG() {\n\t\tif(!this.formatted) {\n\t\t\tthis.format();\n\t\t}\n\t\treturn super.toSVG.call(this);\n\t}\n\ttoString() {\n\t\tif(!this.formatted) {\n\t\t\tthis.format();\n\t\t}\n\t\treturn super.toString.call(this);\n\t}\n\ttoStandalone(style) {\n\t\tif(!this.formatted) {\n\t\t\tthis.format();\n\t\t}\n\t\tconst s = new FakeSVG('style', {}, style || defaultCSS);\n\t\tthis.children.push(s);\n\t\tthis.attrs.xmlns = \"http://www.w3.org/2000/svg\";\n\t\tthis.attrs['xmlns:xlink'] = \"http://www.w3.org/1999/xlink\";\n\t\tconst result = super.toString.call(this);\n\t\tthis.children.pop();\n\t\tdelete this.attrs.xmlns;\n\t\treturn result;\n\t}\n\tstatic fromJSON(input = []) {\n\t\treturn diagramFromJSON(Diagram, input);\n\t}\n}\nfuncs.Diagram = (...args)=>new Diagram(...args);\nfuncs.Diagram.fromJSON = Diagram.fromJSON;\n\n\nexport class ComplexDiagram extends FakeSVG {\n\tconstructor(...items) {\n\t\tvar diagram = new Diagram(...items);\n\t\tdiagram.items[0] = new Start({type:\"complex\"});\n\t\tdiagram.items[diagram.items.length-1] = new End({type:\"complex\"});\n\t\treturn diagram;\n\t}\n\tstatic fromJSON(input = []) {\n\t\treturn diagramFromJSON(ComplexDiagram, input);\n\t}\n}\nfuncs.ComplexDiagram = (...args)=>new ComplexDiagram(...args);\nfuncs.ComplexDiagram.fromJSON = ComplexDiagram.fromJSON;\n\n\nexport class Sequence extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('g', items);\n\t\tvar numberOfItems = this.items.length;\n\t\tthis.needsSpace = true;\n\t\tthis.up = this.down = this.height = this.width = 0;\n\t\tfor(var i = 0; i < numberOfItems; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tthis.width += item.width + (item.needsSpace?20:0);\n\t\t\tthis.up = Math.max(this.up, item.up - this.height);\n\t\t\tthis.height += item.height;\n\t\t\tthis.down = Math.max(this.down - item.height, item.down);\n\t\t}\n\t\tif(this.items[0].needsSpace) this.width -= 10;\n\t\tif(this.items[numberOfItems-1].needsSpace) this.width -= 10;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"sequence\";\n\t\t}\n\t}\n\tformat(x,y,width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tif(item.needsSpace && i > 0) {\n\t\t\t\tnew Path(x,y).h(10).addTo(this);\n\t\t\t\tx += 10;\n\t\t\t}\n\t\t\titem.format(x, y, item.width).addTo(this);\n\t\t\tx += item.width;\n\t\t\ty += item.height;\n\t\t\tif(item.needsSpace && i < this.items.length-1) {\n\t\t\t\tnew Path(x,y).h(10).addTo(this);\n\t\t\t\tx += 10;\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n}\nfuncs.Sequence = (...args)=>new Sequence(...args);\n\n\nexport class Stack extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('g', items);\n\t\tif( items.length === 0 ) {\n\t\t\tthrow new RangeError(\"Stack() must have at least one child.\");\n\t\t}\n\t\tthis.width = Math.max.apply(null, this.items.map(function(e) { return e.width + (e.needsSpace?20:0); }));\n\t\t//if(this.items[0].needsSpace) this.width -= 10;\n\t\t//if(this.items[this.items.length-1].needsSpace) this.width -= 10;\n\t\tif(this.items.length > 1){\n\t\t\tthis.width += Options.AR*2;\n\t\t}\n\t\tthis.needsSpace = true;\n\t\tthis.up = this.items[0].up;\n\t\tthis.down = this.items[this.items.length-1].down;\n\n\t\tthis.height = 0;\n\t\tvar last = this.items.length - 1;\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tthis.height += item.height;\n\t\t\tif(i > 0) {\n\t\t\t\tthis.height += Math.max(Options.AR*2, item.up + Options.VS);\n\t\t\t}\n\t\t\tif(i < last) {\n\t\t\t\tthis.height += Math.max(Options.AR*2, item.down + Options.VS);\n\t\t\t}\n\t\t}\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"stack\";\n\t\t}\n\t}\n\tformat(x,y,width) {\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tx += gaps[0];\n\t\tvar xInitial = x;\n\t\tif(this.items.length > 1) {\n\t\t\tnew Path(x, y).h(Options.AR).addTo(this);\n\t\t\tx += Options.AR;\n\t\t}\n\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tvar innerWidth = this.width - (this.items.length>1 ? Options.AR*2 : 0);\n\t\t\titem.format(x, y, innerWidth).addTo(this);\n\t\t\tx += innerWidth;\n\t\t\ty += item.height;\n\n\t\t\tif(i !== this.items.length-1) {\n\t\t\t\tnew Path(x, y)\n\t\t\t\t\t.arc('ne').down(Math.max(0, item.down + Options.VS - Options.AR*2))\n\t\t\t\t\t.arc('es').left(innerWidth)\n\t\t\t\t\t.arc('nw').down(Math.max(0, this.items[i+1].up + Options.VS - Options.AR*2))\n\t\t\t\t\t.arc('ws').addTo(this);\n\t\t\t\ty += Math.max(item.down + Options.VS, Options.AR*2) + Math.max(this.items[i+1].up + Options.VS, Options.AR*2);\n\t\t\t\t//y += Math.max(Options.AR*4, item.down + Options.VS*2 + this.items[i+1].up)\n\t\t\t\tx = xInitial+Options.AR;\n\t\t\t}\n\n\t\t}\n\n\t\tif(this.items.length > 1) {\n\t\t\tnew Path(x,y).h(Options.AR).addTo(this);\n\t\t\tx += Options.AR;\n\t\t}\n\t\tnew Path(x,y).h(gaps[1]).addTo(this);\n\n\t\treturn this;\n\t}\n}\nfuncs.Stack = (...args)=>new Stack(...args);\n\n\nexport class VerticalSequence extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('g', items);\n\t\tif( items.length === 0 ) {\n\t\t\tthrow new RangeError(\"VerticalSequence() must have at least one child.\");\n\t\t}\n\t\tthis.items = items.map(wrapString);\n\t\tthis.width = Math.max.apply(null, this.items.map(function(e) { return e.width + (e.needsSpace?20:0); }));\n\t\t//if(this.items[0].needsSpace) this.width -= 10;\n\t\t//if(this.items[this.items.length-1].needsSpace) this.width -= 10;\n\t\tif(this.items.length > 1){\n\t\t\tthis.width += Options.AR*2;\n\t\t}\n\t\tthis.needsSpace = true;\n\t\tthis.up = this.items[0].up;\n\t\tthis.down = this.items[this.items.length-1].down;\n\n\t\tthis.height = 0;\n\t\tvar last = this.items.length - 1;\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tif(i !== last) {\n\t\t\t\tthis.height += this.items[i].height + this.items[i].down + this.items[i+1].up + Options.VS*2;\n\t\t\t}\n\t\t}\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down\n\t\t\tthis.attrs['data-type'] = \"verticalsequence\"\n\t\t}\n\t}\n\tformat(x,y,width) {\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tx += gaps[0];\n\t\tvar xInitial = x;\n\t\tvar minWidth = 1000;\n\t\tfor(let itemNum in this.items){\n\t\t\tlet item = this.items[itemNum];\n\t\t\tminWidth = item.width < minWidth ? item.width : minWidth;\n\t\t}\n\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\titem.format(x, y, item.width).addTo(this);\n\t\t\ty += item.height;\n\n\t\t\tif(i !== this.items.length-1) {\n\t\t\t\tx += minWidth / 2;\n\t\t\t\ty += item.down\n\t\t\t\tnew Path(x, y)\n\t\t\t\t\t.v(Options.VS*2).addTo(this)\n\t\t\t\ty += this.items[i+1].up + Options.VS*2;\n\t\t\t\tx = xInitial;\n\t\t\t}\n\t\t\telse{\n\t\t\t\tx += item.width;\n\t\t\t\ty += item.height;\n\t\t\t}\n\n\t\t}\n\n\t\tif(this.items.length > 1) {\n\t\t\tnew Path(x,y).h(this.width - this.items[this.items.length - 1].width).addTo(this);\n\t\t\tx += Options.AR;\n\t\t}\n\t\tnew Path(x,y).h(gaps[1]).addTo(this);\n\n\t\treturn this;\n\t}\n}\nfuncs.VerticalSequence = (...args)=>new VerticalSequence(...args);\n\n\nexport class OptionalSequence extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('g', items);\n\t\tif( items.length === 0 ) {\n\t\t\tthrow new RangeError(\"OptionalSequence() must have at least one child.\");\n\t\t}\n\t\tif( items.length === 1 ) {\n\t\t\treturn new Sequence(items);\n\t\t}\n\t\tvar arc = Options.AR;\n\t\tthis.needsSpace = false;\n\t\tthis.width = 0;\n\t\tthis.up = 0;\n\t\tthis.height = sum(this.items, function(x){return x.height});\n\t\tthis.down = this.items[0].down;\n\t\tvar heightSoFar = 0;\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tthis.up = Math.max(this.up, Math.max(arc*2, item.up + Options.VS) - heightSoFar);\n\t\t\theightSoFar += item.height;\n\t\t\tif(i > 0) {\n\t\t\t\tthis.down = Math.max(this.height + this.down, heightSoFar + Math.max(arc*2, item.down + Options.VS)) - this.height;\n\t\t\t}\n\t\t\tvar itemWidth = (item.needsSpace?10:0) + item.width;\n\t\t\tif(i === 0) {\n\t\t\t\tthis.width += arc + Math.max(itemWidth, arc);\n\t\t\t} else {\n\t\t\t\tthis.width += arc*2 + Math.max(itemWidth, arc) + arc;\n\t\t\t}\n\t\t}\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"optseq\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\tvar arc = Options.AR;\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x, y).right(gaps[0]).addTo(this);\n\t\tnew Path(x + gaps[0] + this.width, y + this.height).right(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\t\tvar upperLineY = y - this.up;\n\t\tvar last = this.items.length - 1;\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tvar item = this.items[i];\n\t\t\tvar itemSpace = (item.needsSpace?10:0);\n\t\t\tvar itemWidth = item.width + itemSpace;\n\t\t\tif(i === 0) {\n\t\t\t\t// Upper skip\n\t\t\t\tnew Path(x,y)\n\t\t\t\t\t.arc('se')\n\t\t\t\t\t.up(y - upperLineY - arc*2)\n\t\t\t\t\t.arc('wn')\n\t\t\t\t\t.right(itemWidth - arc)\n\t\t\t\t\t.arc('ne')\n\t\t\t\t\t.down(y + item.height - upperLineY - arc*2)\n\t\t\t\t\t.arc('ws')\n\t\t\t\t\t.addTo(this);\n\t\t\t\t// Straight line\n\t\t\t\tnew Path(x, y)\n\t\t\t\t\t.right(itemSpace + arc)\n\t\t\t\t\t.addTo(this);\n\t\t\t\titem.format(x + itemSpace + arc, y, item.width).addTo(this);\n\t\t\t\tx += itemWidth + arc;\n\t\t\t\ty += item.height;\n\t\t\t\t// x ends on the far side of the first element,\n\t\t\t\t// where the next element's skip needs to begin\n\t\t\t} else if(i < last) {\n\t\t\t\t// Upper skip\n\t\t\t\tnew Path(x, upperLineY)\n\t\t\t\t\t.right(arc*2 + Math.max(itemWidth, arc) + arc)\n\t\t\t\t\t.arc('ne')\n\t\t\t\t\t.down(y - upperLineY + item.height - arc*2)\n\t\t\t\t\t.arc('ws')\n\t\t\t\t\t.addTo(this);\n\t\t\t\t// Straight line\n\t\t\t\tnew Path(x,y)\n\t\t\t\t\t.right(arc*2)\n\t\t\t\t\t.addTo(this);\n\t\t\t\titem.format(x + arc*2, y, item.width).addTo(this);\n\t\t\t\tnew Path(x + item.width + arc*2, y + item.height)\n\t\t\t\t\t.right(itemSpace + arc)\n\t\t\t\t\t.addTo(this);\n\t\t\t\t// Lower skip\n\t\t\t\tnew Path(x,y)\n\t\t\t\t\t.arc('ne')\n\t\t\t\t\t.down(item.height + Math.max(item.down + Options.VS, arc*2) - arc*2)\n\t\t\t\t\t.arc('ws')\n\t\t\t\t\t.right(itemWidth - arc)\n\t\t\t\t\t.arc('se')\n\t\t\t\t\t.up(item.down + Options.VS - arc*2)\n\t\t\t\t\t.arc('wn')\n\t\t\t\t\t.addTo(this);\n\t\t\t\tx += arc*2 + Math.max(itemWidth, arc) + arc;\n\t\t\t\ty += item.height;\n\t\t\t} else {\n\t\t\t\t// Straight line\n\t\t\t\tnew Path(x, y)\n\t\t\t\t\t.right(arc*2)\n\t\t\t\t\t.addTo(this);\n\t\t\t\titem.format(x + arc*2, y, item.width).addTo(this);\n\t\t\t\tnew Path(x + arc*2 + item.width, y + item.height)\n\t\t\t\t\t.right(itemSpace + arc)\n\t\t\t\t\t.addTo(this);\n\t\t\t\t// Lower skip\n\t\t\t\tnew Path(x,y)\n\t\t\t\t\t.arc('ne')\n\t\t\t\t\t.down(item.height + Math.max(item.down + Options.VS, arc*2) - arc*2)\n\t\t\t\t\t.arc('ws')\n\t\t\t\t\t.right(itemWidth - arc)\n\t\t\t\t\t.arc('se')\n\t\t\t\t\t.up(item.down + Options.VS - arc*2)\n\t\t\t\t\t.arc('wn')\n\t\t\t\t\t.addTo(this);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n}\nfuncs.OptionalSequence = (...args)=>new OptionalSequence(...args);\n\n\nexport class AlternatingSequence extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('g', items);\n\t\tif( items.length === 1 ) {\n\t\t\treturn new Sequence(items);\n\t\t}\n\t\tif( items.length !== 2 ) {\n\t\t\tthrow new RangeError(\"AlternatingSequence() must have one or two children.\");\n\t\t}\n\t\tthis.needsSpace = false;\n\n\t\tconst arc = Options.AR;\n\t\tconst vert = Options.VS;\n\t\tconst max = Math.max;\n\t\tconst first = this.items[0];\n\t\tconst second = this.items[1];\n\n\t\tconst arcX = 1 / Math.sqrt(2) * arc * 2;\n\t\tconst arcY = (1 - 1 / Math.sqrt(2)) * arc * 2;\n\t\tconst crossY = Math.max(arc, Options.VS);\n\t\tconst crossX = (crossY - arcY) + arcX;\n\n\t\tconst firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + first.down);\n\t\tthis.up = firstOut + first.height + first.up;\n\n\t\tconst secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 + vert + second.up);\n\t\tthis.down = secondIn + second.height + second.down;\n\n\t\tthis.height = 0;\n\n\t\tconst firstWidth = 2*(first.needsSpace?10:0) + first.width;\n\t\tconst secondWidth = 2*(second.needsSpace?10:0) + second.width;\n\t\tthis.width = 2*arc + max(firstWidth, crossX, secondWidth) + 2*arc;\n\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"altseq\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\tconst arc = Options.AR;\n\t\tconst gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).right(gaps[0]).addTo(this);\n\t\tx += gaps[0];\n\t\tnew Path(x+this.width, y).right(gaps[1]).addTo(this);\n\t\t// bounding box\n\t\t//new Path(x+gaps[0], y).up(this.up).right(this.width).down(this.up+this.down).left(this.width).up(this.down).addTo(this);\n\t\tconst first = this.items[0];\n\t\tconst second = this.items[1];\n\n\t\t// top\n\t\tconst firstIn = this.up - first.up;\n\t\tconst firstOut = this.up - first.up - first.height;\n\t\tnew Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(this);\n\t\tfirst.format(x + 2*arc, y - firstIn, this.width - 4*arc).addTo(this);\n\t\tnew Path(x + this.width - 2*arc, y - firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(this);\n\n\t\t// bottom\n\t\tconst secondIn = this.down - second.down - second.height;\n\t\tconst secondOut = this.down - second.down;\n\t\tnew Path(x,y).arc('ne').down(secondIn - 2*arc).arc('ws').addTo(this);\n\t\tsecond.format(x + 2*arc, y + secondIn, this.width - 4*arc).addTo(this);\n\t\tnew Path(x + this.width - 2*arc, y + secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(this);\n\n\t\t// crossover\n\t\tconst arcX = 1 / Math.sqrt(2) * arc * 2;\n\t\tconst arcY = (1 - 1 / Math.sqrt(2)) * arc * 2;\n\t\tconst crossY = Math.max(arc, Options.VS);\n\t\tconst crossX = (crossY - arcY) + arcX;\n\t\tconst crossBar = (this.width - 4*arc - crossX)/2;\n\t\tnew Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar)\n\t\t\t.arc_8('n', 'cw').l(crossX - arcX, crossY - arcY).arc_8('sw', 'ccw')\n\t\t\t.right(crossBar).arc('ne').addTo(this);\n\t\tnew Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar)\n\t\t\t.arc_8('s', 'ccw').l(crossX - arcX, -(crossY - arcY)).arc_8('nw', 'cw')\n\t\t\t.right(crossBar).arc('se').addTo(this);\n\n\t\treturn this;\n\t}\n}\nfuncs.AlternatingSequence = (...args)=>new AlternatingSequence(...args);\n\n\nexport class Choice extends DiagramMultiContainer {\n\tconstructor(normal, ...items) {\n\t\tsuper('g', items);\n\t\tif( typeof normal !== \"number\" || normal !== Math.floor(normal) ) {\n\t\t\tthrow new TypeError(\"The first argument of Choice() must be an integer.\");\n\t\t} else if(normal < 0 || normal >= items.length) {\n\t\t\tthrow new RangeError(\"The first argument of Choice() must be an index for one of the items.\");\n\t\t} else {\n\t\t\tthis.normal = normal;\n\t\t}\n\t\tvar first = 0;\n\t\tvar last = items.length - 1;\n\t\tthis.width = Math.max.apply(null, this.items.map(function(el){return el.width})) + Options.AR*4;\n\t\tthis.height = this.items[normal].height;\n\t\tthis.up = this.items[first].up;\n\t\tvar arcs;\n\t\tfor(var i = first; i < normal; i++) {\n\t\t\tif(i == normal-1) arcs = Options.AR*2;\n\t\t\telse arcs = Options.AR;\n\t\t\tthis.up += Math.max(arcs, this.items[i].height + this.items[i].down + Options.VS + this.items[i+1].up);\n\t\t}\n\t\tthis.down = this.items[last].down;\n\t\tfor(i = normal+1; i <= last; i++) {\n\t\t\tif(i == normal+1) arcs = Options.AR*2;\n\t\t\telse arcs = Options.AR;\n\t\t\tthis.down += Math.max(arcs, this.items[i-1].height + this.items[i-1].down + Options.VS + this.items[i].up);\n\t\t}\n\t\tthis.down -= this.items[normal].height; // already counted in Choice.height\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"choice\";\n\t\t}\n\t}\n\tformat(x,y,width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tvar last = this.items.length -1;\n\t\tvar innerWidth = this.width - Options.AR*4;\n\n\t\t// Do the elements that curve above\n\t\tvar distanceFromY;\n\t\tfor(var i = this.normal - 1; i >= 0; i--) {\n\t\t\tlet item = this.items[i];\n\t\t\tif( i == this.normal - 1 ) {\n\t\t\t\tdistanceFromY = Math.max(Options.AR*2, this.items[this.normal].up + Options.VS + item.down + item.height);\n\t\t\t}\n\t\t\tnew Path(x,y)\n\t\t\t\t.arc('se')\n\t\t\t\t.up(distanceFromY - Options.AR*2)\n\t\t\t\t.arc('wn').addTo(this);\n\t\t\titem.format(x+Options.AR*2,y - distanceFromY,innerWidth).addTo(this);\n\t\t\tnew Path(x+Options.AR*2+innerWidth, y-distanceFromY+item.height)\n\t\t\t\t.arc('ne')\n\t\t\t\t.down(distanceFromY - item.height + this.height - Options.AR*2)\n\t\t\t\t.arc('ws').addTo(this);\n\t\t\tdistanceFromY += Math.max(Options.AR, item.up + Options.VS + (i === 0 ? 0 : this.items[i-1].down+this.items[i-1].height));\n\t\t}\n\n\t\t// Do the straight-line path.\n\t\tnew Path(x,y).right(Options.AR*2).addTo(this);\n\t\tthis.items[this.normal].format(x+Options.AR*2, y, innerWidth).addTo(this);\n\t\tnew Path(x+Options.AR*2+innerWidth, y+this.height).right(Options.AR*2).addTo(this);\n\n\t\t// Do the elements that curve below\n\t\tfor(i = this.normal+1; i <= last; i++) {\n\t\t\tlet item = this.items[i];\n\t\t\tif( i == this.normal + 1 ) {\n\t\t\t\tdistanceFromY = Math.max(Options.AR*2, this.height + this.items[this.normal].down + Options.VS + item.up);\n\t\t\t}\n\t\t\tnew Path(x,y)\n\t\t\t\t.arc('ne')\n\t\t\t\t.down(distanceFromY - Options.AR*2)\n\t\t\t\t.arc('ws').addTo(this);\n\t\t\titem.format(x+Options.AR*2, y+distanceFromY, innerWidth).addTo(this);\n\t\t\tnew Path(x+Options.AR*2+innerWidth, y+distanceFromY+item.height)\n\t\t\t\t.arc('se')\n\t\t\t\t.up(distanceFromY - Options.AR*2 + item.height - this.height)\n\t\t\t\t.arc('wn').addTo(this);\n\t\t\tdistanceFromY += Math.max(Options.AR, item.height + item.down + Options.VS + (i == last ? 0 : this.items[i+1].up));\n\t\t}\n\n\t\treturn this;\n\t}\n}\nfuncs.Choice = (...args)=>new Choice(...args);\n\n\nexport class HorizontalChoice extends DiagramMultiContainer {\n\tconstructor(...items) {\n\t\tsuper('g', items);\n\t\tif( items.length === 0 ) {\n\t\t\tthrow new RangeError(\"HorizontalChoice() must have at least one child.\");\n\t\t}\n\t\tif( items.length === 1) {\n\t\t\treturn new Sequence(items);\n\t\t}\n\t\tconst allButLast = this.items.slice(0, -1);\n\t\tconst middles = this.items.slice(1, -1);\n\t\tconst first = this.items[0];\n\t\tconst last = this.items[this.items.length - 1];\n\t\tthis.needsSpace = false;\n\n\t\tthis.width = Options.AR; // starting track\n\t\tthis.width += Options.AR*2 * (this.items.length-1); // inbetween tracks\n\t\tthis.width += sum(this.items, x=>x.width + (x.needsSpace?20:0)); // items\n\t\tthis.width += (last.height > 0 ? Options.AR : 0); // needs space to curve up\n\t\tthis.width += Options.AR; //ending track\n\n\t\t// Always exits at entrance height\n\t\tthis.height = 0;\n\n\t\t// All but the last have a track running above them\n\t\tthis._upperTrack = Math.max(\n\t\t\tOptions.AR*2,\n\t\t\tOptions.VS,\n\t\t\tmax(allButLast, x=>x.up) + Options.VS\n\t\t);\n\t\tthis.up = Math.max(this._upperTrack, last.up);\n\n\t\t// All but the first have a track running below them\n\t\t// Last either straight-lines or curves up, so has different calculation\n\t\tthis._lowerTrack = Math.max(\n\t\t\tOptions.VS,\n\t\t\tmax(middles, x=>x.height+Math.max(x.down+Options.VS, Options.AR*2)),\n\t\t\tlast.height + last.down + Options.VS\n\t\t);\n\t\tif(first.height < this._lowerTrack) {\n\t\t\t// Make sure there's at least 2*AR room between first exit and lower track\n\t\t\tthis._lowerTrack = Math.max(this._lowerTrack, first.height + Options.AR*2);\n\t\t}\n\t\tthis.down = Math.max(this._lowerTrack, first.height + first.down);\n\n\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"horizontalchoice\";\n\t\t}\n\t}\n\tformat(x,y,width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tconst first = this.items[0];\n\t\tconst last = this.items[this.items.length-1];\n\t\tconst allButFirst = this.items.slice(1);\n\t\tconst allButLast = this.items.slice(0, -1);\n\n\t\t// upper track\n\t\tvar upperSpan = (sum(allButLast, x=>x.width+(x.needsSpace?20:0))\n\t\t\t+ (this.items.length - 2) * Options.AR*2\n\t\t\t- Options.AR\n\t\t);\n\t\tnew Path(x,y)\n\t\t\t.arc('se')\n\t\t\t.v(-(this._upperTrack - Options.AR*2))\n\t\t\t.arc('wn')\n\t\t\t.h(upperSpan)\n\t\t\t.addTo(this);\n\n\t\t// lower track\n\t\tvar lowerSpan = (sum(allButFirst, x=>x.width+(x.needsSpace?20:0))\n\t\t\t+ (this.items.length - 2) * Options.AR*2\n\t\t\t+ (last.height > 0 ? Options.AR : 0)\n\t\t\t- Options.AR\n\t\t);\n\t\tvar lowerStart = x + Options.AR + first.width+(first.needsSpace?20:0) + Options.AR*2;\n\t\tnew Path(lowerStart, y+this._lowerTrack)\n\t\t\t.h(lowerSpan)\n\t\t\t.arc('se')\n\t\t\t.v(-(this._lowerTrack - Options.AR*2))\n\t\t\t.arc('wn')\n\t\t\t.addTo(this);\n\n\t\t// Items\n\t\tfor(const [i, item] of enumerate(this.items)) {\n\t\t\t// input track\n\t\t\tif(i === 0) {\n\t\t\t\tnew Path(x,y)\n\t\t\t\t\t.h(Options.AR)\n\t\t\t\t\t.addTo(this);\n\t\t\t\tx += Options.AR;\n\t\t\t} else {\n\t\t\t\tnew Path(x, y - this._upperTrack)\n\t\t\t\t\t.arc('ne')\n\t\t\t\t\t.v(this._upperTrack - Options.AR*2)\n\t\t\t\t\t.arc('ws')\n\t\t\t\t\t.addTo(this);\n\t\t\t\tx += Options.AR*2;\n\t\t\t}\n\n\t\t\t// item\n\t\t\tvar itemWidth = item.width + (item.needsSpace?20:0);\n\t\t\titem.format(x, y, itemWidth).addTo(this);\n\t\t\tx += itemWidth;\n\n\t\t\t// output track\n\t\t\tif(i === this.items.length-1) {\n\t\t\t\tif(item.height === 0) {\n\t\t\t\t\tnew Path(x,y)\n\t\t\t\t\t\t.h(Options.AR)\n\t\t\t\t\t\t.addTo(this);\n\t\t\t\t} else {\n\t\t\t\t\tnew Path(x,y+item.height)\n\t\t\t\t\t.arc('se')\n\t\t\t\t\t.addTo(this);\n\t\t\t\t}\n\t\t\t} else if(i === 0 && item.height > this._lowerTrack) {\n\t\t\t\t// Needs to arc up to meet the lower track, not down.\n\t\t\t\tif(item.height - this._lowerTrack >= Options.AR*2) {\n\t\t\t\t\tnew Path(x, y+item.height)\n\t\t\t\t\t\t.arc('se')\n\t\t\t\t\t\t.v(this._lowerTrack - item.height + Options.AR*2)\n\t\t\t\t\t\t.arc('wn')\n\t\t\t\t\t\t.addTo(this);\n\t\t\t\t} else {\n\t\t\t\t\t// Not enough space to fit two arcs\n\t\t\t\t\t// so just bail and draw a straight line for now.\n\t\t\t\t\tnew Path(x, y+item.height)\n\t\t\t\t\t\t.l(Options.AR*2, this._lowerTrack - item.height)\n\t\t\t\t\t\t.addTo(this);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnew Path(x, y+item.height)\n\t\t\t\t\t.arc('ne')\n\t\t\t\t\t.v(this._lowerTrack - item.height - Options.AR*2)\n\t\t\t\t\t.arc('ws')\n\t\t\t\t\t.addTo(this);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n}\nfuncs.HorizontalChoice = (...args)=>new HorizontalChoice(...args);\n\n\nexport class MultipleChoice extends DiagramMultiContainer {\n\tconstructor(normal, type, ...items) {\n\t\tsuper('g', items);\n\t\tif( typeof normal !== \"number\" || normal !== Math.floor(normal) ) {\n\t\t\tthrow new TypeError(\"The first argument of MultipleChoice() must be an integer.\");\n\t\t} else if(normal < 0 || normal >= items.length) {\n\t\t\tthrow new RangeError(\"The first argument of MultipleChoice() must be an index for one of the items.\");\n\t\t} else {\n\t\t\tthis.normal = normal;\n\t\t}\n\t\tif( type != \"any\" && type != \"all\" ) {\n\t\t\tthrow new SyntaxError(\"The second argument of MultipleChoice must be 'any' or 'all'.\");\n\t\t} else {\n\t\t\tthis.type = type;\n\t\t}\n\t\tthis.needsSpace = true;\n\t\tthis.innerWidth = max(this.items, function(x){return x.width});\n\t\tthis.width = 30 + Options.AR + this.innerWidth + Options.AR + 20;\n\t\tthis.up = this.items[0].up;\n\t\tthis.down = this.items[this.items.length-1].down;\n\t\tthis.height = this.items[normal].height;\n\t\tfor(var i = 0; i < this.items.length; i++) {\n\t\t\tlet item = this.items[i];\n\t\t\tlet minimum;\n\t\t\tif(i == normal - 1 || i == normal + 1) minimum = 10 + Options.AR;\n\t\t\telse minimum = Options.AR;\n\t\t\tif(i < normal) {\n\t\t\t\tthis.up += Math.max(minimum, item.height + item.down + Options.VS + this.items[i+1].up);\n\t\t\t} else if(i > normal) {\n\t\t\t\tthis.down += Math.max(minimum, item.up + Options.VS + this.items[i-1].down + this.items[i-1].height);\n\t\t\t}\n\t\t}\n\t\tthis.down -= this.items[normal].height; // already counted in this.height\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"multiplechoice\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x, y).right(gaps[0]).addTo(this);\n\t\tnew Path(x + gaps[0] + this.width, y + this.height).right(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tvar normal = this.items[this.normal];\n\n\t\t// Do the elements that curve above\n\t\tvar distanceFromY;\n\t\tfor(var i = this.normal - 1; i >= 0; i--) {\n\t\t\tvar item = this.items[i];\n\t\t\tif( i == this.normal - 1 ) {\n\t\t\t\tdistanceFromY = Math.max(10 + Options.AR, normal.up + Options.VS + item.down + item.height);\n\t\t\t}\n\t\t\tnew Path(x + 30,y)\n\t\t\t\t.up(distanceFromY - Options.AR)\n\t\t\t\t.arc('wn').addTo(this);\n\t\t\titem.format(x + 30 + Options.AR, y - distanceFromY, this.innerWidth).addTo(this);\n\t\t\tnew Path(x + 30 + Options.AR + this.innerWidth, y - distanceFromY + item.height)\n\t\t\t\t.arc('ne')\n\t\t\t\t.down(distanceFromY - item.height + this.height - Options.AR - 10)\n\t\t\t\t.addTo(this);\n\t\t\tif(i !== 0) {\n\t\t\t\tdistanceFromY += Math.max(Options.AR, item.up + Options.VS + this.items[i-1].down + this.items[i-1].height);\n\t\t\t}\n\t\t}\n\n\t\tnew Path(x + 30, y).right(Options.AR).addTo(this);\n\t\tnormal.format(x + 30 + Options.AR, y, this.innerWidth).addTo(this);\n\t\tnew Path(x + 30 + Options.AR + this.innerWidth, y + this.height).right(Options.AR).addTo(this);\n\n\t\tfor(i = this.normal+1; i < this.items.length; i++) {\n\t\t\tlet item = this.items[i];\n\t\t\tif(i == this.normal + 1) {\n\t\t\t\tdistanceFromY = Math.max(10+Options.AR, normal.height + normal.down + Options.VS + item.up);\n\t\t\t}\n\t\t\tnew Path(x + 30, y)\n\t\t\t\t.down(distanceFromY - Options.AR)\n\t\t\t\t.arc('ws')\n\t\t\t\t.addTo(this);\n\t\t\titem.format(x + 30 + Options.AR, y + distanceFromY, this.innerWidth).addTo(this);\n\t\t\tnew Path(x + 30 + Options.AR + this.innerWidth, y + distanceFromY + item.height)\n\t\t\t\t.arc('se')\n\t\t\t\t.up(distanceFromY - Options.AR + item.height - normal.height)\n\t\t\t\t.addTo(this);\n\t\t\tif(i != this.items.length - 1) {\n\t\t\t\tdistanceFromY += Math.max(Options.AR, item.height + item.down + Options.VS + this.items[i+1].up);\n\t\t\t}\n\t\t}\n\t\tvar text = new FakeSVG('g', {\"class\": \"diagram-text\"}).addTo(this);\n\t\tnew FakeSVG('title', {}, (this.type==\"any\"?\"take one or more branches, once each, in any order\":\"take all branches, once each, in any order\")).addTo(text);\n\t\tnew FakeSVG('path', {\n\t\t\t\"d\": \"M \"+(x+30)+\" \"+(y-10)+\" h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z\",\n\t\t\t\"class\": \"diagram-text\"\n\t\t\t}).addTo(text);\n\t\tnew FakeSVG('text', {\n\t\t\t\"x\": x + 15,\n\t\t\t\"y\": y + 4,\n\t\t\t\"class\": \"diagram-text\"\n\t\t\t}, (this.type==\"any\"?\"1+\":\"all\")).addTo(text);\n\t\tnew FakeSVG('path', {\n\t\t\t\"d\": \"M \"+(x+this.width-20)+\" \"+(y-10)+\" h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z\",\n\t\t\t\"class\": \"diagram-text\"\n\t\t\t}).addTo(text);\n\t\tnew FakeSVG('path', {\n\t\t\t\"d\": \"M \"+(x+this.width-13)+\" \"+(y-2)+\" a 4 4 0 1 0 6 -1 m 2.75 -1 h -4 v 4 m 0 -3 h 2\",\n\t\t\t\"style\": \"stroke-width: 1.75\"\n\t\t\t}).addTo(text);\n\t\treturn this;\n\t}\n}\nfuncs.MultipleChoice = (...args)=>new MultipleChoice(...args);\n\n\nexport class Optional extends FakeSVG {\n\tconstructor(item, skip) {\n\t\tif( skip === undefined )\n\t\t\treturn new Choice(1, new Skip(), item);\n\t\telse if ( skip === \"skip\" )\n\t\t\treturn new Choice(0, new Skip(), item);\n\t\telse\n\t\t\tthrow \"Unknown value for Optional()'s 'skip' argument.\";\n\t}\n}\nfuncs.Optional = (...args)=>new Optional(...args);\n\n\nexport class OneOrMore extends FakeSVG {\n\tconstructor(item, rep) {\n\t\tsuper('g');\n\t\trep = rep || (new Skip());\n\t\tthis.item = wrapString(item);\n\t\tthis.rep = wrapString(rep);\n\t\tthis.width = Math.max(this.item.width, this.rep.width) + Options.AR*2;\n\t\tthis.height = this.item.height;\n\t\tthis.up = this.item.up;\n\t\tthis.down = Math.max(Options.AR*2, this.item.down + Options.VS + this.rep.up + this.rep.height + this.rep.down);\n\t\tthis.needsSpace = true;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"oneormore\";\n\t\t}\n\t}\n\tformat(x,y,width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\t// Draw item\n\t\tnew Path(x,y).right(Options.AR).addTo(this);\n\t\tthis.item.format(x+Options.AR,y,this.width-Options.AR*2).addTo(this);\n\t\tnew Path(x+this.width-Options.AR,y+this.height).right(Options.AR).addTo(this);\n\n\t\t// Draw repeat arc\n\t\tvar distanceFromY = Math.max(Options.AR*2, this.item.height+this.item.down+Options.VS+this.rep.up);\n\t\tnew Path(x+Options.AR,y).arc('nw').down(distanceFromY-Options.AR*2).arc('ws').addTo(this);\n\t\tthis.rep.format(x+Options.AR, y+distanceFromY, this.width - Options.AR*2).addTo(this);\n\t\tnew Path(x+this.width-Options.AR, y+distanceFromY+this.rep.height).arc('se').up(distanceFromY-Options.AR*2+this.rep.height-this.item.height).arc('en').addTo(this);\n\n\t\treturn this;\n\t}\n\twalk(cb) {\n\t\tcb(this);\n\t\tthis.item.walk(cb);\n\t\tthis.rep.walk(cb);\n\t}\n}\nfuncs.OneOrMore = (...args)=>new OneOrMore(...args);\n\n\nexport class ZeroOrMore extends FakeSVG {\n\tconstructor(item, rep, skip) {\n\t\treturn new Optional(new OneOrMore(item, rep), skip);\n\t}\n}\nfuncs.ZeroOrMore = (...args)=>new ZeroOrMore(...args);\n\n\nexport class Group extends FakeSVG {\n\tconstructor(item, label) {\n\t\tsuper('g');\n\t\tthis.item = wrapString(item);\n\t\tthis.label =\n\t\t\tlabel instanceof FakeSVG\n\t\t\t\t? label\n\t\t\t\t: label\n\t\t\t\t\t? new Comment(label)\n\t\t\t\t\t: undefined;\n\n\t\tthis.width = Math.max(\n\t\t\tthis.item.width + (this.item.needsSpace?20:0),\n\t\t\tthis.label ? this.label.width : 0,\n\t\t\tOptions.AR*2);\n\t\tthis.height = this.item.height;\n\t\tthis.boxUp = this.up = Math.max(this.item.up + Options.VS, Options.AR);\n\t\tif(this.label) {\n\t\t\tthis.up += this.label.up + this.label.height + this.label.down;\n\t\t}\n\t\tthis.down = Math.max(this.item.down + Options.VS, Options.AR);\n\t\tthis.needsSpace = true;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"group\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tnew FakeSVG('rect', {\n\t\t\tx,\n\t\t\ty:y-this.boxUp,\n\t\t\twidth:this.width,\n\t\t\theight:this.boxUp + this.height + this.down,\n\t\t\trx: Options.AR,\n\t\t\try: Options.AR,\n\t\t\t'class':'group-box',\n\t\t}).addTo(this);\n\n\t\tthis.item.format(x,y,this.width).addTo(this);\n\t\tif(this.label) {\n\t\t\tthis.label.format(\n\t\t\t\tx,\n\t\t\t\ty-(this.boxUp+this.label.down+this.label.height),\n\t\t\t\tthis.label.width).addTo(this);\n\t\t}\n\n\t\treturn this;\n\t}\n\twalk(cb) {\n\t\tcb(this);\n\t\tthis.item.walk(cb);\n\t\tthis.label.walk(cb);\n\t}\n}\nfuncs.Group = (...args)=>new Group(...args);\n\n\nexport class Start extends FakeSVG {\n\tconstructor({type=\"simple\", label}={}) {\n\t\tsuper('g');\n\t\tthis.width = 20;\n\t\tthis.height = 0;\n\t\tthis.up = 10;\n\t\tthis.down = 10;\n\t\tthis.type = type;\n\t\tif(label) {\n\t\t\tthis.label = \"\"+label;\n\t\t\tthis.width = Math.max(20, this.label.length * Options.CHAR_WIDTH + 10);\n\t\t}\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"start\";\n\t\t}\n\t}\n\tformat(x,y) {\n\t\tlet path = new Path(x, y-10);\n\t\tif (this.type === \"complex\") {\n\t\t\tpath.down(20)\n\t\t\t\t.m(0, -10)\n\t\t\t\t.right(this.width)\n\t\t\t\t.addTo(this);\n\t\t} else {\n\t\t\tpath.down(20)\n\t\t\t\t.m(10, -20)\n\t\t\t\t.down(20)\n\t\t\t\t.m(-10, -10)\n\t\t\t\t.right(this.width)\n\t\t\t\t.addTo(this);\n\t\t}\n\t\tif(this.label) {\n\t\t\tnew FakeSVG('text', {x:x, y:y-15, style:\"text-anchor:start\"}, this.label).addTo(this);\n\t\t}\n\t\treturn this;\n\t}\n}\nfuncs.Start = (...args)=>new Start(...args);\n\n\nexport class End extends FakeSVG {\n\tconstructor({type=\"simple\"}={}) {\n\t\tsuper('path');\n\t\tthis.width = 20;\n\t\tthis.height = 0;\n\t\tthis.up = 10;\n\t\tthis.down = 10;\n\t\tthis.type = type;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"end\";\n\t\t}\n\t}\n\tformat(x,y) {\n\t\tif (this.type === \"complex\") {\n\t\t\tthis.attrs.d = 'M '+x+' '+y+' h 20 m 0 -10 v 20';\n\t\t} else {\n\t\t\tthis.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 -20 v 20';\n\t\t}\n\t\treturn this;\n\t}\n}\nfuncs.End = (...args)=>new End(...args);\n\n\nexport class Terminal extends FakeSVG {\n\tconstructor(text, {href, title}={}) {\n\t\tsuper('g', {'class': 'terminal'});\n\t\tthis.text = \"\"+text;\n\t\tthis.href = href;\n\t\tthis.title = title;\n\t\tthis.width = this.text.length * Options.CHAR_WIDTH + 20; /* Assume that each char is .5em, and that the em is 16px */\n\t\tthis.height = 0;\n\t\tthis.up = 11;\n\t\tthis.down = 11;\n\t\tthis.needsSpace = true;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"terminal\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tnew FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down, rx:10, ry:10}).addTo(this);\n\t\tvar text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text);\n\t\tif(this.href)\n\t\t\tnew FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this);\n\t\telse\n\t\t\ttext.addTo(this);\n\t\tif(this.title)\n\t\t\tnew FakeSVG('title', {}, [this.title]).addTo(this);\n\t\treturn this;\n\t}\n}\nfuncs.Terminal = (...args)=>new Terminal(...args);\n\n\nexport class NonTerminal extends FakeSVG {\n\tconstructor(text, {href, title}={}) {\n\t\tsuper('g', {'class': 'non-terminal'});\n\t\tthis.text = \"\"+text;\n\t\tthis.href = href;\n\t\tthis.title = title;\n\t\tthis.width = this.text.length * Options.CHAR_WIDTH + 20;\n\t\tthis.height = 0;\n\t\tthis.up = 11;\n\t\tthis.down = 11;\n\t\tthis.needsSpace = true;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"nonterminal\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tnew FakeSVG('rect', {x:x, y:y-11, width:this.width, height:this.up+this.down}).addTo(this);\n\t\tvar text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, this.text);\n\t\tif(this.href)\n\t\t\tnew FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this);\n\t\telse\n\t\t\ttext.addTo(this);\n\t\tif(this.title)\n\t\t\tnew FakeSVG('title', {}, [this.title]).addTo(this);\n\t\treturn this;\n\t}\n}\nfuncs.NonTerminal = (...args)=>new NonTerminal(...args);\n\n\nexport class Comment extends FakeSVG {\n\tconstructor(text, {href, title}={}) {\n\t\tsuper('g');\n\t\tthis.text = \"\"+text;\n\t\tthis.href = href;\n\t\tthis.title = title;\n\t\tthis.width = this.text.length * Options.COMMENT_CHAR_WIDTH + 10;\n\t\tthis.height = 0;\n\t\tthis.up = 8;\n\t\tthis.down = 8;\n\t\tthis.needsSpace = true;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"comment\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tvar text = new FakeSVG('text', {x:x+this.width/2, y:y+5, class:'comment'}, this.text);\n\t\tif(this.href)\n\t\t\tnew FakeSVG('a', {'xlink:href': this.href}, [text]).addTo(this);\n\t\telse\n\t\t\ttext.addTo(this);\n\t\tif(this.title)\n\t\t\tnew FakeSVG('title', {}, this.title).addTo(this);\n\t\treturn this;\n\t}\n}\nfuncs.Comment = (...args)=>new Comment(...args);\n\n\nexport class Skip extends FakeSVG {\n\tconstructor() {\n\t\tsuper('g');\n\t\tthis.width = 0;\n\t\tthis.height = 0;\n\t\tthis.up = 0;\n\t\tthis.down = 0;\n\t\tthis.needsSpace = false;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"skip\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\tnew Path(x,y).right(width).addTo(this);\n\t\treturn this;\n\t}\n}\nfuncs.Skip = (...args)=>new Skip(...args);\n\n\nexport class Block extends FakeSVG {\n\tconstructor({width=50, up=15, height=25, down=15, needsSpace=true}={}) {\n\t\tsuper('g');\n\t\tthis.width = width;\n\t\tthis.height = height;\n\t\tthis.up = up;\n\t\tthis.down = down;\n\t\tthis.needsSpace = needsSpace;\n\t\tif(Options.DEBUG) {\n\t\t\tthis.attrs['data-updown'] = this.up + \" \" + this.height + \" \" + this.down;\n\t\t\tthis.attrs['data-type'] = \"block\";\n\t\t}\n\t}\n\tformat(x, y, width) {\n\t\t// Hook up the two sides if this is narrower than its stated width.\n\t\tvar gaps = determineGaps(width, this.width);\n\t\tnew Path(x,y).h(gaps[0]).addTo(this);\n\t\tnew Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);\n\t\tx += gaps[0];\n\n\t\tnew FakeSVG('rect', {x:x, y:y-this.up, width:this.width, height:this.up+this.height+this.down}).addTo(this);\n\t\treturn this;\n\t}\n}\nfuncs.Block = (...args)=>new Block(...args);\n\n\nfunction unnull(...args) {\n\t// Return the first value that isn't undefined.\n\t// More correct than `v1 || v2 || v3` because falsey values will be returned.\n\treturn args.reduce(function(sofar, x) { return sofar !== undefined ? sofar : x; });\n}\n\nfunction determineGaps(outer, inner) {\n\tvar diff = outer - inner;\n\tswitch(Options.INTERNAL_ALIGNMENT) {\n\t\tcase 'left': return [0, diff];\n\t\tcase 'right': return [diff, 0];\n\t\tdefault: return [diff/2, diff/2];\n\t}\n}\n\nfunction wrapString(value) {\n\t\treturn value instanceof FakeSVG ? value : new Terminal(\"\"+value);\n}\n\nfunction sum(iter, func) {\n\tif(!func) func = function(x) { return x; };\n\treturn iter.map(func).reduce(function(a,b){return a+b}, 0);\n}\n\nfunction max(iter, func) {\n\tif(!func) func = function(x) { return x; };\n\treturn Math.max.apply(null, iter.map(func));\n}\n\nfunction SVG(name, attrs, text) {\n\tattrs = attrs || {};\n\ttext = text || '';\n\tvar el = document.createElementNS(\"http://www.w3.org/2000/svg\",name);\n\tfor(var attr in attrs) {\n\t\tif(attr === 'xlink:href')\n\t\t\tel.setAttributeNS(\"http://www.w3.org/1999/xlink\", 'href', attrs[attr]);\n\t\telse\n\t\t\tel.setAttribute(attr, attrs[attr]);\n\t}\n\tel.textContent = text;\n\treturn el;\n}\n\nfunction escapeString(string) {\n\t// Escape markdown and HTML special characters\n\treturn string.replace(/[*_`[\\]<&]/g, function(charString) {\n\t\treturn '&#' + charString.charCodeAt(0) + ';';\n\t});\n}\n\nfunction* enumerate(iter) {\n\tvar count = 0;\n\tfor(const x of iter) {\n\t\tyield [count, x];\n\t\tcount++;\n\t}\n}\n\nfunction diagramFromJSON(Diagram, input) {\n\tif (!input) return new Diagram();\n\t// Wrap an array of nodes in the diagram type decided by the parent\n\t// class of the calling static fromJSON method.\n\tif (Array.isArray(input)) {\n\t\treturn new Diagram(...input.map(nodeFromJSON));\n\t}\n\t// Retain the diagram type specified in the input regardless the parent\n\t// class of the calling static fromJSON method.\n\tswitch (input.type) {\n\t\tcase 'Diagram':\n\t\tcase 'ComplexDiagram':\n\t\t\treturn nodeFromJSON(input);\n\t}\n\t// Wrap the single node in the diagram type decided by the parent\n\t// class of the calling static fromJSON method.\n\treturn new Diagram(nodeFromJSON(input));\n}\n\nconst classes = {\n\tDiagram, ComplexDiagram, Sequence, Stack, VerticalSequence