Agents as objects after generate

This commit is contained in:
David Evans 2017-11-02 22:14:44 +00:00
parent 1016f9aac0
commit 2d8e3d60e1
7 changed files with 683 additions and 416 deletions

View File

@ -1,31 +1,43 @@
define(() => { define(() => {
'use strict'; 'use strict';
function mergeSets(target, b = null) { function indexOf(list, element, equalityCheck = null) {
if(equalityCheck === null) {
return list.indexOf(element);
}
for(let i = 0; i < list.length; ++ i) {
if(equalityCheck(list[i], element)) {
return i;
}
}
return -1;
}
function mergeSets(target, b = null, equalityCheck = null) {
if(!b) { if(!b) {
return; return;
} }
for(let i = 0; i < b.length; ++ i) { for(let i = 0; i < b.length; ++ i) {
if(target.indexOf(b[i]) === -1) { if(indexOf(target, b[i], equalityCheck) === -1) {
target.push(b[i]); target.push(b[i]);
} }
} }
} }
function removeAll(target, b = null) { function removeAll(target, b = null, equalityCheck = null) {
if(!b) { if(!b) {
return; return;
} }
for(let i = 0; i < b.length; ++ i) { for(let i = 0; i < b.length; ++ i) {
const p = target.indexOf(b[i]); const p = indexOf(target, b[i], equalityCheck);
if(p !== -1) { if(p !== -1) {
target.splice(p, 1); target.splice(p, 1);
} }
} }
} }
function remove(list, item) { function remove(list, item, equalityCheck = null) {
const p = list.indexOf(item); const p = indexOf(list, item, equalityCheck);
if(p !== -1) { if(p !== -1) {
list.splice(p, 1); list.splice(p, 1);
} }
@ -36,6 +48,7 @@ define(() => {
} }
return { return {
indexOf,
mergeSets, mergeSets,
removeAll, removeAll,
remove, remove,

View File

@ -35,6 +35,17 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => {
array.mergeSets(p1, p2); array.mergeSets(p1, p2);
expect(p1).toEqual(['a', 'x', 'c', 'd', 'e']); expect(p1).toEqual(['a', 'x', 'c', 'd', 'e']);
}); });
it('uses the given equality check function', () => {
const p1 = ['a', 'b', 'c', 'd'];
const p2 = ['b', 'B', 'E', 'e'];
array.mergeSets(
p1,
p2,
(a, b) => (a.toLowerCase() === b.toLowerCase())
);
expect(p1).toEqual(['a', 'b', 'c', 'd', 'E']);
});
}); });
describe('.removeAll', () => { describe('.removeAll', () => {
@ -71,32 +82,53 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => {
array.removeAll(p1, p2); array.removeAll(p1, p2);
expect(p1).toEqual(['a', 'x', 'd']); expect(p1).toEqual(['a', 'x', 'd']);
}); });
it('uses the given equality check function', () => {
const p1 = ['a', 'b', 'c', 'd'];
const p2 = ['B', 'e'];
array.removeAll(
p1,
p2,
(a, b) => (a.toLowerCase() === b.toLowerCase())
);
expect(p1).toEqual(['a', 'c', 'd']);
});
}); });
describe('.remove', () => { describe('.remove', () => {
it('removes one element matching the parameter', () => { it('removes one element matching the parameter', () => {
const p1 = ['a', 'b']; const p1 = ['a', 'b'];
array.removeAll(p1, 'b'); array.remove(p1, 'b');
expect(p1).toEqual(['a']); expect(p1).toEqual(['a']);
}); });
it('removes only the first element matching the parameter', () => { it('removes only the first element matching the parameter', () => {
const p1 = ['a', 'b', 'c', 'b']; const p1 = ['a', 'b', 'c', 'b'];
array.removeAll(p1, 'b'); array.remove(p1, 'b');
expect(p1).toEqual(['a', 'c', 'b']); expect(p1).toEqual(['a', 'c', 'b']);
}); });
it('ignores if not found', () => { it('ignores if not found', () => {
const p1 = ['a', 'b', 'c']; const p1 = ['a', 'b', 'c'];
array.removeAll(p1, 'nope'); array.remove(p1, 'nope');
expect(p1).toEqual(['a', 'b', 'c']); expect(p1).toEqual(['a', 'b', 'c']);
}); });
it('maintains input ordering', () => { it('maintains input ordering', () => {
const p1 = ['a', 'b', 'c']; const p1 = ['a', 'b', 'c'];
array.removeAll(p1, 'b'); array.remove(p1, 'b');
expect(p1).toEqual(['a', 'c']); expect(p1).toEqual(['a', 'c']);
}); });
it('uses the given equality check function', () => {
const p1 = ['a', 'b', 'c', 'd'];
array.remove(
p1,
'B',
(a, b) => (a.toLowerCase() === b.toLowerCase())
);
expect(p1).toEqual(['a', 'c', 'd']);
});
}); });
describe('.last', () => { describe('.last', () => {

View File

@ -8,6 +8,22 @@ define(['core/ArrayUtilities'], (array) => {
} }
} }
function agentEqCheck(a, b) {
return a.name === b.name;
}
function makeAgent(name, {anchorRight = false} = {}) {
return {name, anchorRight};
}
function convertAgent(agent) {
return makeAgent(agent.name);
}
function getAgentName(agent) {
return agent.name;
}
const LOCKED_AGENT = new AgentState(false, true); const LOCKED_AGENT = new AgentState(false, true);
const DEFAULT_AGENT = new AgentState(false); const DEFAULT_AGENT = new AgentState(false);
@ -30,12 +46,14 @@ define(['core/ArrayUtilities'], (array) => {
this.stageHandlers = { this.stageHandlers = {
'mark': this.handleMark.bind(this), 'mark': this.handleMark.bind(this),
'async': this.handleAsync.bind(this), 'async': this.handleAsync.bind(this),
'note over': this.handleNote.bind(this),
'note left': this.handleNote.bind(this),
'note right': this.handleNote.bind(this),
'agent define': this.handleAgentDefine.bind(this), 'agent define': this.handleAgentDefine.bind(this),
'agent begin': this.handleAgentBegin.bind(this), 'agent begin': this.handleAgentBegin.bind(this),
'agent end': this.handleAgentEnd.bind(this), 'agent end': this.handleAgentEnd.bind(this),
'connection': this.handleConnection.bind(this),
'note over': this.handleNote.bind(this),
'note left': this.handleNote.bind(this),
'note right': this.handleNote.bind(this),
'note between': this.handleNote.bind(this),
'block begin': this.handleBlockBegin.bind(this), 'block begin': this.handleBlockBegin.bind(this),
'block split': this.handleBlockSplit.bind(this), 'block split': this.handleBlockSplit.bind(this),
'block end': this.handleBlockEnd.bind(this), 'block end': this.handleBlockEnd.bind(this),
@ -44,14 +62,14 @@ define(['core/ArrayUtilities'], (array) => {
} }
addBounds(target, agentL, agentR, involvedAgents = null) { addBounds(target, agentL, agentR, involvedAgents = null) {
array.remove(target, agentL); array.remove(target, agentL, agentEqCheck);
array.remove(target, agentR); array.remove(target, agentR, agentEqCheck);
let indexL = 0; let indexL = 0;
let indexR = target.length; let indexR = target.length;
if(involvedAgents) { if(involvedAgents) {
const found = (involvedAgents const found = (involvedAgents
.map((agent) => target.indexOf(agent)) .map((agent) => array.indexOf(target, agent, agentEqCheck))
.filter((p) => (p !== -1)) .filter((p) => (p !== -1))
); );
indexL = found.reduce((a, b) => Math.min(a, b), target.length); indexL = found.reduce((a, b) => Math.min(a, b), target.length);
@ -62,9 +80,21 @@ define(['core/ArrayUtilities'], (array) => {
target.splice(indexR + 1, 0, agentR); target.splice(indexR + 1, 0, agentR);
} }
addStage(stage, isVisible = true) {
this.currentSection.stages.push(stage);
if(isVisible) {
this.currentNest.hasContent = true;
}
}
defineAgents(agents) {
array.mergeSets(this.currentNest.agents, agents, agentEqCheck);
array.mergeSets(this.agents, agents, agentEqCheck);
}
setAgentVis(agents, visible, mode, checked = false) { setAgentVis(agents, visible, mode, checked = false) {
const filteredAgents = agents.filter((agent) => { const filteredAgents = agents.filter((agent) => {
const state = this.agentStates.get(agent) || DEFAULT_AGENT; const state = this.agentStates.get(agent.name) || DEFAULT_AGENT;
if(state.locked) { if(state.locked) {
if(checked) { if(checked) {
throw new Error('Cannot begin/end agent: ' + agent); throw new Error('Cannot begin/end agent: ' + agent);
@ -78,33 +108,32 @@ define(['core/ArrayUtilities'], (array) => {
return; return;
} }
filteredAgents.forEach((agent) => { filteredAgents.forEach((agent) => {
const state = this.agentStates.get(agent); const state = this.agentStates.get(agent.name);
if(state) { if(state) {
state.visible = visible; state.visible = visible;
} else { } else {
this.agentStates.set(agent, new AgentState(visible)); this.agentStates.set(agent.name, new AgentState(visible));
} }
}); });
const type = (visible ? 'agent begin' : 'agent end'); const type = (visible ? 'agent begin' : 'agent end');
const existing = array.last(this.currentSection.stages) || {}; const existing = array.last(this.currentSection.stages) || {};
const agentNames = filteredAgents.map(getAgentName);
if(existing.type === type && existing.mode === mode) { if(existing.type === type && existing.mode === mode) {
array.mergeSets(existing.agents, filteredAgents); array.mergeSets(existing.agentNames, agentNames);
} else { } else {
this.currentSection.stages.push({ this.addStage({
type, type,
agents: filteredAgents, agentNames,
mode, mode,
}); });
this.currentNest.hasContent = true;
} }
array.mergeSets(this.currentNest.agents, filteredAgents); this.defineAgents(filteredAgents);
array.mergeSets(this.agents, filteredAgents);
} }
beginNested(mode, label, name) { beginNested(mode, label, name) {
const nameL = name + '['; const leftAgent = makeAgent(name + '[', {anchorRight: true});
const nameR = name + ']'; const rightAgent = makeAgent(name + ']');
const agents = [nameL, nameR]; const agents = [leftAgent, rightAgent];
const stages = []; const stages = [];
this.currentSection = { this.currentSection = {
mode, mode,
@ -113,57 +142,80 @@ define(['core/ArrayUtilities'], (array) => {
}; };
this.currentNest = { this.currentNest = {
agents, agents,
leftAgent,
rightAgent,
hasContent: false, hasContent: false,
stage: { stage: {
type: 'block', type: 'block',
sections: [this.currentSection], sections: [this.currentSection],
left: nameL, left: leftAgent.name,
right: nameR, right: rightAgent.name,
}, },
}; };
this.agentStates.set(nameL, LOCKED_AGENT); this.agentStates.set(leftAgent.name, LOCKED_AGENT);
this.agentStates.set(nameR, LOCKED_AGENT); this.agentStates.set(rightAgent.name, LOCKED_AGENT);
this.nesting.push(this.currentNest); this.nesting.push(this.currentNest);
return {agents, stages}; return {agents, stages};
} }
handleMark(stage) { handleMark({name}) {
this.markers.add(stage.name); this.markers.add(name);
this.currentSection.stages.push(stage); this.addStage({type: 'mark', name}, false);
} }
handleAsync(stage) { handleAsync({target}) {
if(stage.target !== '' && !this.markers.has(stage.target)) { if(target !== '' && !this.markers.has(target)) {
throw new Error('Unknown marker: ' + stage.target); throw new Error('Unknown marker: ' + target);
} }
this.currentSection.stages.push(stage); this.addStage({type: 'async', target}, false);
} }
handleNote(stage) { handleConnection({agents, label, line, left, right}) {
if(stage.agents.length === 0) { const colAgents = agents.map(convertAgent);
this.handleUnknownStage(Object.assign({}, stage, { this.setAgentVis(colAgents, true, 'box');
agents: NOTE_DEFAULT_AGENTS[stage.type] || [], this.defineAgents(colAgents);
}));
this.addStage({
type: 'connection',
agentNames: agents.map(getAgentName),
label,
line,
left,
right,
});
}
handleNote({type, agents, mode, label}) {
let colAgents = null;
if(agents.length === 0) {
colAgents = NOTE_DEFAULT_AGENTS[type] || [];
} else { } else {
this.handleUnknownStage(stage); colAgents = agents.map(convertAgent);
} }
this.setAgentVis(colAgents, true, 'box');
this.defineAgents(colAgents);
this.addStage({
type,
agentNames: colAgents.map(getAgentName),
mode,
label,
});
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
const agentNames = agents.map((agent) => agent.name); const colAgents = agents.map(convertAgent);
array.mergeSets(this.currentNest.agents, agentNames); this.defineAgents(colAgents);
array.mergeSets(this.agents, agentNames);
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode}) {
const agentNames = agents.map((agent) => agent.name); this.setAgentVis(agents.map(convertAgent), true, mode, true);
this.setAgentVis(agentNames, true, mode, true);
} }
handleAgentEnd({agents, mode}) { handleAgentEnd({agents, mode}) {
const agentNames = agents.map((agent) => agent.name); this.setAgentVis(agents.map(convertAgent), false, mode, true);
this.setAgentVis(agentNames, false, mode, true);
} }
handleBlockBegin({mode, label}) { handleBlockBegin({mode, label}) {
@ -192,45 +244,23 @@ define(['core/ArrayUtilities'], (array) => {
if(this.nesting.length <= 1) { if(this.nesting.length <= 1) {
throw new Error('Invalid block nesting (too many "end"s)'); throw new Error('Invalid block nesting (too many "end"s)');
} }
const {hasContent, stage, agents} = this.nesting.pop(); const nested = this.nesting.pop();
this.currentNest = array.last(this.nesting); this.currentNest = array.last(this.nesting);
this.currentSection = array.last(this.currentNest.stage.sections); this.currentSection = array.last(this.currentNest.stage.sections);
if(hasContent) { if(nested.hasContent) {
array.mergeSets(this.currentNest.agents, agents); this.defineAgents(nested.agents);
array.mergeSets(this.agents, agents);
this.addBounds( this.addBounds(
this.agents, this.agents,
stage.left, nested.leftAgent,
stage.right, nested.rightAgent,
agents nested.agents
); );
this.currentSection.stages.push(stage); this.addStage(nested.stage);
this.currentNest.hasContent = true;
} }
} }
handleUnknownStage(stage) {
if(stage.agents) {
const agentNames = stage.agents.map((agent) => agent.name);
this.setAgentVis(agentNames, true, 'box');
array.mergeSets(this.currentNest.agents, agentNames);
array.mergeSets(this.agents, agentNames);
this.currentSection.stages.push(Object.assign({}, stage, {
agents: agentNames,
}));
} else {
this.currentSection.stages.push(stage);
}
this.currentNest.hasContent = true;
}
handleStage(stage) { handleStage(stage) {
const handler = this.stageHandlers[stage.type]; this.stageHandlers[stage.type](stage);
if(handler) {
handler(stage);
} else {
this.handleUnknownStage(stage);
}
} }
generate({stages, meta = {}}) { generate({stages, meta = {}}) {
@ -254,8 +284,8 @@ define(['core/ArrayUtilities'], (array) => {
this.addBounds( this.addBounds(
this.agents, this.agents,
this.currentNest.stage.left, this.currentNest.leftAgent,
this.currentNest.stage.right this.currentNest.rightAgent
); );
return { return {

View File

@ -3,13 +3,122 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const generator = new Generator(); const generator = new Generator();
const AGENT_DEFINE = 'agent define'; const parsed = {
const AGENT_BEGIN = 'agent begin'; blockBegin: (mode, label) => {
const AGENT_END = 'agent end'; return {type: 'block begin', mode, label};
},
const BLOCK_BEGIN = 'block begin'; blockSplit: (mode, label) => {
const BLOCK_SPLIT = 'block split'; return {type: 'block split', mode, label};
const BLOCK_END = 'block end'; },
blockEnd: () => {
return {type: 'block end'};
},
defineAgents: (agentNames) => {
return {
type: 'agent define',
agents: agentNames.map((name) => ({name})),
};
},
beginAgents: (agentNames, {mode = 'box'} = {}) => {
return {
type: 'agent begin',
agents: agentNames.map((name) => ({name})),
mode,
};
},
endAgents: (agentNames, {mode = 'cross'} = {}) => {
return {
type: 'agent end',
agents: agentNames.map((name) => ({name})),
mode,
};
},
connect: (agentNames, {
label = '',
line = '',
left = false,
right = false,
} = {}) => {
return {
type: 'connection',
agents: agentNames.map((name) => ({name})),
label,
line,
left,
right,
};
},
note: (agentNames, {
type = 'note over',
mode = '',
label = '',
} = {}) => {
return {
type,
agents: agentNames.map((name) => ({name})),
mode,
label,
};
},
};
const generated = {
beginAgents: (agentNames, {
mode = jasmine.anything(),
} = {}) => {
return {
type: 'agent begin',
agentNames,
mode,
};
},
endAgents: (agentNames, {
mode = jasmine.anything(),
} = {}) => {
return {
type: 'agent end',
agentNames,
mode,
};
},
connect: (agentNames, {
label = jasmine.anything(),
line = jasmine.anything(),
left = jasmine.anything(),
right = jasmine.anything(),
} = {}) => {
return {
type: 'connection',
agentNames,
label,
line,
left,
right,
};
},
note: (agentNames, {
type = jasmine.anything(),
mode = jasmine.anything(),
label = jasmine.anything(),
} = {}) => {
return {
type,
agentNames,
mode,
label,
};
},
};
describe('.generate', () => { describe('.generate', () => {
it('propagates title metadata', () => { it('propagates title metadata', () => {
@ -28,7 +137,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('includes implicit hidden left/right agents', () => { it('includes implicit hidden left/right agents', () => {
const sequence = generator.generate({stages: []}); const sequence = generator.generate({stages: []});
expect(sequence.agents).toEqual(['[', ']']); expect(sequence.agents).toEqual([
{name: '[', anchorRight: true},
{name: ']', anchorRight: false},
]);
}); });
it('passes marks and async through', () => { it('passes marks and async through', () => {
@ -53,39 +165,54 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('returns aggregated agents', () => { it('returns aggregated agents', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: '<-', agents: [{name: 'C'}, {name: 'D'}]}, parsed.connect(['C', 'D']),
{type: AGENT_BEGIN, agents: [{name: 'E'}], mode: 'box'}, parsed.beginAgents(['E']),
]}); ]});
expect(sequence.agents).toEqual( expect(sequence.agents).toEqual([
['[', 'A', 'B', 'C', 'D', 'E', ']'] {name: '[', anchorRight: true},
); {name: 'A', anchorRight: false},
{name: 'B', anchorRight: false},
{name: 'C', anchorRight: false},
{name: 'D', anchorRight: false},
{name: 'E', anchorRight: false},
{name: ']', anchorRight: false},
]);
}); });
it('always puts the implicit right agent on the right', () => { it('always puts the implicit right agent on the right', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: ']'}, {name: 'B'}]}, parsed.connect([']', 'B']),
]}); ]});
expect(sequence.agents).toEqual(['[', 'B', ']']); expect(sequence.agents).toEqual([
{name: '[', anchorRight: true},
{name: 'B', anchorRight: false},
{name: ']', anchorRight: false},
]);
}); });
it('accounts for define calls when ordering agents', () => { it('accounts for define calls when ordering agents', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: [{name: 'B'}]}, parsed.defineAgents(['B']),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
]}); ]});
expect(sequence.agents).toEqual(['[', 'B', 'A', ']']); expect(sequence.agents).toEqual([
{name: '[', anchorRight: true},
{name: 'B', anchorRight: false},
{name: 'A', anchorRight: false},
{name: ']', anchorRight: false},
]);
}); });
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: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: '->', agents: [{name: 'B'}, {name: 'C'}]}, parsed.connect(['B', 'C']),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, generated.beginAgents(['A', 'B']),
jasmine.anything(), jasmine.anything(),
{type: AGENT_BEGIN, agents: ['C'], mode: 'box'}, generated.beginAgents(['C']),
jasmine.anything(), jasmine.anything(),
jasmine.anything(), jasmine.anything(),
]); ]);
@ -93,11 +220,32 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('passes connections through', () => { it('passes connections through', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
{type: '->', agents: ['A', 'B']}, generated.connect(['A', 'B']),
jasmine.anything(),
]);
});
it('propagates connection information', () => {
const sequence = generator.generate({stages: [
parsed.connect(['A', 'B'], {
label: 'foo',
line: 'bar',
left: true,
right: false,
}),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
generated.connect(['A', 'B'], {
label: 'foo',
line: 'bar',
left: true,
right: false,
}),
jasmine.anything(), jasmine.anything(),
]); ]);
}); });
@ -108,189 +256,182 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
terminators: 'foo', terminators: 'foo',
}, },
stages: [ stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
], ],
}); });
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
jasmine.anything(), jasmine.anything(),
{type: AGENT_END, agents: ['A', 'B'], mode: 'foo'}, generated.endAgents(['A', 'B'], {mode: 'foo'}),
]);
});
it('defaults to mode "none" for implicit end stages', () => {
const sequence = generator.generate({stages: [
parsed.connect(['A', 'B']),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
jasmine.anything(),
generated.endAgents(['A', 'B'], {mode: 'none'}),
]);
});
it('defaults to mode "cross" for explicit end stages', () => {
const sequence = generator.generate({stages: [
parsed.connect(['A', 'B']),
parsed.endAgents(['A', 'B']),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
jasmine.anything(),
generated.endAgents(['A', 'B'], {mode: 'cross'}),
]); ]);
}); });
it('does not create duplicate begin stages', () => { it('does not create duplicate begin stages', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: [ parsed.beginAgents(['A', 'B', 'C']),
{name: 'A'}, parsed.connect(['A', 'B']),
{name: 'B'}, parsed.connect(['B', 'C']),
{name: 'C'},
], mode: 'box'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: '->', agents: [{name: 'B'}, {name: 'C'}]},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B', 'C'], mode: 'box'}, generated.beginAgents(['A', 'B', 'C']),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'none'}, generated.endAgents(['A', 'B', 'C']),
]); ]);
}); });
it('redisplays agents if they have been hidden', () => { it('redisplays agents if they have been hidden', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: [ parsed.beginAgents(['A', 'B']),
{name: 'A'}, parsed.connect(['A', 'B']),
{name: 'B'}, parsed.endAgents(['B']),
], mode: 'box'}, parsed.connect(['A', 'B']),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_END, agents: [{name: 'B'}], mode: 'cross'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
jasmine.anything(), jasmine.anything(),
{type: AGENT_END, agents: ['B'], mode: 'cross'},
{type: AGENT_BEGIN, agents: ['B'], mode: 'box'},
jasmine.anything(), jasmine.anything(),
{type: AGENT_END, agents: ['A', 'B'], mode: 'none'}, generated.endAgents(['B']),
generated.beginAgents(['B']),
jasmine.anything(),
jasmine.anything(),
]); ]);
}); });
it('collapses adjacent begin statements', () => { it('collapses adjacent begin statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: AGENT_BEGIN, agents: [{name: 'D'}], mode: 'box'}, parsed.beginAgents(['D']),
{type: '->', agents: [{name: 'B'}, {name: 'C'}]}, parsed.connect(['B', 'C']),
{type: '->', agents: [{name: 'C'}, {name: 'D'}]}, parsed.connect(['C', 'D']),
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, generated.beginAgents(['A', 'B']),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: AGENT_BEGIN, agents: ['D', 'C'], mode: 'box'}, generated.beginAgents(['D', 'C']),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
jasmine.anything(), jasmine.anything(),
]); ]);
}); });
it('removes superfluous begin statements', () => { it('removes superfluous begin statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: AGENT_BEGIN, agents: [ parsed.beginAgents(['A', 'C', 'D']),
{name: 'A'}, parsed.beginAgents(['C', 'E']),
{name: 'C'},
{name: 'D'},
], mode: 'box'},
{type: AGENT_BEGIN, agents: [
{name: 'C'},
{name: 'E'},
], mode: 'box'},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, generated.beginAgents(['A', 'B']),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: AGENT_BEGIN, agents: ['C', 'D', 'E'], mode: 'box'}, generated.beginAgents(['C', 'D', 'E']),
jasmine.anything(), jasmine.anything(),
]); ]);
}); });
it('removes superfluous end statements', () => { it('removes superfluous end statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: [{name: 'E'}]}, parsed.defineAgents(['E']),
{type: AGENT_BEGIN, agents: [ parsed.beginAgents(['C', 'D']),
{name: 'C'}, parsed.connect(['A', 'B']),
{name: 'D'}, parsed.endAgents(['A', 'B', 'C']),
], mode: 'box'}, parsed.endAgents(['A', 'D', 'E']),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_END, agents: [
{name: 'A'},
{name: 'B'},
{name: 'C'},
], mode: 'cross'},
{type: AGENT_END, agents: [
{name: 'A'},
{name: 'D'},
{name: 'E'},
], mode: 'cross'},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: AGENT_END, agents: ['A', 'B', 'C', 'D'], mode: 'cross'}, generated.endAgents(['A', 'B', 'C', 'D']),
]); ]);
}); });
it('does not merge different modes of end', () => { it('does not merge different modes of end', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: [ parsed.beginAgents(['C', 'D']),
{name: 'C'}, parsed.connect(['A', 'B']),
{name: 'D'}, parsed.endAgents(['A', 'B', 'C']),
], mode: 'box'},
{type: '->', agents: [
{name: 'A'},
{name: 'B'},
]},
{type: AGENT_END, agents: [
{name: 'A'},
{name: 'B'},
{name: 'C'},
], mode: 'cross'},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
jasmine.anything(), jasmine.anything(),
{type: '->', agents: jasmine.anything()}, generated.connect(jasmine.anything()),
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'}, generated.endAgents(['A', 'B', 'C'], {mode: 'cross'}),
{type: AGENT_END, agents: ['D'], mode: 'none'}, generated.endAgents(['D'], {mode: 'none'}),
]); ]);
}); });
it('creates virtual agents for block statements', () => { it('creates virtual agents for block statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
expect(sequence.agents).toEqual( expect(sequence.agents).toEqual([
['[', '__BLOCK0[', 'A', 'B', '__BLOCK0]', ']'] {name: '[', anchorRight: true},
); {name: '__BLOCK0[', anchorRight: true},
{name: 'A', anchorRight: false},
{name: 'B', anchorRight: false},
{name: '__BLOCK0]', anchorRight: false},
{name: ']', anchorRight: false},
]);
}); });
it('positions virtual block agents near involved agents', () => { it('positions virtual block agents near involved agents', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'C'}, {name: 'D'}]}, parsed.connect(['C', 'D']),
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'E'}, {name: 'F'}]}, parsed.connect(['E', 'F']),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
{type: '->', agents: [{name: 'G'}, {name: 'H'}]}, parsed.connect(['G', 'H']),
]}); ]});
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
'[', {name: '[', anchorRight: true},
'A', {name: 'A', anchorRight: false},
'B', {name: 'B', anchorRight: false},
'__BLOCK0[', {name: '__BLOCK0[', anchorRight: true},
'C', {name: 'C', anchorRight: false},
'D', {name: 'D', anchorRight: false},
'__BLOCK1[', {name: '__BLOCK1[', anchorRight: true},
'E', {name: 'E', anchorRight: false},
'F', {name: 'F', anchorRight: false},
'__BLOCK1]', {name: '__BLOCK1]', anchorRight: false},
'__BLOCK0]', {name: '__BLOCK0]', anchorRight: false},
'G', {name: 'G', anchorRight: false},
'H', {name: 'H', anchorRight: false},
']', {name: ']', anchorRight: false},
]); ]);
}); });
it('records virtual block agent names in blocks', () => { it('records virtual block agent names in blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
@ -301,47 +442,47 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('records all sections within blocks', () => { it('records all sections within blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: '->', agents: [{name: 'A'}, {name: 'C'}]}, parsed.connect(['A', 'C']),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
expect(block0.sections).toEqual([ expect(block0.sections).toEqual([
{mode: 'if', label: 'abc', stages: [ {mode: 'if', label: 'abc', stages: [
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, generated.beginAgents(['A', 'B']),
{type: '->', agents: ['A', 'B']}, generated.connect(['A', 'B']),
]}, ]},
{mode: 'else', label: 'xyz', stages: [ {mode: 'else', label: 'xyz', stages: [
{type: AGENT_BEGIN, agents: ['C'], mode: 'box'}, generated.beginAgents(['C']),
{type: '->', agents: ['A', 'C']}, generated.connect(['A', 'C']),
]}, ]},
]); ]);
}); });
it('records virtual block agents in nested blocks', () => { it('records virtual block agents in nested blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: BLOCK_BEGIN, mode: 'if', label: 'def'}, parsed.blockBegin('if', 'def'),
{type: '->', agents: [{name: 'A'}, {name: 'C'}]}, parsed.connect(['A', 'C']),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
'[', {name: '[', anchorRight: true},
'__BLOCK0[', {name: '__BLOCK0[', anchorRight: true},
'__BLOCK1[', {name: '__BLOCK1[', anchorRight: true},
'A', {name: 'A', anchorRight: false},
'B', {name: 'B', anchorRight: false},
'C', {name: 'C', anchorRight: false},
'__BLOCK1]', {name: '__BLOCK1]', anchorRight: false},
'__BLOCK0]', {name: '__BLOCK0]', anchorRight: false},
']', {name: ']', anchorRight: false},
]); ]);
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
expect(block0.type).toEqual('block'); expect(block0.type).toEqual('block');
@ -356,23 +497,23 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('preserves block boundaries when agents exist outside', () => { it('preserves block boundaries when agents exist outside', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_BEGIN, mode: 'if', label: 'def'}, parsed.blockBegin('if', 'def'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
'[', {name: '[', anchorRight: true},
'__BLOCK0[', {name: '__BLOCK0[', anchorRight: true},
'__BLOCK1[', {name: '__BLOCK1[', anchorRight: true},
'A', {name: 'A', anchorRight: false},
'B', {name: 'B', anchorRight: false},
'__BLOCK1]', {name: '__BLOCK1]', anchorRight: false},
'__BLOCK0]', {name: '__BLOCK0]', anchorRight: false},
']', {name: ']', anchorRight: false},
]); ]);
const block0 = sequence.stages[2]; const block0 = sequence.stages[2];
expect(block0.type).toEqual('block'); expect(block0.type).toEqual('block');
@ -387,10 +528,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('allows empty block parts after split', () => { it('allows empty block parts after split', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
@ -405,10 +546,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('allows empty block parts before split', () => { it('allows empty block parts before split', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
@ -423,11 +564,11 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes entirely empty blocks', () => { it('removes entirely empty blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
expect(sequence.stages).toEqual([]); expect(sequence.stages).toEqual([]);
@ -435,10 +576,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes blocks containing only define statements / markers', () => { it('removes blocks containing only define statements / markers', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: AGENT_DEFINE, agents: [{name: 'A'}]}, parsed.defineAgents(['A']),
{type: 'mark', name: 'foo'}, {type: 'mark', name: 'foo'},
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
expect(sequence.stages).toEqual([]); expect(sequence.stages).toEqual([]);
@ -446,24 +587,27 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('does not create virtual agents for empty blocks', () => { it('does not create virtual agents for empty blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
expect(sequence.agents).toEqual(['[', ']']); expect(sequence.agents).toEqual([
{name: '[', anchorRight: true},
{name: ']', anchorRight: false},
]);
}); });
it('removes entirely empty nested blocks', () => { it('removes entirely empty nested blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const block0 = sequence.stages[0];
@ -478,83 +622,100 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('rejects unterminated blocks', () => { it('rejects unterminated blocks', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: BLOCK_BEGIN, mode: 'if', label: 'def'}, parsed.blockBegin('if', 'def'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
]})).toThrow(); ]})).toThrow();
}); });
it('rejects extra block terminations', () => { it('rejects extra block terminations', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_END}, parsed.blockEnd(),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_END}, parsed.blockEnd(),
]})).toThrow(); ]})).toThrow();
}); });
it('rejects block splitting without a block', () => { it('rejects block splitting without a block', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, parsed.blockBegin('if', 'abc'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
]})).toThrow(); ]})).toThrow();
}); });
it('rejects block splitting in non-splittable blocks', () => { it('rejects block splitting in non-splittable blocks', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'repeat', label: 'abc'}, parsed.blockBegin('repeat', 'abc'),
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, parsed.blockSplit('else', 'xyz'),
{type: '->', agents: [{name: 'A'}, {name: 'B'}]}, parsed.connect(['A', 'B']),
{type: BLOCK_END}, parsed.blockEnd(),
]})).toThrow(); ]})).toThrow();
}); });
it('passes notes through', () => {
const sequence = generator.generate({stages: [
parsed.note(['A', 'B'], {
type: 'note right',
mode: 'foo',
label: 'bar',
}),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
generated.note(['A', 'B'], {
type: 'note right',
mode: 'foo',
label: 'bar',
}),
jasmine.anything(),
]);
});
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: [
{type: 'note right', agents: [], foo: 'bar'}, parsed.note([], {type: 'note right'}),
{type: 'note left', agents: [], foo: 'bar'}, parsed.note([], {type: 'note left'}),
{type: 'note over', agents: [], foo: 'bar'}, parsed.note([], {type: 'note over'}),
{type: 'note right', agents: [{name: '['}], foo: 'bar'},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: 'note right', agents: [']'], foo: 'bar'}, generated.note([']'], {type: 'note right'}),
{type: 'note left', agents: ['['], foo: 'bar'}, generated.note(['['], {type: 'note left'}),
{type: 'note over', agents: ['[', ']'], foo: 'bar'}, generated.note(['[', ']'], {type: 'note over'}),
{type: 'note right', agents: ['['], foo: 'bar'},
]); ]);
}); });
it('rejects attempts to change implicit agents', () => { it('rejects attempts to change implicit agents', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_BEGIN, agents: [{name: '['}], mode: 'box'}, parsed.beginAgents(['[']),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_BEGIN, agents: [{name: ']'}], mode: 'box'}, parsed.beginAgents([']']),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_END, agents: [{name: '['}], mode: 'cross'}, parsed.endAgents(['[']),
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_END, agents: [{name: ']'}], mode: 'cross'}, parsed.endAgents([']']),
]})).toThrow(); ]})).toThrow();
}); });
}); });

View File

@ -140,11 +140,11 @@ define([
this.sizer = new this.SVGTextBlockClass.SizeTester(this.base); this.sizer = new this.SVGTextBlockClass.SizeTester(this.base);
} }
findExtremes(agents) { findExtremes(agentNames) {
let min = null; let min = null;
let max = null; let max = null;
agents.forEach((agent) => { agentNames.forEach((name) => {
const info = this.agentInfos.get(agent); const info = this.agentInfos.get(name);
if(min === null || info.index < min.index) { if(min === null || info.index < min.index) {
min = info; min = info;
} }
@ -158,32 +158,32 @@ define([
}; };
} }
addSeparation(agent1, agent2, dist) { addSeparation(agentName1, agentName2, dist) {
const info1 = this.agentInfos.get(agent1); const info1 = this.agentInfos.get(agentName1);
const info2 = this.agentInfos.get(agent2); const info2 = this.agentInfos.get(agentName2);
const d1 = info1.separations.get(agent2) || 0; const d1 = info1.separations.get(agentName2) || 0;
info1.separations.set(agent2, Math.max(d1, dist)); info1.separations.set(agentName2, Math.max(d1, dist));
const d2 = info2.separations.get(agent1) || 0; const d2 = info2.separations.get(agentName1) || 0;
info2.separations.set(agent1, Math.max(d2, dist)); info2.separations.set(agentName1, Math.max(d2, dist));
} }
addSeparations(agents, agentSpaces) { addSeparations(agentNames, agentSpaces) {
agents.forEach((agentR) => { agentNames.forEach((agentNameR) => {
const infoR = this.agentInfos.get(agentR); const infoR = this.agentInfos.get(agentNameR);
const sepR = agentSpaces.get(agentR) || SEP_ZERO; const sepR = agentSpaces.get(agentNameR) || SEP_ZERO;
infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right); infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right);
infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left); infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left);
agents.forEach((agentL) => { agentNames.forEach((agentNameL) => {
const infoL = this.agentInfos.get(agentL); const infoL = this.agentInfos.get(agentNameL);
if(infoL.index >= infoR.index) { if(infoL.index >= infoR.index) {
return; return;
} }
const sepL = agentSpaces.get(agentL) || SEP_ZERO; const sepL = agentSpaces.get(agentNameL) || SEP_ZERO;
this.addSeparation( this.addSeparation(
agentR, agentNameR,
agentL, agentNameL,
sepR.left + sepL.right + this.theme.agentMargin sepR.left + sepL.right + this.theme.agentMargin
); );
}); });
@ -236,25 +236,25 @@ define([
return {left: 0, right: 0}; return {left: 0, right: 0};
} }
separationAgent({type, mode, agents}) { separationAgent({type, mode, agentNames}) {
if(type === 'agent begin') { if(type === 'agent begin') {
array.mergeSets(this.visibleAgents, agents); array.mergeSets(this.visibleAgents, agentNames);
} }
const agentSpaces = new Map(); const agentSpaces = new Map();
agents.forEach((agent) => { agentNames.forEach((name) => {
const info = this.agentInfos.get(agent); const info = this.agentInfos.get(name);
const separationFn = this.separationAgentCap[mode]; const separationFn = this.separationAgentCap[mode];
agentSpaces.set(agent, separationFn(info)); agentSpaces.set(name, separationFn(info));
}); });
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
if(type === 'agent end') { if(type === 'agent end') {
array.removeAll(this.visibleAgents, agents); array.removeAll(this.visibleAgents, agentNames);
} }
} }
separationConnection({agents, label}) { separationConnection({agentNames, label}) {
const config = this.theme.connect; const config = this.theme.connect;
const labelWidth = ( const labelWidth = (
@ -263,9 +263,9 @@ define([
); );
const strokeWidth = this.theme.agentLineAttrs['stroke-width']; const strokeWidth = this.theme.agentLineAttrs['stroke-width'];
if(agents[0] === agents[1]) { if(agentNames[0] === agentNames[1]) {
const agentSpaces = new Map(); const agentSpaces = new Map();
agentSpaces.set(agents[0], { agentSpaces.set(agentNames[0], {
left: 0, left: 0,
right: ( right: (
labelWidth + labelWidth +
@ -277,14 +277,14 @@ define([
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
} else { } else {
this.addSeparation( this.addSeparation(
agents[0], agentNames[0],
agents[1], agentNames[1],
labelWidth + config.arrow.width * 2 + strokeWidth labelWidth + config.arrow.width * 2 + strokeWidth
); );
} }
} }
separationNoteOver({agents, mode, label}) { separationNoteOver({agentNames, mode, label}) {
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const width = ( const width = (
this.sizer.measure(config.labelAttrs, label).width + this.sizer.measure(config.labelAttrs, label).width +
@ -293,8 +293,8 @@ define([
); );
const agentSpaces = new Map(); const agentSpaces = new Map();
if(agents.length > 1) { if(agentNames.length > 1) {
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
this.addSeparation( this.addSeparation(
left, left,
@ -308,7 +308,7 @@ define([
agentSpaces.set(left, {left: config.overlap.left, right: 0}); agentSpaces.set(left, {left: config.overlap.left, right: 0});
agentSpaces.set(right, {left: 0, right: config.overlap.right}); agentSpaces.set(right, {left: 0, right: config.overlap.right});
} else { } else {
agentSpaces.set(agents[0], { agentSpaces.set(agentNames[0], {
left: width / 2, left: width / 2,
right: width / 2, right: width / 2,
}); });
@ -316,9 +316,9 @@ define([
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
} }
separationNoteSide(isRight, {agents, mode, label}) { separationNoteSide(isRight, {agentNames, mode, label}) {
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
const width = ( const width = (
this.sizer.measure(config.labelAttrs, label).width + this.sizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
@ -336,9 +336,9 @@ define([
this.addSeparations(this.visibleAgents, agentSpaces); this.addSeparations(this.visibleAgents, agentSpaces);
} }
separationNoteBetween({agents, mode, label}) { separationNoteBetween({agentNames, mode, label}) {
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
this.addSeparation( this.addSeparation(
left, left,
@ -462,8 +462,8 @@ define([
}; };
} }
checkAgentRange(agents) { checkAgentRange(agentNames) {
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
const leftX = this.agentInfos.get(left).x; const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x; const rightX = this.agentInfos.get(right).x;
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
@ -473,8 +473,8 @@ define([
}); });
} }
markAgentRange(agents) { markAgentRange(agentNames) {
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
const leftX = this.agentInfos.get(left).x; const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x; const rightX = this.agentInfos.get(right).x;
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
@ -484,24 +484,24 @@ define([
}); });
} }
renderAgentBegin({mode, agents}) { renderAgentBegin({mode, agentNames}) {
this.checkAgentRange(agents); this.checkAgentRange(agentNames);
let maxHeight = 0; let maxHeight = 0;
agents.forEach((agent) => { agentNames.forEach((name) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(name);
const shifts = this.renderAgentCap[mode](agentInfo); const shifts = this.renderAgentCap[mode](agentInfo);
maxHeight = Math.max(maxHeight, shifts.height); maxHeight = Math.max(maxHeight, shifts.height);
agentInfo.latestYStart = this.currentY + shifts.lineBottom; agentInfo.latestYStart = this.currentY + shifts.lineBottom;
}); });
this.currentY += maxHeight + this.theme.actionMargin; this.currentY += maxHeight + this.theme.actionMargin;
this.markAgentRange(agents); this.markAgentRange(agentNames);
} }
renderAgentEnd({mode, agents}) { renderAgentEnd({mode, agentNames}) {
this.checkAgentRange(agents); this.checkAgentRange(agentNames);
let maxHeight = 0; let maxHeight = 0;
agents.forEach((agent) => { agentNames.forEach((name) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(name);
const x = agentInfo.x; const x = agentInfo.x;
const shifts = this.renderAgentCap[mode](agentInfo); const shifts = this.renderAgentCap[mode](agentInfo);
maxHeight = Math.max(maxHeight, shifts.height); maxHeight = Math.max(maxHeight, shifts.height);
@ -515,12 +515,12 @@ define([
agentInfo.latestYStart = null; agentInfo.latestYStart = null;
}); });
this.currentY += maxHeight + this.theme.actionMargin; this.currentY += maxHeight + this.theme.actionMargin;
this.markAgentRange(agents); this.markAgentRange(agentNames);
} }
renderSelfConnection({label, agents, line, left, right}) { renderSelfConnection({label, agentNames, line, left, right}) {
const config = this.theme.connect; const config = this.theme.connect;
const from = this.agentInfos.get(agents[0]); const from = this.agentInfos.get(agentNames[0]);
const dy = config.arrow.height / 2; const dy = config.arrow.height / 2;
const short = this.theme.agentLineAttrs['stroke-width']; const short = this.theme.agentLineAttrs['stroke-width'];
@ -591,10 +591,10 @@ define([
this.currentY = y1 + dy + this.theme.actionMargin; this.currentY = y1 + dy + this.theme.actionMargin;
} }
renderSimpleConnection({label, agents, line, left, right}) { renderSimpleConnection({label, agentNames, line, left, right}) {
const config = this.theme.connect; const config = this.theme.connect;
const from = this.agentInfos.get(agents[0]); const from = this.agentInfos.get(agentNames[0]);
const to = this.agentInfos.get(agents[1]); const to = this.agentInfos.get(agentNames[1]);
const dy = config.arrow.height / 2; const dy = config.arrow.height / 2;
const dir = (from.x < to.x) ? 1 : -1; const dir = (from.x < to.x) ? 1 : -1;
@ -650,13 +650,13 @@ define([
} }
renderConnection(stage) { renderConnection(stage) {
this.checkAgentRange(stage.agents); this.checkAgentRange(stage.agentNames);
if(stage.agents[0] === stage.agents[1]) { if(stage.agentNames[0] === stage.agentNames[1]) {
this.renderSelfConnection(stage); this.renderSelfConnection(stage);
} else { } else {
this.renderSimpleConnection(stage); this.renderSimpleConnection(stage);
} }
this.markAgentRange(stage.agents); this.markAgentRange(stage.agentNames);
} }
renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) { renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) {
@ -721,53 +721,53 @@ define([
); );
} }
renderNoteOver({agents, mode, label}) { renderNoteOver({agentNames, mode, label}) {
this.checkAgentRange(agents); this.checkAgentRange(agentNames);
const config = this.theme.note[mode]; const config = this.theme.note[mode];
if(agents.length > 1) { if(agentNames.length > 1) {
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
this.renderNote({ this.renderNote({
x0: this.agentInfos.get(left).x - config.overlap.left, x0: this.agentInfos.get(left).x - config.overlap.left,
x1: this.agentInfos.get(right).x + config.overlap.right, x1: this.agentInfos.get(right).x + config.overlap.right,
}, 'middle', mode, label); }, 'middle', mode, label);
} else { } else {
const xMid = this.agentInfos.get(agents[0]).x; const xMid = this.agentInfos.get(agentNames[0]).x;
this.renderNote({xMid}, 'middle', mode, label); this.renderNote({xMid}, 'middle', mode, label);
} }
this.markAgentRange(agents); this.markAgentRange(agentNames);
} }
renderNoteLeft({agents, mode, label}) { renderNoteLeft({agentNames, mode, label}) {
this.checkAgentRange(agents); this.checkAgentRange(agentNames);
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {left} = this.findExtremes(agents); const {left} = this.findExtremes(agentNames);
const x1 = this.agentInfos.get(left).x - config.margin.right; const x1 = this.agentInfos.get(left).x - config.margin.right;
this.renderNote({x1}, 'end', mode, label); this.renderNote({x1}, 'end', mode, label);
this.markAgentRange(agents); this.markAgentRange(agentNames);
} }
renderNoteRight({agents, mode, label}) { renderNoteRight({agentNames, mode, label}) {
this.checkAgentRange(agents); this.checkAgentRange(agentNames);
const config = this.theme.note[mode]; const config = this.theme.note[mode];
const {right} = this.findExtremes(agents); const {right} = this.findExtremes(agentNames);
const x0 = this.agentInfos.get(right).x + config.margin.left; const x0 = this.agentInfos.get(right).x + config.margin.left;
this.renderNote({x0}, 'start', mode, label); this.renderNote({x0}, 'start', mode, label);
this.markAgentRange(agents); this.markAgentRange(agentNames);
} }
renderNoteBetween({agents, mode, label}) { renderNoteBetween({agentNames, mode, label}) {
this.checkAgentRange(agents); this.checkAgentRange(agentNames);
const {left, right} = this.findExtremes(agents); const {left, right} = this.findExtremes(agentNames);
const xMid = ( const xMid = (
this.agentInfos.get(left).x + this.agentInfos.get(left).x +
this.agentInfos.get(right).x this.agentInfos.get(right).x
) / 2; ) / 2;
this.renderNote({xMid}, 'middle', mode, label); this.renderNote({xMid}, 'middle', mode, label);
this.markAgentRange(agents); this.markAgentRange(agentNames);
} }
renderBlockBegin(scope, {left, right}) { renderBlockBegin(scope, {left, right}) {
@ -891,9 +891,9 @@ define([
buildAgentInfos(agents, stages) { buildAgentInfos(agents, stages) {
this.agentInfos = new Map(); this.agentInfos = new Map();
agents.forEach((agent, index) => { agents.forEach((agent, index) => {
this.agentInfos.set(agent, { this.agentInfos.set(agent.name, {
label: agent, label: agent.name,
anchorRight: agent.endsWith('['), anchorRight: agent.anchorRight,
index, index,
x: null, x: null,
latestYStart: null, latestYStart: null,

View File

@ -25,13 +25,13 @@ defineDescribe('Sequence Renderer', [
}); });
}); });
function connectionStage(agents, label = '') { function connectionStage(agentNames, label = '') {
return { return {
type: 'connection', type: 'connection',
line: 'solid', line: 'solid',
left: false, left: false,
right: true, right: true,
agents, agentNames,
label, label,
}; };
} }
@ -40,7 +40,12 @@ defineDescribe('Sequence Renderer', [
it('populates the SVG with content', () => { it('populates the SVG with content', () => {
renderer.render({ renderer.render({
meta: {title: 'Title'}, meta: {title: 'Title'},
agents: ['[', 'Col 1', 'Col 2', ']'], agents: [
{name: '[', anchorRight: true},
{name: 'Col 1', anchorRight: false},
{name: 'Col 2', anchorRight: false},
{name: ']', anchorRight: false},
],
stages: [], stages: [],
}); });
const element = renderer.svg(); const element = renderer.svg();
@ -55,11 +60,16 @@ defineDescribe('Sequence Renderer', [
renderer.render({ renderer.render({
meta: {title: ''}, meta: {title: ''},
agents: ['[', 'A', 'B', ']'], agents: [
{name: '[', anchorRight: true},
{name: 'A', anchorRight: false},
{name: 'B', anchorRight: false},
{name: ']', anchorRight: false},
],
stages: [ stages: [
{type: 'agent begin', agents: ['A', 'B'], mode: 'box'}, {type: 'agent begin', agentNames: ['A', 'B'], mode: 'box'},
connectionStage(['A', 'B']), connectionStage(['A', 'B']),
{type: 'agent end', agents: ['A', 'B'], mode: 'none'}, {type: 'agent end', agentNames: ['A', 'B'], mode: 'none'},
], ],
}); });
@ -80,14 +90,28 @@ defineDescribe('Sequence Renderer', [
renderer.render({ renderer.render({
meta: {title: ''}, meta: {title: ''},
agents: ['[', 'A', 'B', 'C', ']'], agents: [
{name: '[', anchorRight: true},
{name: 'A', anchorRight: false},
{name: 'B', anchorRight: false},
{name: 'C', anchorRight: false},
{name: ']', anchorRight: false},
],
stages: [ stages: [
{type: 'agent begin', agents: ['A', 'B', 'C'], mode: 'box'}, {
type: 'agent begin',
agentNames: ['A', 'B', 'C'],
mode: 'box',
},
connectionStage(['[', 'A']), connectionStage(['[', 'A']),
connectionStage(['A', 'B']), connectionStage(['A', 'B']),
connectionStage(['B', 'C']), connectionStage(['B', 'C']),
connectionStage(['C', ']']), connectionStage(['C', ']']),
{type: 'agent end', agents: ['A', 'B', 'C'], mode: 'none'}, {
type: 'agent end',
agentNames: ['A', 'B', 'C'],
mode: 'none',
},
], ],
}); });
@ -115,17 +139,24 @@ defineDescribe('Sequence Renderer', [
renderer.render({ renderer.render({
meta: {title: ''}, meta: {title: ''},
agents: ['[', 'A', 'B', 'C', 'D', ']'], agents: [
{name: '[', anchorRight: true},
{name: 'A', anchorRight: false},
{name: 'B', anchorRight: false},
{name: 'C', anchorRight: false},
{name: 'D', anchorRight: false},
{name: ']', anchorRight: false},
],
stages: [ stages: [
{type: 'agent begin', agents: ['A', 'B'], mode: 'box'}, {type: 'agent begin', agentNames: ['A', 'B'], mode: 'box'},
connectionStage(['A', 'B'], 'short'), connectionStage(['A', 'B'], 'short'),
{type: 'agent end', agents: ['B'], mode: 'cross'}, {type: 'agent end', agentNames: ['B'], mode: 'cross'},
{type: 'agent begin', agents: ['C'], mode: 'box'}, {type: 'agent begin', agentNames: ['C'], mode: 'box'},
connectionStage(['A', 'C'], 'long description here'), connectionStage(['A', 'C'], 'long description here'),
{type: 'agent end', agents: ['C'], mode: 'cross'}, {type: 'agent end', agentNames: ['C'], mode: 'cross'},
{type: 'agent begin', agents: ['D'], mode: 'box'}, {type: 'agent begin', agentNames: ['D'], mode: 'box'},
connectionStage(['A', 'D'], 'short again'), connectionStage(['A', 'D'], 'short again'),
{type: 'agent end', agents: ['A', 'D'], mode: 'cross'}, {type: 'agent end', agentNames: ['A', 'D'], mode: 'cross'},
], ],
}); });

View File

@ -1,5 +1,5 @@
/* jshint -W072 */ /* jshint -W072 */
defineDescribe('Sequence Renderer', [ defineDescribe('Sequence Integration', [
'./Parser', './Parser',
'./Generator', './Generator',
'./Renderer', './Renderer',