Begin introducing concept of agents and tokens having metadata

This commit is contained in:
David Evans 2017-10-29 22:30:02 +00:00
parent 55b5232fa6
commit 12f81b4f9d
8 changed files with 456 additions and 227 deletions

View File

@ -3,7 +3,7 @@
requirejs.config(window.getRequirejsCDN());
/* jshint -W072 */
/* jshint -W072 */ // Allow several required modules
requirejs([
'interface/Interface',
'sequence/Parser',

View File

@ -20,10 +20,11 @@ define(['core/ArrayUtilities'], (array) => {
const CM_AGENT_LIST_TO_TEXT = makeCMCommaBlock('variable', 'Agent', {
':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}},
});
const CM_AGENT_LIST_TO_OPTTEXT = makeCMCommaBlock('variable', 'Agent', {
const CM_AGENT_TO_OPTTEXT = {type: 'variable', suggest: 'Agent', then: {
'': 0,
':': {type: 'operator', suggest: true, then: {'': CM_TEXT_TO_END}},
'\n': CM_END,
});
}};
const CM_NOTE_SIDE_THEN = {
'of': {type: 'keyword', suggest: true, then: {
@ -35,20 +36,29 @@ define(['core/ArrayUtilities'], (array) => {
'': CM_AGENT_LIST_TO_TEXT,
};
const CM_NOTE_LSIDE = {
type: 'keyword',
suggest: ['left of ', 'left: '],
then: CM_NOTE_SIDE_THEN,
};
const CM_NOTE_RSIDE = {
type: 'keyword',
suggest: ['right of ', 'right: '],
then: CM_NOTE_SIDE_THEN,
};
function makeCMSideNote(side) {
return {
type: 'keyword',
suggest: [side + ' of ', side + ': '],
then: CM_NOTE_SIDE_THEN,
};
}
const CM_CONNECT = {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_OPTTEXT,
'+': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}},
'-': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}},
'': CM_AGENT_TO_OPTTEXT,
}};
const CM_CONNECT_FULL = {type: 'variable', suggest: 'Agent', then: {
'->': CM_CONNECT,
'-->': CM_CONNECT,
'<-': CM_CONNECT,
'<--': CM_CONNECT,
'<->': CM_CONNECT,
'<-->': CM_CONNECT,
':': {type: 'operator', suggest: true, override: 'Label', then: {}},
'': 0,
}};
const CM_COMMANDS = {type: 'error', then: {
@ -98,8 +108,8 @@ define(['core/ArrayUtilities'], (array) => {
'over': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_TEXT,
}},
'left': CM_NOTE_LSIDE,
'right': CM_NOTE_RSIDE,
'left': makeCMSideNote('left'),
'right': makeCMSideNote('right'),
'between': {type: 'keyword', suggest: true, then: {
'': CM_AGENT_LIST_TO_TEXT,
}},
@ -110,8 +120,8 @@ define(['core/ArrayUtilities'], (array) => {
}},
}},
'text': {type: 'keyword', suggest: true, then: {
'left': CM_NOTE_LSIDE,
'right': CM_NOTE_RSIDE,
'left': makeCMSideNote('left'),
'right': makeCMSideNote('right'),
}},
'simultaneously': {type: 'keyword', suggest: true, then: {
':': {type: 'operator', suggest: true, then: {}},
@ -122,31 +132,38 @@ define(['core/ArrayUtilities'], (array) => {
}},
}},
}},
'': {type: 'variable', suggest: 'Agent', then: {
'->': CM_CONNECT,
'-->': CM_CONNECT,
'<-': CM_CONNECT,
'<--': CM_CONNECT,
'<->': CM_CONNECT,
'<-->': CM_CONNECT,
':': {type: 'operator', suggest: true, override: 'Label', then: {}},
'': 0,
}},
'+': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}},
'-': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}},
'': CM_CONNECT_FULL,
}};
function cmGetSuggestions(state, token, {suggest, then}) {
function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) {
return token + ' ';
} else {
return token + '\n';
}
}
function cmGetVarSuggestions(state, previous, current) {
if(
typeof current.suggest !== 'string' ||
previous.suggest === current.suggest
) {
return null;
}
return state['known' + current.suggest];
}
function cmGetSuggestions(state, token, previous, current) {
if(token === '') {
return state['known' + suggest];
} else if(suggest === true) {
if(Object.keys(then).length > 0) {
return [token + ' '];
} else {
return [token + '\n'];
}
} else if(Array.isArray(suggest)) {
return suggest;
} else if(suggest) {
return [suggest];
return cmGetVarSuggestions(state, previous, current);
} else if(current.suggest === true) {
return [cmCappedToken(token, current)];
} else if(Array.isArray(current.suggest)) {
return current.suggest;
} else if(current.suggest) {
return [current.suggest];
} else {
return null;
}
@ -154,13 +171,16 @@ define(['core/ArrayUtilities'], (array) => {
function cmMakeCompletions(state, path) {
const comp = [];
const {then} = array.last(path);
Object.keys(then).forEach((token) => {
let next = then[token];
const current = array.last(path);
Object.keys(current.then).forEach((token) => {
let next = current.then[token];
if(typeof next === 'number') {
next = path[path.length - next - 1];
}
array.mergeSets(comp, cmGetSuggestions(state, token, next));
array.mergeSets(
comp,
cmGetSuggestions(state, token, current, next)
);
});
return comp;
}
@ -213,6 +233,10 @@ define(['core/ArrayUtilities'], (array) => {
return current.type;
}
function getInitialValue(block) {
return (block.baseToken || {}).v || '';
}
return class Mode {
constructor(tokenDefinitions) {
this.tokenDefinitions = tokenDefinitions;
@ -250,7 +274,7 @@ define(['core/ArrayUtilities'], (array) => {
const block = this.tokenDefinitions[i];
if(this._matchPattern(stream, block.start, true)) {
state.currentType = i;
state.current = block.prefix || '';
state.current = getInitialValue(block);
return true;
}
}
@ -271,12 +295,6 @@ define(['core/ArrayUtilities'], (array) => {
return 'comment';
}
state.line.push(state.current);
if(state.current === '\n') {
// quoted newline is interpreted as a command separator;
// probably not what the writer expected, so highlight it
state.line.length = 0;
return 'warning';
}
return cmCheckToken(state, stream.eol());
}

View File

@ -12,9 +12,9 @@ define(['core/ArrayUtilities'], (array) => {
const DEFAULT_AGENT = new AgentState(false);
const NOTE_DEFAULT_AGENTS = {
'note over': ['[', ']'],
'note left': ['['],
'note right': [']'],
'note over': [{name: '['}, {name: ']'}],
'note left': [{name: '['}],
'note right': [{name: ']'}],
};
return class Generator {
@ -151,16 +151,19 @@ define(['core/ArrayUtilities'], (array) => {
}
handleAgentDefine({agents}) {
array.mergeSets(this.currentNest.agents, agents);
array.mergeSets(this.agents, agents);
const agentNames = agents.map((agent) => agent.name);
array.mergeSets(this.currentNest.agents, agentNames);
array.mergeSets(this.agents, agentNames);
}
handleAgentBegin({agents, mode}) {
this.setAgentVis(agents, true, mode, true);
const agentNames = agents.map((agent) => agent.name);
this.setAgentVis(agentNames, true, mode, true);
}
handleAgentEnd({agents, mode}) {
this.setAgentVis(agents, false, mode, true);
const agentNames = agents.map((agent) => agent.name);
this.setAgentVis(agentNames, false, mode, true);
}
handleBlockBegin({mode, label}) {
@ -204,11 +207,16 @@ define(['core/ArrayUtilities'], (array) => {
handleUnknownStage(stage) {
if(stage.agents) {
this.setAgentVis(stage.agents, true, 'box');
array.mergeSets(this.currentNest.agents, stage.agents);
array.mergeSets(this.agents, 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.currentSection.stages.push(stage);
this.currentNest.hasContent = true;
}

View File

@ -53,9 +53,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('returns aggregated agents', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: '<-', agents: ['C', 'D']},
{type: AGENT_BEGIN, agents: ['E'], mode: 'box'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: '<-', agents: [{name: 'C'}, {name: 'D'}]},
{type: AGENT_BEGIN, agents: [{name: 'E'}], mode: 'box'},
]});
expect(sequence.agents).toEqual(
['[', 'A', 'B', 'C', 'D', 'E', ']']
@ -64,23 +64,23 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('always puts the implicit right agent on the right', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: [']', 'B']},
{type: '->', agents: [{name: ']'}, {name: 'B'}]},
]});
expect(sequence.agents).toEqual(['[', 'B', ']']);
});
it('accounts for define calls when ordering agents', () => {
const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: ['B']},
{type: '->', agents: ['A', 'B']},
{type: AGENT_DEFINE, agents: [{name: 'B'}]},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
]});
expect(sequence.agents).toEqual(['[', 'B', 'A', ']']);
});
it('creates implicit begin stages for agents when used', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: '->', agents: ['B', 'C']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: '->', agents: [{name: 'B'}, {name: 'C'}]},
]});
expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -97,7 +97,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
terminators: 'foo',
},
stages: [
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
],
});
expect(sequence.stages).toEqual([
@ -109,9 +109,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('does not create duplicate begin stages', () => {
const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: ['A', 'B', 'C'], mode: 'box'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: ['B', 'C']},
{type: AGENT_BEGIN, agents: [
{name: 'A'},
{name: 'B'},
{name: 'C'},
], mode: 'box'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: '->', agents: [{name: 'B'}, {name: 'C'}]},
]});
expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B', 'C'], mode: 'box'},
@ -123,10 +127,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('redisplays agents if they have been hidden', () => {
const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
{type: '->', agents: ['A', 'B']},
{type: AGENT_END, agents: ['B'], mode: 'cross'},
{type: '->', agents: ['A', 'B']},
{type: AGENT_BEGIN, agents: [
{name: 'A'},
{name: 'B'},
], mode: 'box'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_END, agents: [{name: 'B'}], mode: 'cross'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
]});
expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -140,10 +147,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('collapses adjacent begin statements', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: AGENT_BEGIN, agents: ['D'], mode: 'box'},
{type: '->', agents: ['B', 'C']},
{type: '->', agents: ['C', 'D']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_BEGIN, agents: [{name: 'D'}], mode: 'box'},
{type: '->', agents: [{name: 'B'}, {name: 'C'}]},
{type: '->', agents: [{name: 'C'}, {name: 'D'}]},
]});
expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -157,9 +164,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes superfluous begin statements', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: AGENT_BEGIN, agents: ['A', 'C', 'D'], mode: 'box'},
{type: AGENT_BEGIN, agents: ['C', 'E'], mode: 'box'},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_BEGIN, agents: [
{name: 'A'},
{name: 'C'},
{name: 'D'},
], mode: 'box'},
{type: AGENT_BEGIN, agents: [
{name: 'C'},
{name: 'E'},
], mode: 'box'},
]});
expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -173,11 +187,22 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes superfluous end statements', () => {
const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: ['E']},
{type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'},
{type: '->', agents: ['A', 'B']},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'},
{type: AGENT_END, agents: ['A', 'D', 'E'], mode: 'cross'},
{type: AGENT_DEFINE, agents: [{name: 'E'}]},
{type: AGENT_BEGIN, agents: [
{name: 'C'},
{name: 'D'},
], mode: 'box'},
{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([
{type: AGENT_BEGIN, agents: ['C', 'D', 'A', 'B'], mode: 'box'},
@ -188,9 +213,19 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('does not merge different modes of end', () => {
const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'},
{type: '->', agents: ['A', 'B']},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'},
{type: AGENT_BEGIN, agents: [
{name: 'C'},
{name: 'D'},
], mode: 'box'},
{type: '->', agents: [
{name: 'A'},
{name: 'B'},
]},
{type: AGENT_END, agents: [
{name: 'A'},
{name: 'B'},
{name: 'C'},
], mode: 'cross'},
]});
expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['C', 'D', 'A', 'B'], mode: 'box'},
@ -203,7 +238,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('creates virtual agents for block statements', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
]});
@ -214,14 +249,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('positions virtual block agents near involved agents', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['C', 'D']},
{type: '->', agents: [{name: 'C'}, {name: 'D'}]},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['E', 'F']},
{type: '->', agents: [{name: 'E'}, {name: 'F'}]},
{type: BLOCK_END},
{type: BLOCK_END},
{type: '->', agents: ['G', 'H']},
{type: '->', agents: [{name: 'G'}, {name: 'H'}]},
]});
expect(sequence.agents).toEqual([
@ -245,7 +280,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('records virtual block agent names in blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
]});
@ -258,9 +293,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('records all sections within blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'C']},
{type: '->', agents: [{name: 'A'}, {name: 'C'}]},
{type: BLOCK_END},
]});
@ -280,10 +315,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('records all involved agents in nested blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'C']},
{type: '->', agents: [{name: 'A'}, {name: 'C'}]},
{type: BLOCK_END},
{type: BLOCK_END},
]});
@ -312,10 +347,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('preserves block boundaries when agents exist outside', () => {
const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
{type: BLOCK_END},
]});
@ -344,7 +379,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('allows empty block parts after split', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_END},
]});
@ -363,7 +398,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
]});
@ -392,7 +427,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes blocks containing only define statements / markers', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: AGENT_DEFINE, agents: ['A']},
{type: AGENT_DEFINE, agents: [{name: 'A'}]},
{type: 'mark', name: 'foo'},
{type: BLOCK_END},
]});
@ -415,7 +450,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes entirely empty nested blocks', () => {
const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_END},
@ -435,13 +470,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('rejects unterminated blocks', () => {
expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
]})).toThrow();
expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
]})).toThrow();
});
@ -453,7 +488,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
{type: BLOCK_END},
]})).toThrow();
@ -466,7 +501,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
]})).toThrow();
@ -476,7 +511,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'repeat', label: 'abc'},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'B']},
{type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END},
]})).toThrow();
});
@ -486,7 +521,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: 'note right', agents: [], foo: 'bar'},
{type: 'note left', agents: [], foo: 'bar'},
{type: 'note over', agents: [], foo: 'bar'},
{type: 'note right', agents: ['[']},
{type: 'note right', agents: [{name: '['}]},
]});
expect(sequence.stages).toEqual([
{type: 'note right', agents: [']'], foo: 'bar'},
@ -498,19 +533,19 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('rejects attempts to change implicit agents', () => {
expect(() => generator.generate({stages: [
{type: AGENT_BEGIN, agents: ['['], mode: 'box'},
{type: AGENT_BEGIN, agents: [{name: '['}], mode: 'box'},
]})).toThrow();
expect(() => generator.generate({stages: [
{type: AGENT_BEGIN, agents: [']'], mode: 'box'},
{type: AGENT_BEGIN, agents: [{name: ']'}], mode: 'box'},
]})).toThrow();
expect(() => generator.generate({stages: [
{type: AGENT_END, agents: ['['], mode: 'cross'},
{type: AGENT_END, agents: [{name: '['}], mode: 'cross'},
]})).toThrow();
expect(() => generator.generate({stages: [
{type: AGENT_END, agents: [']'], mode: 'cross'},
{type: AGENT_END, agents: [{name: ']'}], mode: 'cross'},
]})).toThrow();
});
});

View File

@ -24,13 +24,26 @@ define([
const TOKENS = [
{start: /#/y, end: /(?=\n)|$/y, omit: true},
{start: /"/y, end: /"/y, escape: /\\(.)/y, escapeWith: unescape},
{start: /'/y, end: /'/y, escape: /\\(.)/y, escapeWith: unescape},
{
start: /"/y,
end: /"/y,
escape: /\\(.)/y,
escapeWith: unescape,
baseToken: {q: true},
},
{
start: /'/y,
end: /'/y,
escape: /\\(.)/y,
escapeWith:
unescape,
baseToken: {q: true},
},
{start: /(?=[^ \t\r\n:+\-<>,])/y, end: /(?=[ \t\r\n:+\-<>,])|$/y},
{start: /(?=[+\-<>])/y, end: /(?=[^+\-<>])|$/y},
{start: /,/y, prefix: ','},
{start: /:/y, prefix: ':'},
{start: /\n/y, prefix: '\n'},
{start: /,/y, baseToken: {v: ','}},
{start: /:/y, baseToken: {v: ':'}},
{start: /\n/y, baseToken: {v: '\n'}},
];
const BLOCK_TYPES = {
@ -95,7 +108,8 @@ define([
return {
newBlock: block,
end: !block.end,
append: (block.prefix || ''),
appendSpace: '',
appendValue: '',
skip: match[0].length,
};
}
@ -103,7 +117,8 @@ define([
return {
newBlock: null,
end: false,
append: '',
appendSpace: src[i],
appendValue: '',
skip: 1,
};
}
@ -115,7 +130,8 @@ define([
return {
newBlock: null,
end: false,
append: block.escapeWith(match),
appendSpace: '',
appendValue: block.escapeWith(match),
skip: match[0].length,
};
}
@ -125,14 +141,16 @@ define([
return {
newBlock: null,
end: true,
append: '',
appendSpace: '',
appendValue: '',
skip: match[0].length,
};
}
return {
newBlock: null,
end: false,
append: src[i],
appendSpace: '',
appendValue: src[i],
skip: 1,
};
}
@ -145,10 +163,28 @@ define([
}
}
function joinLabel(line, begin, end) {
if(end <= begin) {
return '';
}
let result = line[begin].v;
for(let i = begin + 1; i < end; ++ i) {
result += line[i].s + line[i].v;
}
return result;
}
function debugLine(line) {
return joinLabel(line, 0, line.length);
}
function skipOver(line, start, skip, error = null) {
if(skip.some((token, i) => (line[start + i] !== token))) {
if(skip.some((token, i) => (
!line[start + i] ||
line[start + i].v !== token
))) {
if(error) {
throw new Error(error + ': ' + line.join(' '));
throw new Error(error + ': ' + debugLine(line));
} else {
return start;
}
@ -156,53 +192,83 @@ define([
return start + skip.length;
}
function parseCommaList(tokens) {
function findToken(line, token, start = 0) {
for(let i = start; i < line.length; ++ i) {
if(line[i].v === token) {
return i;
}
}
return -1;
}
function parseAgentList(line, start, end) {
const list = [];
let current = '';
tokens.forEach((token) => {
if(token === ',') {
let first = true;
for(let i = start; i < end; ++ i) {
if(line[i].v === ',') {
if(current) {
list.push(current);
list.push({name: current});
current = '';
first = true;
}
} else {
current += (current ? ' ' : '') + token;
if(!first) {
current += line[i].s;
} else {
first = false;
}
current += line[i].v;
}
});
}
if(current) {
list.push(current);
list.push({name: current});
}
return list;
}
function readAgent(line, begin, end) {
if(line[begin].v === '+' || line[begin].v === '-') {
return {
opt: line[begin].v,
name: joinLabel(line, begin + 1, end),
};
} else {
return {
opt: '',
name: joinLabel(line, begin, end),
};
}
}
const PARSERS = [
(line, meta) => { // title
if(line[0] !== 'title') {
if(line[0].v !== 'title') {
return null;
}
meta.title = line.slice(1).join(' ');
meta.title = joinLabel(line, 1, line.length);
return true;
},
(line, meta) => { // terminators
if(line[0] !== 'terminators') {
if(line[0].v !== 'terminators') {
return null;
}
if(TERMINATOR_TYPES.indexOf(line[1]) === -1) {
throw new Error('Unknown termination: ' + line.join(' '));
if(TERMINATOR_TYPES.indexOf(line[1].v) === -1) {
throw new Error('Unknown termination: ' + debugLine(line));
}
meta.terminators = line[1];
meta.terminators = line[1].v;
return true;
},
(line) => { // block
if(line[0] === 'end' && line.length === 1) {
if(line[0].v === 'end' && line.length === 1) {
return {type: 'block end'};
}
const type = BLOCK_TYPES[line[0]];
const type = BLOCK_TYPES[line[0].v];
if(!type) {
return null;
}
@ -214,36 +280,33 @@ define([
return {
type: type.type,
mode: type.mode,
label: line.slice(skip).join(' '),
label: joinLabel(line, skip, line.length),
};
},
(line) => { // agent
const type = AGENT_MANIPULATION_TYPES[line[0]];
if(!type) {
return null;
}
if(line.length <= 1) {
const type = AGENT_MANIPULATION_TYPES[line[0].v];
if(!type || line.length <= 1) {
return null;
}
return Object.assign({
agents: parseCommaList(line.slice(1)),
agents: parseAgentList(line, 1, line.length),
}, type);
},
(line) => { // async
if(line[0] !== 'simultaneously') {
if(line[0].v !== 'simultaneously') {
return null;
}
if(array.last(line) !== ':') {
if(array.last(line).v !== ':') {
return null;
}
let target = '';
if(line.length > 2) {
if(line[1] !== 'with') {
if(line[1].v !== 'with') {
return null;
}
target = line.slice(2, line.length - 1).join(' ');
target = joinLabel(line, 2, line.length - 1);
}
return {
type: 'async',
@ -252,41 +315,44 @@ define([
},
(line) => { // note
const mode = NOTE_TYPES[line[0]];
const labelSplit = line.indexOf(':');
const mode = NOTE_TYPES[line[0].v];
const labelSplit = findToken(line, ':');
if(!mode || labelSplit === -1) {
return null;
}
const type = mode.types[line[1]];
const type = mode.types[line[1].v];
if(!type) {
return null;
}
let skip = 2;
skip = skipOver(line, skip, type.skip);
const agents = parseCommaList(line.slice(skip, labelSplit));
const agents = parseAgentList(line, skip, labelSplit);
if(
agents.length < type.min ||
(type.max !== null && agents.length > type.max)
) {
throw new Error('Invalid ' + line[0] + ': ' + line.join(' '));
throw new Error(
'Invalid ' + mode.mode +
': ' + debugLine(line)
);
}
return {
type: type.type,
agents,
mode: mode.mode,
label: line.slice(labelSplit + 1).join(' '),
label: joinLabel(line, labelSplit + 1, line.length),
};
},
(line) => { // connection
let labelSplit = line.indexOf(':');
let labelSplit = findToken(line, ':');
if(labelSplit === -1) {
labelSplit = line.length;
}
let typeSplit = -1;
let options = null;
for(let j = 0; j < line.length; ++ j) {
const opts = CONNECTION_TYPES[line[j]];
const opts = CONNECTION_TYPES[line[j].v];
if(opts) {
typeSplit = j;
options = opts;
@ -299,20 +365,20 @@ define([
return Object.assign({
type: 'connection',
agents: [
line.slice(0, typeSplit).join(' '),
line.slice(typeSplit + 1, labelSplit).join(' '),
readAgent(line, 0, typeSplit),
readAgent(line, typeSplit + 1, labelSplit),
],
label: line.slice(labelSplit + 1).join(' '),
label: joinLabel(line, labelSplit + 1, line.length),
}, options);
},
(line) => { // marker
if(line.length < 2 || array.last(line) !== ':') {
if(line.length < 2 || array.last(line).v !== ':') {
return null;
}
return {
type: 'mark',
name: line.slice(0, line.length - 1).join(' '),
name: joinLabel(line, 0, line.length - 1),
};
},
];
@ -326,7 +392,7 @@ define([
}
}
if(!stage) {
throw new Error('Unrecognised command: ' + line.join(' '));
throw new Error('Unrecognised command: ' + debugLine(line));
}
if(typeof stage === 'object') {
stages.push(stage);
@ -337,19 +403,21 @@ define([
tokenise(src) {
const tokens = [];
let block = null;
let current = '';
let current = {s: '', v: '', q: false};
for(let i = 0; i <= src.length;) {
const {newBlock, end, append, skip} = tokAdvance(src, i, block);
if(newBlock) {
block = newBlock;
current = '';
const advance = tokAdvance(src, i, block);
if(advance.newBlock) {
block = advance.newBlock;
Object.assign(current, block.baseToken);
}
current += append;
i += skip;
if(end) {
current.s += advance.appendSpace;
current.v += advance.appendValue;
i += advance.skip;
if(advance.end) {
if(!block.omit) {
tokens.push(current);
}
current = {s: '', v: '', q: false};
block = null;
}
}
@ -371,7 +439,7 @@ define([
const lines = [];
let line = [];
tokens.forEach((token) => {
if(token === '\n') {
if(token.v === '\n' && !token.q) {
if(line.length > 0) {
lines.push(line);
line = [];

View File

@ -7,55 +7,103 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
it('converts the source into atomic tokens', () => {
const input = 'foo bar -> baz';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', 'bar', '->', 'baz']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: 'bar', q: false},
{s: ' ', v: '->', q: false},
{s: ' ', v: 'baz', q: false},
]);
});
it('splits tokens at flexible boundaries', () => {
const input = 'foo bar->baz';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', 'bar', '->', 'baz']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: 'bar', q: false},
{s: '', v: '->', q: false},
{s: '', v: 'baz', q: false},
]);
});
it('parses newlines as tokens', () => {
const input = 'foo bar\nbaz';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', 'bar', '\n', 'baz']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: 'bar', q: false},
{s: '', v: '\n', q: false},
{s: '', v: 'baz', q: false},
]);
});
it('parses quoted newlines as quoted tokens', () => {
const input = 'foo "\n" baz';
const tokens = parser.tokenise(input);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: '\n', q: true},
{s: ' ', v: 'baz', q: false},
]);
});
it('removes leading and trailing whitespace', () => {
const input = ' foo \t bar\t\n baz';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', 'bar', '\n', 'baz']);
expect(tokens).toEqual([
{s: ' ', v: 'foo', q: false},
{s: ' \t ', v: 'bar', q: false},
{s: '\t', v: '\n', q: false},
{s: ' ', v: 'baz', q: false},
]);
});
it('parses quoted strings as single tokens', () => {
const input = 'foo "zig zag" \'abc def\'';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', 'zig zag', 'abc def']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: 'zig zag', q: true},
{s: ' ', v: 'abc def', q: true},
]);
});
it('ignores comments', () => {
const input = 'foo # bar baz\nzig';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', '\n', 'zig']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: '', v: '\n', q: false},
{s: '', v: 'zig', q: false},
]);
});
it('ignores quotes within comments', () => {
const input = 'foo # bar "\'baz\nzig';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', '\n', 'zig']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: '', v: '\n', q: false},
{s: '', v: 'zig', q: false},
]);
});
it('interprets special characters within quoted strings', () => {
const input = 'foo "zig\\" zag\\n"';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', 'zig" zag\n']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: 'zig" zag\n', q: true},
]);
});
it('maintains whitespace and newlines within quoted strings', () => {
const input = 'foo " zig\n zag "';
const tokens = parser.tokenise(input);
expect(tokens).toEqual(['foo', ' zig\n zag ']);
expect(tokens).toEqual([
{s: '', v: 'foo', q: false},
{s: ' ', v: ' zig\n zag ', q: true},
]);
});
it('rejects unterminated quoted values', () => {
@ -65,33 +113,70 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
describe('.splitLines', () => {
it('combines tokens', () => {
const lines = parser.splitLines(['abc', 'd']);
const lines = parser.splitLines([
{s: '', v: 'abc', q: false},
{s: '', v: 'd', q: false},
]);
expect(lines).toEqual([
['abc', 'd'],
[{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}],
]);
});
it('splits at newlines', () => {
const lines = parser.splitLines(['abc', 'd', '\n', 'e']);
const lines = parser.splitLines([
{s: '', v: 'abc', q: false},
{s: '', v: 'd', q: false},
{s: '', v: '\n', q: false},
{s: '', v: 'e', q: false},
]);
expect(lines).toEqual([
['abc', 'd'],
['e'],
[{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}],
[{s: '', v: 'e', q: false}],
]);
});
it('ignores multiple newlines', () => {
const lines = parser.splitLines(['abc', 'd', '\n', '\n', 'e']);
const lines = parser.splitLines([
{s: '', v: 'abc', q: false},
{s: '', v: 'd', q: false},
{s: '', v: '\n', q: false},
{s: '', v: '\n', q: false},
{s: '', v: 'e', q: false},
]);
expect(lines).toEqual([
['abc', 'd'],
['e'],
[{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}],
[{s: '', v: 'e', q: false}],
]);
});
it('ignores trailing newlines', () => {
const lines = parser.splitLines(['abc', 'd', '\n', 'e', '\n']);
const lines = parser.splitLines([
{s: '', v: 'abc', q: false},
{s: '', v: 'd', q: false},
{s: '', v: '\n', q: false},
{s: '', v: 'e', q: false},
{s: '', v: '\n', q: false},
]);
expect(lines).toEqual([
['abc', 'd'],
['e'],
[{s: '', v: 'abc', q: false}, {s: '', v: 'd', q: false}],
[{s: '', v: 'e', q: false}],
]);
});
it('handles quoted newlines as regular tokens', () => {
const lines = parser.splitLines([
{s: '', v: 'abc', q: false},
{s: '', v: 'd', q: false},
{s: '', v: '\n', q: true},
{s: '', v: 'e', q: false},
]);
expect(lines).toEqual([
[
{s: '', v: 'abc', q: false},
{s: '', v: 'd', q: false},
{s: '', v: '\n', q: true},
{s: '', v: 'e', q: false},
],
]);
});
@ -101,13 +186,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
});
});
function connectionStage(agents, label = '') {
function connectionStage(agentNames, label = '') {
return {
type: 'connection',
line: 'solid',
left: false,
right: true,
agents,
agents: agentNames.map((agent) => ({opt: '', name: agent})),
label,
};
}
@ -153,6 +238,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
]);
});
it('respects spacing within agent names', () => {
const parsed = parser.parse('A+B -> C D');
expect(parsed.stages).toEqual([
connectionStage(['A+B', 'C D']),
]);
});
it('parses optional labels', () => {
const parsed = parser.parse('A B -> C D: foo bar');
expect(parsed.stages).toEqual([
@ -191,7 +283,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'solid',
left: false,
right: true,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: '',
},
{
@ -199,7 +291,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'solid',
left: true,
right: false,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: '',
},
{
@ -207,7 +299,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'solid',
left: true,
right: true,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: '',
},
{
@ -215,7 +307,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'dash',
left: false,
right: true,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: '',
},
{
@ -223,7 +315,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'dash',
left: true,
right: false,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: '',
},
{
@ -231,7 +323,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'dash',
left: true,
right: true,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: '',
},
]);
@ -248,7 +340,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'solid',
left: true,
right: false,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: 'B -> A',
},
{
@ -256,7 +348,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
line: 'solid',
left: false,
right: true,
agents: ['A', 'B'],
agents: [{opt: '', name: 'A'}, {opt: '', name: 'B'}],
label: 'B <- A',
},
]);
@ -266,7 +358,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A: hello there');
expect(parsed.stages).toEqual([{
type: 'note over',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'note',
label: 'hello there',
}]);
@ -283,31 +375,31 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.stages).toEqual([
{
type: 'note left',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'note',
label: 'hello there',
},
{
type: 'note left',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'note',
label: 'hello there',
},
{
type: 'note right',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'note',
label: 'hello there',
},
{
type: 'note right',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'note',
label: 'hello there',
},
{
type: 'note between',
agents: ['A', 'B'],
agents: [{name: 'A'}, {name: 'B'}],
mode: 'note',
label: 'hi',
},
@ -318,7 +410,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('note over A B, C D: hi');
expect(parsed.stages).toEqual([{
type: 'note over',
agents: ['A B', 'C D'],
agents: [{name: 'A B'}, {name: 'C D'}],
mode: 'note',
label: 'hi',
}]);
@ -332,7 +424,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('state over A: doing stuff');
expect(parsed.stages).toEqual([{
type: 'note over',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'state',
label: 'doing stuff',
}]);
@ -346,7 +438,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const parsed = parser.parse('text right of A: doing stuff');
expect(parsed.stages).toEqual([{
type: 'note right',
agents: ['A'],
agents: [{name: 'A'}],
mode: 'text',
label: 'doing stuff',
}]);
@ -359,9 +451,20 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
'end A, B\n'
);
expect(parsed.stages).toEqual([
{type: 'agent define', agents: ['A', 'B']},
{type: 'agent begin', agents: ['A', 'B'], mode: 'box'},
{type: 'agent end', agents: ['A', 'B'], mode: 'cross'},
{
type: 'agent define',
agents: [{name: 'A'}, {name: 'B'}],
},
{
type: 'agent begin',
agents: [{name: 'A'}, {name: 'B'}],
mode: 'box',
},
{
type: 'agent end',
agents: [{name: 'A'}, {name: 'B'}],
mode: 'cross',
},
]);
});

View File

@ -103,7 +103,7 @@ define([
top: 0,
left: 3,
right: 3,
bottom: 0,
bottom: 1,
},
maskAttrs: {
'fill': '#FFFFFF',

View File

@ -24,9 +24,6 @@ html, body {
.cm-s-default .cm-string {color: #221111;}
.cm-s-default .cm-error {color: #FF0000;}
.cm-s-default .cm-warning {
background: #FFFF00;
}
.cm-s-default .cm-trailingspace {
background: rgba(255, 0, 0, 0.5);
}