Add support for creating and destroying agents during connections

This commit is contained in:
David Evans 2017-11-05 13:15:41 +00:00
parent b58506d546
commit 7457131d1e
17 changed files with 258 additions and 244 deletions

View File

@ -55,6 +55,10 @@ Foo -> Foo: Foo talks to itself
Foo -> +Bar: Foo asks Bar Foo -> +Bar: Foo asks Bar
-Bar --> Foo: and Bar replies -Bar --> Foo: and Bar replies
# * and ! cause agents to be created and destroyed inline
Bar -> *Baz
Bar <- !Baz
# Arrows leaving on the left and right of the diagram # Arrows leaving on the left and right of the diagram
[ -> Foo: From the left [ -> Foo: From the left
[ <- Foo: To the left [ <- Foo: To the left
@ -258,7 +262,7 @@ Note: the linter can't run from the local filesystem, so you'll need to
run a local HTTP server to ensure linting is successful. One option if run a local HTTP server to ensure linting is successful. One option if
you have NPM installed is: you have NPM installed is:
``` ```shell
# Setup # Setup
npm install http-server -g; npm install http-server -g;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -18,7 +18,7 @@
} }
const SAMPLE_REGEX = new RegExp( const SAMPLE_REGEX = new RegExp(
/<img src="screenshots\/([^"]*)"[^>]*>[\s]*```([^]+?)```/g /<img src="screenshots\/([^"]*)"[^>]*>[\s]*```(?!shell).*\n([^]+?)```/g
); );
function findSamples(content) { function findSamples(content) {

View File

@ -46,24 +46,57 @@ define(['core/ArrayUtilities'], (array) => {
}; };
} }
const CM_CONNECT = {type: 'keyword', suggest: true, then: { function makeCMOperatorBlock(exit) {
'+': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, const op = {type: 'operator', suggest: true, then: {
'-': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, '+': CM_ERROR,
'': CM_AGENT_TO_OPTTEXT, '-': CM_ERROR,
}}; '*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}};
const pm = {type: 'operator', suggest: true, then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': op,
'': exit,
}};
const se = {type: 'operator', suggest: true, then: {
'+': op,
'-': op,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}};
return {
'+': pm,
'-': pm,
'*': se,
'!': se,
'': exit,
};
}
const CM_CONNECT_FULL = {type: 'variable', suggest: 'Agent', then: { function makeCMConnect() {
'->': CM_CONNECT, const connect = {
'-->': CM_CONNECT, type: 'keyword',
'<-': CM_CONNECT, suggest: true,
'<--': CM_CONNECT, then: makeCMOperatorBlock(CM_AGENT_TO_OPTTEXT),
'<->': CM_CONNECT, };
'<-->': CM_CONNECT,
':': {type: 'operator', suggest: true, override: 'Label', then: {}},
'': 0,
}};
const CM_COMMANDS = {type: 'error line-error', then: { return makeCMOperatorBlock({type: 'variable', suggest: 'Agent', then: {
'->': connect,
'-->': connect,
'<-': connect,
'<--': connect,
'<->': connect,
'<-->': connect,
':': {type: 'operator', suggest: true, override: 'Label', then: {}},
'': 0,
}});
}
const CM_COMMANDS = {type: 'error line-error', then: Object.assign({
'title': {type: 'keyword', suggest: true, then: { 'title': {type: 'keyword', suggest: true, then: {
'': CM_TEXT_TO_END, '': CM_TEXT_TO_END,
}}, }},
@ -134,10 +167,7 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
}}, }},
}}, }},
'+': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, }, makeCMConnect())};
'-': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}},
'': CM_CONNECT_FULL,
}};
function cmCappedToken(token, current) { function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) { if(Object.keys(current.then).length > 0) {

View File

@ -25,8 +25,8 @@ define(['core/ArrayUtilities'], (array) => {
return agent.name; return agent.name;
} }
function agentHasFlag(flag) { function agentHasFlag(flag, has = true) {
return (agent) => agent.flags.includes(flag); return (agent) => (agent.flags.includes(flag) === has);
} }
const MERGABLE = { const MERGABLE = {
@ -235,7 +235,7 @@ define(['core/ArrayUtilities'], (array) => {
array.mergeSets(this.agents, agents, agentEqCheck); array.mergeSets(this.agents, agents, agentEqCheck);
} }
setAgentVis(agents, visible, mode, checked = false) { setAgentVisRaw(agents, visible, mode, checked = false) {
const filteredAgents = agents.filter((agent) => { const filteredAgents = agents.filter((agent) => {
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT; const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
if(state.locked) { if(state.locked) {
@ -269,6 +269,15 @@ define(['core/ArrayUtilities'], (array) => {
}; };
} }
setAgentVis(agents, visible, mode, checked = false) {
return this.setAgentVisRaw(
agents.map(convertAgent),
visible,
mode,
checked
);
}
setAgentHighlight(agents, highlighted, checked = false) { setAgentHighlight(agents, highlighted, checked = false) {
const filteredAgents = agents.filter((agent) => { const filteredAgents = agents.filter((agent) => {
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT; const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
@ -340,15 +349,24 @@ define(['core/ArrayUtilities'], (array) => {
} }
handleConnect({agents, label, options}) { handleConnect({agents, label, options}) {
const colAgents = agents.map(convertAgent); const beginAgents = agents.filter(agentHasFlag('begin'));
this.addStage(this.setAgentVis(colAgents, true, 'box')); const endAgents = agents.filter(agentHasFlag('end'));
this.defineAgents(colAgents); if(array.hasIntersection(beginAgents, endAgents, agentEqCheck)) {
throw new Error('Cannot set agent visibility multiple times');
}
const startAgents = agents.filter(agentHasFlag('start')); const startAgents = agents.filter(agentHasFlag('start'));
const stopAgents = agents.filter(agentHasFlag('stop')); const stopAgents = agents.filter(agentHasFlag('stop'));
array.mergeSets(stopAgents, endAgents);
if(array.hasIntersection(startAgents, stopAgents, agentEqCheck)) { if(array.hasIntersection(startAgents, stopAgents, agentEqCheck)) {
throw new Error('Cannot set agent highlighting multiple times'); throw new Error('Cannot set agent highlighting multiple times');
} }
this.defineAgents(agents.map(convertAgent));
const implicitBegin = agents.filter(agentHasFlag('begin', false));
this.addStage(this.setAgentVis(implicitBegin, true, 'box'));
const connectStage = { const connectStage = {
type: 'connect', type: 'connect',
agentNames: agents.map(getAgentName), agentNames: agents.map(getAgentName),
@ -357,9 +375,11 @@ define(['core/ArrayUtilities'], (array) => {
}; };
this.addParallelStages([ this.addParallelStages([
this.setAgentVis(beginAgents, true, 'box', true),
this.setAgentHighlight(startAgents, true, true), this.setAgentHighlight(startAgents, true, true),
connectStage, connectStage,
this.setAgentHighlight(stopAgents, false, true), this.setAgentHighlight(stopAgents, false, true),
this.setAgentVis(endAgents, false, 'cross', true),
]); ]);
} }
@ -371,7 +391,7 @@ define(['core/ArrayUtilities'], (array) => {
colAgents = agents.map(convertAgent); colAgents = agents.map(convertAgent);
} }
this.addStage(this.setAgentVis(colAgents, true, 'box')); this.addStage(this.setAgentVisRaw(colAgents, true, 'box'));
this.defineAgents(colAgents); this.defineAgents(colAgents);
this.addStage({ this.addStage({
@ -383,23 +403,17 @@ define(['core/ArrayUtilities'], (array) => {
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
const colAgents = agents.map(convertAgent); this.defineAgents(agents.map(convertAgent));
this.defineAgents(colAgents);
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode}) {
this.addStage(this.setAgentVis( this.addStage(this.setAgentVis(agents, true, mode, true));
agents.map(convertAgent),
true,
mode,
true
));
} }
handleAgentEnd({agents, mode}) { handleAgentEnd({agents, mode}) {
this.addParallelStages([ this.addParallelStages([
this.setAgentHighlight(agents, false), this.setAgentHighlight(agents, false),
this.setAgentVis(agents.map(convertAgent), false, mode, true), this.setAgentVis(agents, false, mode, true),
]); ]);
} }
@ -471,7 +485,7 @@ define(['core/ArrayUtilities'], (array) => {
this.addParallelStages([ this.addParallelStages([
this.setAgentHighlight(this.agents, false), this.setAgentHighlight(this.agents, false),
this.setAgentVis(this.agents, false, terminators), this.setAgentVisRaw(this.agents, false, terminators),
]); ]);
addBounds( addBounds(

View File

@ -426,6 +426,51 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
}); });
it('adds parallel begin stages', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['begin']}]),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
GENERATED.parallel([
GENERATED.beginAgents(['B']),
GENERATED.connect(['A', 'B']),
]),
GENERATED.endAgents(['A', 'B']),
]);
});
it('adds parallel end stages', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['end']}]),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A', 'B']),
GENERATED.parallel([
GENERATED.connect(['A', 'B']),
GENERATED.endAgents(['B']),
]),
GENERATED.endAgents(['A']),
]);
});
it('implicitly ends highlighting when ending a stage', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', flags: ['end']}]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
jasmine.anything(),
GENERATED.parallel([
GENERATED.connect(['A', 'B']),
GENERATED.highlight(['B'], false),
GENERATED.endAgents(['B']),
]),
GENERATED.endAgents(['A']),
]);
});
it('rejects conflicting flags', () => { it('rejects conflicting flags', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]), PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]),
@ -437,6 +482,17 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{name: 'A', flags: ['stop']}, {name: 'A', flags: ['stop']},
]), ]),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['begin', 'end']}]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect([
{name: 'A', flags: ['begin']},
{name: 'A', flags: ['end']},
]),
]})).toThrow();
}); });
it('adds implicit highlight end with implicit terminator', () => { it('adds implicit highlight end with implicit terminator', () => {

View File

@ -26,8 +26,10 @@ define([
}; };
const CONNECT_AGENT_FLAGS = { const CONNECT_AGENT_FLAGS = {
'*': 'begin',
'+': 'start', '+': 'start',
'-': 'stop', '-': 'stop',
'!': 'end',
}; };
const TERMINATOR_TYPES = [ const TERMINATOR_TYPES = [
@ -116,13 +118,20 @@ define([
const flags = []; const flags = [];
let p = start; let p = start;
for(; p < end; ++ p) { for(; p < end; ++ p) {
const flag = flagTypes[tokenKeyword(line[p])]; const rawFlag = tokenKeyword(line[p]);
const flag = flagTypes[rawFlag];
if(flag) { if(flag) {
if(flags.includes(flag)) {
throw new Error('Duplicate agent flag: ' + rawFlag);
}
flags.push(flag); flags.push(flag);
} else { } else {
break; break;
} }
} }
if(p >= end) {
throw new Error('Missing agent name');
}
return { return {
name: joinLabel(line, p, end), name: joinLabel(line, p, end),
flags, flags,

View File

@ -79,13 +79,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}); });
it('parses optional flags', () => { it('parses optional flags', () => {
const parsed = parser.parse('+A -> -B'); const parsed = parser.parse('+A -> -*!B');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ {
type: 'connect', type: 'connect',
agents: [ agents: [
{name: 'A', flags: ['start']}, {name: 'A', flags: ['start']},
{name: 'B', flags: ['stop']}, {name: 'B', flags: ['stop', 'begin', 'end']},
], ],
label: jasmine.anything(), label: jasmine.anything(),
options: jasmine.anything(), options: jasmine.anything(),
@ -93,6 +93,15 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
]); ]);
}); });
it('rejects duplicate flags', () => {
expect(() => parser.parse('A -> +*+B')).toThrow();
expect(() => parser.parse('A -> **B')).toThrow();
});
it('rejects missing agent names', () => {
expect(() => parser.parse('A -> +')).toThrow();
});
it('converts multiple entries', () => { it('converts multiple entries', () => {
const parsed = parser.parse('A -> B\nB -> A'); const parsed = parser.parse('A -> B\nB -> A');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([

View File

@ -161,10 +161,10 @@ define([
const agentSpaces = new Map(); const agentSpaces = new Map();
const agentNames = this.visibleAgents.slice(); const agentNames = this.visibleAgents.slice();
const addSpacing = (agentName, spacing) => { const addSpacing = (agentName, {left, right}) => {
const current = agentSpaces.get(agentName); const current = agentSpaces.get(agentName);
current.left = Math.max(current.left, spacing.left); current.left = Math.max(current.left, left);
current.right = Math.max(current.right, spacing.right); current.right = Math.max(current.right, right);
}; };
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
@ -366,7 +366,7 @@ define([
const touchedAgentNames = []; const touchedAgentNames = [];
stages.forEach((stage) => { stages.forEach((stage) => {
const component = this.components.get(stage.type); const component = this.components.get(stage.type);
const r = component.renderPre(stage, envPre); const r = component.renderPre(stage, envPre) || {};
if(r.topShift !== undefined) { if(r.topShift !== undefined) {
maxTopShift = Math.max(maxTopShift, r.topShift); maxTopShift = Math.max(maxTopShift, r.topShift);
} }
@ -460,6 +460,7 @@ define([
x: null, x: null,
latestYStart: null, latestYStart: null,
currentRad: 0, currentRad: 0,
currentMaxRad: 0,
latestY: 0, latestY: 0,
maxRPad: 0, maxRPad: 0,
maxLPad: 0, maxLPad: 0,

View File

@ -31,10 +31,13 @@ define(['./CodeMirrorMode'], (CMMode) => {
unescape, unescape,
baseToken: {q: true}, baseToken: {q: true},
}, },
{start: /(?=[^ \t\r\n:+\-<>,])/y, end: /(?=[ \t\r\n:+\-<>,])|$/y}, {start: /(?=[^ \t\r\n:+\-*!<>,])/y, end: /(?=[ \t\r\n:+\-*!<>,])|$/y},
{start: /(?=[+\-<>])/y, end: /(?=[^+\-<>])|$/y}, {start: /(?=[\-<>])/y, end: /(?=[^\-<>])|$/y},
{start: /,/y, baseToken: {v: ','}}, {start: /,/y, baseToken: {v: ','}},
{start: /:/y, baseToken: {v: ':'}}, {start: /:/y, baseToken: {v: ':'}},
{start: /!/y, baseToken: {v: '!'}},
{start: /\+/y, baseToken: {v: '+'}},
{start: /\*/y, baseToken: {v: '*'}},
{start: /\n/y, baseToken: {v: '\n'}}, {start: /\n/y, baseToken: {v: '\n'}},
]; ];

View File

@ -23,11 +23,18 @@ define([
return { return {
left: width / 2, left: width / 2,
right: width / 2, right: width / 2,
radius: width / 2,
}; };
} }
topShift() { topShift({label}, env) {
return 0; const config = env.theme.agentCap.box;
const height = (
env.textSizer.measureHeight(config.labelAttrs, label) +
config.padding.top +
config.padding.bottom
);
return Math.max(0, height - config.arrowBottom);
} }
render(y, {x, label}, env) { render(y, {x, label}, env) {
@ -57,6 +64,7 @@ define([
return { return {
left: config.size / 2, left: config.size / 2,
right: config.size / 2, right: config.size / 2,
radius: 0,
}; };
} }
@ -98,6 +106,7 @@ define([
return { return {
left: width / 2, left: width / 2,
right: width / 2, right: width / 2,
radius: width / 2,
}; };
} }
@ -131,7 +140,11 @@ define([
class CapNone { class CapNone {
separation({currentRad}) { separation({currentRad}) {
return {left: currentRad, right: currentRad}; return {
left: currentRad,
right: currentRad,
radius: currentRad,
};
} }
topShift(agentInfo, env) { topShift(agentInfo, env) {
@ -162,18 +175,24 @@ define([
this.begin = begin; this.begin = begin;
} }
separationPre({mode, agentNames}, env) {
agentNames.forEach((name) => {
const agentInfo = env.agentInfos.get(name);
const sep = AGENT_CAPS[mode].separation(agentInfo, env);
env.addSpacing(name, sep);
agentInfo.currentMaxRad = Math.max(
agentInfo.currentMaxRad,
sep.radius
);
});
}
separation({mode, agentNames}, env) { separation({mode, agentNames}, env) {
if(this.begin) { if(this.begin) {
array.mergeSets(env.visibleAgents, agentNames); array.mergeSets(env.visibleAgents, agentNames);
} else { } else {
array.removeAll(env.visibleAgents, agentNames); array.removeAll(env.visibleAgents, agentNames);
} }
agentNames.forEach((name) => {
const agentInfo = env.agentInfos.get(name);
const separationFn = AGENT_CAPS[mode].separation;
env.addSpacing(name, separationFn(agentInfo, env));
});
} }
renderPre({mode, agentNames}, env) { renderPre({mode, agentNames}, env) {
@ -182,6 +201,9 @@ define([
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(name);
const topShift = AGENT_CAPS[mode].topShift(agentInfo, env); const topShift = AGENT_CAPS[mode].topShift(agentInfo, env);
maxTopShift = Math.max(maxTopShift, topShift); maxTopShift = Math.max(maxTopShift, topShift);
const r = AGENT_CAPS[mode].separation(agentInfo, env).radius;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
}); });
return { return {
agentNames, agentNames,

View File

@ -2,21 +2,32 @@ define(['./BaseComponent'], (BaseComponent) => {
'use strict'; 'use strict';
class AgentHighlight extends BaseComponent { class AgentHighlight extends BaseComponent {
radius(highlighted, env) {
return highlighted ? env.theme.agentLineHighlightRadius : 0;
}
separationPre({agentNames, highlighted}, env) { separationPre({agentNames, highlighted}, env) {
const rad = highlighted ? env.theme.agentLineHighlightRadius : 0; const r = this.radius(highlighted, env);
agentNames.forEach((name) => { agentNames.forEach((name) => {
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(name);
const maxRad = Math.max(agentInfo.currentMaxRad, rad); agentInfo.currentRad = r;
agentInfo.currentRad = rad; agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
agentInfo.currentMaxRad = maxRad; });
}
renderPre({agentNames, highlighted}, env) {
const r = this.radius(highlighted, env);
agentNames.forEach((name) => {
const agentInfo = env.agentInfos.get(name);
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
}); });
} }
render({agentNames, highlighted}, env) { render({agentNames, highlighted}, env) {
const rad = highlighted ? env.theme.agentLineHighlightRadius : 0; const r = this.radius(highlighted, env);
agentNames.forEach((name) => { agentNames.forEach((name) => {
env.drawAgentLine(name, env.primaryY); env.drawAgentLine(name, env.primaryY);
env.agentInfos.get(name).currentRad = rad; env.agentInfos.get(name).currentRad = r;
}); });
} }
} }

View File

@ -35,7 +35,6 @@ define(() => {
textSizer, textSizer,
state, state,
}*/) { }*/) {
return {};
} }
render(/*stage, { render(/*stage, {
@ -49,7 +48,6 @@ define(() => {
SVGTextBlockClass, SVGTextBlockClass,
state, state,
}*/) { }*/) {
return 0;
} }
} }

View File

@ -83,7 +83,7 @@ define([
config.label.margin.bottom config.label.margin.bottom
); );
const lineX = from.x + from.currentRad; const lineX = from.x + from.currentMaxRad;
const y0 = env.primaryY; const y0 = env.primaryY;
const x0 = ( const x0 = (
lineX + lineX +
@ -159,8 +159,8 @@ define([
config.label.margin.bottom config.label.margin.bottom
); );
const x0 = from.x + from.currentRad * dir; const x0 = from.x + from.currentMaxRad * dir;
const x1 = to.x - to.currentRad * dir; const x1 = to.x - to.currentMaxRad * dir;
let y = env.primaryY; let y = env.primaryY;
SVGShapes.renderBoxedText(label, { SVGShapes.renderBoxedText(label, {

View File

@ -131,8 +131,8 @@ define(['./BaseComponent'], (BaseComponent) => {
const infoL = env.agentInfos.get(left); const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
return this.renderNote({ return this.renderNote({
x0: infoL.x - infoL.currentRad - config.overlap.left, x0: infoL.x - infoL.currentMaxRad - config.overlap.left,
x1: infoR.x + infoR.currentRad + config.overlap.right, x1: infoR.x + infoR.currentMaxRad + config.overlap.right,
anchor: 'middle', anchor: 'middle',
mode, mode,
label, label,
@ -186,7 +186,7 @@ define(['./BaseComponent'], (BaseComponent) => {
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentNames);
if(this.isRight) { if(this.isRight) {
const info = env.agentInfos.get(right); const info = env.agentInfos.get(right);
const x0 = info.x + info.currentRad + config.margin.left; const x0 = info.x + info.currentMaxRad + config.margin.left;
return this.renderNote({ return this.renderNote({
x0, x0,
anchor: 'start', anchor: 'start',
@ -195,7 +195,7 @@ define(['./BaseComponent'], (BaseComponent) => {
}, env); }, env);
} else { } else {
const info = env.agentInfos.get(left); const info = env.agentInfos.get(left);
const x1 = info.x - info.currentRad - config.margin.right; const x1 = info.x - info.currentMaxRad - config.margin.right;
return this.renderNote({ return this.renderNote({
x1, x1,
anchor: 'end', anchor: 'end',
@ -232,8 +232,8 @@ define(['./BaseComponent'], (BaseComponent) => {
const infoL = env.agentInfos.get(left); const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
const xMid = ( const xMid = (
infoL.x + infoL.currentRad + infoL.x + infoL.currentMaxRad +
infoR.x - infoR.currentRad infoR.x - infoR.currentMaxRad
) / 2; ) / 2;
return this.renderNote({ return this.renderNote({

View File

@ -108,176 +108,32 @@ defineDescribe('Sequence Integration', [
); );
}); });
it('Renders the "Simple Usage" example without error', () => { const SAMPLE_REGEX = new RegExp(
const parsed = parser.parse( /```(?!shell).*\n([^]+?)```/g
'title Labyrinth\n' + );
'\n' +
'Bowie -> Gremlin: You remind me of the babe\n' +
'Gremlin -> Bowie: What babe?\n' +
'Bowie -> Gremlin: The babe with the power\n' +
'Gremlin -> Bowie: What power?\n' +
'note right of Bowie, Gremlin: Most people get muddled here!\n' +
'Bowie -> Gremlin: \'The power of voodoo\'\n' +
'Gremlin -> Bowie: "Who-do?"\n' +
'Bowie -> Gremlin: You do!\n' +
'Gremlin -> Bowie: Do what?\n' +
'Bowie -> Gremlin: Remind me of the babe!\n' +
'\n' +
'Bowie -> Audience: Sings\n' +
'\n' +
'terminators box\n'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Connection Types" example without error', () => { function findSamples(content) {
const parsed = parser.parse( SAMPLE_REGEX.lastIndex = 0;
'title Connection Types\n' + const results = [];
'\n' + while(true) {
'Foo -> Bar: Simple arrow\n' + const match = SAMPLE_REGEX.exec(content);
'Foo --> Bar: Dashed arrow\n' + if(!match) {
'Foo <- Bar: Reversed arrow\n' + break;
'Foo <-- Bar: Reversed dashed arrow\n' + }
'Foo <-> Bar: Double arrow\n' + results.push(match[1]);
'Foo <--> Bar: Double dashed arrow\n' + }
'\n' + return results;
'# An arrow with no label:\n' + }
'Foo -> Bar\n' +
'\n' +
'Foo -> Foo: Foo talks to itself\n' +
'\n' +
'# Arrows leaving on the left and right of the diagram\n' +
'[ -> Foo: From the left\n' +
'[ <- Foo: To the left\n' +
'Foo -> ]: To the right\n' +
'Foo <- ]: From the right\n' +
'[ -> ]: Left to right!\n' +
'# (etc.)\n'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Notes & State" example without error', () => { return (fetch('README.md')
const parsed = parser.parse( .then((response) => response.text())
'title Note Placements\n' + .then(findSamples)
'\n' + .then((samples) => samples.forEach((code, i) => {
'note over Foo: Foo says something\n' + it('Renders readme example #' + (i + 1) + ' without error', () => {
'note left of Foo: Stuff\n' + const parsed = parser.parse(code);
'note right of Bar: More stuff\n' + const sequence = generator.generate(parsed);
'note over Foo, Bar: "Foo and Bar\n' + expect(() => renderer.render(sequence)).not.toThrow();
'on multiple lines"\n' + });
'note between Foo, Bar: Link\n' + }))
'\n' + );
'text right: \'Comments\\nOver here\!\'\n' +
'\n' +
'state over Foo: Foo is ponderous'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Logic" example without error', () => {
const parsed = parser.parse(
'title At the Bank\n' +
'\n' +
'begin Person, ATM, Bank\n' +
'Person -> ATM: Request money\n' +
'ATM -> Bank: Check funds\n' +
'if fraud detected\n' +
' Bank -> Police: "Get \'em!"\n' +
' Police -> Person: "You\'re nicked"\n' +
' end Police\n' +
'else if sufficient funds\n' +
' ATM -> Bank: Withdraw funds\n' +
' repeat until "all requested money\n' +
' has been handed over"\n' +
' ATM -> Person: Dispense note\n' +
' end\n' +
'else\n' +
' ATM -> Person: Error\n' +
'end'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Multiline Text" example without error', () => {
const parsed = parser.parse(
'title \'My Multiline\n' +
'Title\'\n' +
'\n' +
'note over Foo: \'Also possible\\nwith escapes\'\n' +
'\n' +
'Foo -> Bar: \'Lines of text\\non this arrow\'\n' +
'\n' +
'if \'Even multiline\\ninside conditions like this\'\n' +
' Foo -> \'Multiline\\nagent\'\n' +
'end\n' +
'\n' +
'state over Foo: \'Newlines here,\\ntoo!\''
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Short-Lived Agents" example without error', () => {
const parsed = parser.parse(
'title "Baz doesn\'t live long"\n' +
'\n' +
'Foo -> Bar\n' +
'begin Baz\n' +
'Bar -> Baz\n' +
'Baz -> Foo\n' +
'end Baz\n' +
'Foo -> Bar\n' +
'\n' +
'# Foo and Bar end with black bars\n' +
'terminators bar\n' +
'# (options are: box, bar, cross, none)'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Alternative Agent Ordering" example without error', () => {
const parsed = parser.parse(
'define Baz, Foo\n' +
'Foo -> Bar\n' +
'Bar -> Baz\n'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
it('Renders the "Simultaneous Actions" example without error', () => {
const parsed = parser.parse(
'begin A, B, C, D\n' +
'A -> C\n' +
'\n' +
'# Define a marker which can be returned to later\n' +
'\n' +
'some primary process:\n' +
'A -> B\n' +
'B -> A\n' +
'A -> B\n' +
'B -> A\n' +
'\n' +
'# Return to the defined marker\n' +
'# (should be interpreted as no-higher-then the marker; may be\n' +
'# pushed down to keep relative action ordering consistent)\n' +
'\n' +
'simultaneously with some primary process:\n' +
'C -> D\n' +
'D -> C\n' +
'end D\n' +
'C -> A\n' +
'\n' +
'# The marker name is optional; using "simultaneously:" with no\n' +
'# marker will jump to the top of the entire sequence.'
);
const sequence = generator.generate(parsed);
expect(() => renderer.render(sequence)).not.toThrow();
});
}); });

View File

@ -18,6 +18,7 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
right: 10, right: 10,
bottom: 5, bottom: 5,
}, },
arrowBottom: 5 + 12 * 1.3 / 2,
boxAttrs: { boxAttrs: {
'fill': '#FFFFFF', 'fill': '#FFFFFF',
'stroke': '#000000', 'stroke': '#000000',