From 26bc3acd3e7518f0bbc1eca7230b61eb27901039 Mon Sep 17 00:00:00 2001 From: David Evans Date: Sun, 19 Nov 2017 22:57:46 +0000 Subject: [PATCH] Improve block handling so references can be added [#21] --- scripts/sequence/CodeMirrorMode.js | 38 ++- scripts/sequence/Generator.js | 141 +++++----- scripts/sequence/Generator_spec.js | 251 ++++++++++-------- scripts/sequence/Parser.js | 38 +++ scripts/sequence/Parser_spec.js | 40 +++ scripts/sequence/Renderer.js | 260 ++++--------------- scripts/sequence/components/BaseComponent.js | 20 ++ scripts/sequence/components/Block.js | 146 +++++++++++ scripts/sequence/components/Block_spec.js | 22 ++ scripts/sequence/components/Parallel.js | 76 ++++++ scripts/sequence/components/Parallel_spec.js | 14 + scripts/specs.js | 2 + 12 files changed, 668 insertions(+), 380 deletions(-) create mode 100644 scripts/sequence/components/Block.js create mode 100644 scripts/sequence/components/Block_spec.js create mode 100644 scripts/sequence/components/Parallel.js create mode 100644 scripts/sequence/components/Parallel_spec.js diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index 4112413..fa9df60 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -7,7 +7,11 @@ define(['core/ArrayUtilities'], (array) => { const end = {type: '', suggest: '\n', then: {}}; const hiddenEnd = {type: '', then: {}}; - const textToEnd = {type: 'string', then: {'': 0, '\n': end}}; + function textTo(exit) { + return {type: 'string', then: Object.assign({'': 0}, exit)}; + } + + const textToEnd = textTo({'\n': end}); const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: { '': 0, 'as': {type: 'keyword', suggest: true, then: { @@ -20,11 +24,17 @@ define(['core/ArrayUtilities'], (array) => { ',': {type: 'operator', suggest: true, then: {'': 1}}, '\n': end, }}; - const agentListToText = {type: 'variable', suggest: 'Agent', then: { - '': 0, - ',': {type: 'operator', suggest: true, then: {'': 1}}, + + function agentListTo(exit) { + return {type: 'variable', suggest: 'Agent', then: Object.assign({ + '': 0, + ',': {type: 'operator', suggest: true, then: {'': 1}}, + }, exit)}; + } + + const agentListToText = agentListTo({ ':': {type: 'operator', suggest: true, then: {'': textToEnd}}, - }}; + }); const agentList2ToText = {type: 'variable', suggest: 'Agent', then: { '': 0, ',': {type: 'operator', suggest: true, then: {'': agentListToText}}, @@ -43,6 +53,23 @@ define(['core/ArrayUtilities'], (array) => { }}, '\n': end, }}; + const referenceName = { + ':': {type: 'operator', suggest: true, then: { + '': textTo({ + 'as': {type: 'keyword', suggest: true, then: { + '': {type: 'variable', suggest: 'Agent', then: { + '': 0, + '\n': end, + }}, + }}, + }), + }}, + }; + const refDef = {type: 'keyword', suggest: true, then: Object.assign({ + 'over': {type: 'keyword', suggest: true, then: { + '': agentListTo(referenceName), + }}, + }, referenceName)}; function makeSideNote(side) { return { @@ -158,6 +185,7 @@ define(['core/ArrayUtilities'], (array) => { }}, 'begin': {type: 'keyword', suggest: true, then: { '': aliasListToEnd, + 'reference': refDef, 'as': CM_ERROR, }}, 'end': {type: 'keyword', suggest: true, then: { diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js index 48be6b3..ec8e8d5 100644 --- a/scripts/sequence/Generator.js +++ b/scripts/sequence/Generator.js @@ -213,6 +213,10 @@ define(['core/ArrayUtilities'], (array) => { this.currentNest = null; this.stageHandlers = { + 'block begin': this.handleBlockBegin.bind(this), + 'block split': this.handleBlockSplit.bind(this), + 'block end': this.handleBlockEnd.bind(this), + 'group begin': this.handleGroupBegin.bind(this), 'mark': this.handleMark.bind(this), 'async': this.handleAsync.bind(this), 'agent define': this.handleAgentDefine.bind(this), @@ -224,9 +228,6 @@ define(['core/ArrayUtilities'], (array) => { 'note left': this.handleNote.bind(this), 'note right': this.handleNote.bind(this), 'note between': this.handleNote.bind(this), - 'block begin': this.handleBlockBegin.bind(this), - 'block split': this.handleBlockSplit.bind(this), - 'block end': this.handleBlockEnd.bind(this), }; this.handleStage = this.handleStage.bind(this); this.convertAgent = this.convertAgent.bind(this); @@ -365,22 +366,23 @@ define(['core/ArrayUtilities'], (array) => { const agents = [leftAgent, rightAgent]; const stages = []; this.currentSection = { - mode, - label, + header: { + type: 'block begin', + mode, + label, + left: leftAgent.name, + right: rightAgent.name, + ln, + }, stages, - ln, }; this.currentNest = { + mode, agents, leftAgent, rightAgent, hasContent: false, - stage: { - type: 'block', - sections: [this.currentSection], - left: leftAgent.name, - right: rightAgent.name, - }, + sections: [this.currentSection], }; this.agentStates.set(leftAgent.name, LOCKED_AGENT); this.agentStates.set(rightAgent.name, LOCKED_AGENT); @@ -389,6 +391,69 @@ define(['core/ArrayUtilities'], (array) => { return {agents, stages}; } + handleBlockBegin({ln, mode, label}) { + const name = '__BLOCK' + this.blockCount; + this.beginNested(mode, label, name, ln); + ++ this.blockCount; + } + + handleBlockSplit({ln, mode, label}) { + if(this.currentNest.mode !== 'if') { + throw new Error( + 'Invalid block nesting ("else" inside ' + + this.currentNest.mode + ')' + ); + } + optimiseStages(this.currentSection.stages); + this.currentSection = { + header: { + type: 'block split', + mode, + label, + left: this.currentNest.leftAgent.name, + right: this.currentNest.rightAgent.name, + ln, + }, + stages: [], + }; + this.currentNest.sections.push(this.currentSection); + } + + handleBlockEnd() { + if(this.nesting.length <= 1) { + throw new Error('Invalid block nesting (too many "end"s)'); + } + optimiseStages(this.currentSection.stages); + const nested = this.nesting.pop(); + this.currentNest = array.last(this.nesting); + this.currentSection = array.last(this.currentNest.sections); + + if(nested.hasContent) { + this.defineAgents(nested.agents); + addBounds( + this.agents, + nested.leftAgent, + nested.rightAgent, + nested.agents + ); + nested.sections.forEach((section) => { + this.currentSection.stages.push(section.header); + this.currentSection.stages.push(...section.stages); + }); + this.addStage({ + type: 'block end', + left: nested.leftAgent.name, + right: nested.rightAgent.name, + }); + } else { + throw new Error('Empty block'); + } + } + + handleGroupBegin() { + throw new Error('Groups are not supported yet'); + } + handleMark({name}) { this.markers.add(name); this.addStage({type: 'mark', name}, false); @@ -524,54 +589,14 @@ define(['core/ArrayUtilities'], (array) => { ]); } - handleBlockBegin({ln, mode, label}) { - const name = '__BLOCK' + this.blockCount; - this.beginNested(mode, label, name, ln); - ++ this.blockCount; - } - - handleBlockSplit({ln, mode, label}) { - const containerMode = this.currentNest.stage.sections[0].mode; - if(containerMode !== 'if') { - throw new Error( - 'Invalid block nesting ("else" inside ' + - containerMode + ')' - ); - } - optimiseStages(this.currentSection.stages); - this.currentSection = { - mode, - label, - stages: [], - ln, - }; - this.currentNest.stage.sections.push(this.currentSection); - } - - handleBlockEnd() { - if(this.nesting.length <= 1) { - throw new Error('Invalid block nesting (too many "end"s)'); - } - optimiseStages(this.currentSection.stages); - const nested = this.nesting.pop(); - this.currentNest = array.last(this.nesting); - this.currentSection = array.last(this.currentNest.stage.sections); - if(nested.hasContent) { - this.defineAgents(nested.agents); - addBounds( - this.agents, - nested.leftAgent, - nested.rightAgent, - nested.agents - ); - this.addStage(nested.stage); - } - } - handleStage(stage) { this.latestLine = stage.ln; try { - this.stageHandlers[stage.type](stage); + const handler = this.stageHandlers[stage.type]; + if(!handler) { + throw new Error('Unknown command: ' + stage.type); + } + handler(stage); } catch(e) { if(typeof e === 'object' && e.message) { throw new Error(e.message + ' at line ' + (stage.ln + 1)); @@ -594,7 +619,7 @@ define(['core/ArrayUtilities'], (array) => { if(this.nesting.length !== 1) { throw new Error( 'Unterminated section at line ' + - (this.currentSection.ln + 1) + (this.currentSection.header.ln + 1) ); } diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index 23f5081..06ba545 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -22,14 +22,25 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { return {type: 'block split', mode, label, ln}; }, - blockEnd: () => { - return {type: 'block end'}; + blockEnd: ({ln = 0} = {}) => { + return {type: 'block end', ln}; }, labelPattern: (pattern, {ln = 0} = {}) => { return {type: 'label pattern', pattern, ln}; }, + groupBegin: (alias, agentNames, {label = '', ln = 0} = {}) => { + return { + type: 'group begin', + agents: makeParsedAgents(agentNames), + mode: 'ref', + label, + alias, + ln, + }; + }, + defineAgents: (agentNames, {ln = 0} = {}) => { return { type: 'agent define', @@ -116,6 +127,51 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { }; }, + blockBegin: (mode, { + label = jasmine.anything(), + left = jasmine.anything(), + right = jasmine.anything(), + ln = jasmine.anything(), + } = {}) => { + return { + type: 'block begin', + mode, + label, + left, + right, + ln, + }; + }, + + blockSplit: (mode, { + label = jasmine.anything(), + left = jasmine.anything(), + right = jasmine.anything(), + ln = jasmine.anything(), + } = {}) => { + return { + type: 'block split', + mode, + label, + left, + right, + ln, + }; + }, + + blockEnd: ({ + left = jasmine.anything(), + right = jasmine.anything(), + ln = jasmine.anything(), + } = {}) => { + return { + type: 'block end', + left, + right, + ln, + }; + }, + connect: (agentNames, { label = jasmine.anything(), line = jasmine.anything(), @@ -741,39 +797,44 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { ]); }); - it('records virtual block agent names in blocks', () => { - const sequence = generator.generate({stages: [ - PARSED.blockBegin('if', 'abc'), - PARSED.connect(['A', 'B']), - PARSED.blockEnd(), - ]}); - - const block0 = sequence.stages[0]; - expect(block0.type).toEqual('block'); - expect(block0.left).toEqual('__BLOCK0['); - expect(block0.right).toEqual('__BLOCK0]'); - }); - - it('records all sections within blocks', () => { + it('propagates block statements', () => { const sequence = generator.generate({stages: [ PARSED.blockBegin('if', 'abc', {ln: 10}), PARSED.connect(['A', 'B']), PARSED.blockSplit('else', 'xyz', {ln: 20}), - PARSED.connect(['A', 'C']), + PARSED.connect(['A', 'B']), + PARSED.blockEnd({ln: 30}), + ]}); + + expect(sequence.stages).toEqual([ + GENERATED.blockBegin('if', {label: 'abc', ln: 10}), + jasmine.anything(), + jasmine.anything(), + GENERATED.blockSplit('else', {label: 'xyz', ln: 20}), + jasmine.anything(), + GENERATED.blockEnd({ln: 30}), + jasmine.anything(), + ]); + }); + + it('records virtual block agent names in block commands', () => { + const sequence = generator.generate({stages: [ + PARSED.blockBegin('if', 'abc'), + PARSED.connect(['A', 'B']), + PARSED.blockSplit('else', 'xyz'), + PARSED.connect(['A', 'B']), PARSED.blockEnd(), ]}); - const block0 = sequence.stages[0]; - expect(block0.sections).toEqual([ - {mode: 'if', label: 'abc', ln: 10, stages: [ - GENERATED.beginAgents(['A', 'B']), - GENERATED.connect(['A', 'B']), - ]}, - {mode: 'else', label: 'xyz', ln: 20, stages: [ - GENERATED.beginAgents(['C']), - GENERATED.connect(['A', 'C']), - ]}, - ]); + const bounds = { + left: '__BLOCK0[', + right: '__BLOCK0]', + }; + + const stages = sequence.stages; + expect(stages[0]).toEqual(GENERATED.blockBegin('if', bounds)); + expect(stages[3]).toEqual(GENERATED.blockSplit('else', bounds)); + expect(stages[5]).toEqual(GENERATED.blockEnd(bounds)); }); it('records virtual block agents in nested blocks', () => { @@ -798,15 +859,22 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { {name: '__BLOCK0]', anchorRight: false}, {name: ']', anchorRight: false}, ]); - const block0 = sequence.stages[0]; - expect(block0.type).toEqual('block'); - expect(block0.left).toEqual('__BLOCK0['); - expect(block0.right).toEqual('__BLOCK0]'); - const block1 = block0.sections[1].stages[0]; - expect(block1.type).toEqual('block'); - expect(block1.left).toEqual('__BLOCK1['); - expect(block1.right).toEqual('__BLOCK1]'); + const bounds0 = { + left: '__BLOCK0[', + right: '__BLOCK0]', + }; + + const bounds1 = { + left: '__BLOCK1[', + right: '__BLOCK1]', + }; + + const stages = sequence.stages; + expect(stages[0]).toEqual(GENERATED.blockBegin('if', bounds0)); + expect(stages[4]).toEqual(GENERATED.blockBegin('if', bounds1)); + expect(stages[7]).toEqual(GENERATED.blockEnd(bounds1)); + expect(stages[8]).toEqual(GENERATED.blockEnd(bounds0)); }); it('preserves block boundaries when agents exist outside', () => { @@ -829,123 +897,92 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { {name: '__BLOCK0]', anchorRight: false}, {name: ']', anchorRight: false}, ]); - const block0 = sequence.stages[2]; - expect(block0.type).toEqual('block'); - expect(block0.left).toEqual('__BLOCK0['); - expect(block0.right).toEqual('__BLOCK0]'); - const block1 = block0.sections[0].stages[0]; - expect(block1.type).toEqual('block'); - expect(block1.left).toEqual('__BLOCK1['); - expect(block1.right).toEqual('__BLOCK1]'); + const bounds0 = { + left: '__BLOCK0[', + right: '__BLOCK0]', + }; + + const bounds1 = { + left: '__BLOCK1[', + right: '__BLOCK1]', + }; + + const stages = sequence.stages; + expect(stages[2]).toEqual(GENERATED.blockBegin('if', bounds0)); + expect(stages[3]).toEqual(GENERATED.blockBegin('if', bounds1)); + expect(stages[5]).toEqual(GENERATED.blockEnd(bounds1)); + expect(stages[6]).toEqual(GENERATED.blockEnd(bounds0)); }); it('allows empty block parts after split', () => { - const sequence = generator.generate({stages: [ + expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.connect(['A', 'B']), PARSED.blockSplit('else', 'xyz'), PARSED.blockEnd(), - ]}); - - const block0 = sequence.stages[0]; - expect(block0.sections).toEqual([ - {mode: 'if', label: 'abc', ln: 0, stages: [ - jasmine.anything(), - jasmine.anything(), - ]}, - {mode: 'else', label: 'xyz', ln: 0, stages: []}, - ]); + ]})).not.toThrow(); }); it('allows empty block parts before split', () => { - const sequence = generator.generate({stages: [ + expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.blockSplit('else', 'xyz'), PARSED.connect(['A', 'B']), PARSED.blockEnd(), - ]}); - - const block0 = sequence.stages[0]; - expect(block0.sections).toEqual([ - {mode: 'if', label: 'abc', ln: 0, stages: []}, - {mode: 'else', label: 'xyz', ln: 0, stages: [ - jasmine.anything(), - jasmine.anything(), - ]}, - ]); + ]})).not.toThrow(); }); - it('removes entirely empty blocks', () => { - const sequence = generator.generate({stages: [ + it('allows deeply nested blocks', () => { + expect(() => generator.generate({stages: [ + PARSED.blockBegin('if', 'abc'), + PARSED.blockBegin('if', 'def'), + PARSED.connect(['A', 'B']), + PARSED.blockEnd(), + PARSED.blockEnd(), + ]})).not.toThrow(); + }); + + it('rejects entirely empty blocks', () => { + expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.blockSplit('else', 'xyz'), - PARSED.blockBegin('if', 'abc'), PARSED.blockEnd(), - PARSED.blockEnd(), - ]}); - - expect(sequence.stages).toEqual([]); + ]})).toThrow(); }); - it('removes blocks containing only define statements / markers', () => { - const sequence = generator.generate({stages: [ + it('rejects blocks containing only define statements / markers', () => { + expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.defineAgents(['A']), {type: 'mark', name: 'foo'}, PARSED.blockEnd(), - ]}); - - expect(sequence.stages).toEqual([]); + ]})).toThrow(); }); - it('does not create virtual agents for empty blocks', () => { - const sequence = generator.generate({stages: [ - PARSED.blockBegin('if', 'abc'), - PARSED.blockSplit('else', 'xyz'), - PARSED.blockBegin('if', 'abc'), - PARSED.blockEnd(), - PARSED.blockEnd(), - ]}); - - expect(sequence.agents).toEqual([ - {name: '[', anchorRight: true}, - {name: ']', anchorRight: false}, - ]); - }); - - it('removes entirely empty nested blocks', () => { - const sequence = generator.generate({stages: [ + it('rejects entirely empty nested blocks', () => { + expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.connect(['A', 'B']), PARSED.blockSplit('else', 'xyz'), PARSED.blockBegin('if', 'abc'), PARSED.blockEnd(), PARSED.blockEnd(), - ]}); - - const block0 = sequence.stages[0]; - expect(block0.sections).toEqual([ - {mode: 'if', label: 'abc', ln: 0, stages: [ - jasmine.anything(), - jasmine.anything(), - ]}, - {mode: 'else', label: 'xyz', ln: 0, stages: []}, - ]); + ]})).toThrow(); }); it('rejects unterminated blocks', () => { expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.connect(['A', 'B']), - ]})).toThrow(); + ]})).toThrow(new Error('Unterminated section at line 1')); expect(() => generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'def'), PARSED.connect(['A', 'B']), PARSED.blockEnd(), - ]})).toThrow(); + ]})).toThrow(new Error('Unterminated section at line 1')); }); it('rejects extra block terminations', () => { diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index c29194d..1593d98 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -255,6 +255,9 @@ define([ } const type = tokenKeyword(line[1]); + if(!type) { + throw makeError('Unspecified termination', line[0]); + } if(TERMINATOR_TYPES.indexOf(type) === -1) { throw makeError('Unknown termination "' + type + '"', line[1]); } @@ -268,6 +271,9 @@ define([ } const type = tokenKeyword(line[1]); + if(!type) { + throw makeError('Unspecified header', line[0]); + } if(TERMINATOR_TYPES.indexOf(type) === -1) { throw makeError('Unknown header "' + type + '"', line[1]); } @@ -313,6 +319,38 @@ define([ }; }, + (line) => { // begin reference + if( + tokenKeyword(line[0]) !== 'begin' || + tokenKeyword(line[1]) !== 'reference' + ) { + return null; + } + let agents = []; + const labelSep = findToken(line, ':'); + if(tokenKeyword(line[2]) === 'over' && labelSep > 3) { + agents = readAgentList(line, 3, labelSep); + } else if(labelSep !== 2) { + throw makeError('Expected ":" or "over"', line[2]); + } + const def = readAgent( + line, + labelSep + 1, + line.length, + {aliases: true} + ); + if(!def.alias) { + throw makeError('Reference must have an alias', line[labelSep]); + } + return { + type: 'group begin', + agents, + mode: 'ref', + label: def.name, + alias: def.alias, + }; + }, + (line) => { // agent const type = AGENT_MANIPULATION_TYPES[tokenKeyword(line[0])]; if(!type || line.length <= 1) { diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 06614fd..67eb491 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -425,6 +425,34 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { ]); }); + it('converts reference commands', () => { + const parsed = parser.parse( + 'begin reference: Foo bar as baz\n' + + 'begin reference over A, B: Foo bar as baz\n' + ); + expect(parsed.stages).toEqual([ + { + type: 'group begin', + ln: jasmine.anything(), + agents: [], + mode: 'ref', + label: 'Foo bar', + alias: 'baz', + }, + { + type: 'group begin', + ln: jasmine.anything(), + agents: [ + {name: 'A', alias: '', flags: []}, + {name: 'B', alias: '', flags: []}, + ], + mode: 'ref', + label: 'Foo bar', + alias: 'baz', + }, + ]); + }); + it('converts markers', () => { const parsed = parser.parse('abc:'); expect(parsed.stages).toEqual([{ @@ -530,12 +558,24 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { )); }); + it('rejects missing terminators', () => { + expect(() => parser.parse('terminators')).toThrow(new Error( + 'Unspecified termination at line 1, character 0' + )); + }); + it('rejects invalid headers', () => { expect(() => parser.parse('headers foo')).toThrow(new Error( 'Unknown header "foo" at line 1, character 8' )); }); + it('rejects missing headers', () => { + expect(() => parser.parse('headers')).toThrow(new Error( + 'Unspecified header at line 1, character 0' + )); + }); + it('rejects malformed notes', () => { expect(() => parser.parse('note over A hello')).toThrow(); }); diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index cd34955..fc32e93 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -5,6 +5,8 @@ define([ 'svg/SVGUtilities', 'svg/SVGShapes', './components/BaseComponent', + './components/Block', + './components/Parallel', './components/Marker', './components/AgentCap', './components/AgentHighlight', @@ -20,35 +22,6 @@ define([ /* jshint +W072 */ 'use strict'; - function traverse(stages, callbacks) { - stages.forEach((stage) => { - if(stage.type === 'block') { - const scope = {}; - if(callbacks.blockBeginFn) { - callbacks.blockBeginFn(scope, stage); - } - stage.sections.forEach((section) => { - if(callbacks.sectionBeginFn) { - callbacks.sectionBeginFn(scope, stage, section); - } - traverse(section.stages, callbacks); - if(callbacks.sectionEndFn) { - callbacks.sectionEndFn(scope, stage, section); - } - }); - if(callbacks.blockEndFn) { - callbacks.blockEndFn(scope, stage); - } - } else if(callbacks.stagesFn) { - if(stage.type === 'parallel') { - callbacks.stagesFn(stage.stages); - } else { - callbacks.stagesFn([stage]); - } - } - }); - } - function findExtremes(agentInfos, agentNames) { let min = null; let max = null; @@ -102,20 +75,8 @@ define([ components = BaseComponent.getComponents(); } - this.separationTraversalFns = { - stagesFn: this.separationStages.bind(this), - blockBeginFn: this.separationBlockBegin.bind(this), - sectionBeginFn: this.separationSectionBegin.bind(this), - blockEndFn: this.separationBlockEnd.bind(this), - }; - - this.renderTraversalFns = { - stagesFn: this.renderStages.bind(this), - blockBeginFn: this.renderBlockBegin.bind(this), - sectionBeginFn: this.renderSectionBegin.bind(this), - sectionEndFn: this.renderSectionEnd.bind(this), - blockEndFn: this.renderBlockEnd.bind(this), - }; + this.separationStage = this.separationStage.bind(this); + this.renderStage = this.renderStage.bind(this); this.addSeparation = this.addSeparation.bind(this); this.addDef = this.addDef.bind(this); @@ -196,28 +157,7 @@ define([ info2.separations.set(agentName1, Math.max(d2, dist)); } - separationBlockBegin(scope, {left, right}) { - array.mergeSets(this.visibleAgents, [left, right]); - } - - separationSectionBegin(scope, {left, right}, {mode, label}) { - const config = this.theme.block.section; - const width = ( - this.sizer.measure(config.mode.labelAttrs, mode).width + - config.mode.padding.left + - config.mode.padding.right + - this.sizer.measure(config.label.labelAttrs, label).width + - config.label.padding.left + - config.label.padding.right - ); - this.addSeparation(left, right, width); - } - - separationBlockEnd(scope, {left, right}) { - array.removeAll(this.visibleAgents, [left, right]); - } - - separationStages(stages) { + separationStage(stage) { const agentSpaces = new Map(); const agentNames = this.visibleAgents.slice(); @@ -239,13 +179,14 @@ define([ textSizer: this.sizer, addSpacing, addSeparation: this.addSeparation, + components: this.components, }; - stages.forEach((stage) => { - this.components.get(stage.type).separationPre(stage, env); - }); - stages.forEach((stage) => { - this.components.get(stage.type).separation(stage, env); - }); + const component = this.components.get(stage.type); + if(!component) { + throw new Error('Unknown component: ' + stage.type); + } + component.separationPre(stage, env); + component.separation(stage, env); array.mergeSets(agentNames, this.visibleAgents); agentNames.forEach((agentNameR) => { @@ -327,87 +268,6 @@ define([ } } - renderBlockBegin(scope, {left, right}) { - this.currentY = ( - this.checkAgentRange([left, right], this.currentY) + - this.theme.block.margin.top - ); - - scope.y = this.currentY; - scope.first = true; - this.markAgentRange([left, right], this.currentY); - } - - renderSectionBegin(scope, {left, right}, {mode, label}) { - this.currentY = this.checkAgentRange([left, right], this.currentY); - const config = this.theme.block; - const agentInfoL = this.agentInfos.get(left); - const agentInfoR = this.agentInfos.get(right); - - if(scope.first) { - scope.first = false; - } else { - this.currentY += config.section.padding.bottom; - this.sections.appendChild(svg.make('line', Object.assign({ - 'x1': agentInfoL.x, - 'y1': this.currentY, - 'x2': agentInfoR.x, - 'y2': this.currentY, - }, config.separator.attrs))); - } - - const modeRender = SVGShapes.renderBoxedText(mode, { - x: agentInfoL.x, - y: this.currentY, - padding: config.section.mode.padding, - boxAttrs: config.section.mode.boxAttrs, - labelAttrs: config.section.mode.labelAttrs, - boxLayer: this.blocks, - labelLayer: this.actionLabels, - SVGTextBlockClass: this.SVGTextBlockClass, - }); - - const labelRender = SVGShapes.renderBoxedText(label, { - x: agentInfoL.x + modeRender.width, - y: this.currentY, - padding: config.section.label.padding, - boxAttrs: {'fill': '#000000'}, - labelAttrs: config.section.label.labelAttrs, - boxLayer: this.mask, - labelLayer: this.actionLabels, - SVGTextBlockClass: this.SVGTextBlockClass, - }); - - this.currentY += ( - Math.max(modeRender.height, labelRender.height) + - config.section.padding.top - ); - this.markAgentRange([left, right], this.currentY); - } - - renderSectionEnd(/*scope, block, section*/) { - } - - renderBlockEnd(scope, {left, right}) { - const config = this.theme.block; - this.currentY = ( - this.checkAgentRange([left, right], this.currentY) + - config.section.padding.bottom - ); - - const agentInfoL = this.agentInfos.get(left); - const agentInfoR = this.agentInfos.get(right); - this.blocks.appendChild(svg.make('rect', Object.assign({ - 'x': agentInfoL.x, - 'y': scope.y, - 'width': agentInfoR.x - agentInfoL.x, - 'height': this.currentY - scope.y, - }, config.boxAttrs))); - - this.currentY += config.margin.bottom + this.theme.actionMargin; - this.markAgentRange([left, right], this.currentY); - } - addHighlightObject(line, o) { let list = this.highlights.get(line); if(!list) { @@ -417,44 +277,53 @@ define([ list.push(o); } - renderStages(stages) { + renderStage(stage) { this.agentInfos.forEach((agentInfo) => { const rad = agentInfo.currentRad; agentInfo.currentMaxRad = rad; }); - let topY = 0; - let maxTopShift = 0; - let sequential = true; const envPre = { theme: this.theme, agentInfos: this.agentInfos, textSizer: this.sizer, state: this.state, + components: this.components, }; - const touchedAgentNames = []; - stages.forEach((stage) => { - const component = this.components.get(stage.type); - const r = component.renderPre(stage, envPre) || {}; - if(r.topShift !== undefined) { - maxTopShift = Math.max(maxTopShift, r.topShift); + const component = this.components.get(stage.type); + const result = component.renderPre(stage, envPre); + const {topShift, agentNames, asynchronousY} = + BaseComponent.cleanRenderPreResult(result, this.currentY); + + const topY = this.checkAgentRange(agentNames, asynchronousY); + + const eventOut = () => { + this.trigger('mouseout'); + }; + + const makeRegion = (o, stageOverride = null) => { + if(!o) { + o = svg.make('g'); } - if(r.agentNames) { - array.mergeSets(touchedAgentNames, r.agentNames); - } - if(r.asynchronousY !== undefined) { - topY = Math.max(topY, r.asynchronousY); - sequential = false; - } - }); - topY = this.checkAgentRange(touchedAgentNames, topY); - if(sequential) { - topY = Math.max(topY, this.currentY); - } + const targetStage = (stageOverride || stage); + this.addHighlightObject(targetStage.ln, o); + o.setAttribute('class', 'region'); + o.addEventListener('mouseenter', () => { + this.trigger('mouseover', [targetStage]); + }); + o.addEventListener('mouseleave', eventOut); + o.addEventListener('click', () => { + this.trigger('click', [targetStage]); + }); + this.actionLabels.appendChild(o); + return o; + }; const env = { topY, - primaryY: topY + maxTopShift, + primaryY: topY + topShift, + blockLayer: this.blocks, + sectionLayer: this.sections, shapeLayer: this.actionShapes, labelLayer: this.actionLabels, maskLayer: this.mask, @@ -469,41 +338,12 @@ define([ agentInfo.latestYStart = andStop ? null : toY; }, addDef: this.addDef, + makeRegion, + components: this.components, }; - let bottomY = topY; - stages.forEach((stage) => { - const eventOver = () => { - this.trigger('mouseover', [stage]); - }; - const eventOut = () => { - this.trigger('mouseout'); - }; - - const eventClick = () => { - this.trigger('click', [stage]); - }; - - env.makeRegion = (o) => { - if(!o) { - o = svg.make('g'); - } - this.addHighlightObject(stage.ln, o); - o.setAttribute('class', 'region'); - o.addEventListener('mouseenter', eventOver); - o.addEventListener('mouseleave', eventOut); - o.addEventListener('click', eventClick); - this.actionLabels.appendChild(o); - return o; - }; - - const component = this.components.get(stage.type); - const baseY = component.render(stage, env); - if(baseY !== undefined) { - bottomY = Math.max(bottomY, baseY); - } - }); - this.markAgentRange(touchedAgentNames, bottomY); + const bottomY = Math.max(topY, component.render(stage, env) || 0); + this.markAgentRange(agentNames, bottomY); this.currentY = bottomY; } @@ -564,7 +404,7 @@ define([ }); this.visibleAgents = ['[', ']']; - traverse(stages, this.separationTraversalFns); + stages.forEach(this.separationStage); this.positionAgents(); } @@ -654,7 +494,7 @@ define([ this.buildAgentInfos(sequence.agents, sequence.stages); this.currentY = 0; - traverse(sequence.stages, this.renderTraversalFns); + sequence.stages.forEach(this.renderStage); const bottomY = this.checkAgentRange(['[', ']'], this.currentY); const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0); diff --git a/scripts/sequence/components/BaseComponent.js b/scripts/sequence/components/BaseComponent.js index 26d14bb..77022a9 100644 --- a/scripts/sequence/components/BaseComponent.js +++ b/scripts/sequence/components/BaseComponent.js @@ -16,6 +16,7 @@ define(() => { textSizer, addSpacing, addSeparation, + components, }*/) { } @@ -26,6 +27,7 @@ define(() => { textSizer, addSpacing, addSeparation, + components, }*/) { } @@ -34,12 +36,16 @@ define(() => { agentInfos, textSizer, state, + components, }*/) { + // return {topShift, agentNames, asynchronousY} } render(/*stage, { topY, primaryY, + blockLayer, + sectionLayer, shapeLayer, labelLayer, theme, @@ -49,10 +55,24 @@ define(() => { addDef, makeRegion, state, + components, }*/) { + // return bottom Y coordinate } } + BaseComponent.cleanRenderPreResult = ({ + topShift = 0, + agentNames = [], + asynchronousY = null, + } = {}, currentY = null) => { + return { + topShift, + agentNames, + asynchronousY: (asynchronousY !== null) ? asynchronousY : currentY, + }; + }; + const components = new Map(); BaseComponent.register = (name, component) => { diff --git a/scripts/sequence/components/Block.js b/scripts/sequence/components/Block.js new file mode 100644 index 0000000..51f1e75 --- /dev/null +++ b/scripts/sequence/components/Block.js @@ -0,0 +1,146 @@ +define([ + './BaseComponent', + 'core/ArrayUtilities', + 'svg/SVGUtilities', + 'svg/SVGShapes', +], ( + BaseComponent, + array, + svg, + SVGShapes +) => { + 'use strict'; + + class BlockSplit extends BaseComponent { + separation({left, right, mode, label}, env) { + const config = env.theme.block.section; + const width = ( + env.textSizer.measure(config.mode.labelAttrs, mode).width + + config.mode.padding.left + + config.mode.padding.right + + env.textSizer.measure(config.label.labelAttrs, label).width + + config.label.padding.left + + config.label.padding.right + ); + env.addSeparation(left, right, width); + } + + renderPre({left, right}) { + return { + agentNames: [left, right], + }; + } + + render({left, right, mode, label}, env, first = false) { + const config = env.theme.block; + const agentInfoL = env.agentInfos.get(left); + const agentInfoR = env.agentInfos.get(right); + + let y = env.primaryY; + + if(!first) { + y += config.section.padding.bottom; + env.sectionLayer.appendChild(svg.make('line', Object.assign({ + 'x1': agentInfoL.x, + 'y1': y, + 'x2': agentInfoR.x, + 'y2': y, + }, config.separator.attrs))); + } + + const modeRender = SVGShapes.renderBoxedText(mode, { + x: agentInfoL.x, + y, + padding: config.section.mode.padding, + boxAttrs: config.section.mode.boxAttrs, + labelAttrs: config.section.mode.labelAttrs, + boxLayer: env.blockLayer, + labelLayer: env.labelLayer, + SVGTextBlockClass: env.SVGTextBlockClass, + }); + + const labelRender = SVGShapes.renderBoxedText(label, { + x: agentInfoL.x + modeRender.width, + y, + padding: config.section.label.padding, + boxAttrs: {'fill': '#000000'}, + labelAttrs: config.section.label.labelAttrs, + boxLayer: env.maskLayer, + labelLayer: env.labelLayer, + SVGTextBlockClass: env.SVGTextBlockClass, + }); + + return y + ( + Math.max(modeRender.height, labelRender.height) + + config.section.padding.top + ); + } + } + + class BlockBegin extends BlockSplit { + makeState(state) { + state.blocks = new Map(); + } + + resetState(state) { + state.blocks.clear(); + } + + separation(stage, env) { + array.mergeSets(env.visibleAgents, [stage.left, stage.right]); + super.separation(stage, env); + } + + renderPre({left, right}, env) { + return { + agentNames: [left, right], + topShift: env.theme.block.margin.top, + }; + } + + render(stage, env) { + env.state.blocks.set(stage.left, env.primaryY); + return super.render(stage, env, true); + } + } + + class BlockEnd extends BaseComponent { + separation({left, right}, env) { + array.removeAll(env.visibleAgents, [left, right]); + } + + renderPre({left, right}, env) { + return { + agentNames: [left, right], + topShift: env.theme.block.section.padding.bottom, + }; + } + + render({left, right}, env) { + const config = env.theme.block; + + const startY = env.state.blocks.get(left); + + const agentInfoL = env.agentInfos.get(left); + const agentInfoR = env.agentInfos.get(right); + env.blockLayer.appendChild(svg.make('rect', Object.assign({ + 'x': agentInfoL.x, + 'y': startY, + 'width': agentInfoR.x - agentInfoL.x, + 'height': env.primaryY - startY, + }, config.boxAttrs))); + + return env.primaryY + config.margin.bottom + env.theme.actionMargin; + } + } + + BaseComponent.register('block begin', new BlockBegin()); + BaseComponent.register('block split', new BlockSplit()); + BaseComponent.register('block end', new BlockEnd()); + + return { + BlockBegin, + BlockSplit, + BlockEnd, + }; +}); diff --git a/scripts/sequence/components/Block_spec.js b/scripts/sequence/components/Block_spec.js new file mode 100644 index 0000000..4bcc292 --- /dev/null +++ b/scripts/sequence/components/Block_spec.js @@ -0,0 +1,22 @@ +defineDescribe('Block', [ + './Block', + './BaseComponent', +], ( + Block, + BaseComponent +) => { + 'use strict'; + + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('block begin')).toEqual( + jasmine.any(Block.BlockBegin) + ); + expect(components.get('block split')).toEqual( + jasmine.any(Block.BlockSplit) + ); + expect(components.get('block end')).toEqual( + jasmine.any(Block.BlockEnd) + ); + }); +}); diff --git a/scripts/sequence/components/Parallel.js b/scripts/sequence/components/Parallel.js new file mode 100644 index 0000000..7dd5f49 --- /dev/null +++ b/scripts/sequence/components/Parallel.js @@ -0,0 +1,76 @@ +define([ + './BaseComponent', + 'core/ArrayUtilities', +], ( + BaseComponent, + array +) => { + 'use strict'; + + function nullableMax(a = null, b = null) { + if(a === null) { + return b; + } + if(b === null) { + return a; + } + return Math.max(a, b); + } + + function mergeResults(a, b) { + array.mergeSets(a.agentNames, b.agentNames); + return { + topShift: Math.max(a.topShift, b.topShift), + agentNames: a.agentNames, + asynchronousY: nullableMax(a.asynchronousY, b.asynchronousY), + }; + } + + class Parallel extends BaseComponent { + separationPre(stage, env) { + stage.stages.forEach((subStage) => { + env.components.get(subStage.type).separationPre(subStage, env); + }); + } + + separation(stage, env) { + stage.stages.forEach((subStage) => { + env.components.get(subStage.type).separation(subStage, env); + }); + } + + renderPre(stage, env) { + const baseResults = { + topShift: 0, + agentNames: [], + asynchronousY: null, + }; + + return stage.stages.map((subStage) => { + const component = env.components.get(subStage.type); + const subResult = component.renderPre(subStage, env); + return BaseComponent.cleanRenderPreResult(subResult); + }).reduce(mergeResults, baseResults); + } + + render(stage, env) { + const originalMakeRegion = env.makeRegion; + let bottomY = 0; + stage.stages.forEach((subStage) => { + env.makeRegion = (o, stageOverride = null) => { + return originalMakeRegion(o, stageOverride || subStage); + }; + + const component = env.components.get(subStage.type); + const baseY = component.render(subStage, env) || 0; + bottomY = Math.max(bottomY, baseY); + }); + env.makeRegion = originalMakeRegion; + return bottomY; + } + } + + BaseComponent.register('parallel', new Parallel()); + + return Parallel; +}); diff --git a/scripts/sequence/components/Parallel_spec.js b/scripts/sequence/components/Parallel_spec.js new file mode 100644 index 0000000..5dd90ad --- /dev/null +++ b/scripts/sequence/components/Parallel_spec.js @@ -0,0 +1,14 @@ +defineDescribe('Parallel', [ + './Parallel', + './BaseComponent', +], ( + Parallel, + BaseComponent +) => { + 'use strict'; + + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('parallel')).toEqual(jasmine.any(Parallel)); + }); +}); diff --git a/scripts/specs.js b/scripts/specs.js index 5e8c060..c3a85e7 100644 --- a/scripts/specs.js +++ b/scripts/specs.js @@ -14,8 +14,10 @@ define([ 'sequence/themes/Chunky_spec', 'sequence/components/AgentCap_spec', 'sequence/components/AgentHighlight_spec', + 'sequence/components/Block_spec', 'sequence/components/Connect_spec', 'sequence/components/Marker_spec', 'sequence/components/Note_spec', + 'sequence/components/Parallel_spec', 'sequence/sequence_integration_spec', ]);