UNPKG

@prantlf/railroad-diagrams

Version:

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

1,410 lines (1,405 loc) 45.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.railroadDiagrams = {})); }(this, (function (exports) { 'use strict'; const funcs = {}; const Options = { DEBUG: false, VS: 8, AR: 10, DIAGRAM_CLASS: 'railroad-diagram', STROKE_ODD_PIXEL_LENGTH: true, INTERNAL_ALIGNMENT: 'center', CHAR_WIDTH: 8.5, COMMENT_CHAR_WIDTH: 7, }; 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; }`; class FakeSVG { constructor(tagName, attrs, text) { if(text) this.children = text; else this.children = []; this.tagName = tagName; this.attrs = unnull(attrs, {}); } format() { } addTo(parent) { if(parent instanceof FakeSVG) { parent.children.push(this); return this; } else { var svg = this.toSVG(); parent.appendChild(svg); return svg; } } toSVG() { var el = SVG(this.tagName, this.attrs); if(typeof this.children == 'string') { el.textContent = this.children; } else { this.children.forEach(function(e) { el.appendChild(e.toSVG()); }); } return el; } toString() { var str = '<' + this.tagName; var group = this.tagName == "g" || this.tagName == "svg"; for(var attr in this.attrs) { str += ' ' + attr + '="' + (this.attrs[attr]+'').replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '"'; } str += '>'; if(group) str += "\n"; if(typeof this.children == 'string') { str += escapeString(this.children); } else { this.children.forEach(function(e) { str += e; }); } str += '</' + this.tagName + '>\n'; return str; } walk(cb) { cb(this); } } 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){ 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) { 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() { this.attrs.d += 'h.5'; return this; } } 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)); } } 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; } static fromJSON(input = []) { return diagramFromJSON(Diagram, input); } } funcs.Diagram = (...args)=>new Diagram(...args); funcs.Diagram.fromJSON = Diagram.fromJSON; 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; } static fromJSON(input = []) { return diagramFromJSON(ComplexDiagram, input); } } funcs.ComplexDiagram = (...args)=>new ComplexDiagram(...args); funcs.ComplexDiagram.fromJSON = ComplexDiagram.fromJSON; 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 < numberOfItems; 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[numberOfItems-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) { 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); 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.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); 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); class VerticalSequence extends DiagramMultiContainer { constructor(...items) { super('g', items); if( items.length === 0 ) { throw new RangeError("VerticalSequence() must have at least one child."); } this.items = items.map(wrapString); this.width = Math.max.apply(null, this.items.map(function(e) { return e.width + (e.needsSpace?20:0); })); 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++) { if(i !== last) { this.height += this.items[i].height + this.items[i].down + this.items[i+1].up + Options.VS*2; } } if(Options.DEBUG) { this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; this.attrs['data-type'] = "verticalsequence"; } } 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; var minWidth = 1000; for(let itemNum in this.items){ let item = this.items[itemNum]; minWidth = item.width < minWidth ? item.width : minWidth; } for(var i = 0; i < this.items.length; i++) { var item = this.items[i]; item.format(x, y, item.width).addTo(this); y += item.height; if(i !== this.items.length-1) { x += minWidth / 2; y += item.down; new Path(x, y) .v(Options.VS*2).addTo(this); y += this.items[i+1].up + Options.VS*2; x = xInitial; } else { x += item.width; y += item.height; } } if(this.items.length > 1) { new Path(x,y).h(this.width - this.items[this.items.length - 1].width).addTo(this); x += Options.AR; } new Path(x,y).h(gaps[1]).addTo(this); return this; } } funcs.VerticalSequence = (...args)=>new VerticalSequence(...args); 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) { 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); 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; } else if(i < last) { 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); 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); 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 { 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); 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); 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); const first = this.items[0]; const second = this.items[1]; 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); 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); 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); 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; if(Options.DEBUG) { this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; this.attrs['data-type'] = "choice"; } } 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]; var last = this.items.length -1; var innerWidth = this.width - Options.AR*4; 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)); } 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); 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); 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; this.width += Options.AR*2 * (this.items.length-1); this.width += sum(this.items, x=>x.width + (x.needsSpace?20:0)); this.width += (last.height > 0 ? Options.AR : 0); this.width += Options.AR; this.height = 0; this._upperTrack = Math.max( Options.AR*2, Options.VS, max(allButLast, x=>x.up) + Options.VS ); this.up = Math.max(this._upperTrack, last.up); 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) { 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) { 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); 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); 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); for(const [i, item] of enumerate(this.items)) { 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; } var itemWidth = item.width + (item.needsSpace?20:0); item.format(x, y, itemWidth).addTo(this); x += itemWidth; 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) { 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 { 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); 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; 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]; 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); 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); 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) { 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 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); 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); class ZeroOrMore extends FakeSVG { constructor(item, rep, skip) { return new Optional(new OneOrMore(item, rep), skip); } } funcs.ZeroOrMore = (...args)=>new ZeroOrMore(...args); 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); 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); 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); class Terminal extends FakeSVG { constructor(text, {href, title}={}) { super('g', {'class': 'terminal'}); this.text = ""+text; this.href = href; this.title = title; 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'] = "terminal"; } } 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).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); class NonTerminal extends FakeSVG { constructor(text, {href, title}={}) { super('g', {'class': 'non-terminal'}); this.text = ""+text; this.href = href; this.title = title; 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) { 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); class Comment extends FakeSVG { constructor(text, {href, title}={}) { super('g'); this.text = ""+text; this.href = href; this.title = title; 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) { 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); 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); 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 = needsSpace; if(Options.DEBUG) { this.attrs['data-updown'] = this.up + " " + this.height + " " + this.down; this.attrs['data-type'] = "block"; } } 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).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 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) { 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++; } } function diagramFromJSON(Diagram, input) { if (!input) return new Diagram(); if (Array.isArray(input)) { return new Diagram(...input.map(nodeFromJSON)); } switch (input.type) { case 'Diagram': case 'ComplexDiagram': return nodeFromJSON(input); } return new Diagram(nodeFromJSON(input)); } const classes = { Diagram, ComplexDiagram, Sequence, Stack, VerticalSequence, OptionalSequence, HorizontalChoice, AlternatingSequence, Choice, MultipleChoice, Optional, OneOrMore, ZeroOrMore, Group, Start, End, Terminal, NonTerminal, Comment, Skip }; function nodeFromJSON(node) { if (!node) return; const Node = classes[node.type]; switch (Node) { case Diagram: case ComplexDiagram: case Sequence: case Stack: case VerticalSequence: case OptionalSequence: case HorizontalChoice: return new Node(...itemsFromJSON(node.items)); case AlternatingSequence: return new Node(nodeFromJSON(node.option1), nodeFromJSON(node.option2)); case Choice: return new Node(node.normalIndex || 0, ...itemsFromJSON(node.options)); case MultipleChoice: return new Node(node.normalIndex || 0, node.choiceType, ...itemsFromJSON(node.options)); case Optional: return new Node(nodeFromJSON(node.item), node.skip && 'skip'); case OneOrMore: return new Node(nodeFromJSON(node.item), nodeFromJSON(node.repeat)); case ZeroOrMore: return new Node(nodeFromJSON(node.item), nodeFromJSON(node.repeat), node.skip && 'skip'); case Group: return new Node(nodeFromJSON(node.item), node.label); case Start: return new Node(node.startType, node.label); case End: return new Node(node.endType); case Terminal: case NonTerminal: case Comment: return new Node(node.text, { href: node.href, title: node.title }); case Skip: return new Node(); } throw new Error(`Unknown node type: "${node.type}".`) } function itemsFromJSON(items) { return items ? items.map(nodeFromJSON) : []; } exports.AlternatingSequence = AlternatingSequence; exports.Block = Block; exports.Choice = Choice; exports.Comment = Comment; exports.ComplexDiagram = ComplexDiagram; exports.Diagram = Diagram; exports.DiagramMultiContainer = DiagramMultiContainer; exports.End = End; exports.FakeSVG = FakeSVG; exports.Group = Group; exports.HorizontalChoice = HorizontalChoice; exports.MultipleChoice = MultipleChoice; exports.NonTerminal = NonTerminal; exports.OneOrMore = OneOrMore; exports.Optional = Optional; exports.OptionalSequence = OptionalSequence; exports.Options = Options; exports.Path = Path; exports.Sequence = Sequence; exports.Skip = Skip; exports.Stack = Stack; exports.Start = Start; exports.Terminal = Terminal; exports.VerticalSequence = VerticalSequence; exports.ZeroOrMore = ZeroOrMore; exports.default = funcs; exports.defaultCSS = defaultCSS; Object.defineProperty(exports, '__esModule', { value: true }); }))); //# sourceMappingURL=index.umd.js.map