diff --git a/README.md b/README.md index bbc6b43..4ef3f9a 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,19 @@ terminators bar # (options are: box, bar, cross, none) ``` +### Agent Aliases + +Agent Aliases preview + +``` +define My complicated agent name as A +define "Another agent name, +and this one's multi-line!" as B + +A -> B: this is much easier +A <- B: than writing the whole name +``` + ### Alternative Agent Ordering Alternative Agent Ordering preview diff --git a/screenshots/AgentAliases.png b/screenshots/AgentAliases.png new file mode 100644 index 0000000..0724670 Binary files /dev/null and b/screenshots/AgentAliases.png differ diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index 1ed55d4..258fbb4 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -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) { diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js index 791c301..b1bea09 100644 --- a/scripts/sequence/Generator.js +++ b/scripts/sequence/Generator.js @@ -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( diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index 4f4ed59..b5701f4 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -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', []), diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index 2aaaf69..1992ec9 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -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, diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 503428d..fdc6171 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -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', }, ]); diff --git a/scripts/sequence/components/Note.js b/scripts/sequence/components/Note.js index bc705f9..742c8e1 100644 --- a/scripts/sequence/components/Note.js +++ b/scripts/sequence/components/Note.js @@ -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', diff --git a/scripts/tester/jshintRunner.js b/scripts/tester/jshintRunner.js index 4306364..a3331ac 100644 --- a/scripts/tester/jshintRunner.js +++ b/scripts/tester/jshintRunner.js @@ -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) { diff --git a/styles/main.css b/styles/main.css index 36e38a0..9f20cbb 100644 --- a/styles/main.css +++ b/styles/main.css @@ -88,6 +88,7 @@ html, body { background: #FFFFFF; font-family: sans-serif; overflow: hidden; + user-select: none; } .options.links {