Improve block handling so references can be added [#21]

This commit is contained in:
David Evans 2017-11-19 22:57:46 +00:00
parent 9b819ced63
commit 26bc3acd3e
12 changed files with 668 additions and 380 deletions

View File

@ -7,7 +7,11 @@ define(['core/ArrayUtilities'], (array) => {
const end = {type: '', suggest: '\n', then: {}}; const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', 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: { const aliasListToEnd = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
'as': {type: 'keyword', suggest: true, then: { 'as': {type: 'keyword', suggest: true, then: {
@ -20,11 +24,17 @@ define(['core/ArrayUtilities'], (array) => {
',': {type: 'operator', suggest: true, then: {'': 1}}, ',': {type: 'operator', suggest: true, then: {'': 1}},
'\n': end, '\n': end,
}}; }};
const agentListToText = {type: 'variable', suggest: 'Agent', then: {
'': 0, function agentListTo(exit) {
',': {type: 'operator', suggest: true, then: {'': 1}}, 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}}, ':': {type: 'operator', suggest: true, then: {'': textToEnd}},
}}; });
const agentList2ToText = {type: 'variable', suggest: 'Agent', then: { const agentList2ToText = {type: 'variable', suggest: 'Agent', then: {
'': 0, '': 0,
',': {type: 'operator', suggest: true, then: {'': agentListToText}}, ',': {type: 'operator', suggest: true, then: {'': agentListToText}},
@ -43,6 +53,23 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
'\n': end, '\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) { function makeSideNote(side) {
return { return {
@ -158,6 +185,7 @@ define(['core/ArrayUtilities'], (array) => {
}}, }},
'begin': {type: 'keyword', suggest: true, then: { 'begin': {type: 'keyword', suggest: true, then: {
'': aliasListToEnd, '': aliasListToEnd,
'reference': refDef,
'as': CM_ERROR, 'as': CM_ERROR,
}}, }},
'end': {type: 'keyword', suggest: true, then: { 'end': {type: 'keyword', suggest: true, then: {

View File

@ -213,6 +213,10 @@ define(['core/ArrayUtilities'], (array) => {
this.currentNest = null; this.currentNest = null;
this.stageHandlers = { 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), 'mark': this.handleMark.bind(this),
'async': this.handleAsync.bind(this), 'async': this.handleAsync.bind(this),
'agent define': this.handleAgentDefine.bind(this), 'agent define': this.handleAgentDefine.bind(this),
@ -224,9 +228,6 @@ define(['core/ArrayUtilities'], (array) => {
'note left': this.handleNote.bind(this), 'note left': this.handleNote.bind(this),
'note right': this.handleNote.bind(this), 'note right': this.handleNote.bind(this),
'note between': 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.handleStage = this.handleStage.bind(this);
this.convertAgent = this.convertAgent.bind(this); this.convertAgent = this.convertAgent.bind(this);
@ -365,22 +366,23 @@ define(['core/ArrayUtilities'], (array) => {
const agents = [leftAgent, rightAgent]; const agents = [leftAgent, rightAgent];
const stages = []; const stages = [];
this.currentSection = { this.currentSection = {
mode, header: {
label, type: 'block begin',
mode,
label,
left: leftAgent.name,
right: rightAgent.name,
ln,
},
stages, stages,
ln,
}; };
this.currentNest = { this.currentNest = {
mode,
agents, agents,
leftAgent, leftAgent,
rightAgent, rightAgent,
hasContent: false, hasContent: false,
stage: { sections: [this.currentSection],
type: 'block',
sections: [this.currentSection],
left: leftAgent.name,
right: rightAgent.name,
},
}; };
this.agentStates.set(leftAgent.name, LOCKED_AGENT); this.agentStates.set(leftAgent.name, LOCKED_AGENT);
this.agentStates.set(rightAgent.name, LOCKED_AGENT); this.agentStates.set(rightAgent.name, LOCKED_AGENT);
@ -389,6 +391,69 @@ define(['core/ArrayUtilities'], (array) => {
return {agents, stages}; 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}) { handleMark({name}) {
this.markers.add(name); this.markers.add(name);
this.addStage({type: 'mark', name}, false); 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) { handleStage(stage) {
this.latestLine = stage.ln; this.latestLine = stage.ln;
try { 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) { } catch(e) {
if(typeof e === 'object' && e.message) { if(typeof e === 'object' && e.message) {
throw new Error(e.message + ' at line ' + (stage.ln + 1)); throw new Error(e.message + ' at line ' + (stage.ln + 1));
@ -594,7 +619,7 @@ define(['core/ArrayUtilities'], (array) => {
if(this.nesting.length !== 1) { if(this.nesting.length !== 1) {
throw new Error( throw new Error(
'Unterminated section at line ' + 'Unterminated section at line ' +
(this.currentSection.ln + 1) (this.currentSection.header.ln + 1)
); );
} }

View File

@ -22,14 +22,25 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
return {type: 'block split', mode, label, ln}; return {type: 'block split', mode, label, ln};
}, },
blockEnd: () => { blockEnd: ({ln = 0} = {}) => {
return {type: 'block end'}; return {type: 'block end', ln};
}, },
labelPattern: (pattern, {ln = 0} = {}) => { labelPattern: (pattern, {ln = 0} = {}) => {
return {type: 'label pattern', pattern, ln}; 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} = {}) => { defineAgents: (agentNames, {ln = 0} = {}) => {
return { return {
type: 'agent define', 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, { connect: (agentNames, {
label = jasmine.anything(), label = jasmine.anything(),
line = jasmine.anything(), line = jasmine.anything(),
@ -741,39 +797,44 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
}); });
it('records virtual block agent names in blocks', () => { it('propagates block statements', () => {
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', () => {
const sequence = generator.generate({stages: [ const sequence = generator.generate({stages: [
PARSED.blockBegin('if', 'abc', {ln: 10}), PARSED.blockBegin('if', 'abc', {ln: 10}),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockSplit('else', 'xyz', {ln: 20}), 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(), PARSED.blockEnd(),
]}); ]});
const block0 = sequence.stages[0]; const bounds = {
expect(block0.sections).toEqual([ left: '__BLOCK0[',
{mode: 'if', label: 'abc', ln: 10, stages: [ right: '__BLOCK0]',
GENERATED.beginAgents(['A', 'B']), };
GENERATED.connect(['A', 'B']),
]}, const stages = sequence.stages;
{mode: 'else', label: 'xyz', ln: 20, stages: [ expect(stages[0]).toEqual(GENERATED.blockBegin('if', bounds));
GENERATED.beginAgents(['C']), expect(stages[3]).toEqual(GENERATED.blockSplit('else', bounds));
GENERATED.connect(['A', 'C']), expect(stages[5]).toEqual(GENERATED.blockEnd(bounds));
]},
]);
}); });
it('records virtual block agents in nested blocks', () => { it('records virtual block agents in nested blocks', () => {
@ -798,15 +859,22 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{name: '__BLOCK0]', anchorRight: false}, {name: '__BLOCK0]', anchorRight: false},
{name: ']', 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]; const bounds0 = {
expect(block1.type).toEqual('block'); left: '__BLOCK0[',
expect(block1.left).toEqual('__BLOCK1['); right: '__BLOCK0]',
expect(block1.right).toEqual('__BLOCK1]'); };
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', () => { it('preserves block boundaries when agents exist outside', () => {
@ -829,123 +897,92 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
{name: '__BLOCK0]', anchorRight: false}, {name: '__BLOCK0]', anchorRight: false},
{name: ']', 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]; const bounds0 = {
expect(block1.type).toEqual('block'); left: '__BLOCK0[',
expect(block1.left).toEqual('__BLOCK1['); right: '__BLOCK0]',
expect(block1.right).toEqual('__BLOCK1]'); };
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', () => { it('allows empty block parts after split', () => {
const sequence = generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockSplit('else', 'xyz'), PARSED.blockSplit('else', 'xyz'),
PARSED.blockEnd(), PARSED.blockEnd(),
]}); ]})).not.toThrow();
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: []},
]);
}); });
it('allows empty block parts before split', () => { it('allows empty block parts before split', () => {
const sequence = generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.blockSplit('else', 'xyz'), PARSED.blockSplit('else', 'xyz'),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockEnd(), PARSED.blockEnd(),
]}); ]})).not.toThrow();
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(),
]},
]);
}); });
it('removes entirely empty blocks', () => { it('allows deeply nested blocks', () => {
const sequence = generator.generate({stages: [ 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.blockBegin('if', 'abc'),
PARSED.blockSplit('else', 'xyz'), PARSED.blockSplit('else', 'xyz'),
PARSED.blockBegin('if', 'abc'),
PARSED.blockEnd(), PARSED.blockEnd(),
PARSED.blockEnd(), ]})).toThrow();
]});
expect(sequence.stages).toEqual([]);
}); });
it('removes blocks containing only define statements / markers', () => { it('rejects blocks containing only define statements / markers', () => {
const sequence = generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.defineAgents(['A']), PARSED.defineAgents(['A']),
{type: 'mark', name: 'foo'}, {type: 'mark', name: 'foo'},
PARSED.blockEnd(), PARSED.blockEnd(),
]}); ]})).toThrow();
expect(sequence.stages).toEqual([]);
}); });
it('does not create virtual agents for empty blocks', () => { it('rejects entirely empty nested blocks', () => {
const sequence = generator.generate({stages: [ expect(() => 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: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockSplit('else', 'xyz'), PARSED.blockSplit('else', 'xyz'),
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.blockEnd(), PARSED.blockEnd(),
PARSED.blockEnd(), PARSED.blockEnd(),
]}); ]})).toThrow();
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: []},
]);
}); });
it('rejects unterminated blocks', () => { it('rejects unterminated blocks', () => {
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
]})).toThrow(); ]})).toThrow(new Error('Unterminated section at line 1'));
expect(() => generator.generate({stages: [ expect(() => generator.generate({stages: [
PARSED.blockBegin('if', 'abc'), PARSED.blockBegin('if', 'abc'),
PARSED.blockBegin('if', 'def'), PARSED.blockBegin('if', 'def'),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockEnd(), PARSED.blockEnd(),
]})).toThrow(); ]})).toThrow(new Error('Unterminated section at line 1'));
}); });
it('rejects extra block terminations', () => { it('rejects extra block terminations', () => {

View File

@ -255,6 +255,9 @@ define([
} }
const type = tokenKeyword(line[1]); const type = tokenKeyword(line[1]);
if(!type) {
throw makeError('Unspecified termination', line[0]);
}
if(TERMINATOR_TYPES.indexOf(type) === -1) { if(TERMINATOR_TYPES.indexOf(type) === -1) {
throw makeError('Unknown termination "' + type + '"', line[1]); throw makeError('Unknown termination "' + type + '"', line[1]);
} }
@ -268,6 +271,9 @@ define([
} }
const type = tokenKeyword(line[1]); const type = tokenKeyword(line[1]);
if(!type) {
throw makeError('Unspecified header', line[0]);
}
if(TERMINATOR_TYPES.indexOf(type) === -1) { if(TERMINATOR_TYPES.indexOf(type) === -1) {
throw makeError('Unknown header "' + type + '"', line[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 (line) => { // agent
const type = AGENT_MANIPULATION_TYPES[tokenKeyword(line[0])]; const type = AGENT_MANIPULATION_TYPES[tokenKeyword(line[0])];
if(!type || line.length <= 1) { if(!type || line.length <= 1) {

View File

@ -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', () => { it('converts markers', () => {
const parsed = parser.parse('abc:'); const parsed = parser.parse('abc:');
expect(parsed.stages).toEqual([{ 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', () => { it('rejects invalid headers', () => {
expect(() => parser.parse('headers foo')).toThrow(new Error( expect(() => parser.parse('headers foo')).toThrow(new Error(
'Unknown header "foo" at line 1, character 8' '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', () => { it('rejects malformed notes', () => {
expect(() => parser.parse('note over A hello')).toThrow(); expect(() => parser.parse('note over A hello')).toThrow();
}); });

View File

@ -5,6 +5,8 @@ define([
'svg/SVGUtilities', 'svg/SVGUtilities',
'svg/SVGShapes', 'svg/SVGShapes',
'./components/BaseComponent', './components/BaseComponent',
'./components/Block',
'./components/Parallel',
'./components/Marker', './components/Marker',
'./components/AgentCap', './components/AgentCap',
'./components/AgentHighlight', './components/AgentHighlight',
@ -20,35 +22,6 @@ define([
/* jshint +W072 */ /* jshint +W072 */
'use strict'; '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) { function findExtremes(agentInfos, agentNames) {
let min = null; let min = null;
let max = null; let max = null;
@ -102,20 +75,8 @@ define([
components = BaseComponent.getComponents(); components = BaseComponent.getComponents();
} }
this.separationTraversalFns = { this.separationStage = this.separationStage.bind(this);
stagesFn: this.separationStages.bind(this), this.renderStage = this.renderStage.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.addSeparation = this.addSeparation.bind(this); this.addSeparation = this.addSeparation.bind(this);
this.addDef = this.addDef.bind(this); this.addDef = this.addDef.bind(this);
@ -196,28 +157,7 @@ define([
info2.separations.set(agentName1, Math.max(d2, dist)); info2.separations.set(agentName1, Math.max(d2, dist));
} }
separationBlockBegin(scope, {left, right}) { separationStage(stage) {
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) {
const agentSpaces = new Map(); const agentSpaces = new Map();
const agentNames = this.visibleAgents.slice(); const agentNames = this.visibleAgents.slice();
@ -239,13 +179,14 @@ define([
textSizer: this.sizer, textSizer: this.sizer,
addSpacing, addSpacing,
addSeparation: this.addSeparation, addSeparation: this.addSeparation,
components: this.components,
}; };
stages.forEach((stage) => { const component = this.components.get(stage.type);
this.components.get(stage.type).separationPre(stage, env); if(!component) {
}); throw new Error('Unknown component: ' + stage.type);
stages.forEach((stage) => { }
this.components.get(stage.type).separation(stage, env); component.separationPre(stage, env);
}); component.separation(stage, env);
array.mergeSets(agentNames, this.visibleAgents); array.mergeSets(agentNames, this.visibleAgents);
agentNames.forEach((agentNameR) => { 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) { addHighlightObject(line, o) {
let list = this.highlights.get(line); let list = this.highlights.get(line);
if(!list) { if(!list) {
@ -417,44 +277,53 @@ define([
list.push(o); list.push(o);
} }
renderStages(stages) { renderStage(stage) {
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad; const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad; agentInfo.currentMaxRad = rad;
}); });
let topY = 0;
let maxTopShift = 0;
let sequential = true;
const envPre = { const envPre = {
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
textSizer: this.sizer, textSizer: this.sizer,
state: this.state, state: this.state,
components: this.components,
}; };
const touchedAgentNames = []; const component = this.components.get(stage.type);
stages.forEach((stage) => { const result = component.renderPre(stage, envPre);
const component = this.components.get(stage.type); const {topShift, agentNames, asynchronousY} =
const r = component.renderPre(stage, envPre) || {}; BaseComponent.cleanRenderPreResult(result, this.currentY);
if(r.topShift !== undefined) {
maxTopShift = Math.max(maxTopShift, r.topShift); 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) { const targetStage = (stageOverride || stage);
array.mergeSets(touchedAgentNames, r.agentNames); this.addHighlightObject(targetStage.ln, o);
} o.setAttribute('class', 'region');
if(r.asynchronousY !== undefined) { o.addEventListener('mouseenter', () => {
topY = Math.max(topY, r.asynchronousY); this.trigger('mouseover', [targetStage]);
sequential = false; });
} o.addEventListener('mouseleave', eventOut);
}); o.addEventListener('click', () => {
topY = this.checkAgentRange(touchedAgentNames, topY); this.trigger('click', [targetStage]);
if(sequential) { });
topY = Math.max(topY, this.currentY); this.actionLabels.appendChild(o);
} return o;
};
const env = { const env = {
topY, topY,
primaryY: topY + maxTopShift, primaryY: topY + topShift,
blockLayer: this.blocks,
sectionLayer: this.sections,
shapeLayer: this.actionShapes, shapeLayer: this.actionShapes,
labelLayer: this.actionLabels, labelLayer: this.actionLabels,
maskLayer: this.mask, maskLayer: this.mask,
@ -469,41 +338,12 @@ define([
agentInfo.latestYStart = andStop ? null : toY; agentInfo.latestYStart = andStop ? null : toY;
}, },
addDef: this.addDef, addDef: this.addDef,
makeRegion,
components: this.components,
}; };
let bottomY = topY;
stages.forEach((stage) => {
const eventOver = () => {
this.trigger('mouseover', [stage]);
};
const eventOut = () => { const bottomY = Math.max(topY, component.render(stage, env) || 0);
this.trigger('mouseout'); this.markAgentRange(agentNames, bottomY);
};
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);
this.currentY = bottomY; this.currentY = bottomY;
} }
@ -564,7 +404,7 @@ define([
}); });
this.visibleAgents = ['[', ']']; this.visibleAgents = ['[', ']'];
traverse(stages, this.separationTraversalFns); stages.forEach(this.separationStage);
this.positionAgents(); this.positionAgents();
} }
@ -654,7 +494,7 @@ define([
this.buildAgentInfos(sequence.agents, sequence.stages); this.buildAgentInfos(sequence.agents, sequence.stages);
this.currentY = 0; this.currentY = 0;
traverse(sequence.stages, this.renderTraversalFns); sequence.stages.forEach(this.renderStage);
const bottomY = this.checkAgentRange(['[', ']'], this.currentY); const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0); const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);

View File

@ -16,6 +16,7 @@ define(() => {
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
components,
}*/) { }*/) {
} }
@ -26,6 +27,7 @@ define(() => {
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
components,
}*/) { }*/) {
} }
@ -34,12 +36,16 @@ define(() => {
agentInfos, agentInfos,
textSizer, textSizer,
state, state,
components,
}*/) { }*/) {
// return {topShift, agentNames, asynchronousY}
} }
render(/*stage, { render(/*stage, {
topY, topY,
primaryY, primaryY,
blockLayer,
sectionLayer,
shapeLayer, shapeLayer,
labelLayer, labelLayer,
theme, theme,
@ -49,10 +55,24 @@ define(() => {
addDef, addDef,
makeRegion, makeRegion,
state, 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(); const components = new Map();
BaseComponent.register = (name, component) => { BaseComponent.register = (name, component) => {

View File

@ -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,
};
});

View File

@ -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)
);
});
});

View File

@ -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;
});

View File

@ -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));
});
});

View File

@ -14,8 +14,10 @@ define([
'sequence/themes/Chunky_spec', 'sequence/themes/Chunky_spec',
'sequence/components/AgentCap_spec', 'sequence/components/AgentCap_spec',
'sequence/components/AgentHighlight_spec', 'sequence/components/AgentHighlight_spec',
'sequence/components/Block_spec',
'sequence/components/Connect_spec', 'sequence/components/Connect_spec',
'sequence/components/Marker_spec', 'sequence/components/Marker_spec',
'sequence/components/Note_spec', 'sequence/components/Note_spec',
'sequence/components/Parallel_spec',
'sequence/sequence_integration_spec', 'sequence/sequence_integration_spec',
]); ]);