Add support for aliases, and fix various issues when using the same agent multiple times in a statement [#19]

This commit is contained in:
David Evans 2017-11-05 17:10:06 +00:00
parent 4384afdd03
commit ddb4430ed2
10 changed files with 431 additions and 245 deletions

View File

@ -150,6 +150,19 @@ terminators bar
# (options are: box, bar, cross, none) # (options are: box, bar, cross, none)
``` ```
### Agent Aliases
<img src="screenshots/AgentAliases.png" alt="Agent Aliases preview" width="200" align="right" />
```
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 ### Alternative Agent Ordering
<img src="screenshots/AlternativeAgentOrdering.png" alt="Alternative Agent Ordering preview" width="150" align="right" /> <img src="screenshots/AlternativeAgentOrdering.png" alt="Alternative Agent Ordering preview" width="150" align="right" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,52 +1,62 @@
define(['core/ArrayUtilities'], (array) => { define(['core/ArrayUtilities'], (array) => {
'use strict'; 'use strict';
const CM_END = {type: '', suggest: '\n', then: {}};
const CM_HIDDEN_END = {type: '', then: {}};
const CM_ERROR = {type: 'error line-error', then: {'': 0}}; const CM_ERROR = {type: 'error line-error', then: {'': 0}};
function makeCMCommaBlock(type, suggest, exits = {}) { const CM_COMMANDS = ((() => {
return {type, suggest, then: Object.assign({ const end = {type: '', suggest: '\n', then: {}};
'': 0, const hiddenEnd = {type: '', then: {}};
',': {type: 'operator', suggest: true, then: {
'': 1,
}},
}, exits)};
}
const CM_TEXT_TO_END = {type: 'string', then: {'': 0, '\n': CM_END}}; const ARROWS = [
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 textToEnd = {type: 'string', then: {'': 0, '\n': end}};
const CM_AGENT_TO_OPTTEXT = {type: 'variable', suggest: 'Agent', then: { const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
'': 0,
'as': {type: 'keyword', suggest: true, then: {
'': {type: 'variable', suggest: 'Agent', then: {
'': 0,
',': {type: 'operator', suggest: true, then: {'': 3}},
'\n': end,
}},
}},
',': {type: 'operator', suggest: true, then: {'': 1}},
'\n': end,
}};
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, '': 0,
':': {type: 'operator', suggest: true, then: { ':': {type: 'operator', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
'\n': CM_HIDDEN_END, '\n': hiddenEnd,
}}, }},
'\n': CM_END, '\n': end,
}}; }};
function makeCMSideNote(side) { function makeSideNote(side) {
return { return {
type: 'keyword', type: 'keyword',
suggest: [side + ' of ', side + ': '], suggest: [side + ' of ', side + ': '],
then: { then: {
'of': {type: 'keyword', suggest: true, then: { 'of': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_TEXT, '': agentListToEnd,
}}, }},
':': {type: 'operator', suggest: true, then: { ':': {type: 'operator', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
}}, }},
'': CM_AGENT_LIST_TO_TEXT, '': agentListToEnd,
}, },
}; };
} }
function makeCMOperatorBlock(exit) { function makeOpBlock(exit) {
const op = {type: 'operator', suggest: true, then: { const op = {type: 'operator', suggest: true, then: {
'+': CM_ERROR, '+': CM_ERROR,
'-': CM_ERROR, '-': CM_ERROR,
@ -91,24 +101,25 @@ define(['core/ArrayUtilities'], (array) => {
const connect = { const connect = {
type: 'keyword', type: 'keyword',
suggest: true, suggest: true,
then: makeCMOperatorBlock(CM_AGENT_TO_OPTTEXT), then: makeOpBlock(agentToOptText),
}; };
return makeCMOperatorBlock({type: 'variable', suggest: 'Agent', then: { const then = {
'->': connect, ':': {
'-->': connect, type: 'operator',
'<-': connect, suggest: true,
'<--': connect, override: 'Label',
'<->': connect, then: {},
'<-->': connect, },
':': {type: 'operator', suggest: true, override: 'Label', then: {}},
'': 0, '': 0,
}}); };
ARROWS.forEach((arrow) => (then[arrow] = connect));
return makeOpBlock({type: 'variable', suggest: 'Agent', then});
} }
const CM_COMMANDS = {type: 'error line-error', then: Object.assign({ return {type: 'error line-error', then: Object.assign({
'title': {type: 'keyword', suggest: true, then: { 'title': {type: 'keyword', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
}}, }},
'terminators': {type: 'keyword', suggest: true, then: { 'terminators': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}}, 'none': {type: 'keyword', suggest: true, then: {}},
@ -117,56 +128,56 @@ define(['core/ArrayUtilities'], (array) => {
'bar': {type: 'keyword', suggest: true, then: {}}, 'bar': {type: 'keyword', suggest: true, then: {}},
}}, }},
'define': {type: 'keyword', suggest: true, then: { 'define': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_END, '': aliasListToEnd,
}}, }},
'begin': {type: 'keyword', suggest: true, then: { 'begin': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_END, '': aliasListToEnd,
}}, }},
'end': {type: 'keyword', suggest: true, then: { 'end': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_END, '': aliasListToEnd,
'\n': CM_END, '\n': end,
}}, }},
'if': {type: 'keyword', suggest: true, then: { 'if': {type: 'keyword', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
':': {type: 'operator', suggest: true, then: { ':': {type: 'operator', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
}}, }},
'\n': CM_END, '\n': end,
}}, }},
'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: { 'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
'if': {type: 'keyword', suggest: 'if: ', then: { 'if': {type: 'keyword', suggest: 'if: ', then: {
'': CM_TEXT_TO_END, '': textToEnd,
':': {type: 'operator', suggest: true, then: { ':': {type: 'operator', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
}}, }},
}}, }},
'\n': CM_END, '\n': end,
}}, }},
'repeat': {type: 'keyword', suggest: true, then: { 'repeat': {type: 'keyword', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
':': {type: 'operator', suggest: true, then: { ':': {type: 'operator', suggest: true, then: {
'': CM_TEXT_TO_END, '': textToEnd,
}}, }},
'\n': CM_END, '\n': end,
}}, }},
'note': {type: 'keyword', suggest: true, then: { 'note': {type: 'keyword', suggest: true, then: {
'over': {type: 'keyword', suggest: true, then: { 'over': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_TEXT, '': agentListToEnd,
}}, }},
'left': makeCMSideNote('left'), 'left': makeSideNote('left'),
'right': makeCMSideNote('right'), 'right': makeSideNote('right'),
'between': {type: 'keyword', suggest: true, then: { 'between': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_TEXT, '': agentListToEnd,
}}, }},
}}, }},
'state': {type: 'keyword', suggest: 'state over ', then: { 'state': {type: 'keyword', suggest: 'state over ', then: {
'over': {type: 'keyword', suggest: true, then: { 'over': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_TEXT, '': agentListToEnd,
}}, }},
}}, }},
'text': {type: 'keyword', suggest: true, then: { 'text': {type: 'keyword', suggest: true, then: {
'left': makeCMSideNote('left'), 'left': makeSideNote('left'),
'right': makeCMSideNote('right'), 'right': makeSideNote('right'),
}}, }},
'simultaneously': {type: 'keyword', suggest: true, then: { 'simultaneously': {type: 'keyword', suggest: true, then: {
':': {type: 'operator', suggest: true, then: {}}, ':': {type: 'operator', suggest: true, then: {}},
@ -178,6 +189,7 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
}}, }},
}, makeCMConnect())}; }, makeCMConnect())};
})());
function cmCappedToken(token, current) { function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) { if(Object.keys(current.then).length > 0) {

View File

@ -17,10 +17,6 @@ define(['core/ArrayUtilities'], (array) => {
return {name, anchorRight}; return {name, anchorRight};
} }
function convertAgent(agent) {
return makeAgent(agent.name);
}
function getAgentName(agent) { function getAgentName(agent) {
return agent.name; return agent.name;
} }
@ -181,6 +177,7 @@ define(['core/ArrayUtilities'], (array) => {
return class Generator { return class Generator {
constructor() { constructor() {
this.agentStates = new Map(); this.agentStates = new Map();
this.agentAliases = new Map();
this.agents = []; this.agents = [];
this.blockCount = 0; this.blockCount = 0;
this.nesting = []; this.nesting = [];
@ -204,6 +201,29 @@ define(['core/ArrayUtilities'], (array) => {
'block end': this.handleBlockEnd.bind(this), 'block end': this.handleBlockEnd.bind(this),
}; };
this.handleStage = this.handleStage.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) { addStage(stage, isVisible = true) {
@ -230,13 +250,18 @@ define(['core/ArrayUtilities'], (array) => {
}); });
} }
defineAgents(agents) { defineAgents(colAgents) {
array.mergeSets(this.currentNest.agents, agents, agentEqCheck); array.mergeSets(this.currentNest.agents, colAgents, agentEqCheck);
array.mergeSets(this.agents, agents, agentEqCheck); array.mergeSets(this.agents, colAgents, agentEqCheck);
} }
setAgentVisRaw(agents, visible, mode, checked = false) { setAgentVis(colAgents, visible, mode, checked = false) {
const filteredAgents = agents.filter((agent) => { 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; const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
if(state.locked) { if(state.locked) {
if(checked) { if(checked) {
@ -269,17 +294,8 @@ define(['core/ArrayUtilities'], (array) => {
}; };
} }
setAgentVis(agents, visible, mode, checked = false) { setAgentHighlight(colAgents, highlighted, checked = false) {
return this.setAgentVisRaw( const filteredAgents = colAgents.filter((agent) => {
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; const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
if(state.locked) { if(state.locked) {
if(checked) { if(checked) {
@ -349,27 +365,44 @@ define(['core/ArrayUtilities'], (array) => {
} }
handleConnect({agents, label, options}) { handleConnect({agents, label, options}) {
const beginAgents = agents.filter(agentHasFlag('begin')); const beginAgents = (agents
const endAgents = agents.filter(agentHasFlag('end')); .filter(agentHasFlag('begin'))
.map(this.convertAgent)
);
const endAgents = (agents
.filter(agentHasFlag('end'))
.map(this.convertAgent)
);
if(array.hasIntersection(beginAgents, endAgents, agentEqCheck)) { if(array.hasIntersection(beginAgents, endAgents, agentEqCheck)) {
throw new Error('Cannot set agent visibility multiple times'); throw new Error('Cannot set agent visibility multiple times');
} }
const startAgents = agents.filter(agentHasFlag('start')); const startAgents = (agents
const stopAgents = agents.filter(agentHasFlag('stop')); .filter(agentHasFlag('start'))
.map(this.convertAgent)
);
const stopAgents = (agents
.filter(agentHasFlag('stop'))
.map(this.convertAgent)
);
array.mergeSets(stopAgents, endAgents); 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 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')); this.addStage(this.setAgentVis(implicitBegin, true, 'box'));
const connectStage = { const connectStage = {
type: 'connect', type: 'connect',
agentNames: agents.map(getAgentName), agentNames,
label, label,
options, options,
}; };
@ -388,32 +421,39 @@ define(['core/ArrayUtilities'], (array) => {
if(agents.length === 0) { if(agents.length === 0) {
colAgents = NOTE_DEFAULT_AGENTS[type] || []; colAgents = NOTE_DEFAULT_AGENTS[type] || [];
} else { } 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.defineAgents(colAgents);
this.addStage({ this.addStage({
type, type,
agentNames: colAgents.map(getAgentName), agentNames,
mode, mode,
label, label,
}); });
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
this.defineAgents(agents.map(convertAgent)); this.defineAgents(agents.map(this.convertAgent));
} }
handleAgentBegin({agents, mode}) { 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}) { handleAgentEnd({agents, mode}) {
const colAgents = agents.map(this.convertAgent);
this.addParallelStages([ this.addParallelStages([
this.setAgentHighlight(agents, false), this.setAgentHighlight(colAgents, false),
this.setAgentVis(agents, false, mode, true), this.setAgentVis(colAgents, false, mode, true),
]); ]);
} }
@ -467,6 +507,7 @@ define(['core/ArrayUtilities'], (array) => {
generate({stages, meta = {}}) { generate({stages, meta = {}}) {
this.agentStates.clear(); this.agentStates.clear();
this.markers.clear(); this.markers.clear();
this.agentAliases.clear();
this.agents.length = 0; this.agents.length = 0;
this.blockCount = 0; this.blockCount = 0;
this.nesting.length = 0; this.nesting.length = 0;
@ -485,7 +526,7 @@ define(['core/ArrayUtilities'], (array) => {
this.addParallelStages([ this.addParallelStages([
this.setAgentHighlight(this.agents, false), this.setAgentHighlight(this.agents, false),
this.setAgentVisRaw(this.agents, false, terminators), this.setAgentVis(this.agents, false, terminators),
]); ]);
addBounds( addBounds(

View File

@ -8,7 +8,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
if(typeof item === 'object') { if(typeof item === 'object') {
return item; return item;
} else { } 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', () => { it('creates implicit begin stages for agents when used', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect(['A', 'B']), 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', () => { it('collapses adjacent begin statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect(['A', 'B']), 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', () => { it('removes superfluous end statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.defineAgents(['E']), PARSED.defineAgents(['E']),
@ -409,8 +457,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel highlighting stages', () => { it('adds parallel highlighting stages', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]), PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', flags: ['stop']}]), PARSED.connect(['A', {name: 'B', alias: '', flags: ['stop']}]),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
@ -428,7 +476,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel begin stages', () => { it('adds parallel begin stages', () => {
const sequence = generator.generate({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([ expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']), GENERATED.beginAgents(['A']),
@ -442,7 +490,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel end stages', () => { it('adds parallel end stages', () => {
const sequence = generator.generate({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([ expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A', 'B']), GENERATED.beginAgents(['A', 'B']),
@ -456,8 +504,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('implicitly ends highlighting when ending a stage', () => { it('implicitly ends highlighting when ending a stage', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]), PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', flags: ['end']}]), PARSED.connect(['A', {name: 'B', alias: '', flags: ['end']}]),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
@ -472,32 +520,41 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
}); });
it('rejects conflicting flags', () => { it('rejects conflicting flags', () => {
expect(() => generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]),
]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.connect([ PARSED.connect([
{name: 'A', flags: ['start']}, 'A',
{name: 'A', flags: ['stop']}, {name: 'B', alias: '', flags: ['start', 'stop']},
]), ]),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ 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(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.connect([ PARSED.connect([
{name: 'A', flags: ['begin']}, 'A',
{name: 'A', flags: ['end']}, {name: 'B', alias: '', flags: ['begin', 'end']},
]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect([
{name: 'A', alias: '', flags: ['begin']},
{name: 'A', alias: '', flags: ['end']},
]), ]),
]})).toThrow(); ]})).toThrow();
}); });
it('adds implicit highlight end with implicit terminator', () => { it('adds implicit highlight end with implicit terminator', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]), PARSED.connect([
'A',
{name: 'B', alias: '', flags: ['start']},
]),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
@ -511,7 +568,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds implicit highlight end with explicit terminator', () => { it('adds implicit highlight end with explicit terminator', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]), PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.endAgents(['A', 'B']), PARSED.endAgents(['A', 'B']),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
@ -527,8 +584,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('collapses adjacent end statements containing highlighting', () => { it('collapses adjacent end statements containing highlighting', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.connect([ PARSED.connect([
{name: 'A', flags: ['start']}, {name: 'A', alias: '', flags: ['start']},
{name: 'B', flags: ['start']}, {name: 'B', alias: '', flags: ['start']},
]), ]),
PARSED.endAgents(['A']), PARSED.endAgents(['A']),
PARSED.endAgents(['B']), 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', () => { it('defaults to showing notes around the entire diagram', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.note('note right', []), PARSED.note('note right', []),

View File

@ -114,7 +114,27 @@ define([
return -1; 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 = []; const flags = [];
let p = start; let p = start;
for(; p < end; ++ p) { for(; p < end; ++ p) {
@ -129,23 +149,22 @@ define([
break; break;
} }
} }
if(p >= end) { const {name, alias} = readAgentAlias(line, p, end, aliases);
throw new Error('Missing agent name');
}
return { return {
name: joinLabel(line, p, end), name,
alias,
flags, flags,
}; };
} }
function readAgentList(line, start, end, flagTypes) { function readAgentList(line, start, end, readAgentOpts) {
const list = []; const list = [];
let currentStart = -1; let currentStart = -1;
for(let i = start; i < end; ++ i) { for(let i = start; i < end; ++ i) {
const token = line[i]; const token = line[i];
if(tokenKeyword(token) === ',') { if(tokenKeyword(token) === ',') {
if(currentStart !== -1) { if(currentStart !== -1) {
list.push(readAgent(line, currentStart, i, flagTypes)); list.push(readAgent(line, currentStart, i, readAgentOpts));
currentStart = -1; currentStart = -1;
} }
} else if(currentStart === -1) { } else if(currentStart === -1) {
@ -153,7 +172,7 @@ define([
} }
} }
if(currentStart !== -1) { if(currentStart !== -1) {
list.push(readAgent(line, currentStart, end, flagTypes)); list.push(readAgent(line, currentStart, end, readAgentOpts));
} }
return list; return list;
} }
@ -208,7 +227,7 @@ define([
return null; return null;
} }
return Object.assign({ return Object.assign({
agents: readAgentList(line, 1, line.length), agents: readAgentList(line, 1, line.length, {aliases: true}),
}, type); }, type);
}, },
@ -280,11 +299,14 @@ define([
if(typePos <= 0 || typePos >= labelSep - 1) { if(typePos <= 0 || typePos >= labelSep - 1) {
return null; return null;
} }
const readAgentOpts = {
flagTypes: CONNECT_AGENT_FLAGS,
};
return { return {
type: 'connect', type: 'connect',
agents: [ agents: [
readAgent(line, 0, typePos, CONNECT_AGENT_FLAGS), readAgent(line, 0, typePos, readAgentOpts),
readAgent(line, typePos + 1, labelSep, CONNECT_AGENT_FLAGS), readAgent(line, typePos + 1, labelSep, readAgentOpts),
], ],
label: joinLabel(line, labelSep + 1), label: joinLabel(line, labelSep + 1),
options, options,

View File

@ -12,7 +12,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
} = {}) => { } = {}) => {
return { return {
type: 'connect', type: 'connect',
agents: agentNames.map((name) => ({name, flags: []})), agents: agentNames.map((name) => ({
name,
alias: '',
flags: [],
})),
label, label,
options: { options: {
line, 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', () => { it('respects spacing within agent names', () => {
const parsed = parser.parse('A+B -> C D'); const parsed = parser.parse('A+B -> C D');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
@ -84,8 +97,12 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
{ {
type: 'connect', type: 'connect',
agents: [ agents: [
{name: 'A', flags: ['start']}, {name: 'A', alias: '', flags: ['start']},
{name: 'B', flags: ['stop', 'begin', 'end']}, {name: 'B', alias: '', flags: [
'stop',
'begin',
'end',
]},
], ],
label: jasmine.anything(), label: jasmine.anything(),
options: jasmine.anything(), options: jasmine.anything(),
@ -192,7 +209,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A: hello there'); const parsed = parser.parse('note over A: hello there');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note over', type: 'note over',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}]); }]);
@ -209,31 +226,34 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ {
type: 'note left', type: 'note left',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note left', type: 'note left',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note right', type: 'note right',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note right', type: 'note right',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'note', mode: 'note',
label: 'hello there', label: 'hello there',
}, },
{ {
type: 'note between', type: 'note between',
agents: [{name: 'A', flags: []}, {name: 'B', flags: []}], agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
mode: 'note', mode: 'note',
label: 'hi', label: 'hi',
}, },
@ -244,7 +264,10 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A B, C D: hi'); const parsed = parser.parse('note over A B, C D: hi');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note over', 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', mode: 'note',
label: 'hi', label: 'hi',
}]); }]);
@ -258,7 +281,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('state over A: doing stuff'); const parsed = parser.parse('state over A: doing stuff');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note over', type: 'note over',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'state', mode: 'state',
label: 'doing stuff', label: 'doing stuff',
}]); }]);
@ -272,7 +295,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('text right of A: doing stuff'); const parsed = parser.parse('text right of A: doing stuff');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{
type: 'note right', type: 'note right',
agents: [{name: 'A', flags: []}], agents: [{name: 'A', alias: '', flags: []}],
mode: 'text', mode: 'text',
label: 'doing stuff', label: 'doing stuff',
}]); }]);
@ -287,16 +310,25 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
{ {
type: 'agent define', type: 'agent define',
agents: [{name: 'A', flags: []}, {name: 'B', flags: []}], agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
}, },
{ {
type: 'agent begin', type: 'agent begin',
agents: [{name: 'A', flags: []}, {name: 'B', flags: []}], agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
mode: 'box', mode: 'box',
}, },
{ {
type: 'agent end', type: 'agent end',
agents: [{name: 'A', flags: []}, {name: 'B', flags: []}], agents: [
{name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []},
],
mode: 'cross', mode: 'cross',
}, },
]); ]);

View File

@ -103,11 +103,10 @@ define(['./BaseComponent'], (BaseComponent) => {
config.padding.right config.padding.right
); );
if(agentNames.length > 1) {
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentNames);
const infoL = env.agentInfos.get(left); const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
if(infoL !== infoR) {
const hangL = infoL.currentMaxRad + config.overlap.left; const hangL = infoL.currentMaxRad + config.overlap.left;
const hangR = infoR.currentMaxRad + config.overlap.right; const hangR = infoR.currentMaxRad + config.overlap.right;
@ -116,7 +115,7 @@ define(['./BaseComponent'], (BaseComponent) => {
env.addSpacing(left, {left: hangL, right: 0}); env.addSpacing(left, {left: hangL, right: 0});
env.addSpacing(right, {left: 0, right: hangR}); env.addSpacing(right, {left: 0, right: hangR});
} else { } else {
env.addSpacing(agentNames[0], { env.addSpacing(left, {
left: width / 2, left: width / 2,
right: width / 2, right: width / 2,
}); });
@ -126,10 +125,10 @@ define(['./BaseComponent'], (BaseComponent) => {
render({agentNames, mode, label}, env) { render({agentNames, mode, label}, env) {
const config = env.theme.note[mode]; const config = env.theme.note[mode];
if(agentNames.length > 1) {
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentNames);
const infoL = env.agentInfos.get(left); const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
if(infoL !== infoR) {
return this.renderNote({ return this.renderNote({
x0: infoL.x - infoL.currentMaxRad - config.overlap.left, x0: infoL.x - infoL.currentMaxRad - config.overlap.left,
x1: infoR.x + infoR.currentMaxRad + config.overlap.right, x1: infoR.x + infoR.currentMaxRad + config.overlap.right,
@ -138,7 +137,7 @@ define(['./BaseComponent'], (BaseComponent) => {
label, label,
}, env); }, env);
} else { } else {
const xMid = env.agentInfos.get(agentNames[0]).x; const xMid = infoL.x;
return this.renderNote({ return this.renderNote({
xMid, xMid,
anchor: 'middle', anchor: 'middle',

View File

@ -34,7 +34,7 @@ define(['jshintConfig', 'specs'], (jshintConfig) => {
const OPTS_TEST = Object.assign({}, jshintConfig, { const OPTS_TEST = Object.assign({}, jshintConfig, {
predef: PREDEF_TEST, predef: PREDEF_TEST,
maxstatements: 50, // allow lots of tests maxstatements: 100, // allow lots of tests
}); });
function formatError(error) { function formatError(error) {

View File

@ -88,6 +88,7 @@ html, body {
background: #FFFFFF; background: #FFFFFF;
font-family: sans-serif; font-family: sans-serif;
overflow: hidden; overflow: hidden;
user-select: none;
} }
.options.links { .options.links {