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, '&').replace(/"/g, '"') + '"';
+ }
+ 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, '&').replace(/"/g, '"') + '"';
+ }
+ 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++;
+ }
+}