diff --git a/README.md b/README.md
index 62c1c60..bbc6b43 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,10 @@ Foo -> Foo: Foo talks to itself
Foo -> +Bar: Foo asks Bar
-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
[ -> Foo: From 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
you have NPM installed is:
-```
+```shell
# Setup
npm install http-server -g;
diff --git a/screenshots/ConnectionTypes.png b/screenshots/ConnectionTypes.png
index 428b4f8..a57f090 100644
Binary files a/screenshots/ConnectionTypes.png and b/screenshots/ConnectionTypes.png differ
diff --git a/scripts/readme_images.js b/scripts/readme_images.js
index 43379a8..6ab372e 100644
--- a/scripts/readme_images.js
+++ b/scripts/readme_images.js
@@ -18,7 +18,7 @@
}
const SAMPLE_REGEX = new RegExp(
- /
]*>[\s]*```([^]+?)```/g
+ /
]*>[\s]*```(?!shell).*\n([^]+?)```/g
);
function findSamples(content) {
diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js
index defdc58..169af6b 100644
--- a/scripts/sequence/CodeMirrorMode.js
+++ b/scripts/sequence/CodeMirrorMode.js
@@ -46,24 +46,57 @@ define(['core/ArrayUtilities'], (array) => {
};
}
- const CM_CONNECT = {type: 'keyword', suggest: true, then: {
- '+': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}},
- '-': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}},
- '': CM_AGENT_TO_OPTTEXT,
- }};
+ function makeCMOperatorBlock(exit) {
+ const op = {type: 'operator', suggest: true, then: {
+ '+': CM_ERROR,
+ '-': 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: {
- '->': CM_CONNECT,
- '-->': CM_CONNECT,
- '<-': CM_CONNECT,
- '<--': CM_CONNECT,
- '<->': CM_CONNECT,
- '<-->': CM_CONNECT,
- ':': {type: 'operator', suggest: true, override: 'Label', then: {}},
- '': 0,
- }};
+ function makeCMConnect() {
+ const connect = {
+ type: 'keyword',
+ suggest: true,
+ then: makeCMOperatorBlock(CM_AGENT_TO_OPTTEXT),
+ };
- 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: {
'': CM_TEXT_TO_END,
}},
@@ -134,10 +167,7 @@ define(['core/ArrayUtilities'], (array) => {
}},
}},
}},
- '+': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}},
- '-': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}},
- '': CM_CONNECT_FULL,
- }};
+ }, makeCMConnect())};
function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) {
diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js
index f951689..791c301 100644
--- a/scripts/sequence/Generator.js
+++ b/scripts/sequence/Generator.js
@@ -25,8 +25,8 @@ define(['core/ArrayUtilities'], (array) => {
return agent.name;
}
- function agentHasFlag(flag) {
- return (agent) => agent.flags.includes(flag);
+ function agentHasFlag(flag, has = true) {
+ return (agent) => (agent.flags.includes(flag) === has);
}
const MERGABLE = {
@@ -235,7 +235,7 @@ define(['core/ArrayUtilities'], (array) => {
array.mergeSets(this.agents, agents, agentEqCheck);
}
- setAgentVis(agents, visible, mode, checked = false) {
+ setAgentVisRaw(agents, visible, mode, checked = false) {
const filteredAgents = agents.filter((agent) => {
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
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) {
const filteredAgents = agents.filter((agent) => {
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
@@ -340,15 +349,24 @@ define(['core/ArrayUtilities'], (array) => {
}
handleConnect({agents, label, options}) {
- const colAgents = agents.map(convertAgent);
- this.addStage(this.setAgentVis(colAgents, true, 'box'));
- this.defineAgents(colAgents);
+ const beginAgents = agents.filter(agentHasFlag('begin'));
+ const endAgents = agents.filter(agentHasFlag('end'));
+ if(array.hasIntersection(beginAgents, endAgents, agentEqCheck)) {
+ throw new Error('Cannot set agent visibility multiple times');
+ }
const startAgents = agents.filter(agentHasFlag('start'));
const stopAgents = agents.filter(agentHasFlag('stop'));
+ array.mergeSets(stopAgents, endAgents);
if(array.hasIntersection(startAgents, stopAgents, agentEqCheck)) {
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 = {
type: 'connect',
agentNames: agents.map(getAgentName),
@@ -357,9 +375,11 @@ define(['core/ArrayUtilities'], (array) => {
};
this.addParallelStages([
+ this.setAgentVis(beginAgents, true, 'box', true),
this.setAgentHighlight(startAgents, true, true),
connectStage,
this.setAgentHighlight(stopAgents, false, true),
+ this.setAgentVis(endAgents, false, 'cross', true),
]);
}
@@ -371,7 +391,7 @@ define(['core/ArrayUtilities'], (array) => {
colAgents = agents.map(convertAgent);
}
- this.addStage(this.setAgentVis(colAgents, true, 'box'));
+ this.addStage(this.setAgentVisRaw(colAgents, true, 'box'));
this.defineAgents(colAgents);
this.addStage({
@@ -383,23 +403,17 @@ define(['core/ArrayUtilities'], (array) => {
}
handleAgentDefine({agents}) {
- const colAgents = agents.map(convertAgent);
- this.defineAgents(colAgents);
+ this.defineAgents(agents.map(convertAgent));
}
handleAgentBegin({agents, mode}) {
- this.addStage(this.setAgentVis(
- agents.map(convertAgent),
- true,
- mode,
- true
- ));
+ this.addStage(this.setAgentVis(agents, true, mode, true));
}
handleAgentEnd({agents, mode}) {
this.addParallelStages([
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.setAgentHighlight(this.agents, false),
- this.setAgentVis(this.agents, false, terminators),
+ this.setAgentVisRaw(this.agents, false, terminators),
]);
addBounds(
diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js
index ca1cf6e..4f4ed59 100644
--- a/scripts/sequence/Generator_spec.js
+++ b/scripts/sequence/Generator_spec.js
@@ -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', () => {
expect(() => generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]),
@@ -437,6 +482,17 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{name: 'A', flags: ['stop']},
]),
]})).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', () => {
diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js
index 6ee861d..2aaaf69 100644
--- a/scripts/sequence/Parser.js
+++ b/scripts/sequence/Parser.js
@@ -26,8 +26,10 @@ define([
};
const CONNECT_AGENT_FLAGS = {
+ '*': 'begin',
'+': 'start',
'-': 'stop',
+ '!': 'end',
};
const TERMINATOR_TYPES = [
@@ -116,13 +118,20 @@ define([
const flags = [];
let p = start;
for(; p < end; ++ p) {
- const flag = flagTypes[tokenKeyword(line[p])];
+ const rawFlag = tokenKeyword(line[p]);
+ const flag = flagTypes[rawFlag];
if(flag) {
+ if(flags.includes(flag)) {
+ throw new Error('Duplicate agent flag: ' + rawFlag);
+ }
flags.push(flag);
} else {
break;
}
}
+ if(p >= end) {
+ throw new Error('Missing agent name');
+ }
return {
name: joinLabel(line, p, end),
flags,
diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js
index 5b06a15..503428d 100644
--- a/scripts/sequence/Parser_spec.js
+++ b/scripts/sequence/Parser_spec.js
@@ -79,13 +79,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
});
it('parses optional flags', () => {
- const parsed = parser.parse('+A -> -B');
+ const parsed = parser.parse('+A -> -*!B');
expect(parsed.stages).toEqual([
{
type: 'connect',
agents: [
{name: 'A', flags: ['start']},
- {name: 'B', flags: ['stop']},
+ {name: 'B', flags: ['stop', 'begin', 'end']},
],
label: 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', () => {
const parsed = parser.parse('A -> B\nB -> A');
expect(parsed.stages).toEqual([
diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js
index 942ec85..5f189dc 100644
--- a/scripts/sequence/Renderer.js
+++ b/scripts/sequence/Renderer.js
@@ -161,10 +161,10 @@ define([
const agentSpaces = new Map();
const agentNames = this.visibleAgents.slice();
- const addSpacing = (agentName, spacing) => {
+ const addSpacing = (agentName, {left, right}) => {
const current = agentSpaces.get(agentName);
- current.left = Math.max(current.left, spacing.left);
- current.right = Math.max(current.right, spacing.right);
+ current.left = Math.max(current.left, left);
+ current.right = Math.max(current.right, right);
};
this.agentInfos.forEach((agentInfo) => {
@@ -366,7 +366,7 @@ define([
const touchedAgentNames = [];
stages.forEach((stage) => {
const component = this.components.get(stage.type);
- const r = component.renderPre(stage, envPre);
+ const r = component.renderPre(stage, envPre) || {};
if(r.topShift !== undefined) {
maxTopShift = Math.max(maxTopShift, r.topShift);
}
@@ -460,6 +460,7 @@ define([
x: null,
latestYStart: null,
currentRad: 0,
+ currentMaxRad: 0,
latestY: 0,
maxRPad: 0,
maxLPad: 0,
diff --git a/scripts/sequence/Tokeniser.js b/scripts/sequence/Tokeniser.js
index 4114459..7105d12 100644
--- a/scripts/sequence/Tokeniser.js
+++ b/scripts/sequence/Tokeniser.js
@@ -31,10 +31,13 @@ define(['./CodeMirrorMode'], (CMMode) => {
unescape,
baseToken: {q: true},
},
- {start: /(?=[^ \t\r\n:+\-<>,])/y, end: /(?=[ \t\r\n:+\-<>,])|$/y},
- {start: /(?=[+\-<>])/y, end: /(?=[^+\-<>])|$/y},
+ {start: /(?=[^ \t\r\n:+\-*!<>,])/y, end: /(?=[ \t\r\n:+\-*!<>,])|$/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: /\n/y, baseToken: {v: '\n'}},
];
diff --git a/scripts/sequence/components/AgentCap.js b/scripts/sequence/components/AgentCap.js
index c492cf5..95e0a13 100644
--- a/scripts/sequence/components/AgentCap.js
+++ b/scripts/sequence/components/AgentCap.js
@@ -23,11 +23,18 @@ define([
return {
left: width / 2,
right: width / 2,
+ radius: width / 2,
};
}
- topShift() {
- return 0;
+ topShift({label}, env) {
+ 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) {
@@ -57,6 +64,7 @@ define([
return {
left: config.size / 2,
right: config.size / 2,
+ radius: 0,
};
}
@@ -98,6 +106,7 @@ define([
return {
left: width / 2,
right: width / 2,
+ radius: width / 2,
};
}
@@ -131,7 +140,11 @@ define([
class CapNone {
separation({currentRad}) {
- return {left: currentRad, right: currentRad};
+ return {
+ left: currentRad,
+ right: currentRad,
+ radius: currentRad,
+ };
}
topShift(agentInfo, env) {
@@ -162,18 +175,24 @@ define([
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) {
if(this.begin) {
array.mergeSets(env.visibleAgents, agentNames);
} else {
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) {
@@ -182,6 +201,9 @@ define([
const agentInfo = env.agentInfos.get(name);
const topShift = AGENT_CAPS[mode].topShift(agentInfo, env);
maxTopShift = Math.max(maxTopShift, topShift);
+
+ const r = AGENT_CAPS[mode].separation(agentInfo, env).radius;
+ agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
return {
agentNames,
diff --git a/scripts/sequence/components/AgentHighlight.js b/scripts/sequence/components/AgentHighlight.js
index b379283..314fd94 100644
--- a/scripts/sequence/components/AgentHighlight.js
+++ b/scripts/sequence/components/AgentHighlight.js
@@ -2,21 +2,32 @@ define(['./BaseComponent'], (BaseComponent) => {
'use strict';
class AgentHighlight extends BaseComponent {
+ radius(highlighted, env) {
+ return highlighted ? env.theme.agentLineHighlightRadius : 0;
+ }
+
separationPre({agentNames, highlighted}, env) {
- const rad = highlighted ? env.theme.agentLineHighlightRadius : 0;
+ const r = this.radius(highlighted, env);
agentNames.forEach((name) => {
const agentInfo = env.agentInfos.get(name);
- const maxRad = Math.max(agentInfo.currentMaxRad, rad);
- agentInfo.currentRad = rad;
- agentInfo.currentMaxRad = maxRad;
+ agentInfo.currentRad = r;
+ agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
+ });
+ }
+
+ 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) {
- const rad = highlighted ? env.theme.agentLineHighlightRadius : 0;
+ const r = this.radius(highlighted, env);
agentNames.forEach((name) => {
env.drawAgentLine(name, env.primaryY);
- env.agentInfos.get(name).currentRad = rad;
+ env.agentInfos.get(name).currentRad = r;
});
}
}
diff --git a/scripts/sequence/components/BaseComponent.js b/scripts/sequence/components/BaseComponent.js
index 89ebab3..6223e84 100644
--- a/scripts/sequence/components/BaseComponent.js
+++ b/scripts/sequence/components/BaseComponent.js
@@ -35,7 +35,6 @@ define(() => {
textSizer,
state,
}*/) {
- return {};
}
render(/*stage, {
@@ -49,7 +48,6 @@ define(() => {
SVGTextBlockClass,
state,
}*/) {
- return 0;
}
}
diff --git a/scripts/sequence/components/Connect.js b/scripts/sequence/components/Connect.js
index 1af5be6..ab5d4c5 100644
--- a/scripts/sequence/components/Connect.js
+++ b/scripts/sequence/components/Connect.js
@@ -83,7 +83,7 @@ define([
config.label.margin.bottom
);
- const lineX = from.x + from.currentRad;
+ const lineX = from.x + from.currentMaxRad;
const y0 = env.primaryY;
const x0 = (
lineX +
@@ -159,8 +159,8 @@ define([
config.label.margin.bottom
);
- const x0 = from.x + from.currentRad * dir;
- const x1 = to.x - to.currentRad * dir;
+ const x0 = from.x + from.currentMaxRad * dir;
+ const x1 = to.x - to.currentMaxRad * dir;
let y = env.primaryY;
SVGShapes.renderBoxedText(label, {
diff --git a/scripts/sequence/components/Note.js b/scripts/sequence/components/Note.js
index 4f41388..bc705f9 100644
--- a/scripts/sequence/components/Note.js
+++ b/scripts/sequence/components/Note.js
@@ -131,8 +131,8 @@ define(['./BaseComponent'], (BaseComponent) => {
const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right);
return this.renderNote({
- x0: infoL.x - infoL.currentRad - config.overlap.left,
- x1: infoR.x + infoR.currentRad + config.overlap.right,
+ x0: infoL.x - infoL.currentMaxRad - config.overlap.left,
+ x1: infoR.x + infoR.currentMaxRad + config.overlap.right,
anchor: 'middle',
mode,
label,
@@ -186,7 +186,7 @@ define(['./BaseComponent'], (BaseComponent) => {
const {left, right} = findExtremes(env.agentInfos, agentNames);
if(this.isRight) {
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({
x0,
anchor: 'start',
@@ -195,7 +195,7 @@ define(['./BaseComponent'], (BaseComponent) => {
}, env);
} else {
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({
x1,
anchor: 'end',
@@ -232,8 +232,8 @@ define(['./BaseComponent'], (BaseComponent) => {
const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right);
const xMid = (
- infoL.x + infoL.currentRad +
- infoR.x - infoR.currentRad
+ infoL.x + infoL.currentMaxRad +
+ infoR.x - infoR.currentMaxRad
) / 2;
return this.renderNote({
diff --git a/scripts/sequence/sequence_integration_spec.js b/scripts/sequence/sequence_integration_spec.js
index bbb8667..0a772c5 100644
--- a/scripts/sequence/sequence_integration_spec.js
+++ b/scripts/sequence/sequence_integration_spec.js
@@ -108,176 +108,32 @@ defineDescribe('Sequence Integration', [
);
});
- it('Renders the "Simple Usage" example without error', () => {
- const parsed = parser.parse(
- '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();
- });
+ const SAMPLE_REGEX = new RegExp(
+ /```(?!shell).*\n([^]+?)```/g
+ );
- it('Renders the "Connection Types" example without error', () => {
- const parsed = parser.parse(
- 'title Connection Types\n' +
- '\n' +
- 'Foo -> Bar: Simple arrow\n' +
- 'Foo --> Bar: Dashed arrow\n' +
- 'Foo <- Bar: Reversed arrow\n' +
- 'Foo <-- Bar: Reversed dashed arrow\n' +
- 'Foo <-> Bar: Double arrow\n' +
- 'Foo <--> Bar: Double dashed arrow\n' +
- '\n' +
- '# 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();
- });
+ function findSamples(content) {
+ SAMPLE_REGEX.lastIndex = 0;
+ const results = [];
+ while(true) {
+ const match = SAMPLE_REGEX.exec(content);
+ if(!match) {
+ break;
+ }
+ results.push(match[1]);
+ }
+ return results;
+ }
- it('Renders the "Notes & State" example without error', () => {
- const parsed = parser.parse(
- 'title Note Placements\n' +
- '\n' +
- 'note over Foo: Foo says something\n' +
- 'note left of Foo: Stuff\n' +
- 'note right of Bar: More stuff\n' +
- 'note over Foo, Bar: "Foo and Bar\n' +
- '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();
- });
+ return (fetch('README.md')
+ .then((response) => response.text())
+ .then(findSamples)
+ .then((samples) => samples.forEach((code, i) => {
+ it('Renders readme example #' + (i + 1) + ' without error', () => {
+ const parsed = parser.parse(code);
+ const sequence = generator.generate(parsed);
+ expect(() => renderer.render(sequence)).not.toThrow();
+ });
+ }))
+ );
});
diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js
index 12efaa6..856ab6b 100644
--- a/scripts/sequence/themes/Basic.js
+++ b/scripts/sequence/themes/Basic.js
@@ -18,6 +18,7 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => {
right: 10,
bottom: 5,
},
+ arrowBottom: 5 + 12 * 1.3 / 2,
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',