diff --git a/README.md b/README.md
index bbc6b43..4ef3f9a 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,19 @@ terminators bar
# (options are: box, bar, cross, none)
```
+### Agent Aliases
+
+
+
+```
+define My complicated agent name as A
+define "Another agent name,
+and this one's multi-line!" as B
+
+A -> B: this is much easier
+A <- B: than writing the whole name
+```
+
### Alternative Agent Ordering
diff --git a/screenshots/AgentAliases.png b/screenshots/AgentAliases.png
new file mode 100644
index 0000000..0724670
Binary files /dev/null and b/screenshots/AgentAliases.png differ
diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js
index 1ed55d4..258fbb4 100644
--- a/scripts/sequence/CodeMirrorMode.js
+++ b/scripts/sequence/CodeMirrorMode.js
@@ -1,183 +1,195 @@
define(['core/ArrayUtilities'], (array) => {
'use strict';
- const CM_END = {type: '', suggest: '\n', then: {}};
- const CM_HIDDEN_END = {type: '', then: {}};
const CM_ERROR = {type: 'error line-error', then: {'': 0}};
- function makeCMCommaBlock(type, suggest, exits = {}) {
- return {type, suggest, then: Object.assign({
+ const CM_COMMANDS = ((() => {
+ const end = {type: '', suggest: '\n', then: {}};
+ const hiddenEnd = {type: '', then: {}};
+
+ const ARROWS = [
+ '->', '-->',
+ '<-', '<--',
+ '<->', '<-->',
+ ];
+
+ const textToEnd = {type: 'string', then: {'': 0, '\n': end}};
+ const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
'': 0,
- ',': {type: 'operator', suggest: true, then: {
- '': 1,
+ 'as': {type: 'keyword', suggest: true, then: {
+ '': {type: 'variable', suggest: 'Agent', then: {
+ '': 0,
+ ',': {type: 'operator', suggest: true, then: {'': 3}},
+ '\n': end,
+ }},
}},
- }, exits)};
- }
-
- const CM_TEXT_TO_END = {type: 'string', then: {'': 0, '\n': CM_END}};
- const CM_AGENT_LIST_TO_END = makeCMCommaBlock('variable', 'Agent', {
- '\n': CM_END,
- });
- const CM_AGENT_LIST_TO_TEXT = makeCMCommaBlock('variable', 'Agent', {
- ':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}},
- });
- const CM_AGENT_TO_OPTTEXT = {type: 'variable', suggest: 'Agent', then: {
- '': 0,
- ':': {type: 'operator', suggest: true, then: {
- '': CM_TEXT_TO_END,
- '\n': CM_HIDDEN_END,
- }},
- '\n': CM_END,
- }};
-
- function makeCMSideNote(side) {
- return {
- type: 'keyword',
- suggest: [side + ' of ', side + ': '],
- then: {
- 'of': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_TEXT,
- }},
- ':': {type: 'operator', suggest: true, then: {
- '': CM_TEXT_TO_END,
- }},
- '': CM_AGENT_LIST_TO_TEXT,
- },
- };
- }
-
- function makeCMOperatorBlock(exit) {
- const op = {type: 'operator', suggest: true, then: {
- '+': CM_ERROR,
- '-': CM_ERROR,
- '*': CM_ERROR,
- '!': CM_ERROR,
- '': exit,
+ ',': {type: 'operator', suggest: true, then: {'': 1}},
+ '\n': end,
}};
- return {
- '+': {type: 'operator', suggest: true, then: {
+ const agentListToEnd = {type: 'variable', suggest: 'Agent', then: {
+ '': 0,
+ ',': {type: 'operator', suggest: true, then: {'': 1}},
+ ':': {type: 'operator', suggest: true, then: {'': textToEnd}},
+ }};
+ const agentToOptText = {type: 'variable', suggest: 'Agent', then: {
+ '': 0,
+ ':': {type: 'operator', suggest: true, then: {
+ '': textToEnd,
+ '\n': hiddenEnd,
+ }},
+ '\n': end,
+ }};
+
+ function makeSideNote(side) {
+ return {
+ type: 'keyword',
+ suggest: [side + ' of ', side + ': '],
+ then: {
+ 'of': {type: 'keyword', suggest: true, then: {
+ '': agentListToEnd,
+ }},
+ ':': {type: 'operator', suggest: true, then: {
+ '': textToEnd,
+ }},
+ '': agentListToEnd,
+ },
+ };
+ }
+
+ function makeOpBlock(exit) {
+ const op = {type: 'operator', suggest: true, then: {
'+': CM_ERROR,
'-': CM_ERROR,
- '*': op,
+ '*': CM_ERROR,
'!': CM_ERROR,
'': exit,
- }},
- '-': {type: 'operator', suggest: true, then: {
- '+': CM_ERROR,
- '-': CM_ERROR,
- '*': op,
- '!': {type: 'operator', then: {
+ }};
+ return {
+ '+': {type: 'operator', suggest: true, then: {
'+': CM_ERROR,
'-': CM_ERROR,
+ '*': op,
+ '!': CM_ERROR,
+ '': exit,
+ }},
+ '-': {type: 'operator', suggest: true, then: {
+ '+': CM_ERROR,
+ '-': CM_ERROR,
+ '*': op,
+ '!': {type: 'operator', then: {
+ '+': CM_ERROR,
+ '-': CM_ERROR,
+ '*': CM_ERROR,
+ '!': CM_ERROR,
+ '': exit,
+ }},
+ '': exit,
+ }},
+ '*': {type: 'operator', suggest: true, then: {
+ '+': op,
+ '-': op,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}},
+ '!': op,
'': exit,
- }},
- '*': {type: 'operator', suggest: true, then: {
- '+': op,
- '-': op,
- '*': CM_ERROR,
- '!': CM_ERROR,
- '': exit,
- }},
- '!': op,
- '': exit,
- };
- }
+ };
+ }
- function makeCMConnect() {
- const connect = {
- type: 'keyword',
- suggest: true,
- then: makeCMOperatorBlock(CM_AGENT_TO_OPTTEXT),
- };
+ function makeCMConnect() {
+ const connect = {
+ type: 'keyword',
+ suggest: true,
+ then: makeOpBlock(agentToOptText),
+ };
- return makeCMOperatorBlock({type: 'variable', suggest: 'Agent', then: {
- '->': connect,
- '-->': connect,
- '<-': connect,
- '<--': connect,
- '<->': connect,
- '<-->': connect,
- ':': {type: 'operator', suggest: true, override: 'Label', then: {}},
- '': 0,
- }});
- }
+ const then = {
+ ':': {
+ type: 'operator',
+ suggest: true,
+ override: 'Label',
+ then: {},
+ },
+ '': 0,
+ };
+ ARROWS.forEach((arrow) => (then[arrow] = connect));
+ return makeOpBlock({type: 'variable', suggest: 'Agent', then});
+ }
- const CM_COMMANDS = {type: 'error line-error', then: Object.assign({
- 'title': {type: 'keyword', suggest: true, then: {
- '': CM_TEXT_TO_END,
- }},
- 'terminators': {type: 'keyword', suggest: true, then: {
- 'none': {type: 'keyword', suggest: true, then: {}},
- 'cross': {type: 'keyword', suggest: true, then: {}},
- 'box': {type: 'keyword', suggest: true, then: {}},
- 'bar': {type: 'keyword', suggest: true, then: {}},
- }},
- 'define': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_END,
- }},
- 'begin': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_END,
- }},
- 'end': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_END,
- '\n': CM_END,
- }},
- 'if': {type: 'keyword', suggest: true, then: {
- '': CM_TEXT_TO_END,
- ':': {type: 'operator', suggest: true, then: {
- '': CM_TEXT_TO_END,
+ return {type: 'error line-error', then: Object.assign({
+ 'title': {type: 'keyword', suggest: true, then: {
+ '': textToEnd,
}},
- '\n': CM_END,
- }},
- 'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
- 'if': {type: 'keyword', suggest: 'if: ', then: {
- '': CM_TEXT_TO_END,
+ 'terminators': {type: 'keyword', suggest: true, then: {
+ 'none': {type: 'keyword', suggest: true, then: {}},
+ 'cross': {type: 'keyword', suggest: true, then: {}},
+ 'box': {type: 'keyword', suggest: true, then: {}},
+ 'bar': {type: 'keyword', suggest: true, then: {}},
+ }},
+ 'define': {type: 'keyword', suggest: true, then: {
+ '': aliasListToEnd,
+ }},
+ 'begin': {type: 'keyword', suggest: true, then: {
+ '': aliasListToEnd,
+ }},
+ 'end': {type: 'keyword', suggest: true, then: {
+ '': aliasListToEnd,
+ '\n': end,
+ }},
+ 'if': {type: 'keyword', suggest: true, then: {
+ '': textToEnd,
':': {type: 'operator', suggest: true, then: {
- '': CM_TEXT_TO_END,
+ '': textToEnd,
+ }},
+ '\n': end,
+ }},
+ 'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
+ 'if': {type: 'keyword', suggest: 'if: ', then: {
+ '': textToEnd,
+ ':': {type: 'operator', suggest: true, then: {
+ '': textToEnd,
+ }},
+ }},
+ '\n': end,
+ }},
+ 'repeat': {type: 'keyword', suggest: true, then: {
+ '': textToEnd,
+ ':': {type: 'operator', suggest: true, then: {
+ '': textToEnd,
+ }},
+ '\n': end,
+ }},
+ 'note': {type: 'keyword', suggest: true, then: {
+ 'over': {type: 'keyword', suggest: true, then: {
+ '': agentListToEnd,
+ }},
+ 'left': makeSideNote('left'),
+ 'right': makeSideNote('right'),
+ 'between': {type: 'keyword', suggest: true, then: {
+ '': agentListToEnd,
}},
}},
- '\n': CM_END,
- }},
- 'repeat': {type: 'keyword', suggest: true, then: {
- '': CM_TEXT_TO_END,
- ':': {type: 'operator', suggest: true, then: {
- '': CM_TEXT_TO_END,
- }},
- '\n': CM_END,
- }},
- 'note': {type: 'keyword', suggest: true, then: {
- 'over': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_TEXT,
- }},
- 'left': makeCMSideNote('left'),
- 'right': makeCMSideNote('right'),
- 'between': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_TEXT,
- }},
- }},
- 'state': {type: 'keyword', suggest: 'state over ', then: {
- 'over': {type: 'keyword', suggest: true, then: {
- '': CM_AGENT_LIST_TO_TEXT,
- }},
- }},
- 'text': {type: 'keyword', suggest: true, then: {
- 'left': makeCMSideNote('left'),
- 'right': makeCMSideNote('right'),
- }},
- 'simultaneously': {type: 'keyword', suggest: true, then: {
- ':': {type: 'operator', suggest: true, then: {}},
- 'with': {type: 'keyword', suggest: true, then: {
- '': {type: 'variable', suggest: 'Label', then: {
- '': 0,
- ':': {type: 'operator', suggest: true, then: {}},
+ 'state': {type: 'keyword', suggest: 'state over ', then: {
+ 'over': {type: 'keyword', suggest: true, then: {
+ '': agentListToEnd,
}},
}},
- }},
- }, makeCMConnect())};
+ 'text': {type: 'keyword', suggest: true, then: {
+ 'left': makeSideNote('left'),
+ 'right': makeSideNote('right'),
+ }},
+ 'simultaneously': {type: 'keyword', suggest: true, then: {
+ ':': {type: 'operator', suggest: true, then: {}},
+ 'with': {type: 'keyword', suggest: true, then: {
+ '': {type: 'variable', suggest: 'Label', then: {
+ '': 0,
+ ':': {type: 'operator', suggest: true, then: {}},
+ }},
+ }},
+ }},
+ }, 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 791c301..b1bea09 100644
--- a/scripts/sequence/Generator.js
+++ b/scripts/sequence/Generator.js
@@ -17,10 +17,6 @@ define(['core/ArrayUtilities'], (array) => {
return {name, anchorRight};
}
- function convertAgent(agent) {
- return makeAgent(agent.name);
- }
-
function getAgentName(agent) {
return agent.name;
}
@@ -181,6 +177,7 @@ define(['core/ArrayUtilities'], (array) => {
return class Generator {
constructor() {
this.agentStates = new Map();
+ this.agentAliases = new Map();
this.agents = [];
this.blockCount = 0;
this.nesting = [];
@@ -204,6 +201,29 @@ define(['core/ArrayUtilities'], (array) => {
'block end': this.handleBlockEnd.bind(this),
};
this.handleStage = this.handleStage.bind(this);
+ this.convertAgent = this.convertAgent.bind(this);
+ }
+
+ convertAgent({alias, name}) {
+ if(alias) {
+ if(this.agentAliases.has(name)) {
+ throw new Error(
+ 'Cannot alias ' + name + '; it is already an alias'
+ );
+ }
+ const old = this.agentAliases.get(alias);
+ if(
+ (old && old !== alias) ||
+ this.agents.some((agent) => (agent.name === alias))
+ ) {
+ throw new Error(
+ 'Cannot use ' + alias +
+ ' as an alias; it is already in use'
+ );
+ }
+ this.agentAliases.set(alias, name);
+ }
+ return makeAgent(this.agentAliases.get(name) || name);
}
addStage(stage, isVisible = true) {
@@ -230,13 +250,18 @@ define(['core/ArrayUtilities'], (array) => {
});
}
- defineAgents(agents) {
- array.mergeSets(this.currentNest.agents, agents, agentEqCheck);
- array.mergeSets(this.agents, agents, agentEqCheck);
+ defineAgents(colAgents) {
+ array.mergeSets(this.currentNest.agents, colAgents, agentEqCheck);
+ array.mergeSets(this.agents, colAgents, agentEqCheck);
}
- setAgentVisRaw(agents, visible, mode, checked = false) {
- const filteredAgents = agents.filter((agent) => {
+ setAgentVis(colAgents, visible, mode, checked = false) {
+ const seen = new Set();
+ const filteredAgents = colAgents.filter((agent) => {
+ if(seen.has(agent.name)) {
+ return false;
+ }
+ seen.add(agent.name);
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
if(state.locked) {
if(checked) {
@@ -269,17 +294,8 @@ 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) => {
+ setAgentHighlight(colAgents, highlighted, checked = false) {
+ const filteredAgents = colAgents.filter((agent) => {
const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
if(state.locked) {
if(checked) {
@@ -349,27 +365,44 @@ define(['core/ArrayUtilities'], (array) => {
}
handleConnect({agents, label, options}) {
- const beginAgents = agents.filter(agentHasFlag('begin'));
- const endAgents = agents.filter(agentHasFlag('end'));
+ const beginAgents = (agents
+ .filter(agentHasFlag('begin'))
+ .map(this.convertAgent)
+ );
+ const endAgents = (agents
+ .filter(agentHasFlag('end'))
+ .map(this.convertAgent)
+ );
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'));
+ const startAgents = (agents
+ .filter(agentHasFlag('start'))
+ .map(this.convertAgent)
+ );
+ const stopAgents = (agents
+ .filter(agentHasFlag('stop'))
+ .map(this.convertAgent)
+ );
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 colAgents = agents.map(this.convertAgent);
+ const agentNames = colAgents.map(getAgentName);
+ this.defineAgents(colAgents);
- const implicitBegin = agents.filter(agentHasFlag('begin', false));
+ const implicitBegin = (agents
+ .filter(agentHasFlag('begin', false))
+ .map(this.convertAgent)
+ );
this.addStage(this.setAgentVis(implicitBegin, true, 'box'));
const connectStage = {
type: 'connect',
- agentNames: agents.map(getAgentName),
+ agentNames,
label,
options,
};
@@ -388,32 +421,39 @@ define(['core/ArrayUtilities'], (array) => {
if(agents.length === 0) {
colAgents = NOTE_DEFAULT_AGENTS[type] || [];
} else {
- colAgents = agents.map(convertAgent);
+ colAgents = agents.map(this.convertAgent);
+ }
+ const agentNames = colAgents.map(getAgentName);
+ const uniqueAgents = new Set(agentNames).size;
+ if(type === 'note between' && uniqueAgents < 2) {
+ throw new Error('note between requires at least 2 agents');
}
- this.addStage(this.setAgentVisRaw(colAgents, true, 'box'));
+ this.addStage(this.setAgentVis(colAgents, true, 'box'));
this.defineAgents(colAgents);
this.addStage({
type,
- agentNames: colAgents.map(getAgentName),
+ agentNames,
mode,
label,
});
}
handleAgentDefine({agents}) {
- this.defineAgents(agents.map(convertAgent));
+ this.defineAgents(agents.map(this.convertAgent));
}
handleAgentBegin({agents, mode}) {
- this.addStage(this.setAgentVis(agents, true, mode, true));
+ const colAgents = agents.map(this.convertAgent);
+ this.addStage(this.setAgentVis(colAgents, true, mode, true));
}
handleAgentEnd({agents, mode}) {
+ const colAgents = agents.map(this.convertAgent);
this.addParallelStages([
- this.setAgentHighlight(agents, false),
- this.setAgentVis(agents, false, mode, true),
+ this.setAgentHighlight(colAgents, false),
+ this.setAgentVis(colAgents, false, mode, true),
]);
}
@@ -467,6 +507,7 @@ define(['core/ArrayUtilities'], (array) => {
generate({stages, meta = {}}) {
this.agentStates.clear();
this.markers.clear();
+ this.agentAliases.clear();
this.agents.length = 0;
this.blockCount = 0;
this.nesting.length = 0;
@@ -485,7 +526,7 @@ define(['core/ArrayUtilities'], (array) => {
this.addParallelStages([
this.setAgentHighlight(this.agents, false),
- this.setAgentVisRaw(this.agents, false, terminators),
+ this.setAgentVis(this.agents, false, terminators),
]);
addBounds(
diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js
index 4f4ed59..b5701f4 100644
--- a/scripts/sequence/Generator_spec.js
+++ b/scripts/sequence/Generator_spec.js
@@ -8,7 +8,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
if(typeof item === 'object') {
return item;
} else {
- return {name: item, flags: []};
+ return {name: item, alias: '', flags: []};
}
});
}
@@ -231,6 +231,33 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
+ it('converts aliases', () => {
+ const sequence = generator.generate({stages: [
+ PARSED.defineAgents([{name: 'Baz', alias: 'B', flags: []}]),
+ PARSED.connect(['A', 'B']),
+ ]});
+ expect(sequence.agents).toEqual([
+ {name: '[', anchorRight: true},
+ {name: 'Baz', anchorRight: false},
+ {name: 'A', anchorRight: false},
+ {name: ']', anchorRight: false},
+ ]);
+ });
+
+ it('rejects duplicate aliases', () => {
+ expect(() => generator.generate({stages: [
+ PARSED.defineAgents([{name: 'Foo', alias: 'B', flags: []}]),
+ PARSED.defineAgents([{name: 'Bar', alias: 'B', flags: []}]),
+ ]})).toThrow();
+ });
+
+ it('rejects using agent names as aliases', () => {
+ expect(() => generator.generate({stages: [
+ PARSED.defineAgents([{name: 'Foo', alias: 'B', flags: []}]),
+ PARSED.defineAgents([{name: 'Bar', alias: 'Foo', flags: []}]),
+ ]})).toThrow();
+ });
+
it('creates implicit begin stages for agents when used', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', 'B']),
@@ -347,6 +374,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
+ it('removes duplicate begin agents', () => {
+ const sequence = generator.generate({stages: [
+ PARSED.beginAgents(['A', 'A']),
+ ]});
+ expect(sequence.stages).toEqual([
+ GENERATED.beginAgents(['A']),
+ jasmine.anything(),
+ ]);
+ });
+
it('collapses adjacent begin statements', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', 'B']),
@@ -378,6 +415,17 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
+ it('removes duplicate end agents', () => {
+ const sequence = generator.generate({stages: [
+ PARSED.beginAgents(['A']),
+ PARSED.endAgents(['A', 'A']),
+ ]});
+ expect(sequence.stages).toEqual([
+ jasmine.anything(),
+ GENERATED.endAgents(['A']),
+ ]);
+ });
+
it('removes superfluous end statements', () => {
const sequence = generator.generate({stages: [
PARSED.defineAgents(['E']),
@@ -409,8 +457,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel highlighting stages', () => {
const sequence = generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['start']}]),
- PARSED.connect(['A', {name: 'B', flags: ['stop']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['stop']}]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
@@ -428,7 +476,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel begin stages', () => {
const sequence = generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['begin']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['begin']}]),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
@@ -442,7 +490,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel end stages', () => {
const sequence = generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['end']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['end']}]),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A', 'B']),
@@ -456,8 +504,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['end']}]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
@@ -472,32 +520,41 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
});
it('rejects conflicting flags', () => {
- expect(() => generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]),
- ]})).toThrow();
-
expect(() => generator.generate({stages: [
PARSED.connect([
- {name: 'A', flags: ['start']},
- {name: 'A', flags: ['stop']},
+ 'A',
+ {name: 'B', alias: '', flags: ['start', 'stop']},
]),
]})).toThrow();
expect(() => generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['begin', 'end']}]),
+ PARSED.connect([
+ {name: 'A', alias: '', flags: ['start']},
+ {name: 'A', alias: '', flags: ['stop']},
+ ]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect([
- {name: 'A', flags: ['begin']},
- {name: 'A', flags: ['end']},
+ 'A',
+ {name: 'B', alias: '', flags: ['begin', 'end']},
+ ]),
+ ]})).toThrow();
+
+ expect(() => generator.generate({stages: [
+ PARSED.connect([
+ {name: 'A', alias: '', flags: ['begin']},
+ {name: 'A', alias: '', flags: ['end']},
]),
]})).toThrow();
});
it('adds implicit highlight end with implicit terminator', () => {
const sequence = generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['start']}]),
+ PARSED.connect([
+ 'A',
+ {name: 'B', alias: '', flags: ['start']},
+ ]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
@@ -511,7 +568,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds implicit highlight end with explicit terminator', () => {
const sequence = generator.generate({stages: [
- PARSED.connect(['A', {name: 'B', flags: ['start']}]),
+ PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.endAgents(['A', 'B']),
]});
expect(sequence.stages).toEqual([
@@ -527,8 +584,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('collapses adjacent end statements containing highlighting', () => {
const sequence = generator.generate({stages: [
PARSED.connect([
- {name: 'A', flags: ['start']},
- {name: 'B', flags: ['start']},
+ {name: 'A', alias: '', flags: ['start']},
+ {name: 'B', alias: '', flags: ['start']},
]),
PARSED.endAgents(['A']),
PARSED.endAgents(['B']),
@@ -849,6 +906,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
+ it('rejects note between with a repeated agent', () => {
+ expect(() => generator.generate({stages: [
+ PARSED.note('note between', ['A', 'A'], {
+ mode: 'foo',
+ label: 'bar',
+ }),
+ ]})).toThrow();
+ });
+
it('defaults to showing notes around the entire diagram', () => {
const sequence = generator.generate({stages: [
PARSED.note('note right', []),
diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js
index 2aaaf69..1992ec9 100644
--- a/scripts/sequence/Parser.js
+++ b/scripts/sequence/Parser.js
@@ -114,7 +114,27 @@ define([
return -1;
}
- function readAgent(line, start, end, flagTypes = {}) {
+ function readAgentAlias(line, start, end, enableAlias) {
+ let aliasSep = -1;
+ if(enableAlias) {
+ aliasSep = findToken(line, 'as', start);
+ }
+ if(aliasSep === -1 || aliasSep >= end) {
+ aliasSep = end;
+ }
+ if(start >= aliasSep) {
+ throw new Error('Missing agent name');
+ }
+ return {
+ name: joinLabel(line, start, aliasSep),
+ alias: joinLabel(line, aliasSep + 1, end),
+ };
+ }
+
+ function readAgent(line, start, end, {
+ flagTypes = {},
+ aliases = false,
+ } = {}) {
const flags = [];
let p = start;
for(; p < end; ++ p) {
@@ -129,23 +149,22 @@ define([
break;
}
}
- if(p >= end) {
- throw new Error('Missing agent name');
- }
+ const {name, alias} = readAgentAlias(line, p, end, aliases);
return {
- name: joinLabel(line, p, end),
+ name,
+ alias,
flags,
};
}
- function readAgentList(line, start, end, flagTypes) {
+ function readAgentList(line, start, end, readAgentOpts) {
const list = [];
let currentStart = -1;
for(let i = start; i < end; ++ i) {
const token = line[i];
if(tokenKeyword(token) === ',') {
if(currentStart !== -1) {
- list.push(readAgent(line, currentStart, i, flagTypes));
+ list.push(readAgent(line, currentStart, i, readAgentOpts));
currentStart = -1;
}
} else if(currentStart === -1) {
@@ -153,7 +172,7 @@ define([
}
}
if(currentStart !== -1) {
- list.push(readAgent(line, currentStart, end, flagTypes));
+ list.push(readAgent(line, currentStart, end, readAgentOpts));
}
return list;
}
@@ -208,7 +227,7 @@ define([
return null;
}
return Object.assign({
- agents: readAgentList(line, 1, line.length),
+ agents: readAgentList(line, 1, line.length, {aliases: true}),
}, type);
},
@@ -280,11 +299,14 @@ define([
if(typePos <= 0 || typePos >= labelSep - 1) {
return null;
}
+ const readAgentOpts = {
+ flagTypes: CONNECT_AGENT_FLAGS,
+ };
return {
type: 'connect',
agents: [
- readAgent(line, 0, typePos, CONNECT_AGENT_FLAGS),
- readAgent(line, typePos + 1, labelSep, CONNECT_AGENT_FLAGS),
+ readAgent(line, 0, typePos, readAgentOpts),
+ readAgent(line, typePos + 1, labelSep, readAgentOpts),
],
label: joinLabel(line, labelSep + 1),
options,
diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js
index 503428d..fdc6171 100644
--- a/scripts/sequence/Parser_spec.js
+++ b/scripts/sequence/Parser_spec.js
@@ -12,7 +12,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
} = {}) => {
return {
type: 'connect',
- agents: agentNames.map((name) => ({name, flags: []})),
+ agents: agentNames.map((name) => ({
+ name,
+ alias: '',
+ flags: [],
+ })),
label,
options: {
line,
@@ -64,6 +68,15 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
]);
});
+ it('propagates aliases', () => {
+ const parsed = parser.parse('define Foo Bar as A B');
+ expect(parsed.stages).toEqual([
+ {type: 'agent define', agents: [
+ {name: 'Foo Bar', alias: 'A B', flags: []},
+ ]},
+ ]);
+ });
+
it('respects spacing within agent names', () => {
const parsed = parser.parse('A+B -> C D');
expect(parsed.stages).toEqual([
@@ -84,8 +97,12 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
{
type: 'connect',
agents: [
- {name: 'A', flags: ['start']},
- {name: 'B', flags: ['stop', 'begin', 'end']},
+ {name: 'A', alias: '', flags: ['start']},
+ {name: 'B', alias: '', flags: [
+ 'stop',
+ 'begin',
+ 'end',
+ ]},
],
label: jasmine.anything(),
options: jasmine.anything(),
@@ -192,7 +209,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A: hello there');
expect(parsed.stages).toEqual([{
type: 'note over',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
}]);
@@ -209,31 +226,34 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([
{
type: 'note left',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note left',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note right',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note right',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'note',
label: 'hello there',
},
{
type: 'note between',
- agents: [{name: 'A', flags: []}, {name: 'B', flags: []}],
+ agents: [
+ {name: 'A', alias: '', flags: []},
+ {name: 'B', alias: '', flags: []},
+ ],
mode: 'note',
label: 'hi',
},
@@ -244,7 +264,10 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A B, C D: hi');
expect(parsed.stages).toEqual([{
type: 'note over',
- agents: [{name: 'A B', flags: []}, {name: 'C D', flags: []}],
+ agents: [
+ {name: 'A B', alias: '', flags: []},
+ {name: 'C D', alias: '', flags: []},
+ ],
mode: 'note',
label: 'hi',
}]);
@@ -258,7 +281,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('state over A: doing stuff');
expect(parsed.stages).toEqual([{
type: 'note over',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'state',
label: 'doing stuff',
}]);
@@ -272,7 +295,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('text right of A: doing stuff');
expect(parsed.stages).toEqual([{
type: 'note right',
- agents: [{name: 'A', flags: []}],
+ agents: [{name: 'A', alias: '', flags: []}],
mode: 'text',
label: 'doing stuff',
}]);
@@ -287,16 +310,25 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([
{
type: 'agent define',
- agents: [{name: 'A', flags: []}, {name: 'B', flags: []}],
+ agents: [
+ {name: 'A', alias: '', flags: []},
+ {name: 'B', alias: '', flags: []},
+ ],
},
{
type: 'agent begin',
- agents: [{name: 'A', flags: []}, {name: 'B', flags: []}],
+ agents: [
+ {name: 'A', alias: '', flags: []},
+ {name: 'B', alias: '', flags: []},
+ ],
mode: 'box',
},
{
type: 'agent end',
- agents: [{name: 'A', flags: []}, {name: 'B', flags: []}],
+ agents: [
+ {name: 'A', alias: '', flags: []},
+ {name: 'B', alias: '', flags: []},
+ ],
mode: 'cross',
},
]);
diff --git a/scripts/sequence/components/Note.js b/scripts/sequence/components/Note.js
index bc705f9..742c8e1 100644
--- a/scripts/sequence/components/Note.js
+++ b/scripts/sequence/components/Note.js
@@ -103,11 +103,10 @@ define(['./BaseComponent'], (BaseComponent) => {
config.padding.right
);
- if(agentNames.length > 1) {
- const {left, right} = findExtremes(env.agentInfos, agentNames);
- const infoL = env.agentInfos.get(left);
- const infoR = env.agentInfos.get(right);
-
+ const {left, right} = findExtremes(env.agentInfos, agentNames);
+ const infoL = env.agentInfos.get(left);
+ const infoR = env.agentInfos.get(right);
+ if(infoL !== infoR) {
const hangL = infoL.currentMaxRad + config.overlap.left;
const hangR = infoR.currentMaxRad + config.overlap.right;
@@ -116,7 +115,7 @@ define(['./BaseComponent'], (BaseComponent) => {
env.addSpacing(left, {left: hangL, right: 0});
env.addSpacing(right, {left: 0, right: hangR});
} else {
- env.addSpacing(agentNames[0], {
+ env.addSpacing(left, {
left: width / 2,
right: width / 2,
});
@@ -126,10 +125,10 @@ define(['./BaseComponent'], (BaseComponent) => {
render({agentNames, mode, label}, env) {
const config = env.theme.note[mode];
- if(agentNames.length > 1) {
- const {left, right} = findExtremes(env.agentInfos, agentNames);
- const infoL = env.agentInfos.get(left);
- const infoR = env.agentInfos.get(right);
+ const {left, right} = findExtremes(env.agentInfos, agentNames);
+ const infoL = env.agentInfos.get(left);
+ const infoR = env.agentInfos.get(right);
+ if(infoL !== infoR) {
return this.renderNote({
x0: infoL.x - infoL.currentMaxRad - config.overlap.left,
x1: infoR.x + infoR.currentMaxRad + config.overlap.right,
@@ -138,7 +137,7 @@ define(['./BaseComponent'], (BaseComponent) => {
label,
}, env);
} else {
- const xMid = env.agentInfos.get(agentNames[0]).x;
+ const xMid = infoL.x;
return this.renderNote({
xMid,
anchor: 'middle',
diff --git a/scripts/tester/jshintRunner.js b/scripts/tester/jshintRunner.js
index 4306364..a3331ac 100644
--- a/scripts/tester/jshintRunner.js
+++ b/scripts/tester/jshintRunner.js
@@ -34,7 +34,7 @@ define(['jshintConfig', 'specs'], (jshintConfig) => {
const OPTS_TEST = Object.assign({}, jshintConfig, {
predef: PREDEF_TEST,
- maxstatements: 50, // allow lots of tests
+ maxstatements: 100, // allow lots of tests
});
function formatError(error) {
diff --git a/styles/main.css b/styles/main.css
index 36e38a0..9f20cbb 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -88,6 +88,7 @@ html, body {
background: #FFFFFF;
font-family: sans-serif;
overflow: hidden;
+ user-select: none;
}
.options.links {