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

View File

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

View File

@ -8,7 +8,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
if(typeof item === 'object') {
return item;
} else {
return {name: item, flags: []};
return {name: item, alias: '', flags: []};
}
});
}
@ -231,6 +231,33 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('converts aliases', () => {
const sequence = generator.generate({stages: [
PARSED.defineAgents([{name: 'Baz', alias: 'B', flags: []}]),
PARSED.connect(['A', 'B']),
]});
expect(sequence.agents).toEqual([
{name: '[', anchorRight: true},
{name: 'Baz', anchorRight: false},
{name: 'A', anchorRight: false},
{name: ']', anchorRight: false},
]);
});
it('rejects duplicate aliases', () => {
expect(() => generator.generate({stages: [
PARSED.defineAgents([{name: 'Foo', alias: 'B', flags: []}]),
PARSED.defineAgents([{name: 'Bar', alias: 'B', flags: []}]),
]})).toThrow();
});
it('rejects using agent names as aliases', () => {
expect(() => generator.generate({stages: [
PARSED.defineAgents([{name: 'Foo', alias: 'B', flags: []}]),
PARSED.defineAgents([{name: 'Bar', alias: 'Foo', flags: []}]),
]})).toThrow();
});
it('creates implicit begin stages for agents when used', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', 'B']),
@ -347,6 +374,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('removes duplicate begin agents', () => {
const sequence = generator.generate({stages: [
PARSED.beginAgents(['A', 'A']),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
jasmine.anything(),
]);
});
it('collapses adjacent begin statements', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', 'B']),
@ -378,6 +415,17 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('removes duplicate end agents', () => {
const sequence = generator.generate({stages: [
PARSED.beginAgents(['A']),
PARSED.endAgents(['A', 'A']),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
GENERATED.endAgents(['A']),
]);
});
it('removes superfluous end statements', () => {
const sequence = generator.generate({stages: [
PARSED.defineAgents(['E']),
@ -409,8 +457,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel highlighting stages', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', flags: ['stop']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['stop']}]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
@ -428,7 +476,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel begin stages', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['begin']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['begin']}]),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A']),
@ -442,7 +490,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds parallel end stages', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['end']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['end']}]),
]});
expect(sequence.stages).toEqual([
GENERATED.beginAgents(['A', 'B']),
@ -456,8 +504,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('implicitly ends highlighting when ending a stage', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', flags: ['end']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['end']}]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
@ -472,32 +520,41 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
});
it('rejects conflicting flags', () => {
expect(() => generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect([
{name: 'A', flags: ['start']},
{name: 'A', flags: ['stop']},
'A',
{name: 'B', alias: '', flags: ['start', 'stop']},
]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['begin', 'end']}]),
PARSED.connect([
{name: 'A', alias: '', flags: ['start']},
{name: 'A', alias: '', flags: ['stop']},
]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect([
{name: 'A', flags: ['begin']},
{name: 'A', flags: ['end']},
'A',
{name: 'B', alias: '', flags: ['begin', 'end']},
]),
]})).toThrow();
expect(() => generator.generate({stages: [
PARSED.connect([
{name: 'A', alias: '', flags: ['begin']},
{name: 'A', alias: '', flags: ['end']},
]),
]})).toThrow();
});
it('adds implicit highlight end with implicit terminator', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
PARSED.connect([
'A',
{name: 'B', alias: '', flags: ['start']},
]),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
@ -511,7 +568,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('adds implicit highlight end with explicit terminator', () => {
const sequence = generator.generate({stages: [
PARSED.connect(['A', {name: 'B', flags: ['start']}]),
PARSED.connect(['A', {name: 'B', alias: '', flags: ['start']}]),
PARSED.endAgents(['A', 'B']),
]});
expect(sequence.stages).toEqual([
@ -527,8 +584,8 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('collapses adjacent end statements containing highlighting', () => {
const sequence = generator.generate({stages: [
PARSED.connect([
{name: 'A', flags: ['start']},
{name: 'B', flags: ['start']},
{name: 'A', alias: '', flags: ['start']},
{name: 'B', alias: '', flags: ['start']},
]),
PARSED.endAgents(['A']),
PARSED.endAgents(['B']),
@ -849,6 +906,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('rejects note between with a repeated agent', () => {
expect(() => generator.generate({stages: [
PARSED.note('note between', ['A', 'A'], {
mode: 'foo',
label: 'bar',
}),
]})).toThrow();
});
it('defaults to showing notes around the entire diagram', () => {
const sequence = generator.generate({stages: [
PARSED.note('note right', []),

View File

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

View File

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

View File

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

View File

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

View File

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