This is an automated email from the ASF dual-hosted git repository.

vpyatkov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-website.git


The following commit(s) were added to refs/heads/master by this push:
     new 2247cf5379 Added railroad diagrams building lib (#137)
2247cf5379 is described below

commit 2247cf5379960e072a0697739ccefacd014fd3eb
Author: Alexey Alexandrov <[email protected]>
AuthorDate: Tue Sep 27 11:17:02 2022 +0300

    Added railroad diagrams building lib (#137)
    
    Co-authored-by: Alexey Alexandrov <[email protected]>
---
 _docs/_layouts/default.html         |    1 +
 _docs/_sass/railroad-diagram.scss   |   72 ++
 _docs/assets/css/styles.scss        |    1 +
 _docs/assets/js/railroad-diagram.js |   11 +
 _docs/assets/js/railroad.js         | 1420 +++++++++++++++++++++++++++++++++++
 assets/css/styles.css               |   24 +
 assets/css/styles.css.map           |    6 +-
 assets/js/railroad-diagram.js       |   11 +
 assets/js/railroad.js               | 1420 +++++++++++++++++++++++++++++++++++
 9 files changed, 2964 insertions(+), 2 deletions(-)

diff --git a/_docs/_layouts/default.html b/_docs/_layouts/default.html
index 992df4aa85..4f23c1a18f 100644
--- a/_docs/_layouts/default.html
+++ b/_docs/_layouts/default.html
@@ -104,6 +104,7 @@
     </script>
 <script type='module' src='{{"assets/js/index.js?"|append: timestamp | 
relative_url}}' async crossorigin></script>
 <script type='module' src='{{"assets/js/versioning.js?"|append: timestamp | 
relative_url}}' async crossorigin></script>
+<script type='module' src='{{"assets/js/railroad-diagram.js?"|append: 
timestamp | relative_url}}' async></script>
 
 <link rel="stylesheet" href="{{'assets/css/styles.css?'|append: timestamp 
|relative_url}}" media="print" onload="this.media='all'">
 <noscript><link media="all" rel="stylesheet" 
href="{{'assets/css/styles.css?'|append: timestamp |relative_url}}"></noscript>
diff --git a/_docs/_sass/railroad-diagram.scss 
b/_docs/_sass/railroad-diagram.scss
new file mode 100644
index 0000000000..245a81778d
--- /dev/null
+++ b/_docs/_sass/railroad-diagram.scss
@@ -0,0 +1,72 @@
+.diagram-container {
+
+  svg {
+    background-color: hsl(0, 0%, 100%);
+  }
+
+  path {
+    stroke-width: 2;
+    stroke: black;
+    fill: rgba(0, 0, 0, 0);
+  }
+
+  text {
+    font: bold 14px monospace;
+    text-anchor: middle;
+    white-space: pre;
+
+    &.diagram-text {
+      font-size: 12px;
+    }
+
+    &.diagram-arrow {
+      font-size: 16px;
+    }
+
+    &.label {
+      text-anchor: start;
+    }
+
+    &.comment {
+      font: italic 12px monospace;
+    }
+  }
+
+  g {
+    .non-terminal {
+      text {
+        /*font-style: italic;*/
+      }
+    }
+
+    a {
+      text {
+        fill: blue;
+      }
+    }
+  }
+
+  rect {
+    stroke-width: 2;
+    stroke: black;
+    fill: hsl(0, 0%, 100%);
+
+    &.group-box {
+      stroke: gray;
+      stroke-dasharray: 10 5;
+      fill: none;
+    }
+  }
+
+  path.diagram-text {
+    stroke-width: 2;
+    stroke: black;
+    fill: white;
+    cursor: help;
+  }
+
+  g.diagram-text:hover path.diagram-text {
+    fill: #eee;
+  }
+
+}
diff --git a/_docs/assets/css/styles.scss b/_docs/assets/css/styles.scss
index 078cb22436..1eb1c53578 100644
--- a/_docs/assets/css/styles.scss
+++ b/_docs/assets/css/styles.scss
@@ -10,6 +10,7 @@
 @import "left-nav";
 @import "right-nav";
 @import "footer";
+@import "railroad-diagram";
 
 @import "docs";
 
diff --git a/_docs/assets/js/railroad-diagram.js 
b/_docs/assets/js/railroad-diagram.js
new file mode 100644
index 0000000000..a903c5bbf7
--- /dev/null
+++ b/_docs/assets/js/railroad-diagram.js
@@ -0,0 +1,11 @@
+import rr, * as rrClass from "./railroad.js";
+Object.assign(window, rr);
+window.rrOptions = rrClass.Options;
+
+let elements = document.querySelectorAll('.diagram-container p');
+[].forEach.call(elements, function(el) {
+    let result = eval(el.innerHTML).format();
+    let diagramContainer = el.closest('.diagram-container');
+    diagramContainer.innerHTML = '';
+    result.addTo(diagramContainer);
+})
diff --git a/_docs/assets/js/railroad.js b/_docs/assets/js/railroad.js
new file mode 100644
index 0000000000..d029475095
--- /dev/null
+++ b/_docs/assets/js/railroad.js
@@ -0,0 +1,1420 @@
+"use strict";
+/*
+Railroad Diagrams
+by Tab Atkins Jr. (and others)
+http://xanthir.com
+http://twitter.com/tabatkins
+http://github.com/tabatkins/railroad-diagrams
+
+This document and all associated files in the github project are licensed 
under CC0: http://creativecommons.org/publicdomain/zero/1.0/
+This means you can reuse, remix, or otherwise appropriate this project for 
your own use WITHOUT RESTRICTION.
+(The actual legal meaning can be found at the above link.)
+Don't ask me for permission to use any part of this project, JUST USE IT.
+I would appreciate attribution, but that is not required by the license.
+*/
+
+// Export function versions of all the constructors.
+// Each class will add itself to this object.
+const funcs = {};
+export default funcs;
+
+export const Options = {
+       DEBUG: false, // if true, writes some debug information into attributes
+       VS: 8, // minimum vertical separation between things. For a 3px stroke, 
must be at least 4
+       AR: 10, // radius of arcs
+       DIAGRAM_CLASS: 'railroad-diagram', // class to put on the root <svg>
+       STROKE_ODD_PIXEL_LENGTH: true, // is the stroke width an odd (1px, 3px, 
etc) pixel length?
+       INTERNAL_ALIGNMENT: 'center', // how to align items when they have 
extra space. left/right/center
+       CHAR_WIDTH: 8.5, // width of each monospace character. play until you 
find the right value for your font
+       COMMENT_CHAR_WIDTH: 7, // comments are in smaller text by default
+};
+
+export const defaultCSS = `
+       svg {
+               background-color: hsl(30,20%,95%);
+       }
+       path {
+               stroke-width: 3;
+               stroke: black;
+               fill: rgba(0,0,0,0);
+       }
+       text {
+               font: bold 14px monospace;
+               text-anchor: middle;
+               white-space: pre;
+       }
+       text.diagram-text {
+               font-size: 12px;
+       }
+       text.diagram-arrow {
+               font-size: 16px;
+       }
+       text.label {
+               text-anchor: start;
+       }
+       text.comment {
+               font: italic 12px monospace;
+       }
+       g.non-terminal text {
+               /*font-style: italic;*/
+       }
+       rect {
+               stroke-width: 3;
+               stroke: black;
+               fill: hsl(120,100%,90%);
+       }
+       rect.group-box {
+               stroke: gray;
+               stroke-dasharray: 10 5;
+               fill: none;
+       }
+       path.diagram-text {
+               stroke-width: 3;
+               stroke: black;
+               fill: white;
+               cursor: help;
+       }
+       g.diagram-text:hover path.diagram-text {
+               fill: #eee;
+       }`;
+
+
+export class FakeSVG {
+       constructor(tagName, attrs, text) {
+               if(text) this.children = text;
+               else this.children = [];
+               this.tagName = tagName;
+               this.attrs = unnull(attrs, {});
+       }
+       format(x, y, width) {
+               // Virtual
+       }
+       addTo(parent) {
+               if(parent instanceof FakeSVG) {
+                       parent.children.push(this);
+                       return this;
+               } else {
+                       var svg = this.toSVG();
+                       parent.appendChild(svg);
+                       return svg;
+               }
+       }
+       toSVG() {
+               var el = SVG(this.tagName, this.attrs);
+               if(typeof this.children == 'string') {
+                       el.textContent = this.children;
+               } else {
+                       this.children.forEach(function(e) {
+                               el.appendChild(e.toSVG());
+                       });
+               }
+               return el;
+       }
+       toString() {
+               var str = '<' + this.tagName;
+               var group = this.tagName == "g" || this.tagName == "svg";
+               for(var attr in this.attrs) {
+                       str += ' ' + attr + '="' + 
(this.attrs[attr]+'').replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '"';
+               }
+               str += '>';
+               if(group) str += "\n";
+               if(typeof this.children == 'string') {
+                       str += escapeString(this.children);
+               } else {
+                       this.children.forEach(function(e) {
+                               str += e;
+                       });
+               }
+               str += '</' + this.tagName + '>\n';
+               return str;
+       }
+       walk(cb) {
+               cb(this);
+       }
+}
+
+
+export class Path extends FakeSVG {
+       constructor(x,y) {
+               super('path');
+               this.attrs.d = "M"+x+' '+y;
+       }
+       m(x,y) {
+               this.attrs.d += 'm'+x+' '+y;
+               return this;
+       }
+       h(val) {
+               this.attrs.d += 'h'+val;
+               return this;
+       }
+       right(val) { return this.h(Math.max(0, val)); }
+       left(val) { return this.h(-Math.max(0, val)); }
+       v(val) {
+               this.attrs.d += 'v'+val;
+               return this;
+       }
+       down(val) { return this.v(Math.max(0, val)); }
+       up(val) { return this.v(-Math.max(0, val)); }
+       arc(sweep){
+               // 1/4 of a circle
+               var x = Options.AR;
+               var y = Options.AR;
+               if(sweep[0] == 'e' || sweep[1] == 'w') {
+                       x *= -1;
+               }
+               if(sweep[0] == 's' || sweep[1] == 'n') {
+                       y *= -1;
+               }
+               var cw;
+               if(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 
'wn') {
+                       cw = 1;
+               } else {
+                       cw = 0;
+               }
+               this.attrs.d += "a"+Options.AR+" "+Options.AR+" 0 0 "+cw+' 
'+x+' '+y;
+               return this;
+       }
+       arc_8(start, dir) {
+               // 1/8 of a circle
+               const arc = Options.AR;
+               const s2 = 1/Math.sqrt(2) * arc;
+               const s2inv = (arc - s2);
+               let path = "a " + arc + " " + arc + " 0 0 " + (dir=='cw' ? "1" 
: "0") + " ";
+               const sd = start+dir;
+               const offset =
+                       sd == 'ncw'   ? [s2, s2inv] :
+                       sd == 'necw'  ? [s2inv, s2] :
+                       sd == 'ecw'   ? [-s2inv, s2] :
+                       sd == 'secw'  ? [-s2, s2inv] :
+                       sd == 'scw'   ? [-s2, -s2inv] :
+                       sd == 'swcw'  ? [-s2inv, -s2] :
+                       sd == 'wcw'   ? [s2inv, -s2] :
+                       sd == 'nwcw'  ? [s2, -s2inv] :
+                       sd == 'nccw'  ? [-s2, s2inv] :
+                       sd == 'nwccw' ? [-s2inv, s2] :
+                       sd == 'wccw'  ? [s2inv, s2] :
+                       sd == 'swccw' ? [s2, s2inv] :
+                       sd == 'sccw'  ? [s2, -s2inv] :
+                       sd == 'seccw' ? [s2inv, -s2] :
+                       sd == 'eccw'  ? [-s2inv, -s2] :
+                       sd == 'neccw' ? [-s2, -s2inv] : null
+               ;
+               path += offset.join(" ");
+               this.attrs.d += path;
+               return this;
+       }
+       l(x, y) {
+               this.attrs.d += 'l'+x+' '+y;
+               return this;
+       }
+       format() {
+               // All paths in this library start/end horizontally.
+               // The extra .5 ensures a minor overlap, so there's no seams in 
bad rasterizers.
+               this.attrs.d += 'h.5';
+               return this;
+       }
+}
+
+
+export class DiagramMultiContainer extends FakeSVG {
+       constructor(tagName, items, attrs, text) {
+               super(tagName, attrs, text);
+               this.items = items.map(wrapString);
+       }
+       walk(cb) {
+               cb(this);
+               this.items.forEach(x=>x.walk(cb));
+       }
+}
+
+
+export class Diagram extends DiagramMultiContainer {
+       constructor(...items) {
+               super('svg', items, {class: Options.DIAGRAM_CLASS});
+               if(!(this.items[0] instanceof Start)) {
+                       this.items.unshift(new Start());
+               }
+               if(!(this.items[this.items.length-1] instanceof End)) {
+                       this.items.push(new End());
+               }
+               this.up = this.down = this.height = this.width = 0;
+               for(const item of this.items) {
+                       this.width += item.width + (item.needsSpace?20:0);
+                       this.up = Math.max(this.up, item.up - this.height);
+                       this.height += item.height;
+                       this.down = Math.max(this.down - item.height, 
item.down);
+               }
+               this.formatted = false;
+       }
+       format(paddingt, paddingr, paddingb, paddingl) {
+               paddingt = unnull(paddingt, 20);
+               paddingr = unnull(paddingr, paddingt, 20);
+               paddingb = unnull(paddingb, paddingt, 20);
+               paddingl = unnull(paddingl, paddingr, 20);
+               var x = paddingl;
+               var y = paddingt;
+               y += this.up;
+               var g = new FakeSVG('g', Options.STROKE_ODD_PIXEL_LENGTH ? 
{transform:'translate(.5 .5)'} : {});
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       if(item.needsSpace) {
+                               new Path(x,y).h(10).addTo(g);
+                               x += 10;
+                       }
+                       item.format(x, y, item.width).addTo(g);
+                       x += item.width;
+                       y += item.height;
+                       if(item.needsSpace) {
+                               new Path(x,y).h(10).addTo(g);
+                               x += 10;
+                       }
+               }
+               this.attrs.width = this.width + paddingl + paddingr;
+               this.attrs.height = this.up + this.height + this.down + 
paddingt + paddingb;
+               this.attrs.viewBox = "0 0 " + this.attrs.width + " " + 
this.attrs.height;
+               g.addTo(this);
+               this.formatted = true;
+               return this;
+       }
+       addTo(parent) {
+               if(!parent) {
+                       var scriptTag = document.getElementsByTagName('script');
+                       scriptTag = scriptTag[scriptTag.length - 1];
+                       parent = scriptTag.parentNode;
+               }
+               return super.addTo.call(this, parent);
+       }
+       toSVG() {
+               if(!this.formatted) {
+                       this.format();
+               }
+               return super.toSVG.call(this);
+       }
+       toString() {
+               if(!this.formatted) {
+                       this.format();
+               }
+               return super.toString.call(this);
+       }
+       toStandalone(style) {
+               if(!this.formatted) {
+                       this.format();
+               }
+               const s = new FakeSVG('style', {}, style || defaultCSS);
+               this.children.push(s);
+               this.attrs.xmlns = "http://www.w3.org/2000/svg";;
+               this.attrs['xmlns:xlink'] = "http://www.w3.org/1999/xlink";;
+               const result = super.toString.call(this);
+               this.children.pop();
+               delete this.attrs.xmlns;
+               return result;
+       }
+}
+funcs.Diagram = (...args)=>new Diagram(...args);
+
+
+export class ComplexDiagram extends FakeSVG {
+       constructor(...items) {
+               var diagram = new Diagram(...items);
+               diagram.items[0] = new Start({type:"complex"});
+               diagram.items[diagram.items.length-1] = new 
End({type:"complex"});
+               return diagram;
+       }
+}
+funcs.ComplexDiagram = (...args)=>new ComplexDiagram(...args);
+
+
+export class Sequence extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               var numberOfItems = this.items.length;
+               this.needsSpace = true;
+               this.up = this.down = this.height = this.width = 0;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       this.width += item.width + (item.needsSpace?20:0);
+                       this.up = Math.max(this.up, item.up - this.height);
+                       this.height += item.height;
+                       this.down = Math.max(this.down - item.height, 
item.down);
+               }
+               if(this.items[0].needsSpace) this.width -= 10;
+               if(this.items[this.items.length-1].needsSpace) this.width -= 10;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "sequence";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       if(item.needsSpace && i > 0) {
+                               new Path(x,y).h(10).addTo(this);
+                               x += 10;
+                       }
+                       item.format(x, y, item.width).addTo(this);
+                       x += item.width;
+                       y += item.height;
+                       if(item.needsSpace && i < this.items.length-1) {
+                               new Path(x,y).h(10).addTo(this);
+                               x += 10;
+                       }
+               }
+               return this;
+       }
+}
+funcs.Sequence = (...args)=>new Sequence(...args);
+
+
+export class Stack extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 0 ) {
+                       throw new RangeError("Stack() must have at least one 
child.");
+               }
+               this.width = Math.max.apply(null, this.items.map(function(e) { 
return e.width + (e.needsSpace?20:0); }));
+               //if(this.items[0].needsSpace) this.width -= 10;
+               //if(this.items[this.items.length-1].needsSpace) this.width -= 
10;
+               if(this.items.length > 1){
+                       this.width += Options.AR*2;
+               }
+               this.needsSpace = true;
+               this.up = this.items[0].up;
+               this.down = this.items[this.items.length-1].down;
+
+               this.height = 0;
+               var last = this.items.length - 1;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       this.height += item.height;
+                       if(i > 0) {
+                               this.height += Math.max(Options.AR*2, item.up + 
Options.VS);
+                       }
+                       if(i < last) {
+                               this.height += Math.max(Options.AR*2, item.down 
+ Options.VS);
+                       }
+               }
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "stack";
+               }
+       }
+       format(x,y,width) {
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               x += gaps[0];
+               var xInitial = x;
+               if(this.items.length > 1) {
+                       new Path(x, y).h(Options.AR).addTo(this);
+                       x += Options.AR;
+               }
+
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       var innerWidth = this.width - (this.items.length>1 ? 
Options.AR*2 : 0);
+                       item.format(x, y, innerWidth).addTo(this);
+                       x += innerWidth;
+                       y += item.height;
+
+                       if(i !== this.items.length-1) {
+                               new Path(x, y)
+                                       .arc('ne').down(Math.max(0, item.down + 
Options.VS - Options.AR*2))
+                                       .arc('es').left(innerWidth)
+                                       .arc('nw').down(Math.max(0, 
this.items[i+1].up + Options.VS - Options.AR*2))
+                                       .arc('ws').addTo(this);
+                               y += Math.max(item.down + Options.VS, 
Options.AR*2) + Math.max(this.items[i+1].up + Options.VS, Options.AR*2);
+                               //y += Math.max(Options.AR*4, item.down + 
Options.VS*2 + this.items[i+1].up)
+                               x = xInitial+Options.AR;
+                       }
+
+               }
+
+               if(this.items.length > 1) {
+                       new Path(x,y).h(Options.AR).addTo(this);
+                       x += Options.AR;
+               }
+               new Path(x,y).h(gaps[1]).addTo(this);
+
+               return this;
+       }
+}
+funcs.Stack = (...args)=>new Stack(...args);
+
+
+export class OptionalSequence extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 0 ) {
+                       throw new RangeError("OptionalSequence() must have at 
least one child.");
+               }
+               if( items.length === 1 ) {
+                       return new Sequence(items);
+               }
+               var arc = Options.AR;
+               this.needsSpace = false;
+               this.width = 0;
+               this.up = 0;
+               this.height = sum(this.items, function(x){return x.height});
+               this.down = this.items[0].down;
+               var heightSoFar = 0;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       this.up = Math.max(this.up, Math.max(arc*2, item.up + 
Options.VS) - heightSoFar);
+                       heightSoFar += item.height;
+                       if(i > 0) {
+                               this.down = Math.max(this.height + this.down, 
heightSoFar + Math.max(arc*2, item.down + Options.VS)) - this.height;
+                       }
+                       var itemWidth = (item.needsSpace?10:0) + item.width;
+                       if(i === 0) {
+                               this.width += arc + Math.max(itemWidth, arc);
+                       } else {
+                               this.width += arc*2 + Math.max(itemWidth, arc) 
+ arc;
+                       }
+               }
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "optseq";
+               }
+       }
+       format(x, y, width) {
+               var arc = Options.AR;
+               var gaps = determineGaps(width, this.width);
+               new Path(x, y).right(gaps[0]).addTo(this);
+               new Path(x + gaps[0] + this.width, y + 
this.height).right(gaps[1]).addTo(this);
+               x += gaps[0];
+               var upperLineY = y - this.up;
+               var last = this.items.length - 1;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       var itemSpace = (item.needsSpace?10:0);
+                       var itemWidth = item.width + itemSpace;
+                       if(i === 0) {
+                               // Upper skip
+                               new Path(x,y)
+                                       .arc('se')
+                                       .up(y - upperLineY - arc*2)
+                                       .arc('wn')
+                                       .right(itemWidth - arc)
+                                       .arc('ne')
+                                       .down(y + item.height - upperLineY - 
arc*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                               // Straight line
+                               new Path(x, y)
+                                       .right(itemSpace + arc)
+                                       .addTo(this);
+                               item.format(x + itemSpace + arc, y, 
item.width).addTo(this);
+                               x += itemWidth + arc;
+                               y += item.height;
+                               // x ends on the far side of the first element,
+                               // where the next element's skip needs to begin
+                       } else if(i < last) {
+                               // Upper skip
+                               new Path(x, upperLineY)
+                                       .right(arc*2 + Math.max(itemWidth, arc) 
+ arc)
+                                       .arc('ne')
+                                       .down(y - upperLineY + item.height - 
arc*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                               // Straight line
+                               new Path(x,y)
+                                       .right(arc*2)
+                                       .addTo(this);
+                               item.format(x + arc*2, y, 
item.width).addTo(this);
+                               new Path(x + item.width + arc*2, y + 
item.height)
+                                       .right(itemSpace + arc)
+                                       .addTo(this);
+                               // Lower skip
+                               new Path(x,y)
+                                       .arc('ne')
+                                       .down(item.height + Math.max(item.down 
+ Options.VS, arc*2) - arc*2)
+                                       .arc('ws')
+                                       .right(itemWidth - arc)
+                                       .arc('se')
+                                       .up(item.down + Options.VS - arc*2)
+                                       .arc('wn')
+                                       .addTo(this);
+                               x += arc*2 + Math.max(itemWidth, arc) + arc;
+                               y += item.height;
+                       } else {
+                               // Straight line
+                               new Path(x, y)
+                                       .right(arc*2)
+                                       .addTo(this);
+                               item.format(x + arc*2, y, 
item.width).addTo(this);
+                               new Path(x + arc*2 + item.width, y + 
item.height)
+                                       .right(itemSpace + arc)
+                                       .addTo(this);
+                               // Lower skip
+                               new Path(x,y)
+                                       .arc('ne')
+                                       .down(item.height + Math.max(item.down 
+ Options.VS, arc*2) - arc*2)
+                                       .arc('ws')
+                                       .right(itemWidth - arc)
+                                       .arc('se')
+                                       .up(item.down + Options.VS - arc*2)
+                                       .arc('wn')
+                                       .addTo(this);
+                       }
+               }
+               return this;
+       }
+}
+funcs.OptionalSequence = (...args)=>new OptionalSequence(...args);
+
+
+export class AlternatingSequence extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 1 ) {
+                       return new Sequence(items);
+               }
+               if( items.length !== 2 ) {
+                       throw new RangeError("AlternatingSequence() must have 
one or two children.");
+               }
+               this.needsSpace = false;
+
+               const arc = Options.AR;
+               const vert = Options.VS;
+               const max = Math.max;
+               const first = this.items[0];
+               const second = this.items[1];
+
+               const arcX = 1 / Math.sqrt(2) * arc * 2;
+               const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2;
+               const crossY = Math.max(arc, Options.VS);
+               const crossX = (crossY - arcY) + arcX;
+
+               const firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 
+ vert + first.down);
+               this.up = firstOut + first.height + first.up;
+
+               const secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 
+ vert + second.up);
+               this.down = secondIn + second.height + second.down;
+
+               this.height = 0;
+
+               const firstWidth = 2*(first.needsSpace?10:0) + first.width;
+               const secondWidth = 2*(second.needsSpace?10:0) + second.width;
+               this.width = 2*arc + max(firstWidth, crossX, secondWidth) + 
2*arc;
+
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "altseq";
+               }
+       }
+       format(x, y, width) {
+               const arc = Options.AR;
+               const gaps = determineGaps(width, this.width);
+               new Path(x,y).right(gaps[0]).addTo(this);
+               x += gaps[0];
+               new Path(x+this.width, y).right(gaps[1]).addTo(this);
+               // bounding box
+               //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);
+               const first = this.items[0];
+               const second = this.items[1];
+
+               // top
+               const firstIn = this.up - first.up;
+               const firstOut = this.up - first.up - first.height;
+               new Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(this);
+               first.format(x + 2*arc, y - firstIn, this.width - 
4*arc).addTo(this);
+               new Path(x + this.width - 2*arc, y - 
firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(this);
+
+               // bottom
+               const secondIn = this.down - second.down - second.height;
+               const secondOut = this.down - second.down;
+               new Path(x,y).arc('ne').down(secondIn - 
2*arc).arc('ws').addTo(this);
+               second.format(x + 2*arc, y + secondIn, this.width - 
4*arc).addTo(this);
+               new Path(x + this.width - 2*arc, y + 
secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(this);
+
+               // crossover
+               const arcX = 1 / Math.sqrt(2) * arc * 2;
+               const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2;
+               const crossY = Math.max(arc, Options.VS);
+               const crossX = (crossY - arcY) + arcX;
+               const crossBar = (this.width - 4*arc - crossX)/2;
+               new Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar)
+                       .arc_8('n', 'cw').l(crossX - arcX, crossY - 
arcY).arc_8('sw', 'ccw')
+                       .right(crossBar).arc('ne').addTo(this);
+               new Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar)
+                       .arc_8('s', 'ccw').l(crossX - arcX, -(crossY - 
arcY)).arc_8('nw', 'cw')
+                       .right(crossBar).arc('se').addTo(this);
+
+               return this;
+       }
+}
+funcs.AlternatingSequence = (...args)=>new AlternatingSequence(...args);
+
+
+export class Choice extends DiagramMultiContainer {
+       constructor(normal, ...items) {
+               super('g', items);
+               if( typeof normal !== "number" || normal !== Math.floor(normal) 
) {
+                       throw new TypeError("The first argument of Choice() 
must be an integer.");
+               } else if(normal < 0 || normal >= items.length) {
+                       throw new RangeError("The first argument of Choice() 
must be an index for one of the items.");
+               } else {
+                       this.normal = normal;
+               }
+               var first = 0;
+               var last = items.length - 1;
+               this.width = Math.max.apply(null, 
this.items.map(function(el){return el.width})) + Options.AR*4;
+               this.height = this.items[normal].height;
+               this.up = this.items[first].up;
+               var arcs;
+               for(var i = first; i < normal; i++) {
+                       if(i == normal-1) arcs = Options.AR*2;
+                       else arcs = Options.AR;
+                       this.up += Math.max(arcs, this.items[i].height + 
this.items[i].down + Options.VS + this.items[i+1].up);
+               }
+               this.down = this.items[last].down;
+               for(i = normal+1; i <= last; i++) {
+                       if(i == normal+1) arcs = Options.AR*2;
+                       else arcs = Options.AR;
+                       this.down += Math.max(arcs, this.items[i-1].height + 
this.items[i-1].down + Options.VS + this.items[i].up);
+               }
+               this.down -= this.items[normal].height; // already counted in 
Choice.height
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "choice";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               var last = this.items.length -1;
+               var innerWidth = this.width - Options.AR*4;
+
+               // Do the elements that curve above
+               var distanceFromY;
+               for(var i = this.normal - 1; i >= 0; i--) {
+                       let item = this.items[i];
+                       if( i == this.normal - 1 ) {
+                               distanceFromY = Math.max(Options.AR*2, 
this.items[this.normal].up + Options.VS + item.down + item.height);
+                       }
+                       new Path(x,y)
+                               .arc('se')
+                               .up(distanceFromY - Options.AR*2)
+                               .arc('wn').addTo(this);
+                       item.format(x+Options.AR*2,y - 
distanceFromY,innerWidth).addTo(this);
+                       new Path(x+Options.AR*2+innerWidth, 
y-distanceFromY+item.height)
+                               .arc('ne')
+                               .down(distanceFromY - item.height + this.height 
- Options.AR*2)
+                               .arc('ws').addTo(this);
+                       distanceFromY += Math.max(Options.AR, item.up + 
Options.VS + (i === 0 ? 0 : this.items[i-1].down+this.items[i-1].height));
+               }
+
+               // Do the straight-line path.
+               new Path(x,y).right(Options.AR*2).addTo(this);
+               this.items[this.normal].format(x+Options.AR*2, y, 
innerWidth).addTo(this);
+               new Path(x+Options.AR*2+innerWidth, 
y+this.height).right(Options.AR*2).addTo(this);
+
+               // Do the elements that curve below
+               for(i = this.normal+1; i <= last; i++) {
+                       let item = this.items[i];
+                       if( i == this.normal + 1 ) {
+                               distanceFromY = Math.max(Options.AR*2, 
this.height + this.items[this.normal].down + Options.VS + item.up);
+                       }
+                       new Path(x,y)
+                               .arc('ne')
+                               .down(distanceFromY - Options.AR*2)
+                               .arc('ws').addTo(this);
+                       item.format(x+Options.AR*2, y+distanceFromY, 
innerWidth).addTo(this);
+                       new Path(x+Options.AR*2+innerWidth, 
y+distanceFromY+item.height)
+                               .arc('se')
+                               .up(distanceFromY - Options.AR*2 + item.height 
- this.height)
+                               .arc('wn').addTo(this);
+                       distanceFromY += Math.max(Options.AR, item.height + 
item.down + Options.VS + (i == last ? 0 : this.items[i+1].up));
+               }
+
+               return this;
+       }
+}
+funcs.Choice = (...args)=>new Choice(...args);
+
+
+export class HorizontalChoice extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 0 ) {
+                       throw new RangeError("HorizontalChoice() must have at 
least one child.");
+               }
+               if( items.length === 1) {
+                       return new Sequence(items);
+               }
+               const allButLast = this.items.slice(0, -1);
+               const middles = this.items.slice(1, -1);
+               const first = this.items[0];
+               const last = this.items[this.items.length - 1];
+               this.needsSpace = false;
+
+               this.width = Options.AR; // starting track
+               this.width += Options.AR*2 * (this.items.length-1); // 
inbetween tracks
+               this.width += sum(this.items, x=>x.width + 
(x.needsSpace?20:0)); // items
+               this.width += (last.height > 0 ? Options.AR : 0); // needs 
space to curve up
+               this.width += Options.AR; //ending track
+
+               // Always exits at entrance height
+               this.height = 0;
+
+               // All but the last have a track running above them
+               this._upperTrack = Math.max(
+                       Options.AR*2,
+                       Options.VS,
+                       max(allButLast, x=>x.up) + Options.VS
+               );
+               this.up = Math.max(this._upperTrack, last.up);
+
+               // All but the first have a track running below them
+               // Last either straight-lines or curves up, so has different 
calculation
+               this._lowerTrack = Math.max(
+                       Options.VS,
+                       max(middles, x=>x.height+Math.max(x.down+Options.VS, 
Options.AR*2)),
+                       last.height + last.down + Options.VS
+               );
+               if(first.height < this._lowerTrack) {
+                       // Make sure there's at least 2*AR room between first 
exit and lower track
+                       this._lowerTrack = Math.max(this._lowerTrack, 
first.height + Options.AR*2);
+               }
+               this.down = Math.max(this._lowerTrack, first.height + 
first.down);
+
+
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "horizontalchoice";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               const first = this.items[0];
+               const last = this.items[this.items.length-1];
+               const allButFirst = this.items.slice(1);
+               const allButLast = this.items.slice(0, -1);
+
+               // upper track
+               var upperSpan = (sum(allButLast, x=>x.width+(x.needsSpace?20:0))
+                       + (this.items.length - 2) * Options.AR*2
+                       - Options.AR
+               );
+               new Path(x,y)
+                       .arc('se')
+                       .v(-(this._upperTrack - Options.AR*2))
+                       .arc('wn')
+                       .h(upperSpan)
+                       .addTo(this);
+
+               // lower track
+               var lowerSpan = (sum(allButFirst, 
x=>x.width+(x.needsSpace?20:0))
+                       + (this.items.length - 2) * Options.AR*2
+                       + (last.height > 0 ? Options.AR : 0)
+                       - Options.AR
+               );
+               var lowerStart = x + Options.AR + 
first.width+(first.needsSpace?20:0) + Options.AR*2;
+               new Path(lowerStart, y+this._lowerTrack)
+                       .h(lowerSpan)
+                       .arc('se')
+                       .v(-(this._lowerTrack - Options.AR*2))
+                       .arc('wn')
+                       .addTo(this);
+
+               // Items
+               for(const [i, item] of enumerate(this.items)) {
+                       // input track
+                       if(i === 0) {
+                               new Path(x,y)
+                                       .h(Options.AR)
+                                       .addTo(this);
+                               x += Options.AR;
+                       } else {
+                               new Path(x, y - this._upperTrack)
+                                       .arc('ne')
+                                       .v(this._upperTrack - Options.AR*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                               x += Options.AR*2;
+                       }
+
+                       // item
+                       var itemWidth = item.width + (item.needsSpace?20:0);
+                       item.format(x, y, itemWidth).addTo(this);
+                       x += itemWidth;
+
+                       // output track
+                       if(i === this.items.length-1) {
+                               if(item.height === 0) {
+                                       new Path(x,y)
+                                               .h(Options.AR)
+                                               .addTo(this);
+                               } else {
+                                       new Path(x,y+item.height)
+                                       .arc('se')
+                                       .addTo(this);
+                               }
+                       } else if(i === 0 && item.height > this._lowerTrack) {
+                               // Needs to arc up to meet the lower track, not 
down.
+                               if(item.height - this._lowerTrack >= 
Options.AR*2) {
+                                       new Path(x, y+item.height)
+                                               .arc('se')
+                                               .v(this._lowerTrack - 
item.height + Options.AR*2)
+                                               .arc('wn')
+                                               .addTo(this);
+                               } else {
+                                       // Not enough space to fit two arcs
+                                       // so just bail and draw a straight 
line for now.
+                                       new Path(x, y+item.height)
+                                               .l(Options.AR*2, 
this._lowerTrack - item.height)
+                                               .addTo(this);
+                               }
+                       } else {
+                               new Path(x, y+item.height)
+                                       .arc('ne')
+                                       .v(this._lowerTrack - item.height - 
Options.AR*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                       }
+               }
+               return this;
+       }
+}
+funcs.HorizontalChoice = (...args)=>new HorizontalChoice(...args);
+
+
+export class MultipleChoice extends DiagramMultiContainer {
+       constructor(normal, type, ...items) {
+               super('g', items);
+               if( typeof normal !== "number" || normal !== Math.floor(normal) 
) {
+                       throw new TypeError("The first argument of 
MultipleChoice() must be an integer.");
+               } else if(normal < 0 || normal >= items.length) {
+                       throw new RangeError("The first argument of 
MultipleChoice() must be an index for one of the items.");
+               } else {
+                       this.normal = normal;
+               }
+               if( type != "any" && type != "all" ) {
+                       throw new SyntaxError("The second argument of 
MultipleChoice must be 'any' or 'all'.");
+               } else {
+                       this.type = type;
+               }
+               this.needsSpace = true;
+               this.innerWidth = max(this.items, function(x){return x.width});
+               this.width = 30 + Options.AR + this.innerWidth + Options.AR + 
20;
+               this.up = this.items[0].up;
+               this.down = this.items[this.items.length-1].down;
+               this.height = this.items[normal].height;
+               for(var i = 0; i < this.items.length; i++) {
+                       let item = this.items[i];
+                       let minimum;
+                       if(i == normal - 1 || i == normal + 1) minimum = 10 + 
Options.AR;
+                       else minimum = Options.AR;
+                       if(i < normal) {
+                               this.up += Math.max(minimum, item.height + 
item.down + Options.VS + this.items[i+1].up);
+                       } else if(i > normal) {
+                               this.down += Math.max(minimum, item.up + 
Options.VS + this.items[i-1].down + this.items[i-1].height);
+                       }
+               }
+               this.down -= this.items[normal].height; // already counted in 
this.height
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "multiplechoice";
+               }
+       }
+       format(x, y, width) {
+               var gaps = determineGaps(width, this.width);
+               new Path(x, y).right(gaps[0]).addTo(this);
+               new Path(x + gaps[0] + this.width, y + 
this.height).right(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               var normal = this.items[this.normal];
+
+               // Do the elements that curve above
+               var distanceFromY;
+               for(var i = this.normal - 1; i >= 0; i--) {
+                       var item = this.items[i];
+                       if( i == this.normal - 1 ) {
+                               distanceFromY = Math.max(10 + Options.AR, 
normal.up + Options.VS + item.down + item.height);
+                       }
+                       new Path(x + 30,y)
+                               .up(distanceFromY - Options.AR)
+                               .arc('wn').addTo(this);
+                       item.format(x + 30 + Options.AR, y - distanceFromY, 
this.innerWidth).addTo(this);
+                       new Path(x + 30 + Options.AR + this.innerWidth, y - 
distanceFromY + item.height)
+                               .arc('ne')
+                               .down(distanceFromY - item.height + this.height 
- Options.AR - 10)
+                               .addTo(this);
+                       if(i !== 0) {
+                               distanceFromY += Math.max(Options.AR, item.up + 
Options.VS + this.items[i-1].down + this.items[i-1].height);
+                       }
+               }
+
+               new Path(x + 30, y).right(Options.AR).addTo(this);
+               normal.format(x + 30 + Options.AR, y, 
this.innerWidth).addTo(this);
+               new Path(x + 30 + Options.AR + this.innerWidth, y + 
this.height).right(Options.AR).addTo(this);
+
+               for(i = this.normal+1; i < this.items.length; i++) {
+                       let item = this.items[i];
+                       if(i == this.normal + 1) {
+                               distanceFromY = Math.max(10+Options.AR, 
normal.height + normal.down + Options.VS + item.up);
+                       }
+                       new Path(x + 30, y)
+                               .down(distanceFromY - Options.AR)
+                               .arc('ws')
+                               .addTo(this);
+                       item.format(x + 30 + Options.AR, y + distanceFromY, 
this.innerWidth).addTo(this);
+                       new Path(x + 30 + Options.AR + this.innerWidth, y + 
distanceFromY + item.height)
+                               .arc('se')
+                               .up(distanceFromY - Options.AR + item.height - 
normal.height)
+                               .addTo(this);
+                       if(i != this.items.length - 1) {
+                               distanceFromY += Math.max(Options.AR, 
item.height + item.down + Options.VS + this.items[i+1].up);
+                       }
+               }
+               var text = new FakeSVG('g', {"class": 
"diagram-text"}).addTo(this);
+               new 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);
+               new FakeSVG('path', {
+                       "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",
+                       "class": "diagram-text"
+                       }).addTo(text);
+               new FakeSVG('text', {
+                       "x": x + 15,
+                       "y": y + 4,
+                       "class": "diagram-text"
+                       }, (this.type=="any"?"1+":"all")).addTo(text);
+               new FakeSVG('path', {
+                       "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",
+                       "class": "diagram-text"
+                       }).addTo(text);
+               new FakeSVG('path', {
+                       "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",
+                       "style": "stroke-width: 1.75"
+                       }).addTo(text);
+               return this;
+       }
+}
+funcs.MultipleChoice = (...args)=>new MultipleChoice(...args);
+
+
+export class Optional extends FakeSVG {
+       constructor(item, skip) {
+               if( skip === undefined )
+                       return new Choice(1, new Skip(), item);
+               else if ( skip === "skip" )
+                       return new Choice(0, new Skip(), item);
+               else
+                       throw "Unknown value for Optional()'s 'skip' argument.";
+       }
+}
+funcs.Optional = (...args)=>new Optional(...args);
+
+
+export class OneOrMore extends FakeSVG {
+       constructor(item, rep) {
+               super('g');
+               rep = rep || (new Skip());
+               this.item = wrapString(item);
+               this.rep = wrapString(rep);
+               this.width = Math.max(this.item.width, this.rep.width) + 
Options.AR*2;
+               this.height = this.item.height;
+               this.up = this.item.up;
+               this.down = Math.max(Options.AR*2, this.item.down + Options.VS 
+ this.rep.up + this.rep.height + this.rep.down);
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "oneormore";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               // Draw item
+               new Path(x,y).right(Options.AR).addTo(this);
+               
this.item.format(x+Options.AR,y,this.width-Options.AR*2).addTo(this);
+               new 
Path(x+this.width-Options.AR,y+this.height).right(Options.AR).addTo(this);
+
+               // Draw repeat arc
+               var distanceFromY = Math.max(Options.AR*2, 
this.item.height+this.item.down+Options.VS+this.rep.up);
+               new 
Path(x+Options.AR,y).arc('nw').down(distanceFromY-Options.AR*2).arc('ws').addTo(this);
+               this.rep.format(x+Options.AR, y+distanceFromY, this.width - 
Options.AR*2).addTo(this);
+               new 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);
+
+               return this;
+       }
+       walk(cb) {
+               cb(this);
+               this.item.walk(cb);
+               this.rep.walk(cb);
+       }
+}
+funcs.OneOrMore = (...args)=>new OneOrMore(...args);
+
+
+export class ZeroOrMore extends FakeSVG {
+       constructor(item, rep, skip) {
+               return new Optional(new OneOrMore(item, rep), skip);
+       }
+}
+funcs.ZeroOrMore = (...args)=>new ZeroOrMore(...args);
+
+
+export class Group extends FakeSVG {
+       constructor(item, label) {
+               super('g');
+               this.item = wrapString(item);
+               this.label =
+                       label instanceof FakeSVG
+                         ? label
+                       : label
+                         ? new Comment(label)
+                         : undefined;
+
+               this.width = Math.max(
+                       this.item.width + (this.item.needsSpace?20:0),
+                       this.label ? this.label.width : 0,
+                       Options.AR*2);
+               this.height = this.item.height;
+               this.boxUp = this.up = Math.max(this.item.up + Options.VS, 
Options.AR);
+               if(this.label) {
+                       this.up += this.label.up + this.label.height + 
this.label.down;
+               }
+               this.down = Math.max(this.item.down + Options.VS, Options.AR);
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "group";
+               }
+       }
+       format(x, y, width) {
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {
+                       x,
+                       y:y-this.boxUp,
+                       width:this.width,
+                       height:this.boxUp + this.height + this.down,
+                       rx: Options.AR,
+                       ry: Options.AR,
+                       'class':'group-box',
+               }).addTo(this);
+
+               this.item.format(x,y,this.width).addTo(this);
+               if(this.label) {
+                       this.label.format(
+                               x,
+                               
y-(this.boxUp+this.label.down+this.label.height),
+                               this.label.width).addTo(this);
+               }
+
+               return this;
+       }
+       walk(cb) {
+               cb(this);
+               this.item.walk(cb);
+               this.label.walk(cb);
+       }
+}
+funcs.Group = (...args)=>new Group(...args);
+
+
+export class Start extends FakeSVG {
+       constructor({type="simple", label}={}) {
+               super('g');
+               this.width = 20;
+               this.height = 0;
+               this.up = 10;
+               this.down = 10;
+               this.type = type;
+               if(label) {
+                       this.label = ""+label;
+                       this.width = Math.max(20, this.label.length * 
Options.CHAR_WIDTH + 10);
+               }
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "start";
+               }
+       }
+       format(x,y) {
+               let path = new Path(x, y-10);
+               if (this.type === "complex") {
+                       path.down(20)
+                               .m(0, -10)
+                               .right(this.width)
+                               .addTo(this);
+               } else {
+                       path.down(20)
+                               .m(10, -20)
+                               .down(20)
+                               .m(-10, -10)
+                               .right(this.width)
+                               .addTo(this);
+               }
+               if(this.label) {
+                       new FakeSVG('text', {x:x, y:y-15, 
style:"text-anchor:start"}, this.label).addTo(this);
+               }
+               return this;
+       }
+}
+funcs.Start = (...args)=>new Start(...args);
+
+
+export class End extends FakeSVG {
+       constructor({type="simple"}={}) {
+               super('path');
+               this.width = 20;
+               this.height = 0;
+               this.up = 10;
+               this.down = 10;
+               this.type = type;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "end";
+               }
+       }
+       format(x,y) {
+               if (this.type === "complex") {
+                       this.attrs.d = 'M '+x+' '+y+' h 20 m 0 -10 v 20';
+               } else {
+                       this.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 
-20 v 20';
+               }
+               return this;
+       }
+}
+funcs.End = (...args)=>new End(...args);
+
+
+export class Terminal extends FakeSVG {
+       constructor(text, {href, title, cls}={}) {
+               super('g', {'class': ['terminal', cls].join(" ")});
+               this.text = ""+text;
+               this.href = href;
+               this.title = title;
+               this.cls = cls;
+               this.width = this.text.length * Options.CHAR_WIDTH + 20; /* 
Assume that each char is .5em, and that the em is 16px */
+               this.height = 0;
+               this.up = 11;
+               this.down = 11;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "terminal";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {x:x, y:y-11, width:this.width, 
height:this.up+this.down, rx:10, ry:10}).addTo(this);
+               var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, 
this.text);
+               if(this.href)
+                       new FakeSVG('a', {'xlink:href': this.href}, 
[text]).addTo(this);
+               else
+                       text.addTo(this);
+               if(this.title)
+                       new FakeSVG('title', {}, [this.title]).addTo(this);
+               return this;
+       }
+}
+funcs.Terminal = (...args)=>new Terminal(...args);
+
+
+export class NonTerminal extends FakeSVG {
+       constructor(text, {href, title, cls=""}={}) {
+               super('g', {'class': ['non-terminal', cls].join(" ")});
+               this.text = ""+text;
+               this.href = href;
+               this.title = title;
+               this.cls = cls;
+               this.width = this.text.length * Options.CHAR_WIDTH + 20;
+               this.height = 0;
+               this.up = 11;
+               this.down = 11;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "nonterminal";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {x:x, y:y-11, width:this.width, 
height:this.up+this.down}).addTo(this);
+               var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, 
this.text);
+               if(this.href)
+                       new FakeSVG('a', {'xlink:href': this.href}, 
[text]).addTo(this);
+               else
+                       text.addTo(this);
+               if(this.title)
+                       new FakeSVG('title', {}, [this.title]).addTo(this);
+               return this;
+       }
+}
+funcs.NonTerminal = (...args)=>new NonTerminal(...args);
+
+
+export class Comment extends FakeSVG {
+       constructor(text, {href, title, cls=""}={}) {
+               super('g', {'class': ['comment', cls].join(" ")});
+               this.text = ""+text;
+               this.href = href;
+               this.title = title;
+               this.cls = cls;
+               this.width = this.text.length * Options.COMMENT_CHAR_WIDTH + 10;
+               this.height = 0;
+               this.up = 8;
+               this.down = 8;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "comment";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               var text = new FakeSVG('text', {x:x+this.width/2, y:y+5, 
class:'comment'}, this.text);
+               if(this.href)
+                       new FakeSVG('a', {'xlink:href': this.href}, 
[text]).addTo(this);
+               else
+                       text.addTo(this);
+               if(this.title)
+                       new FakeSVG('title', {}, this.title).addTo(this);
+               return this;
+       }
+}
+funcs.Comment = (...args)=>new Comment(...args);
+
+
+export class Skip extends FakeSVG {
+       constructor() {
+               super('g');
+               this.width = 0;
+               this.height = 0;
+               this.up = 0;
+               this.down = 0;
+               this.needsSpace = false;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "skip";
+               }
+       }
+       format(x, y, width) {
+               new Path(x,y).right(width).addTo(this);
+               return this;
+       }
+}
+funcs.Skip = (...args)=>new Skip(...args);
+
+
+export class Block extends FakeSVG {
+       constructor({width=50, up=15, height=25, down=15, needsSpace=true}={}) {
+               super('g');
+               this.width = width;
+               this.height = height;
+               this.up = up;
+               this.down = down;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "block";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {x:x, y:y-this.up, width:this.width, 
height:this.up+this.height+this.down}).addTo(this);
+               return this;
+       }
+}
+funcs.Block = (...args)=>new Block(...args);
+
+
+function unnull(...args) {
+       // Return the first value that isn't undefined.
+       // More correct than `v1 || v2 || v3` because falsey values will be 
returned.
+       return args.reduce(function(sofar, x) { return sofar !== undefined ? 
sofar : x; });
+}
+
+function determineGaps(outer, inner) {
+       var diff = outer - inner;
+       switch(Options.INTERNAL_ALIGNMENT) {
+               case 'left': return [0, diff];
+               case 'right': return [diff, 0];
+               default: return [diff/2, diff/2];
+       }
+}
+
+function wrapString(value) {
+               return value instanceof FakeSVG ? value : new 
Terminal(""+value);
+}
+
+function sum(iter, func) {
+       if(!func) func = function(x) { return x; };
+       return iter.map(func).reduce(function(a,b){return a+b}, 0);
+}
+
+function max(iter, func) {
+       if(!func) func = function(x) { return x; };
+       return Math.max.apply(null, iter.map(func));
+}
+
+function SVG(name, attrs, text) {
+       attrs = attrs || {};
+       text = text || '';
+       var el = document.createElementNS("http://www.w3.org/2000/svg",name);
+       for(var attr in attrs) {
+               if(attr === 'xlink:href')
+                       el.setAttributeNS("http://www.w3.org/1999/xlink";, 
'href', attrs[attr]);
+               else
+                       el.setAttribute(attr, attrs[attr]);
+       }
+       el.textContent = text;
+       return el;
+}
+
+function escapeString(string) {
+       // Escape markdown and HTML special characters
+       return string.replace(/[*_\`\[\]<&]/g, function(charString) {
+               return '&#' + charString.charCodeAt(0) + ';';
+       });
+}
+
+function* enumerate(iter) {
+       var count = 0;
+       for(const x of iter) {
+               yield [count, x];
+               count++;
+       }
+}
diff --git a/assets/css/styles.css b/assets/css/styles.css
index 590c1cd445..af0a5bf3e8 100644
--- a/assets/css/styles.css
+++ b/assets/css/styles.css
@@ -357,6 +357,30 @@ body > footer { border-top: 2px solid #dddddd; height: 
var(--footer-height); fon
 
 @media (max-width: 570px) { body > footer .copyright__extra { display: none; } 
}
 
+.diagram-container svg { background-color: white; }
+
+.diagram-container path { stroke-width: 2; stroke: black; fill: rgba(0, 0, 0, 
0); }
+
+.diagram-container text { font: bold 14px monospace; text-anchor: middle; 
white-space: pre; }
+
+.diagram-container text.diagram-text { font-size: 12px; }
+
+.diagram-container text.diagram-arrow { font-size: 16px; }
+
+.diagram-container text.label { text-anchor: start; }
+
+.diagram-container text.comment { font: italic 12px monospace; }
+
+.diagram-container g .non-terminal text { /*font-style: italic;*/ }
+
+.diagram-container rect { stroke-width: 2; stroke: black; fill: white; }
+
+.diagram-container rect.group-box { stroke: gray; stroke-dasharray: 10 5; 
fill: none; }
+
+.diagram-container path.diagram-text { stroke-width: 2; stroke: black; fill: 
white; cursor: help; }
+
+.diagram-container g.diagram-text:hover path.diagram-text { fill: #eee; }
+
 section.page-docs { display: grid; transition: grid-template-columns 0.15s; 
grid-template-columns: auto 1fr auto; grid-template-rows: 100%; 
grid-template-areas: 'left-nav content right-nav'; line-height: 20px; 
max-width: 1440px; margin: auto; width: 100%; }
 
 section.page-docs > article { border-left: 1px solid #eeeeee; 
background-color: #ffffff; padding: 0 50px 30px; grid-area: content; overflow: 
hidden; font-family: sans-serif; font-size: 16px; color: #545454; line-height: 
1.6em; }
diff --git a/assets/css/styles.css.map b/assets/css/styles.css.map
index 42b2c93cc0..ef19767ceb 100644
--- a/assets/css/styles.css.map
+++ b/assets/css/styles.css.map
@@ -13,11 +13,12 @@
                "_sass/left-nav.scss",
                "_sass/right-nav.scss",
                "_sass/footer.scss",
+               "_sass/railroad-diagram.scss",
                "_sass/docs.scss",
                "_sass/asciidoc-pygments.scss"
        ],
        "sourcesContent": [
-               "@import \"variables\";\n@import \"header\";\n@import 
\"code\";\n@import \"rouge-base16-solarized\";\n@import \"text\";\n@import 
\"callouts\";\n@import \"layout\";\n@import \"left-nav\";\n@import 
\"right-nav\";\n@import \"footer\";\n\n@import \"docs\";\n\n@import 
\"asciidoc-pygments\";",
+               "@import \"variables\";\n@import \"header\";\n@import 
\"code\";\n@import \"rouge-base16-solarized\";\n@import \"text\";\n@import 
\"callouts\";\n@import \"layout\";\n@import \"left-nav\";\n@import 
\"right-nav\";\n@import \"footer\";\n@import 'railroad-diagram';\n\n@import 
\"docs\";\n\n@import \"asciidoc-pygments\";",
                ":root {\n    --gg-red: #ec1c24;\n    --gg-orange: #ec1c24;\n   
 --gg-orange-dark: #bc440b;\n    --gg-orange-filter: invert(47%) sepia(61%) 
saturate(1950%) hue-rotate(345deg) brightness(100%) contrast(95%);\n    
--gg-dark-gray: #333333;\n    --orange-line-thickness: 3px;\n    
--block-code-background: rgba(241, 241, 241, 20%);\n    
--inline-code-background: rgba(241, 241, 241, 90%);\n    --padding-top: 25px; 
\n    --link-color: #ec1c24;\n    --body-background: #fcfcfc;\n}\n\n@font-face  
[...]
                "header {\n\n    min-height: var(--header-height);\n    
background: white;\n    box-shadow: 0 4px 10px 0 #eeeeee, 0 0 4px 0 #d5d5d5;\n  
  \n\n    z-index: 1;\n\n    #promotion-bar {\n        background-color: 
#333333;\n        padding: 8px;\n        \n        p {\n            font-size: 
14px;\n            line-height: 1.4em;\n            font-weight: 600;\n         
   padding: 0;\n            margin: 0;\n\n            color: #f0f0f0;\n         
   text-align: center;\n\n            a {\ [...]
                "pre, pre.rouge {\n    padding: 8px 15px;\n    background: 
var(--block-code-background) !important;\n    border-radius: 5px;\n    border: 
1px solid #e5e5e5;\n    overflow-x: auto;\n    // So code copy button doesn't 
overflow\n    min-height: 36px;\n\tline-height: 18px;\n    color: 
#545454;\n}\n\ncode {\n    color: #545454;\n}\n\npre.rouge code {\n    
background: none !important;\n}\n\npre.rouge .tok-err {\n  \tborder: none 
!important;\n  }\n\ncode-tabs.code-tabs__initialized {\n    dis [...]
@@ -28,9 +29,10 @@
                ".left-nav {\n    padding: 10px 20px;\n    width: 289px;\n    
overflow-y: auto;\n    top: calc(var(--header-height) + 
var(--promotion-bar-height));\n    height: calc(100vh - var(--header-height) - 
var(--promotion-bar-height));\n    font-family: 'Open Sans';\n    padding-top: 
var(--padding-top);\n    background-color: var(--body-background);\n\n    li 
{\n        list-style: none;\n    }    \n    a, button {\n        
text-decoration: none;\n        color: #757575;\n        font-size: 16p [...]
                ".right-nav {\n    width: 289px;\n    padding: 12px 26px;\n    
overflow-y: auto;\n    height: calc(100vh - var(--header-height));\n    top: 
var(--header-height);\n    position: -webkit-sticky;\n    position: sticky;\n   
 display: flex;\n    flex-direction: column;\n    font-family: 'Open sans';\n   
 padding-top: var(--padding-top);\n    background-color: #ffffff;\n    \n    h6 
{\n        margin: 12px 0;\n        font-size: 16px;\n        font-weight: 
normal;\n    }\n\n    ul {\n        [...]
                "body > footer {\n    border-top: 2px solid #dddddd;\n    
height: var(--footer-height);\n    font-size: 16px;\n    color: #393939;\n    
display: flex;\n    justify-content: space-between;\n    align-items: 
center;\n\n\n    @media (max-width: 570px) {\n        .copyright__extra {\n     
       display: none;\n        }\n    }\n}\n// .right-nav footer {\n//     
font-size: 12px;\n//     padding: calc(var(--footer-gap) * 0.3) 0 5px;;\n//     
text-align: left;\n//     margin: auto 0 0;\n\n// [...]
+               ".diagram-container {\n\n  svg {\n    background-color: hsl(0, 
0%, 100%);\n  }\n\n  path {\n    stroke-width: 2;\n    stroke: black;\n    
fill: rgba(0, 0, 0, 0);\n  }\n\n  text {\n    font: bold 14px monospace;\n    
text-anchor: middle;\n    white-space: pre;\n\n    &.diagram-text {\n      
font-size: 12px;\n    }\n\n    &.diagram-arrow {\n      font-size: 16px;\n    
}\n\n    &.label {\n      text-anchor: start;\n    }\n\n    &.comment {\n      
font: italic 12px monospace;\n    }\n  }\n [...]
                "section.page-docs {\n    display: grid;\n    transition: 
grid-template-columns 0.15s;\n    grid-template-columns: auto 1fr auto;\n    
grid-template-rows: 100%;\n    grid-template-areas: 'left-nav content 
right-nav';\n    line-height: 20px;\n    max-width: 1440px;\n    margin: 
auto;\n    width: 100%;\n\n    &>article {\n        // box-shadow: -1px 13px 
20px 0 #696c70;\n        border-left: 1px solid #eeeeee;\n        
background-color: #ffffff;\n        padding: 0 50px 30px;\n        gr [...]
                "pre.pygments .hll { background-color: #ffffcc }\npre.pygments, 
pre.pygments code { background: #ffffff; }\npre.pygments .tok-c { color: 
#008000 } /* Comment */\npre.pygments .tok-err { border: 1px solid #FF0000 } /* 
Error */\npre.pygments .tok-k { color: #0000ff } /* Keyword */\npre.pygments 
.tok-ch { color: #008000 } /* Comment.Hashbang */\npre.pygments .tok-cm { 
color: #008000 } /* Comment.Multiline */\npre.pygments .tok-cp { color: #0000ff 
} /* Comment.Preproc */\npre.pygments .tok [...]
        ],
        "names": [],
-       "mappings": 
"CCAC,AAAD,IAAK,CAAC,EACF,QAAQ,CAAA,QAAC,EACT,WAAW,CAAA,QAAC,EACZ,gBAAgB,CAAA,QAAC,EACjB,kBAAkB,CAAA,yFAAC,EACnB,cAAc,CAAA,QAAC,EACf,uBAAuB,CAAA,IAAC,EACxB,uBAAuB,CAAA,yBAAC,EACxB,wBAAwB,CAAA,yBAAC,EACzB,aAAa,CAAA,KAAC,EACd,YAAY,CAAA,QAAC,EACb,iBAAiB,CAAA,QAAC,GACrB;;AAED,UAAU,GACN,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,GAAG,EAChB,YAAY,EAAE,IAAI,EAClB,UAAU,EAAE,MAAM;;AClBtB,AAAA,MAAM,CAAC,EAEH,UAAU,EAAE,oBAAoB,EAChC,UAAU,EAAE,KAAK,EACjB,UAAU,EAAE,uCAAuC,EAGnD,OAAO,EAAE,CAAC,GAiYb;;A
 [...]
+       "mappings": 
"CCAC,AAAD,IAAK,CAAC,EACF,QAAQ,CAAA,QAAC,EACT,WAAW,CAAA,QAAC,EACZ,gBAAgB,CAAA,QAAC,EACjB,kBAAkB,CAAA,yFAAC,EACnB,cAAc,CAAA,QAAC,EACf,uBAAuB,CAAA,IAAC,EACxB,uBAAuB,CAAA,yBAAC,EACxB,wBAAwB,CAAA,yBAAC,EACzB,aAAa,CAAA,KAAC,EACd,YAAY,CAAA,QAAC,EACb,iBAAiB,CAAA,QAAC,GACrB;;AAED,UAAU,GACN,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,GAAG,EAChB,YAAY,EAAE,IAAI,EAClB,UAAU,EAAE,MAAM;;AClBtB,AAAA,MAAM,CAAC,EAEH,UAAU,EAAE,oBAAoB,EAChC,UAAU,EAAE,KAAK,EACjB,UAAU,EAAE,uCAAuC,EAGnD,OAAO,EAAE,CAAC,GAiYb;;A
 [...]
 }
\ No newline at end of file
diff --git a/assets/js/railroad-diagram.js b/assets/js/railroad-diagram.js
new file mode 100644
index 0000000000..a903c5bbf7
--- /dev/null
+++ b/assets/js/railroad-diagram.js
@@ -0,0 +1,11 @@
+import rr, * as rrClass from "./railroad.js";
+Object.assign(window, rr);
+window.rrOptions = rrClass.Options;
+
+let elements = document.querySelectorAll('.diagram-container p');
+[].forEach.call(elements, function(el) {
+    let result = eval(el.innerHTML).format();
+    let diagramContainer = el.closest('.diagram-container');
+    diagramContainer.innerHTML = '';
+    result.addTo(diagramContainer);
+})
diff --git a/assets/js/railroad.js b/assets/js/railroad.js
new file mode 100644
index 0000000000..d029475095
--- /dev/null
+++ b/assets/js/railroad.js
@@ -0,0 +1,1420 @@
+"use strict";
+/*
+Railroad Diagrams
+by Tab Atkins Jr. (and others)
+http://xanthir.com
+http://twitter.com/tabatkins
+http://github.com/tabatkins/railroad-diagrams
+
+This document and all associated files in the github project are licensed 
under CC0: http://creativecommons.org/publicdomain/zero/1.0/
+This means you can reuse, remix, or otherwise appropriate this project for 
your own use WITHOUT RESTRICTION.
+(The actual legal meaning can be found at the above link.)
+Don't ask me for permission to use any part of this project, JUST USE IT.
+I would appreciate attribution, but that is not required by the license.
+*/
+
+// Export function versions of all the constructors.
+// Each class will add itself to this object.
+const funcs = {};
+export default funcs;
+
+export const Options = {
+       DEBUG: false, // if true, writes some debug information into attributes
+       VS: 8, // minimum vertical separation between things. For a 3px stroke, 
must be at least 4
+       AR: 10, // radius of arcs
+       DIAGRAM_CLASS: 'railroad-diagram', // class to put on the root <svg>
+       STROKE_ODD_PIXEL_LENGTH: true, // is the stroke width an odd (1px, 3px, 
etc) pixel length?
+       INTERNAL_ALIGNMENT: 'center', // how to align items when they have 
extra space. left/right/center
+       CHAR_WIDTH: 8.5, // width of each monospace character. play until you 
find the right value for your font
+       COMMENT_CHAR_WIDTH: 7, // comments are in smaller text by default
+};
+
+export const defaultCSS = `
+       svg {
+               background-color: hsl(30,20%,95%);
+       }
+       path {
+               stroke-width: 3;
+               stroke: black;
+               fill: rgba(0,0,0,0);
+       }
+       text {
+               font: bold 14px monospace;
+               text-anchor: middle;
+               white-space: pre;
+       }
+       text.diagram-text {
+               font-size: 12px;
+       }
+       text.diagram-arrow {
+               font-size: 16px;
+       }
+       text.label {
+               text-anchor: start;
+       }
+       text.comment {
+               font: italic 12px monospace;
+       }
+       g.non-terminal text {
+               /*font-style: italic;*/
+       }
+       rect {
+               stroke-width: 3;
+               stroke: black;
+               fill: hsl(120,100%,90%);
+       }
+       rect.group-box {
+               stroke: gray;
+               stroke-dasharray: 10 5;
+               fill: none;
+       }
+       path.diagram-text {
+               stroke-width: 3;
+               stroke: black;
+               fill: white;
+               cursor: help;
+       }
+       g.diagram-text:hover path.diagram-text {
+               fill: #eee;
+       }`;
+
+
+export class FakeSVG {
+       constructor(tagName, attrs, text) {
+               if(text) this.children = text;
+               else this.children = [];
+               this.tagName = tagName;
+               this.attrs = unnull(attrs, {});
+       }
+       format(x, y, width) {
+               // Virtual
+       }
+       addTo(parent) {
+               if(parent instanceof FakeSVG) {
+                       parent.children.push(this);
+                       return this;
+               } else {
+                       var svg = this.toSVG();
+                       parent.appendChild(svg);
+                       return svg;
+               }
+       }
+       toSVG() {
+               var el = SVG(this.tagName, this.attrs);
+               if(typeof this.children == 'string') {
+                       el.textContent = this.children;
+               } else {
+                       this.children.forEach(function(e) {
+                               el.appendChild(e.toSVG());
+                       });
+               }
+               return el;
+       }
+       toString() {
+               var str = '<' + this.tagName;
+               var group = this.tagName == "g" || this.tagName == "svg";
+               for(var attr in this.attrs) {
+                       str += ' ' + attr + '="' + 
(this.attrs[attr]+'').replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '"';
+               }
+               str += '>';
+               if(group) str += "\n";
+               if(typeof this.children == 'string') {
+                       str += escapeString(this.children);
+               } else {
+                       this.children.forEach(function(e) {
+                               str += e;
+                       });
+               }
+               str += '</' + this.tagName + '>\n';
+               return str;
+       }
+       walk(cb) {
+               cb(this);
+       }
+}
+
+
+export class Path extends FakeSVG {
+       constructor(x,y) {
+               super('path');
+               this.attrs.d = "M"+x+' '+y;
+       }
+       m(x,y) {
+               this.attrs.d += 'm'+x+' '+y;
+               return this;
+       }
+       h(val) {
+               this.attrs.d += 'h'+val;
+               return this;
+       }
+       right(val) { return this.h(Math.max(0, val)); }
+       left(val) { return this.h(-Math.max(0, val)); }
+       v(val) {
+               this.attrs.d += 'v'+val;
+               return this;
+       }
+       down(val) { return this.v(Math.max(0, val)); }
+       up(val) { return this.v(-Math.max(0, val)); }
+       arc(sweep){
+               // 1/4 of a circle
+               var x = Options.AR;
+               var y = Options.AR;
+               if(sweep[0] == 'e' || sweep[1] == 'w') {
+                       x *= -1;
+               }
+               if(sweep[0] == 's' || sweep[1] == 'n') {
+                       y *= -1;
+               }
+               var cw;
+               if(sweep == 'ne' || sweep == 'es' || sweep == 'sw' || sweep == 
'wn') {
+                       cw = 1;
+               } else {
+                       cw = 0;
+               }
+               this.attrs.d += "a"+Options.AR+" "+Options.AR+" 0 0 "+cw+' 
'+x+' '+y;
+               return this;
+       }
+       arc_8(start, dir) {
+               // 1/8 of a circle
+               const arc = Options.AR;
+               const s2 = 1/Math.sqrt(2) * arc;
+               const s2inv = (arc - s2);
+               let path = "a " + arc + " " + arc + " 0 0 " + (dir=='cw' ? "1" 
: "0") + " ";
+               const sd = start+dir;
+               const offset =
+                       sd == 'ncw'   ? [s2, s2inv] :
+                       sd == 'necw'  ? [s2inv, s2] :
+                       sd == 'ecw'   ? [-s2inv, s2] :
+                       sd == 'secw'  ? [-s2, s2inv] :
+                       sd == 'scw'   ? [-s2, -s2inv] :
+                       sd == 'swcw'  ? [-s2inv, -s2] :
+                       sd == 'wcw'   ? [s2inv, -s2] :
+                       sd == 'nwcw'  ? [s2, -s2inv] :
+                       sd == 'nccw'  ? [-s2, s2inv] :
+                       sd == 'nwccw' ? [-s2inv, s2] :
+                       sd == 'wccw'  ? [s2inv, s2] :
+                       sd == 'swccw' ? [s2, s2inv] :
+                       sd == 'sccw'  ? [s2, -s2inv] :
+                       sd == 'seccw' ? [s2inv, -s2] :
+                       sd == 'eccw'  ? [-s2inv, -s2] :
+                       sd == 'neccw' ? [-s2, -s2inv] : null
+               ;
+               path += offset.join(" ");
+               this.attrs.d += path;
+               return this;
+       }
+       l(x, y) {
+               this.attrs.d += 'l'+x+' '+y;
+               return this;
+       }
+       format() {
+               // All paths in this library start/end horizontally.
+               // The extra .5 ensures a minor overlap, so there's no seams in 
bad rasterizers.
+               this.attrs.d += 'h.5';
+               return this;
+       }
+}
+
+
+export class DiagramMultiContainer extends FakeSVG {
+       constructor(tagName, items, attrs, text) {
+               super(tagName, attrs, text);
+               this.items = items.map(wrapString);
+       }
+       walk(cb) {
+               cb(this);
+               this.items.forEach(x=>x.walk(cb));
+       }
+}
+
+
+export class Diagram extends DiagramMultiContainer {
+       constructor(...items) {
+               super('svg', items, {class: Options.DIAGRAM_CLASS});
+               if(!(this.items[0] instanceof Start)) {
+                       this.items.unshift(new Start());
+               }
+               if(!(this.items[this.items.length-1] instanceof End)) {
+                       this.items.push(new End());
+               }
+               this.up = this.down = this.height = this.width = 0;
+               for(const item of this.items) {
+                       this.width += item.width + (item.needsSpace?20:0);
+                       this.up = Math.max(this.up, item.up - this.height);
+                       this.height += item.height;
+                       this.down = Math.max(this.down - item.height, 
item.down);
+               }
+               this.formatted = false;
+       }
+       format(paddingt, paddingr, paddingb, paddingl) {
+               paddingt = unnull(paddingt, 20);
+               paddingr = unnull(paddingr, paddingt, 20);
+               paddingb = unnull(paddingb, paddingt, 20);
+               paddingl = unnull(paddingl, paddingr, 20);
+               var x = paddingl;
+               var y = paddingt;
+               y += this.up;
+               var g = new FakeSVG('g', Options.STROKE_ODD_PIXEL_LENGTH ? 
{transform:'translate(.5 .5)'} : {});
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       if(item.needsSpace) {
+                               new Path(x,y).h(10).addTo(g);
+                               x += 10;
+                       }
+                       item.format(x, y, item.width).addTo(g);
+                       x += item.width;
+                       y += item.height;
+                       if(item.needsSpace) {
+                               new Path(x,y).h(10).addTo(g);
+                               x += 10;
+                       }
+               }
+               this.attrs.width = this.width + paddingl + paddingr;
+               this.attrs.height = this.up + this.height + this.down + 
paddingt + paddingb;
+               this.attrs.viewBox = "0 0 " + this.attrs.width + " " + 
this.attrs.height;
+               g.addTo(this);
+               this.formatted = true;
+               return this;
+       }
+       addTo(parent) {
+               if(!parent) {
+                       var scriptTag = document.getElementsByTagName('script');
+                       scriptTag = scriptTag[scriptTag.length - 1];
+                       parent = scriptTag.parentNode;
+               }
+               return super.addTo.call(this, parent);
+       }
+       toSVG() {
+               if(!this.formatted) {
+                       this.format();
+               }
+               return super.toSVG.call(this);
+       }
+       toString() {
+               if(!this.formatted) {
+                       this.format();
+               }
+               return super.toString.call(this);
+       }
+       toStandalone(style) {
+               if(!this.formatted) {
+                       this.format();
+               }
+               const s = new FakeSVG('style', {}, style || defaultCSS);
+               this.children.push(s);
+               this.attrs.xmlns = "http://www.w3.org/2000/svg";;
+               this.attrs['xmlns:xlink'] = "http://www.w3.org/1999/xlink";;
+               const result = super.toString.call(this);
+               this.children.pop();
+               delete this.attrs.xmlns;
+               return result;
+       }
+}
+funcs.Diagram = (...args)=>new Diagram(...args);
+
+
+export class ComplexDiagram extends FakeSVG {
+       constructor(...items) {
+               var diagram = new Diagram(...items);
+               diagram.items[0] = new Start({type:"complex"});
+               diagram.items[diagram.items.length-1] = new 
End({type:"complex"});
+               return diagram;
+       }
+}
+funcs.ComplexDiagram = (...args)=>new ComplexDiagram(...args);
+
+
+export class Sequence extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               var numberOfItems = this.items.length;
+               this.needsSpace = true;
+               this.up = this.down = this.height = this.width = 0;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       this.width += item.width + (item.needsSpace?20:0);
+                       this.up = Math.max(this.up, item.up - this.height);
+                       this.height += item.height;
+                       this.down = Math.max(this.down - item.height, 
item.down);
+               }
+               if(this.items[0].needsSpace) this.width -= 10;
+               if(this.items[this.items.length-1].needsSpace) this.width -= 10;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "sequence";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       if(item.needsSpace && i > 0) {
+                               new Path(x,y).h(10).addTo(this);
+                               x += 10;
+                       }
+                       item.format(x, y, item.width).addTo(this);
+                       x += item.width;
+                       y += item.height;
+                       if(item.needsSpace && i < this.items.length-1) {
+                               new Path(x,y).h(10).addTo(this);
+                               x += 10;
+                       }
+               }
+               return this;
+       }
+}
+funcs.Sequence = (...args)=>new Sequence(...args);
+
+
+export class Stack extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 0 ) {
+                       throw new RangeError("Stack() must have at least one 
child.");
+               }
+               this.width = Math.max.apply(null, this.items.map(function(e) { 
return e.width + (e.needsSpace?20:0); }));
+               //if(this.items[0].needsSpace) this.width -= 10;
+               //if(this.items[this.items.length-1].needsSpace) this.width -= 
10;
+               if(this.items.length > 1){
+                       this.width += Options.AR*2;
+               }
+               this.needsSpace = true;
+               this.up = this.items[0].up;
+               this.down = this.items[this.items.length-1].down;
+
+               this.height = 0;
+               var last = this.items.length - 1;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       this.height += item.height;
+                       if(i > 0) {
+                               this.height += Math.max(Options.AR*2, item.up + 
Options.VS);
+                       }
+                       if(i < last) {
+                               this.height += Math.max(Options.AR*2, item.down 
+ Options.VS);
+                       }
+               }
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "stack";
+               }
+       }
+       format(x,y,width) {
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               x += gaps[0];
+               var xInitial = x;
+               if(this.items.length > 1) {
+                       new Path(x, y).h(Options.AR).addTo(this);
+                       x += Options.AR;
+               }
+
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       var innerWidth = this.width - (this.items.length>1 ? 
Options.AR*2 : 0);
+                       item.format(x, y, innerWidth).addTo(this);
+                       x += innerWidth;
+                       y += item.height;
+
+                       if(i !== this.items.length-1) {
+                               new Path(x, y)
+                                       .arc('ne').down(Math.max(0, item.down + 
Options.VS - Options.AR*2))
+                                       .arc('es').left(innerWidth)
+                                       .arc('nw').down(Math.max(0, 
this.items[i+1].up + Options.VS - Options.AR*2))
+                                       .arc('ws').addTo(this);
+                               y += Math.max(item.down + Options.VS, 
Options.AR*2) + Math.max(this.items[i+1].up + Options.VS, Options.AR*2);
+                               //y += Math.max(Options.AR*4, item.down + 
Options.VS*2 + this.items[i+1].up)
+                               x = xInitial+Options.AR;
+                       }
+
+               }
+
+               if(this.items.length > 1) {
+                       new Path(x,y).h(Options.AR).addTo(this);
+                       x += Options.AR;
+               }
+               new Path(x,y).h(gaps[1]).addTo(this);
+
+               return this;
+       }
+}
+funcs.Stack = (...args)=>new Stack(...args);
+
+
+export class OptionalSequence extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 0 ) {
+                       throw new RangeError("OptionalSequence() must have at 
least one child.");
+               }
+               if( items.length === 1 ) {
+                       return new Sequence(items);
+               }
+               var arc = Options.AR;
+               this.needsSpace = false;
+               this.width = 0;
+               this.up = 0;
+               this.height = sum(this.items, function(x){return x.height});
+               this.down = this.items[0].down;
+               var heightSoFar = 0;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       this.up = Math.max(this.up, Math.max(arc*2, item.up + 
Options.VS) - heightSoFar);
+                       heightSoFar += item.height;
+                       if(i > 0) {
+                               this.down = Math.max(this.height + this.down, 
heightSoFar + Math.max(arc*2, item.down + Options.VS)) - this.height;
+                       }
+                       var itemWidth = (item.needsSpace?10:0) + item.width;
+                       if(i === 0) {
+                               this.width += arc + Math.max(itemWidth, arc);
+                       } else {
+                               this.width += arc*2 + Math.max(itemWidth, arc) 
+ arc;
+                       }
+               }
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "optseq";
+               }
+       }
+       format(x, y, width) {
+               var arc = Options.AR;
+               var gaps = determineGaps(width, this.width);
+               new Path(x, y).right(gaps[0]).addTo(this);
+               new Path(x + gaps[0] + this.width, y + 
this.height).right(gaps[1]).addTo(this);
+               x += gaps[0];
+               var upperLineY = y - this.up;
+               var last = this.items.length - 1;
+               for(var i = 0; i < this.items.length; i++) {
+                       var item = this.items[i];
+                       var itemSpace = (item.needsSpace?10:0);
+                       var itemWidth = item.width + itemSpace;
+                       if(i === 0) {
+                               // Upper skip
+                               new Path(x,y)
+                                       .arc('se')
+                                       .up(y - upperLineY - arc*2)
+                                       .arc('wn')
+                                       .right(itemWidth - arc)
+                                       .arc('ne')
+                                       .down(y + item.height - upperLineY - 
arc*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                               // Straight line
+                               new Path(x, y)
+                                       .right(itemSpace + arc)
+                                       .addTo(this);
+                               item.format(x + itemSpace + arc, y, 
item.width).addTo(this);
+                               x += itemWidth + arc;
+                               y += item.height;
+                               // x ends on the far side of the first element,
+                               // where the next element's skip needs to begin
+                       } else if(i < last) {
+                               // Upper skip
+                               new Path(x, upperLineY)
+                                       .right(arc*2 + Math.max(itemWidth, arc) 
+ arc)
+                                       .arc('ne')
+                                       .down(y - upperLineY + item.height - 
arc*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                               // Straight line
+                               new Path(x,y)
+                                       .right(arc*2)
+                                       .addTo(this);
+                               item.format(x + arc*2, y, 
item.width).addTo(this);
+                               new Path(x + item.width + arc*2, y + 
item.height)
+                                       .right(itemSpace + arc)
+                                       .addTo(this);
+                               // Lower skip
+                               new Path(x,y)
+                                       .arc('ne')
+                                       .down(item.height + Math.max(item.down 
+ Options.VS, arc*2) - arc*2)
+                                       .arc('ws')
+                                       .right(itemWidth - arc)
+                                       .arc('se')
+                                       .up(item.down + Options.VS - arc*2)
+                                       .arc('wn')
+                                       .addTo(this);
+                               x += arc*2 + Math.max(itemWidth, arc) + arc;
+                               y += item.height;
+                       } else {
+                               // Straight line
+                               new Path(x, y)
+                                       .right(arc*2)
+                                       .addTo(this);
+                               item.format(x + arc*2, y, 
item.width).addTo(this);
+                               new Path(x + arc*2 + item.width, y + 
item.height)
+                                       .right(itemSpace + arc)
+                                       .addTo(this);
+                               // Lower skip
+                               new Path(x,y)
+                                       .arc('ne')
+                                       .down(item.height + Math.max(item.down 
+ Options.VS, arc*2) - arc*2)
+                                       .arc('ws')
+                                       .right(itemWidth - arc)
+                                       .arc('se')
+                                       .up(item.down + Options.VS - arc*2)
+                                       .arc('wn')
+                                       .addTo(this);
+                       }
+               }
+               return this;
+       }
+}
+funcs.OptionalSequence = (...args)=>new OptionalSequence(...args);
+
+
+export class AlternatingSequence extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 1 ) {
+                       return new Sequence(items);
+               }
+               if( items.length !== 2 ) {
+                       throw new RangeError("AlternatingSequence() must have 
one or two children.");
+               }
+               this.needsSpace = false;
+
+               const arc = Options.AR;
+               const vert = Options.VS;
+               const max = Math.max;
+               const first = this.items[0];
+               const second = this.items[1];
+
+               const arcX = 1 / Math.sqrt(2) * arc * 2;
+               const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2;
+               const crossY = Math.max(arc, Options.VS);
+               const crossX = (crossY - arcY) + arcX;
+
+               const firstOut = max(arc + arc, crossY/2 + arc + arc, crossY/2 
+ vert + first.down);
+               this.up = firstOut + first.height + first.up;
+
+               const secondIn = max(arc + arc, crossY/2 + arc + arc, crossY/2 
+ vert + second.up);
+               this.down = secondIn + second.height + second.down;
+
+               this.height = 0;
+
+               const firstWidth = 2*(first.needsSpace?10:0) + first.width;
+               const secondWidth = 2*(second.needsSpace?10:0) + second.width;
+               this.width = 2*arc + max(firstWidth, crossX, secondWidth) + 
2*arc;
+
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "altseq";
+               }
+       }
+       format(x, y, width) {
+               const arc = Options.AR;
+               const gaps = determineGaps(width, this.width);
+               new Path(x,y).right(gaps[0]).addTo(this);
+               x += gaps[0];
+               new Path(x+this.width, y).right(gaps[1]).addTo(this);
+               // bounding box
+               //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);
+               const first = this.items[0];
+               const second = this.items[1];
+
+               // top
+               const firstIn = this.up - first.up;
+               const firstOut = this.up - first.up - first.height;
+               new Path(x,y).arc('se').up(firstIn-2*arc).arc('wn').addTo(this);
+               first.format(x + 2*arc, y - firstIn, this.width - 
4*arc).addTo(this);
+               new Path(x + this.width - 2*arc, y - 
firstOut).arc('ne').down(firstOut - 2*arc).arc('ws').addTo(this);
+
+               // bottom
+               const secondIn = this.down - second.down - second.height;
+               const secondOut = this.down - second.down;
+               new Path(x,y).arc('ne').down(secondIn - 
2*arc).arc('ws').addTo(this);
+               second.format(x + 2*arc, y + secondIn, this.width - 
4*arc).addTo(this);
+               new Path(x + this.width - 2*arc, y + 
secondOut).arc('se').up(secondOut - 2*arc).arc('wn').addTo(this);
+
+               // crossover
+               const arcX = 1 / Math.sqrt(2) * arc * 2;
+               const arcY = (1 - 1 / Math.sqrt(2)) * arc * 2;
+               const crossY = Math.max(arc, Options.VS);
+               const crossX = (crossY - arcY) + arcX;
+               const crossBar = (this.width - 4*arc - crossX)/2;
+               new Path(x+arc, y - crossY/2 - arc).arc('ws').right(crossBar)
+                       .arc_8('n', 'cw').l(crossX - arcX, crossY - 
arcY).arc_8('sw', 'ccw')
+                       .right(crossBar).arc('ne').addTo(this);
+               new Path(x+arc, y + crossY/2 + arc).arc('wn').right(crossBar)
+                       .arc_8('s', 'ccw').l(crossX - arcX, -(crossY - 
arcY)).arc_8('nw', 'cw')
+                       .right(crossBar).arc('se').addTo(this);
+
+               return this;
+       }
+}
+funcs.AlternatingSequence = (...args)=>new AlternatingSequence(...args);
+
+
+export class Choice extends DiagramMultiContainer {
+       constructor(normal, ...items) {
+               super('g', items);
+               if( typeof normal !== "number" || normal !== Math.floor(normal) 
) {
+                       throw new TypeError("The first argument of Choice() 
must be an integer.");
+               } else if(normal < 0 || normal >= items.length) {
+                       throw new RangeError("The first argument of Choice() 
must be an index for one of the items.");
+               } else {
+                       this.normal = normal;
+               }
+               var first = 0;
+               var last = items.length - 1;
+               this.width = Math.max.apply(null, 
this.items.map(function(el){return el.width})) + Options.AR*4;
+               this.height = this.items[normal].height;
+               this.up = this.items[first].up;
+               var arcs;
+               for(var i = first; i < normal; i++) {
+                       if(i == normal-1) arcs = Options.AR*2;
+                       else arcs = Options.AR;
+                       this.up += Math.max(arcs, this.items[i].height + 
this.items[i].down + Options.VS + this.items[i+1].up);
+               }
+               this.down = this.items[last].down;
+               for(i = normal+1; i <= last; i++) {
+                       if(i == normal+1) arcs = Options.AR*2;
+                       else arcs = Options.AR;
+                       this.down += Math.max(arcs, this.items[i-1].height + 
this.items[i-1].down + Options.VS + this.items[i].up);
+               }
+               this.down -= this.items[normal].height; // already counted in 
Choice.height
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "choice";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               var last = this.items.length -1;
+               var innerWidth = this.width - Options.AR*4;
+
+               // Do the elements that curve above
+               var distanceFromY;
+               for(var i = this.normal - 1; i >= 0; i--) {
+                       let item = this.items[i];
+                       if( i == this.normal - 1 ) {
+                               distanceFromY = Math.max(Options.AR*2, 
this.items[this.normal].up + Options.VS + item.down + item.height);
+                       }
+                       new Path(x,y)
+                               .arc('se')
+                               .up(distanceFromY - Options.AR*2)
+                               .arc('wn').addTo(this);
+                       item.format(x+Options.AR*2,y - 
distanceFromY,innerWidth).addTo(this);
+                       new Path(x+Options.AR*2+innerWidth, 
y-distanceFromY+item.height)
+                               .arc('ne')
+                               .down(distanceFromY - item.height + this.height 
- Options.AR*2)
+                               .arc('ws').addTo(this);
+                       distanceFromY += Math.max(Options.AR, item.up + 
Options.VS + (i === 0 ? 0 : this.items[i-1].down+this.items[i-1].height));
+               }
+
+               // Do the straight-line path.
+               new Path(x,y).right(Options.AR*2).addTo(this);
+               this.items[this.normal].format(x+Options.AR*2, y, 
innerWidth).addTo(this);
+               new Path(x+Options.AR*2+innerWidth, 
y+this.height).right(Options.AR*2).addTo(this);
+
+               // Do the elements that curve below
+               for(i = this.normal+1; i <= last; i++) {
+                       let item = this.items[i];
+                       if( i == this.normal + 1 ) {
+                               distanceFromY = Math.max(Options.AR*2, 
this.height + this.items[this.normal].down + Options.VS + item.up);
+                       }
+                       new Path(x,y)
+                               .arc('ne')
+                               .down(distanceFromY - Options.AR*2)
+                               .arc('ws').addTo(this);
+                       item.format(x+Options.AR*2, y+distanceFromY, 
innerWidth).addTo(this);
+                       new Path(x+Options.AR*2+innerWidth, 
y+distanceFromY+item.height)
+                               .arc('se')
+                               .up(distanceFromY - Options.AR*2 + item.height 
- this.height)
+                               .arc('wn').addTo(this);
+                       distanceFromY += Math.max(Options.AR, item.height + 
item.down + Options.VS + (i == last ? 0 : this.items[i+1].up));
+               }
+
+               return this;
+       }
+}
+funcs.Choice = (...args)=>new Choice(...args);
+
+
+export class HorizontalChoice extends DiagramMultiContainer {
+       constructor(...items) {
+               super('g', items);
+               if( items.length === 0 ) {
+                       throw new RangeError("HorizontalChoice() must have at 
least one child.");
+               }
+               if( items.length === 1) {
+                       return new Sequence(items);
+               }
+               const allButLast = this.items.slice(0, -1);
+               const middles = this.items.slice(1, -1);
+               const first = this.items[0];
+               const last = this.items[this.items.length - 1];
+               this.needsSpace = false;
+
+               this.width = Options.AR; // starting track
+               this.width += Options.AR*2 * (this.items.length-1); // 
inbetween tracks
+               this.width += sum(this.items, x=>x.width + 
(x.needsSpace?20:0)); // items
+               this.width += (last.height > 0 ? Options.AR : 0); // needs 
space to curve up
+               this.width += Options.AR; //ending track
+
+               // Always exits at entrance height
+               this.height = 0;
+
+               // All but the last have a track running above them
+               this._upperTrack = Math.max(
+                       Options.AR*2,
+                       Options.VS,
+                       max(allButLast, x=>x.up) + Options.VS
+               );
+               this.up = Math.max(this._upperTrack, last.up);
+
+               // All but the first have a track running below them
+               // Last either straight-lines or curves up, so has different 
calculation
+               this._lowerTrack = Math.max(
+                       Options.VS,
+                       max(middles, x=>x.height+Math.max(x.down+Options.VS, 
Options.AR*2)),
+                       last.height + last.down + Options.VS
+               );
+               if(first.height < this._lowerTrack) {
+                       // Make sure there's at least 2*AR room between first 
exit and lower track
+                       this._lowerTrack = Math.max(this._lowerTrack, 
first.height + Options.AR*2);
+               }
+               this.down = Math.max(this._lowerTrack, first.height + 
first.down);
+
+
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "horizontalchoice";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               const first = this.items[0];
+               const last = this.items[this.items.length-1];
+               const allButFirst = this.items.slice(1);
+               const allButLast = this.items.slice(0, -1);
+
+               // upper track
+               var upperSpan = (sum(allButLast, x=>x.width+(x.needsSpace?20:0))
+                       + (this.items.length - 2) * Options.AR*2
+                       - Options.AR
+               );
+               new Path(x,y)
+                       .arc('se')
+                       .v(-(this._upperTrack - Options.AR*2))
+                       .arc('wn')
+                       .h(upperSpan)
+                       .addTo(this);
+
+               // lower track
+               var lowerSpan = (sum(allButFirst, 
x=>x.width+(x.needsSpace?20:0))
+                       + (this.items.length - 2) * Options.AR*2
+                       + (last.height > 0 ? Options.AR : 0)
+                       - Options.AR
+               );
+               var lowerStart = x + Options.AR + 
first.width+(first.needsSpace?20:0) + Options.AR*2;
+               new Path(lowerStart, y+this._lowerTrack)
+                       .h(lowerSpan)
+                       .arc('se')
+                       .v(-(this._lowerTrack - Options.AR*2))
+                       .arc('wn')
+                       .addTo(this);
+
+               // Items
+               for(const [i, item] of enumerate(this.items)) {
+                       // input track
+                       if(i === 0) {
+                               new Path(x,y)
+                                       .h(Options.AR)
+                                       .addTo(this);
+                               x += Options.AR;
+                       } else {
+                               new Path(x, y - this._upperTrack)
+                                       .arc('ne')
+                                       .v(this._upperTrack - Options.AR*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                               x += Options.AR*2;
+                       }
+
+                       // item
+                       var itemWidth = item.width + (item.needsSpace?20:0);
+                       item.format(x, y, itemWidth).addTo(this);
+                       x += itemWidth;
+
+                       // output track
+                       if(i === this.items.length-1) {
+                               if(item.height === 0) {
+                                       new Path(x,y)
+                                               .h(Options.AR)
+                                               .addTo(this);
+                               } else {
+                                       new Path(x,y+item.height)
+                                       .arc('se')
+                                       .addTo(this);
+                               }
+                       } else if(i === 0 && item.height > this._lowerTrack) {
+                               // Needs to arc up to meet the lower track, not 
down.
+                               if(item.height - this._lowerTrack >= 
Options.AR*2) {
+                                       new Path(x, y+item.height)
+                                               .arc('se')
+                                               .v(this._lowerTrack - 
item.height + Options.AR*2)
+                                               .arc('wn')
+                                               .addTo(this);
+                               } else {
+                                       // Not enough space to fit two arcs
+                                       // so just bail and draw a straight 
line for now.
+                                       new Path(x, y+item.height)
+                                               .l(Options.AR*2, 
this._lowerTrack - item.height)
+                                               .addTo(this);
+                               }
+                       } else {
+                               new Path(x, y+item.height)
+                                       .arc('ne')
+                                       .v(this._lowerTrack - item.height - 
Options.AR*2)
+                                       .arc('ws')
+                                       .addTo(this);
+                       }
+               }
+               return this;
+       }
+}
+funcs.HorizontalChoice = (...args)=>new HorizontalChoice(...args);
+
+
+export class MultipleChoice extends DiagramMultiContainer {
+       constructor(normal, type, ...items) {
+               super('g', items);
+               if( typeof normal !== "number" || normal !== Math.floor(normal) 
) {
+                       throw new TypeError("The first argument of 
MultipleChoice() must be an integer.");
+               } else if(normal < 0 || normal >= items.length) {
+                       throw new RangeError("The first argument of 
MultipleChoice() must be an index for one of the items.");
+               } else {
+                       this.normal = normal;
+               }
+               if( type != "any" && type != "all" ) {
+                       throw new SyntaxError("The second argument of 
MultipleChoice must be 'any' or 'all'.");
+               } else {
+                       this.type = type;
+               }
+               this.needsSpace = true;
+               this.innerWidth = max(this.items, function(x){return x.width});
+               this.width = 30 + Options.AR + this.innerWidth + Options.AR + 
20;
+               this.up = this.items[0].up;
+               this.down = this.items[this.items.length-1].down;
+               this.height = this.items[normal].height;
+               for(var i = 0; i < this.items.length; i++) {
+                       let item = this.items[i];
+                       let minimum;
+                       if(i == normal - 1 || i == normal + 1) minimum = 10 + 
Options.AR;
+                       else minimum = Options.AR;
+                       if(i < normal) {
+                               this.up += Math.max(minimum, item.height + 
item.down + Options.VS + this.items[i+1].up);
+                       } else if(i > normal) {
+                               this.down += Math.max(minimum, item.up + 
Options.VS + this.items[i-1].down + this.items[i-1].height);
+                       }
+               }
+               this.down -= this.items[normal].height; // already counted in 
this.height
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "multiplechoice";
+               }
+       }
+       format(x, y, width) {
+               var gaps = determineGaps(width, this.width);
+               new Path(x, y).right(gaps[0]).addTo(this);
+               new Path(x + gaps[0] + this.width, y + 
this.height).right(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               var normal = this.items[this.normal];
+
+               // Do the elements that curve above
+               var distanceFromY;
+               for(var i = this.normal - 1; i >= 0; i--) {
+                       var item = this.items[i];
+                       if( i == this.normal - 1 ) {
+                               distanceFromY = Math.max(10 + Options.AR, 
normal.up + Options.VS + item.down + item.height);
+                       }
+                       new Path(x + 30,y)
+                               .up(distanceFromY - Options.AR)
+                               .arc('wn').addTo(this);
+                       item.format(x + 30 + Options.AR, y - distanceFromY, 
this.innerWidth).addTo(this);
+                       new Path(x + 30 + Options.AR + this.innerWidth, y - 
distanceFromY + item.height)
+                               .arc('ne')
+                               .down(distanceFromY - item.height + this.height 
- Options.AR - 10)
+                               .addTo(this);
+                       if(i !== 0) {
+                               distanceFromY += Math.max(Options.AR, item.up + 
Options.VS + this.items[i-1].down + this.items[i-1].height);
+                       }
+               }
+
+               new Path(x + 30, y).right(Options.AR).addTo(this);
+               normal.format(x + 30 + Options.AR, y, 
this.innerWidth).addTo(this);
+               new Path(x + 30 + Options.AR + this.innerWidth, y + 
this.height).right(Options.AR).addTo(this);
+
+               for(i = this.normal+1; i < this.items.length; i++) {
+                       let item = this.items[i];
+                       if(i == this.normal + 1) {
+                               distanceFromY = Math.max(10+Options.AR, 
normal.height + normal.down + Options.VS + item.up);
+                       }
+                       new Path(x + 30, y)
+                               .down(distanceFromY - Options.AR)
+                               .arc('ws')
+                               .addTo(this);
+                       item.format(x + 30 + Options.AR, y + distanceFromY, 
this.innerWidth).addTo(this);
+                       new Path(x + 30 + Options.AR + this.innerWidth, y + 
distanceFromY + item.height)
+                               .arc('se')
+                               .up(distanceFromY - Options.AR + item.height - 
normal.height)
+                               .addTo(this);
+                       if(i != this.items.length - 1) {
+                               distanceFromY += Math.max(Options.AR, 
item.height + item.down + Options.VS + this.items[i+1].up);
+                       }
+               }
+               var text = new FakeSVG('g', {"class": 
"diagram-text"}).addTo(this);
+               new 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);
+               new FakeSVG('path', {
+                       "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",
+                       "class": "diagram-text"
+                       }).addTo(text);
+               new FakeSVG('text', {
+                       "x": x + 15,
+                       "y": y + 4,
+                       "class": "diagram-text"
+                       }, (this.type=="any"?"1+":"all")).addTo(text);
+               new FakeSVG('path', {
+                       "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",
+                       "class": "diagram-text"
+                       }).addTo(text);
+               new FakeSVG('path', {
+                       "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",
+                       "style": "stroke-width: 1.75"
+                       }).addTo(text);
+               return this;
+       }
+}
+funcs.MultipleChoice = (...args)=>new MultipleChoice(...args);
+
+
+export class Optional extends FakeSVG {
+       constructor(item, skip) {
+               if( skip === undefined )
+                       return new Choice(1, new Skip(), item);
+               else if ( skip === "skip" )
+                       return new Choice(0, new Skip(), item);
+               else
+                       throw "Unknown value for Optional()'s 'skip' argument.";
+       }
+}
+funcs.Optional = (...args)=>new Optional(...args);
+
+
+export class OneOrMore extends FakeSVG {
+       constructor(item, rep) {
+               super('g');
+               rep = rep || (new Skip());
+               this.item = wrapString(item);
+               this.rep = wrapString(rep);
+               this.width = Math.max(this.item.width, this.rep.width) + 
Options.AR*2;
+               this.height = this.item.height;
+               this.up = this.item.up;
+               this.down = Math.max(Options.AR*2, this.item.down + Options.VS 
+ this.rep.up + this.rep.height + this.rep.down);
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "oneormore";
+               }
+       }
+       format(x,y,width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               // Draw item
+               new Path(x,y).right(Options.AR).addTo(this);
+               
this.item.format(x+Options.AR,y,this.width-Options.AR*2).addTo(this);
+               new 
Path(x+this.width-Options.AR,y+this.height).right(Options.AR).addTo(this);
+
+               // Draw repeat arc
+               var distanceFromY = Math.max(Options.AR*2, 
this.item.height+this.item.down+Options.VS+this.rep.up);
+               new 
Path(x+Options.AR,y).arc('nw').down(distanceFromY-Options.AR*2).arc('ws').addTo(this);
+               this.rep.format(x+Options.AR, y+distanceFromY, this.width - 
Options.AR*2).addTo(this);
+               new 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);
+
+               return this;
+       }
+       walk(cb) {
+               cb(this);
+               this.item.walk(cb);
+               this.rep.walk(cb);
+       }
+}
+funcs.OneOrMore = (...args)=>new OneOrMore(...args);
+
+
+export class ZeroOrMore extends FakeSVG {
+       constructor(item, rep, skip) {
+               return new Optional(new OneOrMore(item, rep), skip);
+       }
+}
+funcs.ZeroOrMore = (...args)=>new ZeroOrMore(...args);
+
+
+export class Group extends FakeSVG {
+       constructor(item, label) {
+               super('g');
+               this.item = wrapString(item);
+               this.label =
+                       label instanceof FakeSVG
+                         ? label
+                       : label
+                         ? new Comment(label)
+                         : undefined;
+
+               this.width = Math.max(
+                       this.item.width + (this.item.needsSpace?20:0),
+                       this.label ? this.label.width : 0,
+                       Options.AR*2);
+               this.height = this.item.height;
+               this.boxUp = this.up = Math.max(this.item.up + Options.VS, 
Options.AR);
+               if(this.label) {
+                       this.up += this.label.up + this.label.height + 
this.label.down;
+               }
+               this.down = Math.max(this.item.down + Options.VS, Options.AR);
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "group";
+               }
+       }
+       format(x, y, width) {
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {
+                       x,
+                       y:y-this.boxUp,
+                       width:this.width,
+                       height:this.boxUp + this.height + this.down,
+                       rx: Options.AR,
+                       ry: Options.AR,
+                       'class':'group-box',
+               }).addTo(this);
+
+               this.item.format(x,y,this.width).addTo(this);
+               if(this.label) {
+                       this.label.format(
+                               x,
+                               
y-(this.boxUp+this.label.down+this.label.height),
+                               this.label.width).addTo(this);
+               }
+
+               return this;
+       }
+       walk(cb) {
+               cb(this);
+               this.item.walk(cb);
+               this.label.walk(cb);
+       }
+}
+funcs.Group = (...args)=>new Group(...args);
+
+
+export class Start extends FakeSVG {
+       constructor({type="simple", label}={}) {
+               super('g');
+               this.width = 20;
+               this.height = 0;
+               this.up = 10;
+               this.down = 10;
+               this.type = type;
+               if(label) {
+                       this.label = ""+label;
+                       this.width = Math.max(20, this.label.length * 
Options.CHAR_WIDTH + 10);
+               }
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "start";
+               }
+       }
+       format(x,y) {
+               let path = new Path(x, y-10);
+               if (this.type === "complex") {
+                       path.down(20)
+                               .m(0, -10)
+                               .right(this.width)
+                               .addTo(this);
+               } else {
+                       path.down(20)
+                               .m(10, -20)
+                               .down(20)
+                               .m(-10, -10)
+                               .right(this.width)
+                               .addTo(this);
+               }
+               if(this.label) {
+                       new FakeSVG('text', {x:x, y:y-15, 
style:"text-anchor:start"}, this.label).addTo(this);
+               }
+               return this;
+       }
+}
+funcs.Start = (...args)=>new Start(...args);
+
+
+export class End extends FakeSVG {
+       constructor({type="simple"}={}) {
+               super('path');
+               this.width = 20;
+               this.height = 0;
+               this.up = 10;
+               this.down = 10;
+               this.type = type;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "end";
+               }
+       }
+       format(x,y) {
+               if (this.type === "complex") {
+                       this.attrs.d = 'M '+x+' '+y+' h 20 m 0 -10 v 20';
+               } else {
+                       this.attrs.d = 'M '+x+' '+y+' h 20 m -10 -10 v 20 m 10 
-20 v 20';
+               }
+               return this;
+       }
+}
+funcs.End = (...args)=>new End(...args);
+
+
+export class Terminal extends FakeSVG {
+       constructor(text, {href, title, cls}={}) {
+               super('g', {'class': ['terminal', cls].join(" ")});
+               this.text = ""+text;
+               this.href = href;
+               this.title = title;
+               this.cls = cls;
+               this.width = this.text.length * Options.CHAR_WIDTH + 20; /* 
Assume that each char is .5em, and that the em is 16px */
+               this.height = 0;
+               this.up = 11;
+               this.down = 11;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "terminal";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {x:x, y:y-11, width:this.width, 
height:this.up+this.down, rx:10, ry:10}).addTo(this);
+               var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, 
this.text);
+               if(this.href)
+                       new FakeSVG('a', {'xlink:href': this.href}, 
[text]).addTo(this);
+               else
+                       text.addTo(this);
+               if(this.title)
+                       new FakeSVG('title', {}, [this.title]).addTo(this);
+               return this;
+       }
+}
+funcs.Terminal = (...args)=>new Terminal(...args);
+
+
+export class NonTerminal extends FakeSVG {
+       constructor(text, {href, title, cls=""}={}) {
+               super('g', {'class': ['non-terminal', cls].join(" ")});
+               this.text = ""+text;
+               this.href = href;
+               this.title = title;
+               this.cls = cls;
+               this.width = this.text.length * Options.CHAR_WIDTH + 20;
+               this.height = 0;
+               this.up = 11;
+               this.down = 11;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "nonterminal";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {x:x, y:y-11, width:this.width, 
height:this.up+this.down}).addTo(this);
+               var text = new FakeSVG('text', {x:x+this.width/2, y:y+4}, 
this.text);
+               if(this.href)
+                       new FakeSVG('a', {'xlink:href': this.href}, 
[text]).addTo(this);
+               else
+                       text.addTo(this);
+               if(this.title)
+                       new FakeSVG('title', {}, [this.title]).addTo(this);
+               return this;
+       }
+}
+funcs.NonTerminal = (...args)=>new NonTerminal(...args);
+
+
+export class Comment extends FakeSVG {
+       constructor(text, {href, title, cls=""}={}) {
+               super('g', {'class': ['comment', cls].join(" ")});
+               this.text = ""+text;
+               this.href = href;
+               this.title = title;
+               this.cls = cls;
+               this.width = this.text.length * Options.COMMENT_CHAR_WIDTH + 10;
+               this.height = 0;
+               this.up = 8;
+               this.down = 8;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "comment";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new 
Path(x+gaps[0]+this.width,y+this.height).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               var text = new FakeSVG('text', {x:x+this.width/2, y:y+5, 
class:'comment'}, this.text);
+               if(this.href)
+                       new FakeSVG('a', {'xlink:href': this.href}, 
[text]).addTo(this);
+               else
+                       text.addTo(this);
+               if(this.title)
+                       new FakeSVG('title', {}, this.title).addTo(this);
+               return this;
+       }
+}
+funcs.Comment = (...args)=>new Comment(...args);
+
+
+export class Skip extends FakeSVG {
+       constructor() {
+               super('g');
+               this.width = 0;
+               this.height = 0;
+               this.up = 0;
+               this.down = 0;
+               this.needsSpace = false;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "skip";
+               }
+       }
+       format(x, y, width) {
+               new Path(x,y).right(width).addTo(this);
+               return this;
+       }
+}
+funcs.Skip = (...args)=>new Skip(...args);
+
+
+export class Block extends FakeSVG {
+       constructor({width=50, up=15, height=25, down=15, needsSpace=true}={}) {
+               super('g');
+               this.width = width;
+               this.height = height;
+               this.up = up;
+               this.down = down;
+               this.needsSpace = true;
+               if(Options.DEBUG) {
+                       this.attrs['data-updown'] = this.up + " " + this.height 
+ " " + this.down;
+                       this.attrs['data-type'] = "block";
+               }
+       }
+       format(x, y, width) {
+               // Hook up the two sides if this is narrower than its stated 
width.
+               var gaps = determineGaps(width, this.width);
+               new Path(x,y).h(gaps[0]).addTo(this);
+               new Path(x+gaps[0]+this.width,y).h(gaps[1]).addTo(this);
+               x += gaps[0];
+
+               new FakeSVG('rect', {x:x, y:y-this.up, width:this.width, 
height:this.up+this.height+this.down}).addTo(this);
+               return this;
+       }
+}
+funcs.Block = (...args)=>new Block(...args);
+
+
+function unnull(...args) {
+       // Return the first value that isn't undefined.
+       // More correct than `v1 || v2 || v3` because falsey values will be 
returned.
+       return args.reduce(function(sofar, x) { return sofar !== undefined ? 
sofar : x; });
+}
+
+function determineGaps(outer, inner) {
+       var diff = outer - inner;
+       switch(Options.INTERNAL_ALIGNMENT) {
+               case 'left': return [0, diff];
+               case 'right': return [diff, 0];
+               default: return [diff/2, diff/2];
+       }
+}
+
+function wrapString(value) {
+               return value instanceof FakeSVG ? value : new 
Terminal(""+value);
+}
+
+function sum(iter, func) {
+       if(!func) func = function(x) { return x; };
+       return iter.map(func).reduce(function(a,b){return a+b}, 0);
+}
+
+function max(iter, func) {
+       if(!func) func = function(x) { return x; };
+       return Math.max.apply(null, iter.map(func));
+}
+
+function SVG(name, attrs, text) {
+       attrs = attrs || {};
+       text = text || '';
+       var el = document.createElementNS("http://www.w3.org/2000/svg",name);
+       for(var attr in attrs) {
+               if(attr === 'xlink:href')
+                       el.setAttributeNS("http://www.w3.org/1999/xlink";, 
'href', attrs[attr]);
+               else
+                       el.setAttribute(attr, attrs[attr]);
+       }
+       el.textContent = text;
+       return el;
+}
+
+function escapeString(string) {
+       // Escape markdown and HTML special characters
+       return string.replace(/[*_\`\[\]<&]/g, function(charString) {
+               return '&#' + charString.charCodeAt(0) + ';';
+       });
+}
+
+function* enumerate(iter) {
+       var count = 0;
+       for(const x of iter) {
+               yield [count, x];
+               count++;
+       }
+}

Reply via email to