diff --git a/README.md b/README.md index 88e0490..b6e64f1 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,28 @@ Foo -> Bar Bar -> Baz ``` +### Asynchronous Communication + +Asynchronous Communication preview + +``` +begin Initiator as I, Receiver as R + +# the '...id' syntax allows connections to span multiple lines + +I -> ...fin1 +...fin1 -> R: FIN + +# they can even inter-mix! +R -> ...ack1 +R -> ...fin2 +...ack1 -> I: ACK +...fin2 -> I: FIN + +!I -> ...ack2 +...ack2 -> !R: ACK +``` + ### Simultaneous Actions (Beta!) This is a work-in-progress feature. There are situations where this can diff --git a/lib/sequence-diagram.js b/lib/sequence-diagram.js index c70da39..692da92 100644 --- a/lib/sequence-diagram.js +++ b/lib/sequence-diagram.js @@ -808,24 +808,67 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => { const connect = { type: 'keyword', suggest: true, - then: makeOpBlock(agentToOptText, { + then: Object.assign({}, makeOpBlock(agentToOptText, { ':': colonTextToEnd, '\n': hiddenEnd, + }), { + '...': {type: 'operator', suggest: true, then: { + '': { + type: 'variable', + suggest: {known: 'DelayedAgent'}, + then: { + '': 0, + ':': CM_ERROR, + '\n': end, + }, + }, + }}, }), }; - const then = {'': 0}; - arrows.forEach((arrow) => (then[arrow] = connect)); - then[':'] = { + const connectors = {}; + arrows.forEach((arrow) => (connectors[arrow] = connect)); + + const labelIndicator = { type: 'operator', suggest: true, override: 'Label', then: {}, }; - return makeOpBlock( - {type: 'variable', suggest: {known: 'Agent'}, then}, - then - ); + + const hiddenLabelIndicator = { + type: 'operator', + suggest: false, + override: 'Label', + then: {}, + }; + + const firstAgent = { + type: 'variable', + suggest: {known: 'Agent'}, + then: Object.assign({ + '': 0, + ':': labelIndicator, + }, connectors), + }; + + const firstAgentDelayed = { + type: 'variable', + suggest: {known: 'DelayedAgent'}, + then: Object.assign({ + '': 0, + ':': hiddenLabelIndicator, + }, connectors), + }; + + return Object.assign({ + '...': {type: 'operator', suggest: true, then: { + '': firstAgentDelayed, + }}, + }, makeOpBlock(firstAgent, Object.assign({ + '': firstAgent, + ':': hiddenLabelIndicator, + }, connectors))); } const BASE_THEN = { @@ -1083,6 +1126,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => { currentSpace: '', currentQuoted: false, knownAgent: [], + knownDelayedAgent: [], knownLabel: [], beginCompletions: cmMakeCompletions({}, [this.commands]), completions: [], @@ -1229,6 +1273,7 @@ define('sequence/Tokeniser',['./CodeMirrorMode'], (CMMode) => { escapeWith: unescape, baseToken: {q: true}, }, + {start: /\.\.\./y, baseToken: {v: '...'}}, {start: /(?=[^ \t\r\n:+\-~*!<>,])/y, end: /(?=[ \t\r\n:+\-~*!<>,])|$/y}, { start: /(?=[\-~<])/y, @@ -1741,17 +1786,6 @@ define('sequence/Parser',[ return new Error(message + suffix); } - function errToken(line, pos) { - if(pos < line.length) { - return line[pos]; - } - const last = array.last(line); - if(!last) { - return null; - } - return {b: last.e}; - } - function joinLabel(line, begin = 0, end = null) { if(end === null) { end = line.length; @@ -1815,6 +1849,19 @@ define('sequence/Parser',[ return orEnd ? limit : -1; } + function findFirstToken(line, tokenMap, {start = 0, limit = null} = {}) { + if(limit === null) { + limit = line.length; + } + for(let pos = start; pos < limit; ++ pos) { + const value = tokenMap.get(tokenKeyword(line[pos])); + if(value) { + return {pos, value}; + } + } + return null; + } + function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) { let aliasSep = -1; if(enableAlias) { @@ -1824,7 +1871,11 @@ define('sequence/Parser',[ aliasSep = end; } if(start >= aliasSep && !allowBlankName) { - throw makeError('Missing agent name', errToken(line, start)); + let errPosToken = line[start]; + if(!errPosToken) { + errPosToken = {b: array.last(line).e}; + } + throw makeError('Missing agent name', errPosToken); } return { name: joinLabel(line, start, aliasSep), @@ -2100,35 +2151,54 @@ define('sequence/Parser',[ }, (line) => { // connect - let labelSep = findToken(line, ':'); - if(labelSep === -1) { - labelSep = line.length; - } - let typePos = -1; - let options = null; - for(let j = 0; j < line.length; ++ j) { - const opts = CONNECT.types.get(tokenKeyword(line[j])); - if(opts) { - typePos = j; - options = opts; - break; - } - } - if(typePos <= 0 || typePos >= labelSep - 1) { + const labelSep = findToken(line, ':', {orEnd: true}); + const connectionToken = findFirstToken( + line, + CONNECT.types, + {start: 0, limit: labelSep - 1} + ); + if(!connectionToken) { return null; } - const readAgentOpts = { + + const connectPos = connectionToken.pos; + + const readOpts = { flagTypes: CONNECT.agentFlags, }; - return { - type: 'connect', - agents: [ - readAgent(line, 0, typePos, readAgentOpts), - readAgent(line, typePos + 1, labelSep, readAgentOpts), - ], - label: joinLabel(line, labelSep + 1), - options, - }; + + if(tokenKeyword(line[0]) === '...') { + return { + type: 'connect-delay-end', + tag: joinLabel(line, 1, connectPos), + agent: readAgent(line, connectPos + 1, labelSep, readOpts), + label: joinLabel(line, labelSep + 1), + options: connectionToken.value, + }; + } else if(tokenKeyword(line[connectPos + 1]) === '...') { + if(labelSep !== line.length) { + throw makeError( + 'Cannot label beginning of delayed connection', + line[labelSep] + ); + } + return { + type: 'connect-delay-begin', + tag: joinLabel(line, connectPos + 2, labelSep), + agent: readAgent(line, 0, connectPos, readOpts), + options: connectionToken.value, + }; + } else { + return { + type: 'connect', + agents: [ + readAgent(line, 0, connectPos, readOpts), + readAgent(line, connectPos + 1, labelSep, readOpts), + ], + label: joinLabel(line, labelSep + 1), + options: connectionToken.value, + }; + } }, (line) => { // marker @@ -2454,6 +2524,8 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { 'divider': this.handleDivider.bind(this), 'label pattern': this.handleLabelPattern.bind(this), 'connect': this.handleConnect.bind(this), + 'connect-delay-begin': this.handleConnectDelayBegin.bind(this), + 'connect-delay-end': this.handleConnectDelayEnd.bind(this), 'note over': this.handleNote.bind(this), 'note left': this.handleNote.bind(this), 'note right': this.handleNote.bind(this), @@ -2634,23 +2706,39 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { }; } + _makeSection(header, stages) { + return { + header, + delayedConnections: new Map(), + 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 = { - header: { - type: 'block begin', - blockType, - tag: this.textFormatter(tag), - label: this.textFormatter(label), - left: leftGAgent.id, - right: rightGAgent.id, - ln, - }, - stages, - }; + this.currentSection = this._makeSection({ + type: 'block begin', + blockType, + tag: this.textFormatter(tag), + label: this.textFormatter(label), + left: leftGAgent.id, + right: rightGAgent.id, + ln, + }, stages); this.currentNest = { blockType, gAgents, @@ -2694,18 +2782,16 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { this.currentNest.blockType + ')' ); } - this.currentSection = { - header: { - type: 'block split', - blockType, - tag: this.textFormatter(tag), - label: this.textFormatter(label), - left: this.currentNest.leftGAgent.id, - right: this.currentNest.rightGAgent.id, - ln, - }, - stages: [], - }; + this._checkSectionEnd(); + this.currentSection = this._makeSection({ + type: 'block split', + blockType, + tag: this.textFormatter(tag), + label: this.textFormatter(label), + left: this.currentNest.leftGAgent.id, + right: this.currentNest.rightGAgent.id, + ln, + }, []); this.currentNest.sections.push(this.currentSection); } @@ -2713,6 +2799,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { if(this.nesting.length <= 1) { throw new Error('Invalid block nesting (too many "end"s)'); } + this._checkSectionEnd(); const nested = this.nesting.pop(); this.currentNest = array.last(this.nesting); this.currentSection = array.last(this.currentNest.sections); @@ -2991,24 +3078,20 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { return gAgents; } - handleConnect({agents, label, options}) { + _handlePartialConnect(agents) { const flags = this.filterConnectFlags(agents); - let gAgents = agents.map(this.toGAgent); + const gAgents = agents.map(this.toGAgent); this.validateGAgents(gAgents, { allowGrouped: true, allowVirtual: true, }); - const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent); - this.defineGAgents(allGAgents + this.defineGAgents(array + .flatMap(gAgents, this.expandGroupedGAgent) .filter((gAgent) => !gAgent.isVirtualSource) ); - gAgents = this.expandGroupedGAgentConnection(gAgents); - gAgents = this.expandVirtualSourceAgents(gAgents); - const agentIDs = gAgents.map((gAgent) => gAgent.id); - const implicitBeginGAgents = (agents .filter(PAgent.hasFlag('begin', false)) .map(this.toGAgent) @@ -3016,20 +3099,105 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { ); this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); - const connectStage = { - type: 'connect', - agentIDs, - label: this.textFormatter(this.applyLabelPattern(label)), - options, - }; + return {flags, gAgents}; + } - this.addParallelStages([ + _makeConnectParallelStages(flags, connectStage) { + return [ 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), + ]; + } + + handleConnect({agents, label, options}) { + let {flags, gAgents} = this._handlePartialConnect(agents); + + gAgents = this.expandGroupedGAgentConnection(gAgents); + gAgents = this.expandVirtualSourceAgents(gAgents); + + const connectStage = { + type: 'connect', + agentIDs: gAgents.map((gAgent) => gAgent.id), + label: this.textFormatter(this.applyLabelPattern(label)), + options, + }; + + this.addParallelStages(this._makeConnectParallelStages( + flags, + connectStage + )); + } + + handleConnectDelayBegin({agent, tag, options, ln}) { + const dcs = this.currentSection.delayedConnections; + if(dcs.has(tag)) { + throw new Error('Duplicate delayed connection "' + tag + '"'); + } + + const {flags, gAgents} = this._handlePartialConnect([agent]); + const uniqueTag = this.nextVirtualAgentName(); + + const connectStage = { + type: 'connect-delay-begin', + tag: uniqueTag, + agentIDs: null, + label: null, + options, + }; + + dcs.set(tag, {tag, uniqueTag, ln, gAgents, connectStage}); + + this.addParallelStages(this._makeConnectParallelStages( + flags, + connectStage + )); + } + + handleConnectDelayEnd({agent, tag, label, options}) { + 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]); + + 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 = { + type: 'connect-delay-end', + tag: dcInfo.uniqueTag, + }; + + this.addParallelStages(this._makeConnectParallelStages( + flags, + connectEndStage + )); + + dcs.delete(tag); } handleNote({type, agents, mode, label}) { @@ -3150,6 +3318,8 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { throw new Error('Unterminated group'); } + this._checkSectionEnd(); + const terminators = meta.terminators || 'none'; this.addParallelStages([ this.setGAgentHighlight(this.gAgents, false), @@ -3514,7 +3684,7 @@ define('svg/PatternedLine',[],() => { (cx - this.x) * (cx - this.x) + (cy - this.y) * (cy - this.y) ); - const theta1 = Math.atan2(cx - this.x, cy - this.y); + const theta1 = Math.atan2(this.x - cx, cy - this.y); const nextX = cx + Math.sin(theta1 + theta) * radius; const nextY = cy - Math.cos(theta1 + theta) * radius; @@ -3534,8 +3704,8 @@ define('svg/PatternedLine',[],() => { this.points.push( this.x + ' ' + this.y + 'A' + radius + ' ' + radius + ' 0 ' + + ((Math.abs(theta) >= Math.PI) ? '1 ' : '0 ') + ((theta < 0) ? '0 ' : '1 ') + - '1 ' + nextX + ' ' + nextY ); this.disconnect = 0; @@ -3855,6 +4025,7 @@ define('sequence/components/Block',[ 'width': agentInfoR.x - agentInfoL.x, 'height': labelHeight, 'fill': 'transparent', + 'class': 'vis', }), clickable.firstChild); if(!first) { @@ -4138,6 +4309,7 @@ define('sequence/components/AgentCap',[ 'width': width, 'height': height, 'fill': 'transparent', + 'class': 'vis', }), clickable.firstChild); return { @@ -4175,6 +4347,7 @@ define('sequence/components/AgentCap',[ 'width': d * 2, 'height': d * 2, 'fill': 'transparent', + 'class': 'vis', })); return { @@ -4229,6 +4402,7 @@ define('sequence/components/AgentCap',[ 'width': width, 'height': height, 'fill': 'transparent', + 'class': 'vis', })); return { @@ -4289,6 +4463,7 @@ define('sequence/components/AgentCap',[ 'width': config.width, 'height': config.height, 'fill': 'transparent', + 'class': 'vis', })); return { @@ -4323,6 +4498,7 @@ define('sequence/components/AgentCap',[ 'width': w, 'height': config.height, 'fill': 'transparent', + 'class': 'vis', })); return { @@ -4494,11 +4670,13 @@ define('sequence/components/Connect',[ render(layer, theme, pt, dir) { const config = this.getConfig(theme); + const short = this.short(theme); layer.appendChild(config.render(config.attrs, { - x: pt.x + this.short(theme) * dir, - y: pt.y, - dx: config.width * dir, - dy: config.height / 2, + x: pt.x + short * dir.dx, + y: pt.y + short * dir.dy, + width: config.width, + height: config.height, + dir, })); } @@ -4532,8 +4710,8 @@ define('sequence/components/Connect',[ render(layer, theme, pt, dir) { const config = this.getConfig(theme); layer.appendChild(config.render({ - x: pt.x + config.short * dir, - y: pt.y, + x: pt.x + config.short * dir.dx, + y: pt.y + config.short * dir.dy, radius: config.radius, })); } @@ -4622,24 +4800,51 @@ define('sequence/components/Connect',[ array.mergeSets(env.momentaryAgentIDs, agentIDs); } - renderSelfConnect({label, agentIDs, options}, env) { - /* jshint -W071 */ // TODO: find appropriate abstractions + renderRevArrowLine({x1, y1, x2, y2, xR}, options, env) { + const config = env.theme.connect; + const line = config.line[options.line]; + const lArrow = ARROWHEADS[options.left]; + const rArrow = ARROWHEADS[options.right]; + + const dx1 = lArrow.lineGap(env.theme, line.attrs); + const dx2 = rArrow.lineGap(env.theme, line.attrs); + const rendered = line.renderRev(line.attrs, { + x1: x1 + dx1, + y1, + x2: x2 + dx2, + y2, + xR, + rad: config.loopbackRadius, + }); + env.shapeLayer.appendChild(rendered.shape); + + lArrow.render(env.shapeLayer, env.theme, { + x: rendered.p1.x - dx1, + y: rendered.p1.y, + }, {dx: 1, dy: 0}); + + rArrow.render(env.shapeLayer, env.theme, { + x: rendered.p2.x - dx2, + y: rendered.p2.y, + }, {dx: 1, dy: 0}); + } + + renderSelfConnect({label, agentIDs, options}, env, from, yBegin) { const config = env.theme.connect; - const from = env.agentInfos.get(agentIDs[0]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; + const to = env.agentInfos.get(agentIDs[1]); + const height = label ? ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ) : 0; - const lineX = from.x + from.currentMaxRad; - const y0 = env.primaryY; - const x0 = ( - lineX + + const xL = ( + from.x + from.currentMaxRad + lArrow.width(env.theme) + (label ? config.label.padding : 0) ); @@ -4647,8 +4852,8 @@ define('sequence/components/Connect',[ const clickable = env.makeRegion(); const renderedText = SVGShapes.renderBoxedText(label, { - x: x0 - config.mask.padding.left, - y: y0 - height + config.label.margin.top, + x: xL - config.mask.padding.left, + y: yBegin - height + config.label.margin.top, padding: config.mask.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.label.loopbackAttrs, @@ -4662,68 +4867,74 @@ define('sequence/components/Connect',[ config.mask.padding.left - config.mask.padding.right ) : 0); - const r = config.loopbackRadius; - const x1 = Math.max(lineX + rArrow.width(env.theme), x0 + labelW); - const y1 = y0 + r * 2; - const line = config.line[options.line]; - const rendered = line.renderRev(line.attrs, { - xL: lineX, - dx1: lArrow.lineGap(env.theme, line.attrs), - dx2: rArrow.lineGap(env.theme, line.attrs), - y1: y0, - y2: y1, - xR: x1, - }); - env.shapeLayer.appendChild(rendered.shape); + const xR = Math.max( + to.x + to.currentMaxRad + rArrow.width(env.theme), + xL + labelW + ); + const y2 = Math.max( + yBegin + config.loopbackRadius * 2, + env.primaryY + ); - lArrow.render(env.shapeLayer, env.theme, rendered.p1, 1); - rArrow.render(env.shapeLayer, env.theme, rendered.p2, 1); + this.renderRevArrowLine({ + x1: from.x + from.currentMaxRad, + y1: yBegin, + x2: to.x + to.currentMaxRad, + y2, + xR, + }, options, env); const raise = Math.max(height, lArrow.height(env.theme) / 2); const arrowDip = rArrow.height(env.theme) / 2; clickable.insertBefore(svg.make('rect', { - 'x': lineX, - 'y': y0 - raise, - 'width': x1 + r - lineX, - 'height': raise + r * 2 + arrowDip, + 'x': from.x, + 'y': yBegin - raise, + 'width': xR + config.loopbackRadius - from.x, + 'height': raise + y2 - yBegin + arrowDip, 'fill': 'transparent', + 'class': 'vis', }), clickable.firstChild); - return y1 + Math.max( - arrowDip + env.theme.minActionMargin, - env.theme.actionMargin - ); + return y2 + Math.max(arrowDip, 0) + env.theme.actionMargin; } - renderSimpleLine(x0, x1, options, env) { - const dir = (x0 < x1) ? 1 : -1; - + renderArrowLine({x1, y1, x2, y2}, options, env) { const config = env.theme.connect; const line = config.line[options.line]; const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; + const len = Math.sqrt( + (x2 - x1) * (x2 - x1) + + (y2 - y1) * (y2 - y1) + ); + const d1 = lArrow.lineGap(env.theme, line.attrs); + const d2 = rArrow.lineGap(env.theme, line.attrs); + const dx = (x2 - x1) / len; + const dy = (y2 - y1) / len; + const rendered = line.renderFlat(line.attrs, { - x1: x0, - dx1: lArrow.lineGap(env.theme, line.attrs) * dir, - x2: x1, - dx2: -rArrow.lineGap(env.theme, line.attrs) * dir, - y: env.primaryY, + x1: x1 + d1 * dx, + y1: y1 + d1 * dy, + x2: x2 - d2 * dx, + y2: y2 - d2 * dy, }); env.shapeLayer.appendChild(rendered.shape); - return rendered; - } - renderSimpleArrowheads(options, renderedLine, env, dir) { - const lArrow = ARROWHEADS[options.left]; - const rArrow = ARROWHEADS[options.right]; + const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy}; + const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy}; - lArrow.render(env.shapeLayer, env.theme, renderedLine.p1, dir); - rArrow.render(env.shapeLayer, env.theme, renderedLine.p2, -dir); + lArrow.render(env.shapeLayer, env.theme, p1, {dx, dy}); + rArrow.render(env.shapeLayer, env.theme, p2, {dx: -dx, dy: -dy}); - return {lArrow, rArrow}; + return { + p1, + p2, + lArrow, + rArrow, + }; } renderVirtualSources(from, to, renderedLine, env) { @@ -4745,9 +4956,41 @@ define('sequence/components/Connect',[ } } - renderSimpleConnect({label, agentIDs, options}, env) { + renderSimpleLabel(label, {layer, x1, x2, y1, y2, height}, env) { + const config = env.theme.connect; + + const midX = (x1 + x2) / 2; + const midY = (y1 + y2) / 2; + + let labelLayer = layer; + const boxAttrs = {'fill': '#000000'}; + if(y1 !== y2) { + const angle = Math.atan((y2 - y1) / (x2 - x1)); + const transform = ( + 'rotate(' + + (angle * 180 / Math.PI) + + ' ' + midX + ',' + midY + + ')' + ); + boxAttrs.transform = transform; + labelLayer = svg.make('g', {'transform': transform}); + layer.appendChild(labelLayer); + } + + SVGShapes.renderBoxedText(label, { + x: midX, + y: midY + config.label.margin.top - height, + padding: config.mask.padding, + boxAttrs, + labelAttrs: config.label.attrs, + boxLayer: env.maskLayer, + labelLayer, + SVGTextBlockClass: env.SVGTextBlockClass, + }); + } + + renderSimpleConnect({label, agentIDs, options}, env, from, yBegin) { const config = env.theme.connect; - const from = env.agentInfos.get(agentIDs[0]); const to = env.agentInfos.get(agentIDs[1]); const dir = (from.x < to.x) ? 1 : -1; @@ -4758,44 +5001,49 @@ define('sequence/components/Connect',[ config.label.margin.bottom ); - const x0 = from.x + from.currentMaxRad * dir; - const x1 = to.x - to.currentMaxRad * dir; - const y = env.primaryY; + const x1 = from.x + from.currentMaxRad * dir; + const x2 = to.x - to.currentMaxRad * dir; const clickable = env.makeRegion(); - SVGShapes.renderBoxedText(label, { - x: (x0 + x1) / 2, - y: y - height + config.label.margin.top, - padding: config.mask.padding, - boxAttrs: {'fill': '#000000'}, - labelAttrs: config.label.attrs, - boxLayer: env.maskLayer, - labelLayer: clickable, - SVGTextBlockClass: env.SVGTextBlockClass, - }); - - const rendered = this.renderSimpleLine(x0, x1, options, env); - const { - lArrow, - rArrow - } = this.renderSimpleArrowheads(options, rendered, env, dir); - this.renderVirtualSources(from, to, rendered, env); + const rendered = this.renderArrowLine({ + x1, + y1: yBegin, + x2, + y2: env.primaryY, + }, options, env); const arrowSpread = Math.max( - lArrow.height(env.theme), - rArrow.height(env.theme) + rendered.lArrow.height(env.theme), + rendered.rArrow.height(env.theme) ) / 2; - clickable.insertBefore(svg.make('rect', { - 'x': Math.min(x0, x1), - 'y': y - Math.max(height, arrowSpread), - 'width': Math.abs(x1 - x0), - 'height': Math.max(height, arrowSpread) + arrowSpread, - 'fill': 'transparent', - }), clickable.firstChild); + const lift = Math.max(height, arrowSpread); - return y + Math.max( + this.renderVirtualSources(from, to, rendered, env); + + clickable.appendChild(svg.make('path', { + 'd': ( + 'M' + x1 + ',' + (yBegin - lift) + + 'L' + x2 + ',' + (env.primaryY - lift) + + 'L' + x2 + ',' + (env.primaryY + arrowSpread) + + 'L' + x1 + ',' + (yBegin + arrowSpread) + + 'Z' + ), + 'fill': 'transparent', + 'class': 'vis', + })); + + this.renderSimpleLabel(label, { + layer: clickable, + x1, + y1: yBegin, + x2, + y2: env.primaryY, + height, + }, env); + + return env.primaryY + Math.max( arrowSpread + env.theme.minActionMargin, env.theme.actionMargin ); @@ -4824,18 +5072,79 @@ define('sequence/components/Connect',[ }; } - render(stage, env) { + render(stage, env, from = null, yBegin = null) { + if(from === null) { + from = env.agentInfos.get(stage.agentIDs[0]); + yBegin = env.primaryY; + } if(stage.agentIDs[0] === stage.agentIDs[1]) { - return this.renderSelfConnect(stage, env); + return this.renderSelfConnect(stage, env, from, yBegin); } else { - return this.renderSimpleConnect(stage, env); + return this.renderSimpleConnect(stage, env, from, yBegin); } } } - BaseComponent.register('connect', new Connect()); + class ConnectDelayBegin extends Connect { + makeState(state) { + state.delayedConnections = new Map(); + } - return Connect; + resetState(state) { + state.delayedConnections.clear(); + } + + separation(stage, env) { + super.separation(stage, env); + array.mergeSets(env.momentaryAgentIDs, [stage.agentIDs[0]]); + } + + renderPre(stage, env) { + return Object.assign(super.renderPre(stage, env), { + agentIDs: [stage.agentIDs[0]], + }); + } + + render(stage, env) { + const dc = env.state.delayedConnections; + dc.set(stage.tag, { + stage, + from: Object.assign({}, env.agentInfos.get(stage.agentIDs[0])), + y: env.primaryY, + }); + return env.primaryY + env.theme.actionMargin; + } + } + + class ConnectDelayEnd extends Connect { + separationPre() {} + + separation() {} + + renderPre({tag}, env) { + const dc = env.state.delayedConnections; + const beginStage = dc.get(tag).stage; + return Object.assign(super.renderPre(beginStage, env), { + agentIDs: [beginStage.agentIDs[1]], + }); + } + + render({tag}, env) { + const dc = env.state.delayedConnections; + const begin = dc.get(tag); + return super.render(begin.stage, env, begin.from, begin.y); + } + } + + BaseComponent.register('connect', new Connect()); + BaseComponent.register('connect-delay-begin', new ConnectDelayBegin()); + BaseComponent.register('connect-delay-end', new ConnectDelayEnd()); + + return { + Connect, + ConnectDelayBegin, + ConnectDelayEnd, + }; }); define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => { @@ -4932,6 +5241,7 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base 'width': x1 - x0, 'height': fullH, 'fill': 'transparent', + 'class': 'vis', }), clickable.firstChild); return ( @@ -5190,6 +5500,7 @@ define('sequence/components/Divider',[ 'width': right.x - left.x + config.extend * 2, 'height': fullHeight, 'fill': 'transparent', + 'class': 'vis', }), clickable.firstChild); return env.primaryY + fullHeight + env.theme.actionMargin; @@ -5888,8 +6199,11 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => { 'use strict'; const TRIMMER = /^([ \t]*)(.*)$/; - const SQUASH_START = /^[ \t\r\n:,]/; - const SQUASH_END = /[ \t\r\n]$/; + const SQUASH = { + start: /^[ \t\r\n:,]/, + end: /[ \t\r\n]$/, + after: '.!+', // cannot squash after * or - in all cases + }; const ONGOING_QUOTE = /^"(\\.|[^"])*$/; const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/; const QUOTE_ESCAPE = /["\\]/g; @@ -5910,6 +6224,9 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => { squash: {line: line, ch: chFrom}, }; if(chFrom > 0 && ln[chFrom - 1] === ' ') { + if(SQUASH.after.includes(ln[chFrom - 2])) { + ranges.word.ch --; + } ranges.squash.ch --; } return ranges; @@ -5955,8 +6272,8 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => { text: quoted, displayText: quoted.trim(), className: null, - from: SQUASH_START.test(quoted) ? from.squash : from.word, - to: SQUASH_END.test(quoted) ? ranges.to.squash : ranges.to.word, + from: SQUASH.start.test(quoted) ? from.squash : from.word, + to: SQUASH.end.test(quoted) ? ranges.to.squash : ranges.to.word, displayFrom: from.word, }; } @@ -6185,14 +6502,18 @@ define('sequence/themes/BaseTheme',[ } } - BaseTheme.renderHorizArrowHead = (attrs, {x, y, dx, dy}) => { + BaseTheme.renderArrowHead = (attrs, {x, y, width, height, dir}) => { + 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 svg.make( attrs.fill === 'none' ? 'polyline' : 'polygon', Object.assign({ 'points': ( - (x + dx) + ' ' + (y - dy) + ' ' + + (x + wx - hx) + ' ' + (y + wy - hy) + ' ' + x + ' ' + y + ' ' + - (x + dx) + ' ' + (y + dy) + (x + wx + hx) + ' ' + (y + wy + hy) ), }, attrs) ); @@ -6261,37 +6582,50 @@ define('sequence/themes/BaseTheme',[ } }; - BaseTheme.renderFlatConnector = (pattern, attrs, {x1, dx1, x2, dx2, y}) => { + BaseTheme.renderFlatConnector = ( + pattern, + attrs, + {x1, y1, x2, y2} + ) => { return { shape: svg.make('path', Object.assign({ d: new SVGShapes.PatternedLine(pattern) - .move(x1 + dx1, y) - .line(x2 + dx2, y) + .move(x1, y1) + .line(x2, y2) .cap() .asPath(), }, attrs)), - p1: {x: x1, y}, - p2: {x: x2, y}, + p1: {x: x1, y: y1}, + p2: {x: x2, y: y2}, }; }; BaseTheme.renderRevConnector = ( pattern, attrs, - {xL, dx1, dx2, y1, y2, xR} + {x1, y1, x2, y2, xR, rad} ) => { + const maxRad = (y2 - y1) / 2; + const line = new SVGShapes.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 { shape: svg.make('path', Object.assign({ - d: new SVGShapes.PatternedLine(pattern) - .move(xL + dx1, y1) - .line(xR, y1) - .arc(xR, (y1 + y2) / 2, Math.PI) - .line(xL + dx2, y2) + d: line + .line(x2, y2) .cap() .asPath(), }, attrs)), - p1: {x: xL, y: y1}, - p2: {x: xL, y: y2}, + p1: {x: x1, y: y1}, + p2: {x: x2, y: y2}, }; }; @@ -6546,7 +6880,7 @@ define('sequence/themes/Basic',[ 'single': { width: 5, height: 10, - render: BaseTheme.renderHorizArrowHead, + render: BaseTheme.renderArrowHead, attrs: { 'fill': '#000000', 'stroke-width': 0, @@ -6556,7 +6890,7 @@ define('sequence/themes/Basic',[ 'double': { width: 4, height: 6, - render: BaseTheme.renderHorizArrowHead, + render: BaseTheme.renderArrowHead, attrs: { 'fill': 'none', 'stroke': '#000000', @@ -6934,7 +7268,7 @@ define('sequence/themes/Monospace',[ 'single': { width: 4, height: 8, - render: BaseTheme.renderHorizArrowHead, + render: BaseTheme.renderArrowHead, attrs: { 'fill': '#000000', 'stroke-width': 0, @@ -6944,7 +7278,7 @@ define('sequence/themes/Monospace',[ 'double': { width: 3, height: 6, - render: BaseTheme.renderHorizArrowHead, + render: BaseTheme.renderArrowHead, attrs: { 'fill': 'none', 'stroke': '#000000', @@ -7317,7 +7651,7 @@ define('sequence/themes/Chunky',[ 'single': { width: 10, height: 12, - render: BaseTheme.renderHorizArrowHead, + render: BaseTheme.renderArrowHead, attrs: { 'fill': '#000000', 'stroke': '#000000', @@ -7328,7 +7662,7 @@ define('sequence/themes/Chunky',[ 'double': { width: 10, height: 12, - render: BaseTheme.renderHorizArrowHead, + render: BaseTheme.renderArrowHead, attrs: { 'fill': 'none', 'stroke': '#000000', @@ -8694,23 +9028,23 @@ define('sequence/themes/Sketch',[ return {shape}; } - renderFlatConnector(attrs, {x1, dx1, x2, dx2, y}) { + renderFlatConnector(attrs, {x1, y1, x2, y2}) { const ln = this.lineNodes( - {x: x1 + dx1, y}, - {x: x2 + dx2, y}, + {x: x1, y: y1}, + {x: x2, y: y2}, {varX: 0.3} ); return { shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)), - p1: {x: ln.p1.x - dx1, y: ln.p1.y}, - p2: {x: ln.p2.x - dx2, y: ln.p2.y}, + p1: ln.p1, + p2: ln.p2, }; } - renderRevConnector(attrs, {xL, dx1, dx2, y1, y2, xR}) { - const variance = Math.min((xR - xL) * 0.06, 3); - const overshoot = Math.min((xR - xL) * 0.5, 6); - const p1x = xL + dx1 + this.vary(variance, -1); + renderRevConnector(attrs, {x1, y1, x2, y2, xR}) { + const variance = Math.min((xR - x1) * 0.06, 3); + const overshoot = Math.min((xR - x1) * 0.5, 6); + const p1x = x1 + this.vary(variance, -1); const p1y = y1 + this.vary(variance, -1); const b1x = xR - overshoot * this.vary(0.2, 1); const b1y = y1 - this.vary(1, 2); @@ -8718,7 +9052,7 @@ define('sequence/themes/Sketch',[ const p2y = y1 + this.vary(1, 1); const b2x = xR; const b2y = y2 + this.vary(2); - const p3x = xL + dx2 + this.vary(variance, -1); + const p3x = x2 + this.vary(variance, -1); const p3y = y2 + this.vary(variance, -1); return { @@ -8732,41 +9066,21 @@ define('sequence/themes/Sketch',[ ',' + p3x + ' ' + p3y ), }, attrs)), - p1: {x: p1x - dx1, y: p1y}, - p2: {x: p3x - dx2, y: p3y}, + p1: {x: p1x, y: p1y}, + p2: {x: p3x, y: p3y}, }; } - renderFlatConnectorWave(attrs, {x1, dx1, x2, dx2, y}) { + renderFlatConnectorWave(attrs, {x1, y1, x2, y2}) { const x1v = x1 + this.vary(0.3); const x2v = x2 + this.vary(0.3); - const y1v = y + this.vary(1); - const y2v = y + this.vary(1); - return { - shape: svg.make('path', Object.assign({ - d: new SVGShapes.PatternedLine(this.wave) - .move(x1v + dx1, y1v) - .line(x2v + dx2, y2v) - .cap() - .asPath(), - }, attrs)), - p1: {x: x1v, y: y1v}, - p2: {x: x2v, y: y2v}, - }; - } - - renderRevConnectorWave(attrs, {xL, dx1, dx2, y1, y2, xR}) { - const x1v = xL + this.vary(0.3); - const x2v = xL + this.vary(0.3); const y1v = y1 + this.vary(1); const y2v = y2 + this.vary(1); return { shape: svg.make('path', Object.assign({ d: new SVGShapes.PatternedLine(this.wave) - .move(x1v + dx1, y1v) - .line(xR, y1) - .arc(xR, (y1 + y2) / 2, Math.PI) - .line(x2v + dx2, y2v) + .move(x1v, y1v) + .line(x2v, y2v) .cap() .asPath(), }, attrs)), @@ -8775,17 +9089,41 @@ define('sequence/themes/Sketch',[ }; } - renderArrowHead(attrs, {x, y, dx, dy}) { - const w = dx * this.vary(0.2, 1); - const h = dy * this.vary(0.3, 1); + renderRevConnectorWave(attrs, {x1, y1, x2, y2, xR}) { + const x1v = x1 + this.vary(0.3); + const x2v = x2 + this.vary(0.3); + const y1v = y1 + this.vary(1); + const y2v = y2 + this.vary(1); + return { + shape: svg.make('path', Object.assign({ + d: new SVGShapes.PatternedLine(this.wave) + .move(x1v, y1v) + .line(xR, y1) + .arc(xR, (y1 + y2) / 2, Math.PI) + .line(x2v, y2v) + .cap() + .asPath(), + }, attrs)), + p1: {x: x1v, y: y1v}, + p2: {x: x2v, y: y2v}, + }; + } + + renderArrowHead(attrs, {x, y, width, height, dir}) { + const w = width * this.vary(0.2, 1); + const h = height * this.vary(0.3, 1); + const wx = w * dir.dx; + const wy = w * dir.dy; + const hy = h * 0.5 * dir.dx; + const hx = -h * 0.5 * dir.dy; const l1 = this.lineNodes( - {x: x + w, y: y - h}, + {x: x + wx - hx, y: y + wy - hy}, {x, y}, {var1: 2.0, var2: 0.2} ); const l2 = this.lineNodes( l1.p2, - {x: x + w, y: y + h}, + {x: x + wx + hx, y: y + wy + hy}, {var1: 0, var2: 2.0, move: false} ); const l3 = (attrs.fill === 'none') ? {nodes: ''} : this.lineNodes( diff --git a/lib/sequence-diagram.min.js b/lib/sequence-diagram.min.js index 125820d..561d372 100644 --- a/lib/sequence-diagram.min.js +++ b/lib/sequence-diagram.min.js @@ -1 +1 @@ -!function(){var e,t,n;!function(r){function i(e,t){return y.call(e,t)}function s(e,t){var n,r,i,s,a,o,l,h,d,g,c,u=t&&t.split("/"),p=b.map,f=p&&p["*"]||{};if(e){for(a=(e=e.split("/")).length-1,b.nodeIdCompat&&w.test(e[a])&&(e[a]=e[a].replace(w,"")),"."===e[0].charAt(0)&&u&&(e=u.slice(0,u.length-1).concat(e)),d=0;d0&&(e.splice(d-1,2),d-=2)}e=e.join("/")}if((u||f)&&p){for(d=(n=e.split("/")).length;d>0;d-=1){if(r=n.slice(0,d).join("/"),u)for(g=u.length;g>0;g-=1)if((i=p[u.slice(0,g).join("/")])&&(i=i[r])){s=i,o=d;break}if(s)break;!l&&f&&f[r]&&(l=f[r],h=d)}!s&&l&&(s=l,o=h),s&&(n.splice(0,o,s),e=n.join("/"))}return e}function a(e,t){return function(){var n=k.call(arguments,0);return"string"!=typeof n[0]&&1===n.length&&n.push(null),c.apply(r,n.concat([e,t]))}}function o(e){return function(t){f[e]=t}}function l(e){if(i(m,e)){var t=m[e];delete m[e],x[e]=!0,g.apply(r,t)}if(!i(f,e)&&!i(x,e))throw new Error("No "+e);return f[e]}function h(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function d(e){return e?h(e):[]}var g,c,u,p,f={},m={},b={},x={},y=Object.prototype.hasOwnProperty,k=[].slice,w=/\.js$/;u=function(e,t){var n,r=h(e),i=r[0],a=t[1];return e=r[1],i&&(n=l(i=s(i,a))),i?e=n&&n.normalize?n.normalize(e,function(e){return function(t){return s(t,e)}}(a)):s(e,a):(i=(r=h(e=s(e,a)))[0],e=r[1],i&&(n=l(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},p={require:function(e){return a(e)},exports:function(e){var t=f[e];return void 0!==t?t:f[e]={}},module:function(e){return{id:e,uri:"",exports:f[e],config:function(e){return function(){return b&&b.config&&b.config[e]||{}}}(e)}}},g=function(e,t,n,s){var h,g,c,b,y,k,w,A=[],v=typeof n;if(s=s||e,k=d(s),"undefined"===v||"function"===v){for(t=!t.length&&n.length?["require","exports","module"]:t,y=0;y{"use strict";return class{constructor(){this.listeners=new Map,this.forwards=new Set}addEventListener(e,t){const n=this.listeners.get(e);n?n.push(t):this.listeners.set(e,[t])}removeEventListener(e,t){const n=this.listeners.get(e);if(!n)return;const r=n.indexOf(t);-1!==r&&n.splice(r,1)}countEventListeners(e){return(this.listeners.get(e)||[]).length}removeAllEventListeners(e){e?this.listeners.delete(e):this.listeners.clear()}addEventForwarding(e){this.forwards.add(e)}removeEventForwarding(e){this.forwards.delete(e)}removeAllEventForwardings(){this.forwards.clear()}trigger(e,t=[]){(this.listeners.get(e)||[]).forEach(e=>e.apply(null,t)),this.forwards.forEach(n=>n.trigger(e,t))}}}),n("core/ArrayUtilities",[],()=>{"use strict";function e(e,t,n=null){if(null===n)return e.indexOf(t);for(let r=0;r=e.length)return void i.push(r.slice());const s=e[n];if(!Array.isArray(s))return r.push(s),t(e,n+1,r,i),void r.pop();for(let a=0;a{n.push(...t(e))}),n}}}),n("sequence/CodeMirrorMode",["core/ArrayUtilities"],e=>{"use strict";function t(e,t){return e.v===t.v&&e.prefix===t.prefix&&e.suffix===t.suffix&&e.q===t.q}function n(t,n,r){let i=r.suggest;return Array.isArray(i)||(i=[i]),e.flatMap(i,e=>!0===e?[function(e,t){return Object.keys(t.then).length>0?{v:e,suffix:" ",q:!1}:{v:e,suffix:"\n",q:!1}}(n,r)]:"object"==typeof e?e.known?t["known"+e.known]||[]:[e]:"string"==typeof e&&e?[{v:e,q:""===n}]:[])}function r(r,i){const s=[],a=e.last(i);return Object.keys(a.then).forEach(o=>{let l=a.then[o];"number"==typeof l&&(l=i[i.length-l-1]),e.mergeSets(s,n(r,o,l),t)}),s}function i(n,r,i,{suggest:s,override:a}){let o=null;"object"==typeof s&&s.known&&(o=s.known),r.type&&o!==r.type&&(a&&(r.type=a),e.mergeSets(n["known"+r.type],[{v:r.value,suffix:" ",q:!0}],t),r.type="",r.value=""),o&&(r.type=o,r.value&&(r.value+=i.s),r.value+=i.v)}function s(t,n,s){const a={type:"",value:""};let l=s;const h=[l];return t.line.forEach((n,s)=>{s===t.line.length-1&&(t.completions=r(t,h));const d=n.q?"":n.v;let g=l.then[d];void 0===g?(g=l.then[""],t.isVar=!0):t.isVar=n.q,"number"==typeof g?h.length-=g:h.push(g||o),l=e.last(h),i(t,a,n,l)}),n&&i(t,a,null,{}),t.nextCompletions=r(t,h),t.valid=Boolean(l.then["\n"])||0===Object.keys(l.then).length,l.type}function a(e){const t=e.baseToken||{};return{value:t.v||"",quoted:t.q||!1}}const o={type:"error line-error",then:{"":0}},l=(()=>{function e(e,t){return{type:"string",suggest:t,then:Object.assign({"":0},e)}}function t(e){return{type:"variable",suggest:{known:"Agent"},then:Object.assign({},e,{"":0,",":{type:"operator",suggest:!0,then:{"":1}}})}}function n(e){return{type:"keyword",suggest:[e+" of ",e+": "],then:{of:{type:"keyword",suggest:!0,then:{"":d}},":":{type:"operator",suggest:!0,then:{"":a}},"":d}}}function r(e,t){const n={type:"operator",suggest:!0,then:{"+":o,"-":o,"*":o,"!":o,"":e}};return{"+":{type:"operator",suggest:!0,then:{"+":o,"-":o,"*":n,"!":o,"":e}},"-":{type:"operator",suggest:!0,then:{"+":o,"-":o,"*":n,"!":{type:"operator",then:{"+":o,"-":o,"*":o,"!":o,"":e}},"":e}},"*":{type:"operator",suggest:!0,then:Object.assign({"+":n,"-":n,"*":o,"!":o,"":e},t)},"!":n,"":e}}const i={type:"",suggest:"\n",then:{}},s={type:"",then:{}},a=e({"\n":i}),l={type:"variable",suggest:{known:"Agent"},then:{"":0,"\n":i,",":{type:"operator",suggest:!0,then:{"":1}},as:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,",":{type:"operator",suggest:!0,then:{"":3}},"\n":i}}}}}},h={type:"operator",suggest:!0,then:{"":a,"\n":s}},d=t({":":h}),g={type:"variable",suggest:{known:"Agent"},then:{"":0,",":{type:"operator",suggest:!0,then:{"":d}},":":o}},c={type:"variable",suggest:{known:"Agent"},then:{"":0,",":o,":":h}},u={type:"variable",suggest:{known:"Agent"},then:{"":0,":":{type:"operator",suggest:!0,then:{"":a,"\n":s}},"\n":i}},p={":":{type:"operator",suggest:!0,then:{"":e({as:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,"\n":i}}}}})}}},f={type:"keyword",suggest:!0,then:Object.assign({over:{type:"keyword",suggest:!0,then:{"":t(p)}}},p)},m={"\n":i,":":{type:"operator",suggest:!0,then:{"":a,"\n":s}},with:{type:"keyword",suggest:["with height "],then:{height:{type:"keyword",suggest:!0,then:{"":{type:"number",suggest:["6 ","30 "],then:{"\n":i,":":{type:"operator",suggest:!0,then:{"":a,"\n":s}}}}}}}}},b={title:{type:"keyword",suggest:!0,then:{"":a}},theme:{type:"keyword",suggest:!0,then:{"":{type:"string",suggest:{global:"themes",suffix:"\n"},then:{"":0,"\n":i}}}},headers:{type:"keyword",suggest:!0,then:{none:{type:"keyword",suggest:!0,then:{}},cross:{type:"keyword",suggest:!0,then:{}},box:{type:"keyword",suggest:!0,then:{}},fade:{type:"keyword",suggest:!0,then:{}},bar:{type:"keyword",suggest:!0,then:{}}}},terminators:{type:"keyword",suggest:!0,then:{none:{type:"keyword",suggest:!0,then:{}},cross:{type:"keyword",suggest:!0,then:{}},box:{type:"keyword",suggest:!0,then:{}},fade:{type:"keyword",suggest:!0,then:{}},bar:{type:"keyword",suggest:!0,then:{}}}},divider:{type:"keyword",suggest:!0,then:Object.assign({line:{type:"keyword",suggest:!0,then:m},space:{type:"keyword",suggest:!0,then:m},delay:{type:"keyword",suggest:!0,then:m},tear:{type:"keyword",suggest:!0,then:m}},m)},define:{type:"keyword",suggest:!0,then:{"":l,as:o}},begin:{type:"keyword",suggest:!0,then:{"":l,reference:f,as:o}},end:{type:"keyword",suggest:!0,then:{"":l,as:o,"\n":i}},if:{type:"keyword",suggest:!0,then:{"":a,":":{type:"operator",suggest:!0,then:{"":a}},"\n":i}},else:{type:"keyword",suggest:["else\n","else if: "],then:{if:{type:"keyword",suggest:"if: ",then:{"":a,":":{type:"operator",suggest:!0,then:{"":a}}}},"\n":i}},repeat:{type:"keyword",suggest:!0,then:{"":a,":":{type:"operator",suggest:!0,then:{"":a}},"\n":i}},note:{type:"keyword",suggest:!0,then:{over:{type:"keyword",suggest:!0,then:{"":d}},left:n("left"),right:n("right"),between:{type:"keyword",suggest:!0,then:{"":g}}}},state:{type:"keyword",suggest:"state over ",then:{over:{type:"keyword",suggest:!0,then:{"":c}}}},text:{type:"keyword",suggest:!0,then:{left:n("left"),right:n("right")}},autolabel:{type:"keyword",suggest:!0,then:{off:{type:"keyword",suggest:!0,then:{}},"":e({"\n":i},[{v:"