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()); requirejs.config(window.getRequirejsCDN());
/* jshint -W072 */ /* jshint -W072 */ // Allow several required modules
requirejs([ requirejs([
'interface/Interface', 'interface/Interface',
'sequence/Parser', 'sequence/Parser',

View File

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

View File

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

View File

@ -53,9 +53,9 @@ 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: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: '<-', agents: ['C', 'D']}, {type: '<-', agents: [{name: 'C'}, {name: 'D'}]},
{type: AGENT_BEGIN, agents: ['E'], mode: 'box'}, {type: AGENT_BEGIN, agents: [{name: 'E'}], mode: 'box'},
]}); ]});
expect(sequence.agents).toEqual( expect(sequence.agents).toEqual(
['[', 'A', 'B', 'C', 'D', 'E', ']'] ['[', 'A', 'B', 'C', 'D', 'E', ']']
@ -64,23 +64,23 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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: [']', 'B']}, {type: '->', agents: [{name: ']'}, {name: 'B'}]},
]}); ]});
expect(sequence.agents).toEqual(['[', 'B', ']']); expect(sequence.agents).toEqual(['[', 'B', ']']);
}); });
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: ['B']}, {type: AGENT_DEFINE, agents: [{name: 'B'}]},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
]}); ]});
expect(sequence.agents).toEqual(['[', 'B', 'A', ']']); expect(sequence.agents).toEqual(['[', 'B', 'A', ']']);
}); });
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: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: '->', agents: ['B', 'C']}, {type: '->', agents: [{name: 'B'}, {name: 'C'}]},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -97,7 +97,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
terminators: 'foo', terminators: 'foo',
}, },
stages: [ stages: [
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
], ],
}); });
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
@ -109,9 +109,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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: ['A', 'B', 'C'], mode: 'box'}, {type: AGENT_BEGIN, agents: [
{type: '->', agents: ['A', 'B']}, {name: 'A'},
{type: '->', agents: ['B', 'C']}, {name: 'B'},
{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'}, {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', () => { it('redisplays agents if they have been hidden', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: AGENT_BEGIN, agents: [
{type: '->', agents: ['A', 'B']}, {name: 'A'},
{type: AGENT_END, agents: ['B'], mode: 'cross'}, {name: 'B'},
{type: '->', agents: ['A', '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([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -140,10 +147,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('collapses adjacent begin statements', () => { it('collapses adjacent begin statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_BEGIN, agents: ['D'], mode: 'box'}, {type: AGENT_BEGIN, agents: [{name: 'D'}], mode: 'box'},
{type: '->', agents: ['B', 'C']}, {type: '->', agents: [{name: 'B'}, {name: 'C'}]},
{type: '->', agents: ['C', 'D']}, {type: '->', agents: [{name: 'C'}, {name: 'D'}]},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'}, {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -157,9 +164,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes superfluous begin statements', () => { it('removes superfluous begin statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: AGENT_BEGIN, agents: ['A', 'C', 'D'], mode: 'box'}, {type: AGENT_BEGIN, agents: [
{type: AGENT_BEGIN, agents: ['C', 'E'], mode: 'box'}, {name: 'A'},
{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'}, {type: AGENT_BEGIN, agents: ['A', 'B'], mode: 'box'},
@ -173,11 +187,22 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('removes superfluous end statements', () => { it('removes superfluous end statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_DEFINE, agents: ['E']}, {type: AGENT_DEFINE, agents: [{name: 'E'}]},
{type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'}, {type: AGENT_BEGIN, agents: [
{type: '->', agents: ['A', 'B']}, {name: 'C'},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'}, {name: 'D'},
{type: AGENT_END, agents: ['A', 'D', 'E'], mode: 'cross'}, ], 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([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['C', 'D', 'A', 'B'], mode: 'box'}, {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', () => { it('does not merge different modes of end', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: AGENT_BEGIN, agents: ['C', 'D'], mode: 'box'}, {type: AGENT_BEGIN, agents: [
{type: '->', agents: ['A', 'B']}, {name: 'C'},
{type: AGENT_END, agents: ['A', 'B', 'C'], mode: 'cross'}, {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([ expect(sequence.stages).toEqual([
{type: AGENT_BEGIN, agents: ['C', 'D', 'A', 'B'], mode: 'box'}, {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', () => { it('creates virtual agents for block statements', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -214,14 +249,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {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: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['E', 'F']}, {type: '->', agents: [{name: 'E'}, {name: 'F'}]},
{type: BLOCK_END}, {type: BLOCK_END},
{type: BLOCK_END}, {type: BLOCK_END},
{type: '->', agents: ['G', 'H']}, {type: '->', agents: [{name: 'G'}, {name: 'H'}]},
]}); ]});
expect(sequence.agents).toEqual([ expect(sequence.agents).toEqual([
@ -245,7 +280,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -258,9 +293,9 @@ 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'}, {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_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'C']}, {type: '->', agents: [{name: 'A'}, {name: 'C'}]},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -280,10 +315,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
it('records all involved agents in nested blocks', () => { it('records all involved agents in nested blocks', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {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_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'}, {type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'C']}, {type: '->', agents: [{name: 'A'}, {name: 'C'}]},
{type: BLOCK_END}, {type: BLOCK_END},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -312,10 +347,10 @@ 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: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'}, {type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -344,7 +379,7 @@ 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'}, {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_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -363,7 +398,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -392,7 +427,7 @@ 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'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: AGENT_DEFINE, agents: ['A']}, {type: AGENT_DEFINE, agents: [{name: 'A'}]},
{type: 'mark', name: 'foo'}, {type: 'mark', name: 'foo'},
{type: BLOCK_END}, {type: BLOCK_END},
]}); ]});
@ -415,7 +450,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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'}, {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_SPLIT, mode: 'else', label: 'xyz'},
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_END}, {type: BLOCK_END},
@ -435,13 +470,13 @@ 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'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: BLOCK_BEGIN, mode: 'if', label: 'def'}, {type: BLOCK_BEGIN, mode: 'if', label: 'def'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
]})).toThrow(); ]})).toThrow();
}); });
@ -453,7 +488,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
{type: BLOCK_END}, {type: BLOCK_END},
]})).toThrow(); ]})).toThrow();
@ -466,7 +501,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'if', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'if', label: 'abc'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
]})).toThrow(); ]})).toThrow();
@ -476,7 +511,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: BLOCK_BEGIN, mode: 'repeat', label: 'abc'}, {type: BLOCK_BEGIN, mode: 'repeat', label: 'abc'},
{type: BLOCK_SPLIT, mode: 'else', label: 'xyz'}, {type: BLOCK_SPLIT, mode: 'else', label: 'xyz'},
{type: '->', agents: ['A', 'B']}, {type: '->', agents: [{name: 'A'}, {name: 'B'}]},
{type: BLOCK_END}, {type: BLOCK_END},
]})).toThrow(); ]})).toThrow();
}); });
@ -486,7 +521,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{type: 'note right', agents: [], foo: 'bar'}, {type: 'note right', agents: [], foo: 'bar'},
{type: 'note left', agents: [], foo: 'bar'}, {type: 'note left', agents: [], foo: 'bar'},
{type: 'note over', agents: [], foo: 'bar'}, {type: 'note over', agents: [], foo: 'bar'},
{type: 'note right', agents: ['[']}, {type: 'note right', agents: [{name: '['}]},
]}); ]});
expect(sequence.stages).toEqual([ expect(sequence.stages).toEqual([
{type: 'note right', agents: [']'], foo: 'bar'}, {type: 'note right', agents: [']'], foo: 'bar'},
@ -498,19 +533,19 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
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: ['['], mode: 'box'}, {type: AGENT_BEGIN, agents: [{name: '['}], mode: 'box'},
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_BEGIN, agents: [']'], mode: 'box'}, {type: AGENT_BEGIN, agents: [{name: ']'}], mode: 'box'},
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_END, agents: ['['], mode: 'cross'}, {type: AGENT_END, agents: [{name: '['}], mode: 'cross'},
]})).toThrow(); ]})).toThrow();
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
{type: AGENT_END, agents: [']'], mode: 'cross'}, {type: AGENT_END, agents: [{name: ']'}], mode: 'cross'},
]})).toThrow(); ]})).toThrow();
}); });
}); });

View File

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

View File

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

View File

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