(function () { 'use strict'; function optionsAttributes(attributes, options) { const attrs = Object.assign({}, attributes['']); options.forEach((opt) => { Object.assign(attrs, attributes[opt] || {}); }); return attrs; } class WavePattern { constructor(width, height) { if(Array.isArray(height)) { this.deltas = height; } else { this.deltas = [ 0, -height * 2 / 3, -height, -height * 2 / 3, 0, height * 2 / 3, height, height * 2 / 3, ]; } this.partWidth = width / this.deltas.length; } getDelta(p) { return this.deltas[p % this.deltas.length]; } } class BaseTheme { constructor(svg) { this.svg = svg; } // PUBLIC API reset() { // No-op } addDefs() { // No-op } getBlock(type) { return this.blocks[type] || this.blocks['']; } getNote(type) { return this.notes[type] || this.notes['']; } getDivider(type) { return this.dividers[type] || this.dividers['']; } optionsAttributes(attributes, options) { return optionsAttributes(attributes, options); } renderAgentLine({className, options, width, x, y0, y1}) { const attrs = this.optionsAttributes(this.agentLineAttrs, options); if(width > 0) { return this.svg.box(attrs, { height: y1 - y0, width, x: x - width / 2, y: y0, }).addClass(className); } else { return this.svg.line(attrs, { 'x1': x, 'x2': x, 'y1': y0, 'y2': y1, }).addClass(className); } } // INTERNAL HELPERS renderArrowHead(attrs, {dir, height, width, x, y}) { const wx = width * dir.dx; const wy = width * dir.dy; const hy = height * 0.5 * dir.dx; const hx = -height * 0.5 * dir.dy; return this.svg.el(attrs.fill === 'none' ? 'polyline' : 'polygon') .attr('points', ( (x + wx - hx) + ' ' + (y + wy - hy) + ' ' + x + ' ' + y + ' ' + (x + wx + hx) + ' ' + (y + wy + hy) )) .attrs(attrs); } renderTag(attrs, {height, width, x, y}) { const {rx, ry} = attrs; const x2 = x + width; const y2 = y + height; const line = ( 'M' + x2 + ' ' + y + 'L' + x2 + ' ' + (y2 - ry) + 'L' + (x2 - rx) + ' ' + y2 + 'L' + x + ' ' + y2 ); const g = this.svg.el('g'); if(attrs.fill !== 'none') { g.add(this.svg.el('path') .attr('d', line + 'L' + x + ' ' + y) .attrs(attrs) .attr('stroke', 'none')); } if(attrs.stroke !== 'none') { g.add(this.svg.el('path') .attr('d', line) .attrs(attrs) .attr('fill', 'none')); } return g; } renderDB(attrs, position) { const z = attrs['db-z']; return this.svg.el('g').add( this.svg.box({ 'rx': position.width / 2, 'ry': z, }, position).attrs(attrs), this.svg.el('path') .attr('d', ( 'M' + position.x + ' ' + (position.y + z) + 'a' + (position.width / 2) + ' ' + z + ' 0 0 0 ' + position.width + ' 0' )) .attrs(attrs) .attr('fill', 'none') ); } renderRef(options, position) { return { fill: this.svg.box(options, position).attrs({'stroke': 'none'}), mask: this.svg.box(options, position).attrs({ 'fill': '#000000', 'stroke': 'none', }), shape: this.svg.box(options, position).attrs({'fill': 'none'}), }; } renderFlatConnect( pattern, attrs, {x1, y1, x2, y2} ) { return { p1: {x: x1, y: y1}, p2: {x: x2, y: y2}, shape: this.svg.el('path') .attr('d', this.svg.patternedLine(pattern) .move(x1, y1) .line(x2, y2) .cap() .asPath()) .attrs(attrs), }; } renderRevConnect( pattern, attrs, {rad, x1, x2, xR, y1, y2} ) { const maxRad = (y2 - y1) / 2; const line = this.svg.patternedLine(pattern) .move(x1, y1) .line(xR, y1); if(rad < maxRad) { line .arc(xR, y1 + rad, Math.PI / 2) .line(xR + rad, y2 - rad) .arc(xR, y2 - rad, Math.PI / 2); } else { line.arc(xR, (y1 + y2) / 2, Math.PI); } return { p1: {x: x1, y: y1}, p2: {x: x2, y: y2}, shape: this.svg.el('path') .attr('d', line .line(x2, y2) .cap() .asPath()) .attrs(attrs), }; } renderLineDivider( {lineAttrs}, {height, labelWidth, width, x, y} ) { let shape = null; const yPos = y + height / 2; if(labelWidth > 0) { shape = this.svg.el('g').add( this.svg.line({'fill': 'none'}, { 'x1': x, 'x2': x + (width - labelWidth) / 2, 'y1': yPos, 'y2': yPos, }).attrs(lineAttrs), this.svg.line({'fill': 'none'}, { 'x1': x + (width + labelWidth) / 2, 'x2': x + width, 'y1': yPos, 'y2': yPos, }).attrs(lineAttrs) ); } else { shape = this.svg.line({'fill': 'none'}, { 'x1': x, 'x2': x + width, 'y1': yPos, 'y2': yPos, }).attrs(lineAttrs); } return {shape}; } renderDelayDivider( {dotSize, gapSize}, {height, width, x, y} ) { const mask = this.svg.el('g'); for(let i = 0; i + gapSize <= height; i += dotSize + gapSize) { mask.add(this.svg.box({ 'fill': '#000000', }, { height: gapSize, width, x, y: y + i, })); } return {mask}; } renderTearDivider( {fadeBegin, fadeSize, lineAttrs, pattern, zigHeight, zigWidth}, {env, height, labelHeight, labelWidth, width, x, y} ) { const maskGradID = env.addDef('tear-grad', () => { const px = 100 / width; return this.svg.linearGradient({}, [ { 'offset': (fadeBegin * px) + '%', 'stop-color': '#000000', }, { 'offset': ((fadeBegin + fadeSize) * px) + '%', 'stop-color': '#FFFFFF', }, { 'offset': (100 - (fadeBegin + fadeSize) * px) + '%', 'stop-color': '#FFFFFF', }, { 'offset': (100 - fadeBegin * px) + '%', 'stop-color': '#000000', }, ]); }); const shapeMask = this.svg.el('mask') .attr('maskUnits', 'userSpaceOnUse') .add( this.svg.box({ 'fill': 'url(#' + maskGradID + ')', }, { height: height + 10, width, x, y: y - 5, }) ); const shapeMaskID = env.addDef(shapeMask); if(labelWidth > 0) { shapeMask.add(this.svg.box({ 'fill': '#000000', 'rx': 2, 'ry': 2, }, { 'height': labelHeight + 2, 'width': labelWidth, 'x': x + (width - labelWidth) / 2, 'y': y + (height - labelHeight) / 2 - 1, })); } const p = pattern || new WavePattern(zigWidth, [zigHeight, -zigHeight]); let mask = null; const pathTop = this.svg.patternedLine(p) .move(x, y) .line(x + width, y); const shape = this.svg.el('g') .attr('mask', 'url(#' + shapeMaskID + ')') .add( this.svg.el('path') .attrs({ 'd': pathTop.asPath(), 'fill': 'none', }) .attrs(lineAttrs) ); if(height > 0) { const pathBase = this.svg.patternedLine(p) .move(x, y + height) .line(x + width, y + height); shape.add( this.svg.el('path') .attrs({ 'd': pathBase.asPath(), 'fill': 'none', }) .attrs(lineAttrs) ); pathTop .line(pathBase.x, pathBase.y, {patterned: false}) .cap(); pathTop.points.push(...pathBase.points.reverse()); mask = this.svg.el('path').attrs({ 'd': pathTop.asPath(), 'fill': '#000000', }); } return {mask, shape}; } } /* eslint-disable sort-keys */ // Maybe later const FONT = 'Helvetica,Arial,Liberation Sans,sans-serif'; const LINE_HEIGHT = 1.3; const WAVE = new WavePattern(6, 0.5); const NOTE_ATTRS = { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, }; const DIVIDER_LABEL_ATTRS = { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, 'text-anchor': 'middle', }; class BasicTheme extends BaseTheme { constructor(svg) { super(svg); const sharedBlockSection = { padding: { top: 3, bottom: 2, }, tag: { padding: { top: 1, left: 3, right: 3, bottom: 0, }, boxRenderer: this.renderTag.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, 'rx': 2, 'ry': 2, }), labelAttrs: { 'font-family': FONT, 'font-weight': 'bold', 'font-size': 9, 'line-height': LINE_HEIGHT, 'text-anchor': 'left', }, }, label: { minHeight: 4, padding: { top: 1, left: 5, right: 3, bottom: 1, }, labelAttrs: { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, 'text-anchor': 'left', }, }, }; Object.assign(this, { titleMargin: 10, outerMargin: 5, agentMargin: 10, actionMargin: 10, minActionMargin: 3, agentLineHighlightRadius: 4, agentCap: { box: { padding: { top: 5, left: 10, right: 10, bottom: 5, }, arrowBottom: 5 + 12 * 1.3 / 2, boxAttrs: { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, }, labelAttrs: { 'font-family': FONT, 'font-size': 12, 'line-height': LINE_HEIGHT, 'text-anchor': 'middle', }, }, database: { padding: { top: 12, left: 10, right: 10, bottom: 3, }, arrowBottom: 5 + 12 * 1.3 / 2, boxRenderer: this.renderDB.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, 'db-z': 5, }), labelAttrs: { 'font-family': FONT, 'font-size': 12, 'line-height': LINE_HEIGHT, 'text-anchor': 'middle', }, }, cross: { size: 20, render: svg.crossFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), }, bar: { height: 4, render: svg.boxFactory({ 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 1, }), }, fade: { width: 5, height: 6, extend: 1, }, none: { height: 10, }, }, connect: { loopbackRadius: 6, line: { 'solid': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }, renderFlat: this.renderFlatConnect.bind(this, null), renderRev: this.renderRevConnect.bind(this, null), }, 'dash': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, 'stroke-dasharray': '4, 2', }, renderFlat: this.renderFlatConnect.bind(this, null), renderRev: this.renderRevConnect.bind(this, null), }, 'wave': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', }, renderFlat: this.renderFlatConnect.bind(this, WAVE), renderRev: this.renderRevConnect.bind(this, WAVE), }, }, arrow: { 'single': { width: 5, height: 10, render: this.renderArrowHead.bind(this), attrs: { 'fill': '#000000', 'stroke-width': 0, 'stroke-linejoin': 'miter', }, }, 'double': { width: 4, height: 6, render: this.renderArrowHead.bind(this), attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, 'stroke-linejoin': 'miter', }, }, 'cross': { short: 7, radius: 3, render: svg.crossFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), }, }, label: { padding: 6, margin: {top: 2, bottom: 1}, attrs: { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, 'text-anchor': 'middle', }, loopbackAttrs: { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, }, }, source: { radius: 2, render: svg.circleFactory({ 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 1, }), }, mask: { padding: { top: 0, left: 3, right: 3, bottom: 1, }, }, }, titleAttrs: { 'font-family': FONT, 'font-size': 20, 'line-height': LINE_HEIGHT, 'text-anchor': 'middle', 'class': 'title', }, agentLineAttrs: { '': { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }, 'red': { 'stroke': '#CC0000', }, }, blocks: { 'ref': { margin: { top: 0, bottom: 0, }, boxRenderer: this.renderRef.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1.5, 'rx': 2, 'ry': 2, }), section: sharedBlockSection, }, '': { margin: { top: 0, bottom: 0, }, boxRenderer: svg.boxFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1.5, 'rx': 2, 'ry': 2, }), collapsedBoxRenderer: this.renderRef.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1.5, 'rx': 2, 'ry': 2, }), section: sharedBlockSection, sepRenderer: svg.lineFactory({ 'stroke': '#000000', 'stroke-width': 1.5, 'stroke-dasharray': '4, 2', }), }, }, notes: { 'text': { margin: {top: 0, left: 2, right: 2, bottom: 0}, padding: {top: 2, left: 2, right: 2, bottom: 2}, overlap: {left: 10, right: 10}, boxRenderer: svg.boxFactory({ 'fill': '#FFFFFF', }), labelAttrs: NOTE_ATTRS, }, 'note': { margin: {top: 0, left: 5, right: 5, bottom: 0}, padding: {top: 5, left: 5, right: 10, bottom: 5}, overlap: {left: 10, right: 10}, boxRenderer: svg.noteFactory({ 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, }, { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), labelAttrs: NOTE_ATTRS, }, 'state': { margin: {top: 0, left: 5, right: 5, bottom: 0}, padding: {top: 7, left: 7, right: 7, bottom: 7}, overlap: {left: 10, right: 10}, boxRenderer: svg.boxFactory({ 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, 'rx': 10, 'ry': 10, }), labelAttrs: NOTE_ATTRS, }, }, dividers: { '': { labelAttrs: DIVIDER_LABEL_ATTRS, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 0, margin: 0, render: () => ({}), }, 'line': { labelAttrs: DIVIDER_LABEL_ATTRS, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 10, margin: 0, render: this.renderLineDivider.bind(this, { lineAttrs: { 'stroke': '#000000', }, }), }, 'delay': { labelAttrs: DIVIDER_LABEL_ATTRS, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 0, margin: 0, render: this.renderDelayDivider.bind(this, { dotSize: 1, gapSize: 2, }), }, 'tear': { labelAttrs: DIVIDER_LABEL_ATTRS, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 10, margin: 10, render: this.renderTearDivider.bind(this, { fadeBegin: 5, fadeSize: 10, zigWidth: 6, zigHeight: 1, lineAttrs: { 'stroke': '#000000', }, }), }, }, }); } } class Factory { constructor() { this.name = 'basic'; } build(svg) { return new BasicTheme(svg); } } /* eslint-disable sort-keys */ // Maybe later const FONT$1 = 'Helvetica,Arial,Liberation Sans,sans-serif'; const LINE_HEIGHT$1 = 1.3; const WAVE$1 = new WavePattern(10, 1); const NOTE_ATTRS$1 = { 'font-family': FONT$1, 'font-size': 8, 'line-height': LINE_HEIGHT$1, }; const DIVIDER_LABEL_ATTRS$1 = { 'font-family': FONT$1, 'font-size': 8, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'middle', }; class ChunkyTheme extends BaseTheme { constructor(svg) { super(svg); const sharedBlockSection = { padding: { top: 3, bottom: 4, }, tag: { padding: { top: 2, left: 5, right: 5, bottom: 1, }, boxRenderer: this.renderTag.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 2, 'rx': 3, 'ry': 3, }), labelAttrs: { 'font-family': FONT$1, 'font-weight': 'bold', 'font-size': 9, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'left', }, }, label: { minHeight: 5, padding: { top: 2, left: 5, right: 3, bottom: 1, }, labelAttrs: { 'font-family': FONT$1, 'font-size': 8, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'left', }, }, }; Object.assign(this, { titleMargin: 12, outerMargin: 5, agentMargin: 8, actionMargin: 5, minActionMargin: 5, agentLineHighlightRadius: 4, agentCap: { box: { padding: { top: 1, left: 3, right: 3, bottom: 1, }, arrowBottom: 2 + 14 * 1.3 / 2, boxAttrs: { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 3, 'rx': 4, 'ry': 4, }, labelAttrs: { 'font-family': FONT$1, 'font-weight': 'bold', 'font-size': 14, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'middle', }, }, database: { padding: { top: 4, left: 3, right: 3, bottom: 0, }, arrowBottom: 2 + 14 * 1.3 / 2, boxRenderer: this.renderDB.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 3, 'db-z': 2, }), labelAttrs: { 'font-family': FONT$1, 'font-weight': 'bold', 'font-size': 14, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'middle', }, }, cross: { size: 20, render: svg.crossFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, 'stroke-linecap': 'round', }), }, bar: { height: 4, render: svg.boxFactory({ 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 3, 'rx': 2, 'ry': 2, }), }, fade: { width: 5, height: 10, extend: 1, }, none: { height: 10, }, }, connect: { loopbackRadius: 8, line: { 'solid': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, }, renderFlat: this.renderFlatConnect.bind(this, null), renderRev: this.renderRevConnect.bind(this, null), }, 'dash': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, 'stroke-dasharray': '10, 4', }, renderFlat: this.renderFlatConnect.bind(this, null), renderRev: this.renderRevConnect.bind(this, null), }, 'wave': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', }, renderFlat: this.renderFlatConnect.bind(this, WAVE$1), renderRev: this.renderRevConnect.bind(this, WAVE$1), }, }, arrow: { 'single': { width: 10, height: 12, render: this.renderArrowHead.bind(this), attrs: { 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 3, 'stroke-linejoin': 'round', }, }, 'double': { width: 10, height: 12, render: this.renderArrowHead.bind(this), attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', }, }, 'cross': { short: 10, radius: 5, render: svg.crossFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', }), }, }, label: { padding: 7, margin: {top: 2, bottom: 3}, attrs: { 'font-family': FONT$1, 'font-size': 8, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'middle', }, loopbackAttrs: { 'font-family': FONT$1, 'font-size': 8, 'line-height': LINE_HEIGHT$1, }, }, source: { radius: 5, render: svg.circleFactory({ 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 3, }), }, mask: { padding: { top: 1, left: 5, right: 5, bottom: 3, }, }, }, titleAttrs: { 'font-family': FONT$1, 'font-weight': 'bolder', 'font-size': 20, 'line-height': LINE_HEIGHT$1, 'text-anchor': 'middle', 'class': 'title', }, agentLineAttrs: { '': { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 3, }, 'red': { 'stroke': '#DD0000', }, }, blocks: { 'ref': { margin: { top: 0, bottom: 0, }, boxRenderer: this.renderRef.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 4, 'rx': 5, 'ry': 5, }), section: sharedBlockSection, }, '': { margin: { top: 0, bottom: 0, }, boxRenderer: svg.boxFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 4, 'rx': 5, 'ry': 5, }), collapsedBoxRenderer: this.renderRef.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 4, 'rx': 5, 'ry': 5, }), section: sharedBlockSection, sepRenderer: svg.lineFactory({ 'stroke': '#000000', 'stroke-width': 2, 'stroke-dasharray': '5, 3', }), }, }, notes: { 'text': { margin: {top: 0, left: 2, right: 2, bottom: 0}, padding: {top: 2, left: 2, right: 2, bottom: 2}, overlap: {left: 10, right: 10}, boxRenderer: svg.boxFactory({ 'fill': '#FFFFFF', }), labelAttrs: NOTE_ATTRS$1, }, 'note': { margin: {top: 0, left: 5, right: 5, bottom: 0}, padding: {top: 3, left: 3, right: 10, bottom: 3}, overlap: {left: 10, right: 10}, boxRenderer: svg.noteFactory({ 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 2, 'stroke-linejoin': 'round', }, { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), labelAttrs: NOTE_ATTRS$1, }, 'state': { margin: {top: 0, left: 5, right: 5, bottom: 0}, padding: {top: 5, left: 7, right: 7, bottom: 5}, overlap: {left: 10, right: 10}, boxRenderer: svg.boxFactory({ 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 3, 'rx': 10, 'ry': 10, }), labelAttrs: NOTE_ATTRS$1, }, }, dividers: { '': { labelAttrs: DIVIDER_LABEL_ATTRS$1, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 0, margin: 0, render: () => ({}), }, 'line': { labelAttrs: DIVIDER_LABEL_ATTRS$1, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 10, margin: 0, render: this.renderLineDivider.bind(this, { lineAttrs: { 'stroke': '#000000', 'stroke-width': 2, 'stroke-linecap': 'round', }, }), }, 'delay': { labelAttrs: DIVIDER_LABEL_ATTRS$1, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 0, margin: 0, render: this.renderDelayDivider.bind(this, { dotSize: 3, gapSize: 3, }), }, 'tear': { labelAttrs: DIVIDER_LABEL_ATTRS$1, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 10, margin: 10, render: this.renderTearDivider.bind(this, { fadeBegin: 5, fadeSize: 10, zigWidth: 6, zigHeight: 1, lineAttrs: { 'stroke': '#000000', 'stroke-width': 2, 'stroke-linejoin': 'round', }, }), }, }, }); } } class Factory$1 { constructor() { this.name = 'chunky'; } build(svg) { return new ChunkyTheme(svg); } } class EventObject { constructor() { this.listeners = new Map(); this.forwards = new Set(); } addEventListener(type, callback) { const l = this.listeners.get(type); if(l) { l.push(callback); } else { this.listeners.set(type, [callback]); } } removeEventListener(type, fn) { const l = this.listeners.get(type); if(!l) { return; } const i = l.indexOf(fn); if(i !== -1) { l.splice(i, 1); } } on(type, fn) { this.addEventListener(type, fn); return this; } off(type, fn) { this.removeEventListener(type, fn); return this; } countEventListeners(type) { return (this.listeners.get(type) || []).length; } removeAllEventListeners(type) { if(type) { this.listeners.delete(type); } else { this.listeners.clear(); } } addEventForwarding(target) { this.forwards.add(target); } removeEventForwarding(target) { this.forwards.delete(target); } removeAllEventForwardings() { this.forwards.clear(); } trigger(type, params = []) { (this.listeners.get(type) || []).forEach( (listener) => listener(...params) ); this.forwards.forEach((fwd) => fwd.trigger(type, params)); } } const nodejs = (typeof window === 'undefined'); // Thanks, https://stackoverflow.com/a/23522755/1180785 const safari = ( !nodejs && (/^((?!chrome|android).)*safari/i).test(window.navigator.userAgent) ); // Thanks, https://stackoverflow.com/a/9851769/1180785 const firefox = ( !nodejs && typeof window.InstallTrigger !== 'undefined' ); class Exporter { constructor() { this.latestSVG = null; this.latestInternalSVG = null; this.canvas = null; this.context = null; this.indexPNG = 0; this.latestPNGIndex = 0; this.latestPNG = null; } getSVGContent(renderer) { let code = renderer.dom().outerHTML; /* * Firefox fails to render SVGs as unless they have size * attributes on the tag, so we must set this when * exporting from any environment, in case it is opened in FireFox */ code = code.replace( (/^ { this.canvas.width = width; this.canvas.height = height; this.context.drawImage(img, 0, 0, width, height); if(safariHackaround) { document.body.removeChild(safariHackaround); } callback(this.canvas); }; img.addEventListener('load', () => { if(safariHackaround) { // Wait for custom fonts to load (Safari takes a moment) setTimeout(render, 50); } else { render(); } }, {once: true}); img.src = this.getSVGURL(renderer); } getPNGBlob(renderer, resolution, callback) { this.getCanvas(renderer, resolution, (canvas) => { canvas.toBlob(callback, 'image/png'); }); } getPNGURL(renderer, resolution, callback) { ++ this.indexPNG; const index = this.indexPNG; this.getPNGBlob(renderer, resolution, (blob) => { const url = URL.createObjectURL(blob); const isLatest = index >= this.latestPNGIndex; if(isLatest) { if(this.latestPNG) { URL.revokeObjectURL(this.latestPNG); } this.latestPNG = url; this.latestPNGIndex = index; callback(url, true); } else { callback(url, false); URL.revokeObjectURL(url); } }); } } function indexOf(list, element, equalityCheck = null) { if(equalityCheck === null) { return list.indexOf(element); } for(let i = 0; i < list.length; ++ i) { if(equalityCheck(list[i], element)) { return i; } } return -1; } function mergeSets(target, b = null, equalityCheck = null) { if(!b) { return; } for(let i = 0; i < b.length; ++ i) { if(indexOf(target, b[i], equalityCheck) === -1) { target.push(b[i]); } } } function hasIntersection(a, b, equalityCheck = null) { for(let i = 0; i < b.length; ++ i) { if(indexOf(a, b[i], equalityCheck) !== -1) { return true; } } return false; } function removeAll(target, b = null, equalityCheck = null) { if(!b) { return; } for(let i = 0; i < b.length; ++ i) { const p = indexOf(target, b[i], equalityCheck); if(p !== -1) { target.splice(p, 1); } } } function remove(list, item, equalityCheck = null) { const p = indexOf(list, item, equalityCheck); if(p !== -1) { list.splice(p, 1); } } function last(list) { return list[list.length - 1]; } function combineRecur(parts, position, current, target) { if(position >= parts.length) { target.push(current.slice()); return; } const choices = parts[position]; if(!Array.isArray(choices)) { current.push(choices); combineRecur(parts, position + 1, current, target); current.pop(); return; } for(let i = 0; i < choices.length; ++ i) { current.push(choices[i]); combineRecur(parts, position + 1, current, target); current.pop(); } } function combine(parts) { const target = []; combineRecur(parts, 0, [], target); return target; } function flatMap(list, fn) { const result = []; list.forEach((item) => { result.push(...fn(item)); }); return result; } /* eslint-disable max-lines */ class AgentState { constructor({ blocked = false, covered = false, group = null, highlighted = false, locked = false, visible = false, } = {}) { this.blocked = blocked; this.covered = covered; this.group = group; this.highlighted = highlighted; this.locked = locked; this.visible = visible; } } AgentState.LOCKED = new AgentState({locked: true}); AgentState.DEFAULT = new AgentState(); // Agent from Parser: {name, flags} const PAgent = { equals: (a, b) => (a.name === b.name), hasFlag: (flag, has = true) => (pAgent) => ( pAgent.flags.includes(flag) === has), }; // Agent from Generator: {id, formattedLabel, anchorRight} const GAgent = { addNearby: (target, reference, item, offset) => { const p = indexOf(target, reference, GAgent.equals); if(p === -1) { target.push(item); } else { target.splice(p + offset, 0, item); } }, equals: (a, b) => (a.id === b.id), hasIntersection: (a, b) => hasIntersection(a, b, GAgent.equals), indexOf: (list, gAgent) => indexOf(list, gAgent, GAgent.equals), make: (id, {anchorRight = false, isVirtualSource = false} = {}) => ({ anchorRight, id, isVirtualSource, options: [], }), }; function isExpiredGroupAlias(state) { return state.blocked && state.group === null; } function isReservedAgentName(name) { return name.startsWith('__'); } const NOTE_DEFAULT_G_AGENTS = { 'note left': [GAgent.make('[')], 'note over': [GAgent.make('['), GAgent.make(']')], 'note right': [GAgent.make(']')], }; const SPECIAL_AGENT_IDS = ['[', ']']; const MERGABLE = { 'agent begin': { check: ['mode'], merge: ['agentIDs'], siblings: new Set(['agent highlight']), }, 'agent end': { check: ['mode'], merge: ['agentIDs'], siblings: new Set(['agent highlight']), }, 'agent highlight': { check: ['highlighted'], merge: ['agentIDs'], siblings: new Set(['agent begin', 'agent end']), }, }; function mergableParallel(target, copy) { const info = MERGABLE[target.type]; if(!info || target.type !== copy.type) { return false; } if(info.check.some((c) => target[c] !== copy[c])) { return false; } return true; } function performMerge(target, copy) { const info = MERGABLE[target.type]; info.merge.forEach((m) => { mergeSets(target[m], copy[m]); }); } function iterateRemoval(list, fn) { for(let i = 0; i < list.length;) { const rm = fn(list[i], i); if(rm) { list.splice(i, 1); } else { ++ i; } } } function performParallelMergers(stages) { iterateRemoval(stages, (stage, i) => { for(let j = 0; j < i; ++ j) { if(mergableParallel(stages[j], stage)) { performMerge(stages[j], stage); return true; } } return false; }); } function findViableSequentialMergers(stages) { const mergers = new Set(); const types = stages.map(({type}) => type); types.forEach((type) => { const info = MERGABLE[type]; if(!info) { return; } if(types.every((t) => (type === t || info.siblings.has(t)))) { mergers.add(type); } }); return mergers; } function performSequentialMergers(lastViable, viable, lastStages, stages) { iterateRemoval(stages, (stage) => { if(!lastViable.has(stage.type) || !viable.has(stage.type)) { return false; } for(let j = 0; j < lastStages.length; ++ j) { if(mergableParallel(lastStages[j], stage)) { performMerge(lastStages[j], stage); return true; } } return false; }); } function optimiseStages(stages) { let lastStages = []; let lastViable = new Set(); for(let i = 0; i < stages.length;) { const stage = stages[i]; let subStages = null; if(stage.type === 'parallel') { subStages = stage.stages; } else { subStages = [stage]; } performParallelMergers(subStages); const viable = findViableSequentialMergers(subStages); performSequentialMergers(lastViable, viable, lastStages, subStages); if(subStages.length === 0) { stages.splice(i, 1); } else { if(stage.type === 'parallel' && subStages.length === 1) { stages.splice(i, 1, subStages[0]); } lastViable = viable; lastStages = subStages; ++ i; } } } function extractParallel(target, stages) { for(const stage of stages) { if(!stage) { continue; } if(stage.type === 'parallel') { extractParallel(target, stage.stages); } else { target.push(stage); } } } function checkAgentConflicts(allStages) { const createIDs = flatMap( allStages .filter((stage) => (stage.type === 'agent begin')), (stage) => stage.agentIDs ); for(const stage of allStages) { if(stage.type !== 'agent end') { continue; } for(const id of stage.agentIDs) { if(createIDs.indexOf(id) !== -1) { return 'Cannot create and destroy ' + id + ' simultaneously'; } } } return null; } function checkReferenceConflicts(allStages) { const count = allStages .filter((stage) => ( stage.type === 'block begin' || stage.type === 'block end' )) .length; if(!count) { return null; } else if(count !== allStages.length) { return 'Cannot use parallel here'; } const leftIDs = allStages .filter((stage) => (stage.type === 'block begin')) .map((stage) => stage.left); for(const stage of allStages) { if(stage.type !== 'block end') { continue; } if(leftIDs.indexOf(stage.left) !== -1) { return 'Cannot create and destroy reference simultaneously'; } } return null; } function checkDelayedConflicts(allStages) { const tags = allStages .filter((stage) => (stage.type === 'connect-delay-begin')) .map((stage) => stage.tag); for(const stage of allStages) { if(stage.type !== 'connect-delay-end') { continue; } if(tags.indexOf(stage.tag) !== -1) { return 'Cannot start and finish delayed connection simultaneously'; } } return null; } const PARALLEL_STAGES = [ 'agent begin', 'agent end', 'agent highlight', 'block begin', 'block end', 'connect', 'connect-delay-begin', 'connect-delay-end', 'note over', 'note right', 'note left', 'note between', ]; function errorForParallel(existing, latest) { if(!existing) { return 'Nothing to run statement in parallel with'; } const allStages = []; extractParallel(allStages, [existing]); extractParallel(allStages, [latest]); if(allStages.some((stage) => !PARALLEL_STAGES.includes(stage.type))) { return 'Cannot use parallel here'; } return ( checkAgentConflicts(allStages) || checkReferenceConflicts(allStages) || checkDelayedConflicts(allStages) ); } function swapBegin(stage, mode) { if(stage.type === 'agent begin') { stage.mode = mode; return true; } if(stage.type === 'parallel') { let any = false; stage.stages.forEach((subStage) => { if(subStage.type === 'agent begin') { subStage.mode = mode; any = true; } }); return any; } return false; } function swapFirstBegin(stages, mode) { for(let i = 0; i < stages.length; ++ i) { if(swapBegin(stages[i], mode)) { break; } } } function addBounds(allGAgents, gAgentL, gAgentR, involvedGAgents = null) { remove(allGAgents, gAgentL, GAgent.equals); remove(allGAgents, gAgentR, GAgent.equals); let indexL = 0; let indexR = allGAgents.length; if(involvedGAgents) { const found = (involvedGAgents .map((gAgent) => GAgent.indexOf(allGAgents, gAgent)) .filter((p) => (p !== -1)) ); indexL = found.reduce((a, b) => Math.min(a, b), allGAgents.length); indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1; } allGAgents.splice(indexL, 0, gAgentL); allGAgents.splice(indexR + 1, 0, gAgentR); return {indexL, indexR: indexR + 1}; } class Generator { constructor() { this.agentStates = new Map(); this.agentAliases = new Map(); this.activeGroups = new Map(); this.gAgents = []; this.labelPattern = null; this.nextID = 0; this.nesting = []; this.markers = new Set(); this.currentSection = null; this.currentNest = null; this.stageHandlers = { 'agent begin': this.handleAgentBegin.bind(this), 'agent define': this.handleAgentDefine.bind(this), 'agent end': this.handleAgentEnd.bind(this), 'agent options': this.handleAgentOptions.bind(this), 'async': this.handleAsync.bind(this), 'block begin': this.handleBlockBegin.bind(this), 'block end': this.handleBlockEnd.bind(this), 'block split': this.handleBlockSplit.bind(this), 'connect': this.handleConnect.bind(this), 'connect-delay-begin': this.handleConnectDelayBegin.bind(this), 'connect-delay-end': this.handleConnectDelayEnd.bind(this), 'divider': this.handleDivider.bind(this), 'group begin': this.handleGroupBegin.bind(this), 'label pattern': this.handleLabelPattern.bind(this), 'mark': this.handleMark.bind(this), 'note between': this.handleNote.bind(this), 'note left': this.handleNote.bind(this), 'note over': this.handleNote.bind(this), 'note right': this.handleNote.bind(this), }; this.expandGroupedGAgent = this.expandGroupedGAgent.bind(this); this.handleStage = this.handleStage.bind(this); this.toGAgent = this.toGAgent.bind(this); this.endGroup = this.endGroup.bind(this); } _aliasInUse(alias) { const old = this.agentAliases.get(alias); if(old && old !== alias) { return true; } return this.gAgents.some((gAgent) => (gAgent.id === alias)); } toGAgent({name, alias, flags}) { if(alias) { if(this.agentAliases.has(name)) { throw new Error( 'Cannot alias ' + name + '; it is already an alias' ); } if(this._aliasInUse(alias)) { throw new Error( 'Cannot use ' + alias + ' as an alias; it is already in use' ); } this.agentAliases.set(alias, name); } return GAgent.make(this.agentAliases.get(name) || name, { isVirtualSource: flags.includes('source'), }); } addStage(stage, {isVisible = true, parallel = false} = {}) { if(!stage) { return; } if(isVisible) { this.currentNest.hasContent = true; } if(typeof stage.ln === 'undefined') { stage.ln = this.latestLine; } const {stages} = this.currentSection; if(parallel) { const target = last(stages); const err = errorForParallel(target, stage); if(err) { throw new Error(err); } const pstage = this.makeParallel([target, stage]); pstage.ln = stage.ln; -- stages.length; stages.push(pstage); } else { stages.push(stage); } } addImpStage(stage, {parallel = false} = {}) { if(!stage) { return; } if(typeof stage.ln === 'undefined') { stage.ln = this.latestLine; } const {stages} = this.currentSection; if(parallel) { const target = stages[stages.length - 2]; if(stages.length === 0) { throw new Error('Nothing to run statement in parallel with'); } if(errorForParallel(target, stage)) { stages.splice(stages.length - 1, 0, stage); } else { const pstage = this.makeParallel([target, stage]); pstage.ln = stage.ln; stages.splice(stages.length - 2, 1, pstage); } } else { stages.push(stage); } } makeParallel(stages) { const viableStages = []; extractParallel(viableStages, stages); if(viableStages.length === 0) { return null; } if(viableStages.length === 1) { return viableStages[0]; } viableStages.forEach((stage) => { if(typeof stage.ln === 'undefined') { stage.ln = this.latestLine; } }); return { stages: viableStages, type: 'parallel', }; } defineGAgents(gAgents) { mergeSets( this.currentNest.gAgents, gAgents.filter((gAgent) => !SPECIAL_AGENT_IDS.includes(gAgent.id)), GAgent.equals ); mergeSets(this.gAgents, gAgents, GAgent.equals); } getGAgentState(gAgent) { return this.agentStates.get(gAgent.id) || AgentState.DEFAULT; } updateGAgentState(gAgent, change) { const state = this.agentStates.get(gAgent.id); if(state) { Object.assign(state, change); } else { this.agentStates.set(gAgent.id, new AgentState(change)); } } replaceGAgentState(gAgent, state) { this.agentStates.set(gAgent.id, state); } validateGAgents(gAgents, { allowGrouped = false, allowCovered = false, allowVirtual = false, } = {}) { /* eslint-disable complexity */ // The checks are quite simple gAgents.forEach((gAgent) => { /* eslint-enable complexity */ const state = this.getGAgentState(gAgent); const name = gAgent.id; if(isExpiredGroupAlias(state)) { // Used to be a group alias; can never be reused throw new Error('Duplicate agent name: ' + name); } if(!allowCovered && state.covered) { throw new Error('Agent ' + name + ' is hidden behind group'); } if(!allowGrouped && state.group !== null) { throw new Error('Agent ' + name + ' is in a group'); } if(!allowVirtual && gAgent.isVirtualSource) { throw new Error('Cannot use message source here'); } if(isReservedAgentName(name)) { throw new Error(name + ' is a reserved name'); } }); } setGAgentVis(gAgents, visible, mode, checked = false) { const seen = new Set(); const filteredGAgents = gAgents.filter((gAgent) => { if(seen.has(gAgent.id)) { return false; } seen.add(gAgent.id); const state = this.getGAgentState(gAgent); if(state.locked || state.blocked) { if(checked) { throw new Error('Cannot begin/end agent: ' + gAgent.id); } else { return false; } } return state.visible !== visible; }); if(filteredGAgents.length === 0) { return null; } filteredGAgents.forEach((gAgent) => { this.updateGAgentState(gAgent, {visible}); }); this.defineGAgents(filteredGAgents); return { agentIDs: filteredGAgents.map((gAgent) => gAgent.id), mode, type: (visible ? 'agent begin' : 'agent end'), }; } setGAgentHighlight(gAgents, highlighted, checked = false) { const filteredGAgents = gAgents.filter((gAgent) => { const state = this.getGAgentState(gAgent); if(state.locked || state.blocked) { if(checked) { throw new Error('Cannot highlight agent: ' + gAgent.id); } else { return false; } } return state.visible && (state.highlighted !== highlighted); }); if(filteredGAgents.length === 0) { return null; } filteredGAgents.forEach((gAgent) => { this.updateGAgentState(gAgent, {highlighted}); }); return { agentIDs: filteredGAgents.map((gAgent) => gAgent.id), highlighted, type: 'agent highlight', }; } _makeSection(header, stages) { return { delayedConnections: new Map(), header, stages, }; } _checkSectionEnd() { const dcs = this.currentSection.delayedConnections; if(dcs.size > 0) { const dc = dcs.values().next().value; throw new Error( 'Unused delayed connection "' + dc.tag + '" at line ' + (dc.ln + 1) ); } } beginNested(blockType, {tag, label, name, ln}) { const leftGAgent = GAgent.make(name + '[', {anchorRight: true}); const rightGAgent = GAgent.make(name + ']'); const gAgents = [leftGAgent, rightGAgent]; const stages = []; this.currentSection = this._makeSection({ blockType, canHide: true, label: this.textFormatter(label), left: leftGAgent.id, ln, right: rightGAgent.id, tag: this.textFormatter(tag), type: 'block begin', }, stages); this.currentNest = { blockType, gAgents, hasContent: false, leftGAgent, rightGAgent, sections: [this.currentSection], }; this.replaceGAgentState(leftGAgent, AgentState.LOCKED); this.replaceGAgentState(rightGAgent, AgentState.LOCKED); this.nesting.push(this.currentNest); return {stages}; } nextBlockName() { const name = '__BLOCK' + this.nextID; ++ this.nextID; return name; } nextVirtualAgentName() { const name = '__' + this.nextID; ++ this.nextID; return name; } handleBlockBegin({ln, blockType, tag, label, parallel}) { if(parallel) { throw new Error('Cannot use parallel here'); } this.beginNested(blockType, { label, ln, name: this.nextBlockName(), tag, }); } handleBlockSplit({ln, blockType, tag, label, parallel}) { if(parallel) { throw new Error('Cannot use parallel here'); } if(this.currentNest.blockType !== 'if') { throw new Error( 'Invalid block nesting ("else" inside ' + this.currentNest.blockType + ')' ); } this._checkSectionEnd(); this.currentSection = this._makeSection({ blockType, label: this.textFormatter(label), left: this.currentNest.leftGAgent.id, ln, right: this.currentNest.rightGAgent.id, tag: this.textFormatter(tag), type: 'block split', }, []); this.currentNest.sections.push(this.currentSection); } handleBlockEnd({parallel}) { if(this.nesting.length <= 1) { throw new Error('Invalid block nesting (too many "end"s)'); } this._checkSectionEnd(); const nested = this.nesting.pop(); this.currentNest = last(this.nesting); this.currentSection = last(this.currentNest.sections); if(!nested.hasContent) { throw new Error('Empty block'); } this.defineGAgents(nested.gAgents); addBounds( this.gAgents, nested.leftGAgent, nested.rightGAgent, nested.gAgents ); nested.sections.forEach((section) => { this.currentSection.stages.push(section.header); this.currentSection.stages.push(...section.stages); }); this.addStage({ left: nested.leftGAgent.id, right: nested.rightGAgent.id, type: 'block end', }, {parallel}); } makeGroupDetails(pAgents, alias) { const gAgents = pAgents.map(this.toGAgent); this.validateGAgents(gAgents); if(this.agentStates.has(alias)) { throw new Error('Duplicate agent name: ' + alias); } const name = this.nextBlockName(); const leftGAgent = GAgent.make(name + '[', {anchorRight: true}); const rightGAgent = GAgent.make(name + ']'); this.replaceGAgentState(leftGAgent, AgentState.LOCKED); this.replaceGAgentState(rightGAgent, AgentState.LOCKED); this.updateGAgentState( GAgent.make(alias), {blocked: true, group: alias} ); this.defineGAgents([...gAgents, leftGAgent, rightGAgent]); const {indexL, indexR} = addBounds( this.gAgents, leftGAgent, rightGAgent, gAgents ); const gAgentsCovered = []; const gAgentsContained = gAgents.slice(); for(let i = indexL + 1; i < indexR; ++ i) { gAgentsCovered.push(this.gAgents[i]); } removeAll(gAgentsCovered, gAgentsContained, GAgent.equals); return { gAgents, gAgentsContained, gAgentsCovered, leftGAgent, rightGAgent, }; } handleGroupBegin({agents, blockType, tag, label, alias, parallel}) { const details = this.makeGroupDetails(agents, alias); details.gAgentsContained.forEach((gAgent) => { this.updateGAgentState(gAgent, {group: alias}); }); details.gAgentsCovered.forEach((gAgent) => { this.updateGAgentState(gAgent, {covered: true}); }); this.activeGroups.set(alias, details); this.addImpStage( this.setGAgentVis(details.gAgents, true, 'box'), {parallel} ); this.addStage({ blockType, canHide: false, label: this.textFormatter(label), left: details.leftGAgent.id, right: details.rightGAgent.id, tag: this.textFormatter(tag), type: 'block begin', }, {parallel}); } endGroup({name}) { const details = this.activeGroups.get(name); if(!details) { return null; } this.activeGroups.delete(name); details.gAgentsContained.forEach((gAgent) => { this.updateGAgentState(gAgent, {group: null}); }); details.gAgentsCovered.forEach((gAgent) => { this.updateGAgentState(gAgent, {covered: false}); }); this.updateGAgentState(GAgent.make(name), {group: null}); return { left: details.leftGAgent.id, right: details.rightGAgent.id, type: 'block end', }; } handleMark({name, parallel}) { this.markers.add(name); this.addStage({name, type: 'mark'}, {isVisible: false, parallel}); } handleDivider({mode, height, label, parallel}) { this.addStage({ formattedLabel: this.textFormatter(label), height, mode, type: 'divider', }, {isVisible: false, parallel}); } handleAsync({target, parallel}) { if(target !== '' && !this.markers.has(target)) { throw new Error('Unknown marker: ' + target); } this.addStage({target, type: 'async'}, {isVisible: false, parallel}); } handleLabelPattern({pattern}) { this.labelPattern = pattern.slice(); for(let i = 0; i < this.labelPattern.length; ++ i) { const part = this.labelPattern[i]; if(typeof part === 'object' && typeof part.start !== 'undefined') { this.labelPattern[i] = Object.assign({ current: part.start, }, part); } } } applyLabelPattern(label) { let result = ''; const tokens = {label}; this.labelPattern.forEach((part) => { if(typeof part === 'string') { result += part; } else if(typeof part.token !== 'undefined') { result += tokens[part.token]; } else if(typeof part.current !== 'undefined') { result += part.current.toFixed(part.dp); part.current += part.inc; } }); return result; } expandGroupedGAgent(gAgent) { const {group} = this.getGAgentState(gAgent); if(!group) { return [gAgent]; } const details = this.activeGroups.get(group); return [details.leftGAgent, details.rightGAgent]; } expandGroupedGAgentConnection(gAgents) { const gAgents1 = this.expandGroupedGAgent(gAgents[0]); const gAgents2 = this.expandGroupedGAgent(gAgents[1]); let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]); let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]); if(ind1 === -1) { /* * Virtual sources written as '* -> Ref' will spawn to the left, * not the right (as non-virtual agents would) */ ind1 = gAgents1[0].isVirtualSource ? -1 : this.gAgents.length; } if(ind2 === -1) { /* * Virtual and non-virtual agents written as 'Ref -> *' will * spawn to the right */ ind2 = this.gAgents.length; } if(ind1 === ind2) { // Self-connection return [last(gAgents1), last(gAgents2)]; } else if(ind1 < ind2) { return [last(gAgents1), gAgents2[0]]; } else { return [gAgents1[0], last(gAgents2)]; } } filterConnectFlags(pAgents) { const beginGAgents = (pAgents .filter(PAgent.hasFlag('begin')) .map(this.toGAgent) ); const endGAgents = (pAgents .filter(PAgent.hasFlag('end')) .map(this.toGAgent) ); if(GAgent.hasIntersection(beginGAgents, endGAgents)) { throw new Error('Cannot set agent visibility multiple times'); } const startGAgents = (pAgents .filter(PAgent.hasFlag('start')) .map(this.toGAgent) ); const stopGAgents = (pAgents .filter(PAgent.hasFlag('stop')) .map(this.toGAgent) ); mergeSets(stopGAgents, endGAgents); if(GAgent.hasIntersection(startGAgents, stopGAgents)) { throw new Error('Cannot set agent highlighting multiple times'); } this.validateGAgents(beginGAgents); this.validateGAgents(endGAgents); this.validateGAgents(startGAgents); this.validateGAgents(stopGAgents); return {beginGAgents, endGAgents, startGAgents, stopGAgents}; } makeVirtualAgent(anchorRight) { const virtualGAgent = GAgent.make(this.nextVirtualAgentName(), { anchorRight, isVirtualSource: true, }); this.replaceGAgentState(virtualGAgent, AgentState.LOCKED); return virtualGAgent; } addNearbyAgent(gAgentReference, gAgent, offset) { GAgent.addNearby( this.currentNest.gAgents, gAgentReference, gAgent, offset ); GAgent.addNearby( this.gAgents, gAgentReference, gAgent, offset ); } expandVirtualSourceAgents(gAgents) { if(gAgents[0].isVirtualSource) { if(gAgents[1].isVirtualSource) { throw new Error('Cannot connect found messages'); } if(SPECIAL_AGENT_IDS.includes(gAgents[1].id)) { throw new Error( 'Cannot connect found messages to special agents' ); } const virtualGAgent = this.makeVirtualAgent(true); this.addNearbyAgent(gAgents[1], virtualGAgent, 0); return [virtualGAgent, gAgents[1]]; } if(gAgents[1].isVirtualSource) { if(SPECIAL_AGENT_IDS.includes(gAgents[0].id)) { throw new Error( 'Cannot connect found messages to special agents' ); } const virtualGAgent = this.makeVirtualAgent(false); this.addNearbyAgent(gAgents[0], virtualGAgent, 1); return [gAgents[0], virtualGAgent]; } return gAgents; } _handlePartialConnect(agents, parallel) { const flags = this.filterConnectFlags(agents); const gAgents = agents.map(this.toGAgent); this.validateGAgents(gAgents, { allowGrouped: true, allowVirtual: true, }); this.defineGAgents(flatMap(gAgents, this.expandGroupedGAgent) .filter((gAgent) => !gAgent.isVirtualSource)); const implicitBeginGAgents = (agents .filter(PAgent.hasFlag('begin', false)) .map(this.toGAgent) .filter((gAgent) => !gAgent.isVirtualSource) ); this.addImpStage( this.setGAgentVis(implicitBeginGAgents, true, 'box'), {parallel} ); return {flags, gAgents}; } _makeConnectParallelStages(flags, connectStage) { return this.makeParallel([ this.setGAgentVis(flags.beginGAgents, true, 'box', true), this.setGAgentHighlight(flags.startGAgents, true, true), connectStage, this.setGAgentHighlight(flags.stopGAgents, false, true), this.setGAgentVis(flags.endGAgents, false, 'cross', true), ]); } _isSelfConnect(agents) { const gAgents = agents.map(this.toGAgent); const expandedGAgents = this.expandGroupedGAgentConnection(gAgents); if(expandedGAgents[0].id !== expandedGAgents[1].id) { return false; } if(expandedGAgents.some((gAgent) => gAgent.isVirtualSource)) { return false; } return true; } handleConnect({agents, label, options, parallel}) { if(this._isSelfConnect(agents)) { const tag = {}; this.handleConnectDelayBegin({ agent: agents[0], ln: 0, options, parallel, tag, }); this.handleConnectDelayEnd({ agent: agents[1], label, options, tag, }); return; } let {flags, gAgents} = this._handlePartialConnect(agents, parallel); gAgents = this.expandGroupedGAgentConnection(gAgents); gAgents = this.expandVirtualSourceAgents(gAgents); const connectStage = { agentIDs: gAgents.map((gAgent) => gAgent.id), label: this.textFormatter(this.applyLabelPattern(label)), options, type: 'connect', }; this.addStage( this._makeConnectParallelStages(flags, connectStage), {parallel} ); } handleConnectDelayBegin({agent, tag, options, ln, parallel}) { const dcs = this.currentSection.delayedConnections; if(dcs.has(tag)) { throw new Error('Duplicate delayed connection "' + tag + '"'); } const {flags, gAgents} = this._handlePartialConnect([agent], parallel); const uniqueTag = this.nextVirtualAgentName(); const connectStage = { agentIDs: null, label: null, options, tag: uniqueTag, type: 'connect-delay-begin', }; dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag}); this.addStage( this._makeConnectParallelStages(flags, connectStage), {parallel} ); } handleConnectDelayEnd({agent, tag, label, options, parallel}) { const dcs = this.currentSection.delayedConnections; const dcInfo = dcs.get(tag); if(!dcInfo) { throw new Error('Unknown delayed connection "' + tag + '"'); } let {flags, gAgents} = this._handlePartialConnect([agent], parallel); gAgents = this.expandGroupedGAgentConnection([ ...dcInfo.gAgents, ...gAgents, ]); gAgents = this.expandVirtualSourceAgents(gAgents); let combinedOptions = dcInfo.connectStage.options; if(combinedOptions.line !== options.line) { throw new Error('Mismatched delayed connection arrows'); } if(options.right) { combinedOptions = Object.assign({}, combinedOptions, { right: options.right, }); } Object.assign(dcInfo.connectStage, { agentIDs: gAgents.map((gAgent) => gAgent.id), label: this.textFormatter(this.applyLabelPattern(label)), options: combinedOptions, }); const connectEndStage = { tag: dcInfo.uniqueTag, type: 'connect-delay-end', }; this.addStage( this._makeConnectParallelStages(flags, connectEndStage), {parallel} ); dcs.delete(tag); } handleNote({type, agents, mode, label, parallel}) { let gAgents = null; if(agents.length === 0) { gAgents = NOTE_DEFAULT_G_AGENTS[type] || []; } else { gAgents = agents.map(this.toGAgent); } this.validateGAgents(gAgents, {allowGrouped: true}); gAgents = flatMap(gAgents, this.expandGroupedGAgent); const agentIDs = gAgents.map((gAgent) => gAgent.id); const uniqueAgents = new Set(agentIDs).size; if(type === 'note between' && uniqueAgents < 2) { throw new Error('note between requires at least 2 agents'); } this.defineGAgents(gAgents); this.addImpStage(this.setGAgentVis(gAgents, true, 'box'), {parallel}); this.addStage({ agentIDs, label: this.textFormatter(label), mode, type, }, {parallel}); } handleAgentDefine({agents}) { const gAgents = agents.map(this.toGAgent); this.validateGAgents(gAgents, { allowCovered: true, allowGrouped: true, }); mergeSets(this.gAgents, gAgents, GAgent.equals); } handleAgentOptions({agent, options}) { const gAgent = this.toGAgent(agent); const gAgents = [gAgent]; this.validateGAgents(gAgents, { allowCovered: true, allowGrouped: true, }); mergeSets(this.gAgents, gAgents, GAgent.equals); this.gAgents .filter(({id}) => (id === gAgent.id)) .forEach((storedGAgent) => { mergeSets(storedGAgent.options, options); }); } handleAgentBegin({agents, mode, parallel}) { const gAgents = agents.map(this.toGAgent); this.validateGAgents(gAgents); this.addStage(this.setGAgentVis(gAgents, true, mode, true), {parallel}); } handleAgentEnd({agents, mode, parallel}) { const groupPAgents = (agents .filter((pAgent) => this.activeGroups.has(pAgent.name)) ); const gAgents = (agents .filter((pAgent) => !this.activeGroups.has(pAgent.name)) .map(this.toGAgent) ); this.validateGAgents(gAgents); this.addStage(this.makeParallel([ this.setGAgentHighlight(gAgents, false), this.setGAgentVis(gAgents, false, mode, true), ...groupPAgents.map(this.endGroup), ]), {parallel}); } handleStage(stage) { this.latestLine = stage.ln; try { const handler = this.stageHandlers[stage.type]; if(!handler) { throw new Error('Unknown command: ' + stage.type); } handler(stage); } catch(e) { if(typeof e === 'object' && e.message) { e.message += ' at line ' + (stage.ln + 1); throw e; } } } _reset() { this.agentStates.clear(); this.markers.clear(); this.agentAliases.clear(); this.activeGroups.clear(); this.gAgents.length = 0; this.nextID = 0; this.nesting.length = 0; this.labelPattern = [{token: 'label'}]; } _finalise(globals) { addBounds( this.gAgents, this.currentNest.leftGAgent, this.currentNest.rightGAgent ); optimiseStages(globals.stages); this.gAgents.forEach((gAgent) => { gAgent.formattedLabel = this.textFormatter(gAgent.id); }); } generate({stages, meta = {}}) { this._reset(); this.textFormatter = meta.textFormatter; const globals = this.beginNested('global', { label: '', ln: 0, name: '', tag: '', }); stages.forEach(this.handleStage); if(this.nesting.length !== 1) { throw new Error( 'Unterminated section at line ' + (this.currentSection.header.ln + 1) ); } if(this.activeGroups.size > 0) { throw new Error('Unterminated group'); } this._checkSectionEnd(); const terminators = meta.terminators || 'none'; this.addStage(this.makeParallel([ this.setGAgentHighlight(this.gAgents, false), this.setGAgentVis(this.gAgents, false, terminators), ])); this._finalise(globals); swapFirstBegin(globals.stages, meta.headers || 'box'); return { agents: this.gAgents.slice(), meta: { code: meta.code, theme: meta.theme, title: this.textFormatter(meta.title), }, stages: globals.stages, }; } } /* eslint-disable sort-keys */ // Maybe later const FONT$2 = 'Courier New,Liberation Mono,monospace'; const LINE_HEIGHT$2 = 1.3; const WAVE$2 = new WavePattern(6, [ +0, -0.25, -0.5, -0.25, +0, +0.25, +0.5, +0.25, ]); const NOTE_ATTRS$2 = { 'font-family': FONT$2, 'font-size': 8, 'line-height': LINE_HEIGHT$2, }; const DIVIDER_LABEL_ATTRS$2 = { 'font-family': FONT$2, 'font-size': 8, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'middle', }; class MonospaceTheme extends BaseTheme { constructor(svg) { super(svg); const sharedBlockSection = { padding: { top: 3, bottom: 2, }, tag: { padding: { top: 2, left: 4, right: 4, bottom: 2, }, boxRenderer: this.renderTag.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, 'rx': 3, 'ry': 3, }), labelAttrs: { 'font-family': FONT$2, 'font-weight': 'bold', 'font-size': 9, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'left', }, }, label: { minHeight: 8, padding: { top: 2, left: 8, right: 8, bottom: 2, }, labelAttrs: { 'font-family': FONT$2, 'font-size': 8, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'left', }, }, }; Object.assign(this, { titleMargin: 8, outerMargin: 4, agentMargin: 12, actionMargin: 12, minActionMargin: 4, agentLineHighlightRadius: 4, agentCap: { box: { padding: { top: 4, left: 8, right: 8, bottom: 4, }, arrowBottom: 12, boxAttrs: { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, }, labelAttrs: { 'font-family': FONT$2, 'font-size': 12, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'middle', }, }, database: { padding: { top: 9, left: 8, right: 8, bottom: 3, }, arrowBottom: 12, boxRenderer: this.renderDB.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, 'db-z': 4, }), labelAttrs: { 'font-family': FONT$2, 'font-size': 12, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'middle', }, }, cross: { size: 16, render: svg.crossFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), }, bar: { height: 4, render: svg.boxFactory({ 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 1, }), }, fade: { width: 5, height: 8, extend: 1, }, none: { height: 8, }, }, connect: { loopbackRadius: 4, line: { 'solid': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }, renderFlat: this.renderFlatConnect.bind(this, null), renderRev: this.renderRevConnect.bind(this, null), }, 'dash': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, 'stroke-dasharray': '4, 4', }, renderFlat: this.renderFlatConnect.bind(this, null), renderRev: this.renderRevConnect.bind(this, null), }, 'wave': { attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }, renderFlat: this.renderFlatConnect.bind(this, WAVE$2), renderRev: this.renderRevConnect.bind(this, WAVE$2), }, }, arrow: { 'single': { width: 4, height: 8, render: this.renderArrowHead.bind(this), attrs: { 'fill': '#000000', 'stroke-width': 0, 'stroke-linejoin': 'miter', }, }, 'double': { width: 3, height: 6, render: this.renderArrowHead.bind(this), attrs: { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, 'stroke-linejoin': 'miter', }, }, 'cross': { short: 8, radius: 4, render: svg.crossFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), }, }, label: { padding: 4, margin: {top: 2, bottom: 1}, attrs: { 'font-family': FONT$2, 'font-size': 8, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'middle', }, loopbackAttrs: { 'font-family': FONT$2, 'font-size': 8, 'line-height': LINE_HEIGHT$2, }, }, source: { radius: 2, render: svg.circleFactory({ 'fill': '#000000', 'stroke': '#000000', 'stroke-width': 1, }), }, mask: { padding: { top: 0, left: 3, right: 3, bottom: 1, }, }, }, titleAttrs: { 'font-family': FONT$2, 'font-size': 20, 'line-height': LINE_HEIGHT$2, 'text-anchor': 'middle', 'class': 'title', }, agentLineAttrs: { '': { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }, 'red': { 'stroke': '#AA0000', }, }, blocks: { 'ref': { margin: { top: 0, bottom: 0, }, boxRenderer: this.renderRef.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 2, }), section: sharedBlockSection, }, '': { margin: { top: 0, bottom: 0, }, boxRenderer: svg.boxFactory({ 'fill': 'none', 'stroke': '#000000', 'stroke-width': 2, }), collapsedBoxRenderer: this.renderRef.bind(this, { 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 2, }), section: sharedBlockSection, sepRenderer: svg.lineFactory({ 'stroke': '#000000', 'stroke-width': 2, 'stroke-dasharray': '8, 4', }), }, }, notes: { 'text': { margin: {top: 0, left: 8, right: 8, bottom: 0}, padding: {top: 4, left: 4, right: 4, bottom: 4}, overlap: {left: 8, right: 8}, boxRenderer: svg.boxFactory({ 'fill': '#FFFFFF', }), labelAttrs: NOTE_ATTRS$2, }, 'note': { margin: {top: 0, left: 8, right: 8, bottom: 0}, padding: {top: 8, left: 8, right: 8, bottom: 8}, overlap: {left: 8, right: 8}, boxRenderer: svg.noteFactory({ 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, }, { 'fill': 'none', 'stroke': '#000000', 'stroke-width': 1, }), labelAttrs: NOTE_ATTRS$2, }, 'state': { margin: {top: 0, left: 8, right: 8, bottom: 0}, padding: {top: 8, left: 8, right: 8, bottom: 8}, overlap: {left: 8, right: 8}, boxRenderer: svg.boxFactory({ 'fill': '#FFFFFF', 'stroke': '#000000', 'stroke-width': 1, 'rx': 8, 'ry': 8, }), labelAttrs: NOTE_ATTRS$2, }, }, dividers: { '': { labelAttrs: DIVIDER_LABEL_ATTRS$2, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 0, margin: 0, render: () => ({}), }, 'line': { labelAttrs: DIVIDER_LABEL_ATTRS$2, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 8, margin: 0, render: this.renderLineDivider.bind(this, { lineAttrs: { 'stroke': '#000000', }, }), }, 'delay': { labelAttrs: DIVIDER_LABEL_ATTRS$2, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 0, margin: 0, render: this.renderDelayDivider.bind(this, { dotSize: 2, gapSize: 2, }), }, 'tear': { labelAttrs: DIVIDER_LABEL_ATTRS$2, padding: {top: 2, left: 5, right: 5, bottom: 2}, extend: 8, margin: 8, render: this.renderTearDivider.bind(this, { fadeBegin: 4, fadeSize: 4, zigWidth: 4, zigHeight: 1, lineAttrs: { 'stroke': '#000000', }, }), }, }, }); } } class Factory$2 { constructor() { this.name = 'monospace'; } build(svg) { return new MonospaceTheme(svg); } } /* * The order of commands inside "then" blocks directly influences the * order they are displayed to the user in autocomplete menus. * This relies on the fact that common JS engines maintain insertion * order in objects, though this is not guaranteed. It could be switched * to use Map objects instead for strict compliance, at the cost of * extra syntax. */ const CM_ERROR = {type: 'error line-error', suggest: [], then: {'': 0}}; function textTo(exit, suggest = []) { return { type: 'string', suggest, then: Object.assign({'': 0}, exit), }; } function suggestionsEqual(a, b) { return ( (a.v === b.v) && (a.prefix === b.prefix) && (a.suffix === b.suffix) && (a.q === b.q) ); } const AGENT_INFO_TYPES = [ 'database', 'red', ]; const PARALLEL_TASKS = [ 'begin', 'end', 'note', 'state', 'text', ]; const makeCommands = ((() => { function agentListTo(exit, next = 1) { return { type: 'variable', suggest: [{known: 'Agent'}], then: Object.assign({}, exit, { '': 0, ',': {type: 'operator', then: {'': next}}, }), }; } const end = {type: '', suggest: ['\n'], then: {}}; const hiddenEnd = {type: '', suggest: [], then: {}}; const textToEnd = textTo({'\n': end}); const colonTextToEnd = { type: 'operator', then: {'': textToEnd, '\n': hiddenEnd}, }; const aliasListToEnd = agentListTo({ '\n': end, 'as': {type: 'keyword', then: { '': {type: 'variable', suggest: [{known: 'Agent'}], then: { '': 0, ',': {type: 'operator', then: {'': 3}}, '\n': end, }}, }}, }); const agentListToText = agentListTo({':': colonTextToEnd}); const agentToOptText = { type: 'variable', suggest: [{known: 'Agent'}], then: { '': 0, ':': {type: 'operator', then: { '': textToEnd, '\n': hiddenEnd, }}, '\n': end, }, }; const referenceName = { ':': {type: 'operator', then: { '': textTo({ 'as': {type: 'keyword', then: { '': { type: 'variable', suggest: [{known: 'Agent'}], then: { '': 0, '\n': end, }, }, }}, }), }}, }; const refDef = {type: 'keyword', then: Object.assign({ 'over': {type: 'keyword', then: { '': agentListTo(referenceName), }}, }, referenceName)}; const divider = { '\n': end, ':': {type: 'operator', then: { '': textToEnd, '\n': hiddenEnd, }}, 'with': {type: 'keyword', suggest: ['with height '], then: { 'height': {type: 'keyword', then: { '': {type: 'number', suggest: ['6 ', '30 '], then: { '\n': end, ':': {type: 'operator', then: { '': textToEnd, '\n': hiddenEnd, }}, }}, }}, }}, }; function simpleList(type, keywords, exit) { const first = {}; const recur = Object.assign({}, exit); keywords.forEach((keyword) => { first[keyword] = {type, then: recur}; recur[keyword] = 0; }); return first; } function optionalKeywords(type, keywords, then) { const result = Object.assign({}, then); keywords.forEach((keyword) => { result[keyword] = {type, then}; }); return result; } const agentInfoList = optionalKeywords( 'keyword', ['a', 'an'], simpleList('keyword', AGENT_INFO_TYPES, {'\n': end}) ); function makeSideNote(side) { return { type: 'keyword', suggest: [side + ' of ', side + ': '], then: { 'of': {type: 'keyword', then: { '': agentListToText, }}, ':': {type: 'operator', then: { '': textToEnd, }}, '': agentListToText, }, }; } function makeOpBlock({exit, sourceExit, blankExit}) { const op = {type: 'operator', then: { '+': CM_ERROR, '-': CM_ERROR, '*': CM_ERROR, '!': CM_ERROR, '': exit, }}; return { '+': {type: 'operator', then: { '+': CM_ERROR, '-': CM_ERROR, '*': op, '!': CM_ERROR, '': exit, }}, '-': {type: 'operator', then: { '+': CM_ERROR, '-': CM_ERROR, '*': op, '!': {type: 'operator', then: { '+': CM_ERROR, '-': CM_ERROR, '*': CM_ERROR, '!': CM_ERROR, '': exit, }}, '': exit, }}, '*': {type: 'operator', then: Object.assign({ '+': op, '-': op, '*': CM_ERROR, '!': CM_ERROR, '': exit, }, sourceExit || exit)}, '!': op, '': blankExit || exit, }; } function makeCMConnect(arrows) { const connect = { type: 'keyword', then: Object.assign({}, makeOpBlock({ exit: agentToOptText, sourceExit: { ':': colonTextToEnd, '\n': hiddenEnd, }, }), { '...': {type: 'operator', then: { '': { type: 'variable', suggest: [{known: 'DelayedAgent'}], then: { '': 0, ':': CM_ERROR, '\n': end, }, }, }}, }), }; const connectors = {}; arrows.forEach((arrow) => (connectors[arrow] = connect)); const labelIndicator = { type: 'operator', override: 'Label', then: {}, }; const hiddenLabelIndicator = { type: 'operator', suggest: [], override: 'Label', then: {}, }; const firstAgent = { type: 'variable', suggest: [{known: 'Agent'}], then: Object.assign({ '': 0, }, connectors, { ':': labelIndicator, }), }; const firstAgentDelayed = { type: 'variable', suggest: [{known: 'DelayedAgent'}], then: Object.assign({ '': 0, ':': hiddenLabelIndicator, }, connectors), }; const firstAgentNoFlags = Object.assign({}, firstAgent, { then: Object.assign({}, firstAgent.then, { 'is': {type: 'keyword', then: agentInfoList}, }), }); return Object.assign({ '...': {type: 'operator', then: { '': firstAgentDelayed, }}, }, makeOpBlock({ exit: firstAgent, sourceExit: Object.assign({ '': firstAgent, ':': hiddenLabelIndicator, }, connectors), blankExit: firstAgentNoFlags, })); } const commonGroup = {type: 'keyword', then: { '': textToEnd, ':': {type: 'operator', then: { '': textToEnd, }}, '\n': end, }}; const BASE_THEN = { 'title': {type: 'keyword', then: { '': textToEnd, }}, 'theme': {type: 'keyword', then: { '': { type: 'string', suggest: [{global: 'themes', suffix: '\n'}], then: { '': 0, '\n': end, }, }, }}, 'headers': {type: 'keyword', then: { 'none': {type: 'keyword', then: {}}, 'cross': {type: 'keyword', then: {}}, 'box': {type: 'keyword', then: {}}, 'fade': {type: 'keyword', then: {}}, 'bar': {type: 'keyword', then: {}}, }}, 'terminators': {type: 'keyword', then: { 'none': {type: 'keyword', then: {}}, 'cross': {type: 'keyword', then: {}}, 'box': {type: 'keyword', then: {}}, 'fade': {type: 'keyword', then: {}}, 'bar': {type: 'keyword', then: {}}, }}, 'divider': {type: 'keyword', then: Object.assign({ 'line': {type: 'keyword', then: divider}, 'space': {type: 'keyword', then: divider}, 'delay': {type: 'keyword', then: divider}, 'tear': {type: 'keyword', then: divider}, }, divider)}, 'define': {type: 'keyword', then: { '': aliasListToEnd, 'as': CM_ERROR, }}, 'begin': {type: 'keyword', then: { '': aliasListToEnd, 'reference': refDef, 'as': CM_ERROR, }}, 'end': {type: 'keyword', then: { '': aliasListToEnd, 'as': CM_ERROR, '\n': end, }}, 'if': commonGroup, 'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: { 'if': {type: 'keyword', suggest: ['if: '], then: { '': textToEnd, ':': {type: 'operator', then: { '': textToEnd, }}, }}, '\n': end, }}, 'repeat': commonGroup, 'group': commonGroup, 'note': {type: 'keyword', then: { 'over': {type: 'keyword', then: { '': agentListToText, }}, 'left': makeSideNote('left'), 'right': makeSideNote('right'), 'between': {type: 'keyword', then: { '': agentListTo({':': CM_ERROR}, agentListToText), }}, }}, 'state': {type: 'keyword', suggest: ['state over '], then: { 'over': {type: 'keyword', then: { '': { type: 'variable', suggest: [{known: 'Agent'}], then: { '': 0, ',': CM_ERROR, ':': colonTextToEnd, }, }, }}, }}, 'text': {type: 'keyword', then: { 'left': makeSideNote('left'), 'right': makeSideNote('right'), }}, 'autolabel': {type: 'keyword', then: { 'off': {type: 'keyword', then: {}}, '': textTo({'\n': end}, [ {v: '