Add label templates [#22], also Bowie is the *goblin* king... oops!

This commit is contained in:
David Evans 2017-11-12 12:23:06 +00:00
parent af2e786be8
commit 16095cf78a
12 changed files with 339 additions and 21 deletions

View File

@ -17,16 +17,16 @@ other projects.
``` ```
title Labyrinth title Labyrinth
Bowie -> Gremlin: You remind me of the babe Bowie -> Goblin: You remind me of the babe
Gremlin -> Bowie: What babe? Goblin -> Bowie: What babe?
Bowie -> Gremlin: The babe with the power Bowie -> Goblin: The babe with the power
Gremlin -> Bowie: What power? Goblin -> Bowie: What power?
note right of Bowie, Gremlin: Most people get muddled here! note right of Bowie, Goblin: Most people get muddled here!
Bowie -> Gremlin: 'The power of voodoo' Bowie -> Goblin: 'The power of voodoo'
Gremlin -> Bowie: "Who-do?" Goblin -> Bowie: "Who-do?"
Bowie -> Gremlin: You do! Bowie -> Goblin: You do!
Gremlin -> Bowie: Do what? Goblin -> Bowie: Do what?
Bowie -> Gremlin: Remind me of the babe! Bowie -> Goblin: Remind me of the babe!
Bowie -> Audience: Sings Bowie -> Audience: Sings
@ -113,6 +113,19 @@ else
end end
``` ```
### Label Templates
<img src="screenshots/LabelTemplates.png" alt="Label Templates preview" width="200" align="right" />
```
autolabel "[<inc>] <label>"
begin "Underpants\nGnomes" as A
A <- ]: Collect underpants
A <-> ]: ???
A <- ]: Profit!
```
### Multiline Text ### Multiline Text
<img src="screenshots/MultilineText.png" alt="Multiline Text preview" width="200" align="right" /> <img src="screenshots/MultilineText.png" alt="Multiline Text preview" width="200" align="right" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -25,16 +25,16 @@
const defaultCode = ( const defaultCode = (
'title Labyrinth\n' + 'title Labyrinth\n' +
'\n' + '\n' +
'Bowie -> Gremlin: You remind me of the babe\n' + 'Bowie -> Goblin: You remind me of the babe\n' +
'Gremlin -> Bowie: What babe?\n' + 'Goblin -> Bowie: What babe?\n' +
'Bowie -> Gremlin: The babe with the power\n' + 'Bowie -> Goblin: The babe with the power\n' +
'Gremlin -> Bowie: What power?\n' + 'Goblin -> Bowie: What power?\n' +
'note right of Bowie, Gremlin: Most people get muddled here!\n' + 'note right of Bowie, Goblin: Most people get muddled here!\n' +
'Bowie -> Gremlin: \'The power of voodoo\'\n' + 'Bowie -> Goblin: \'The power of voodoo\'\n' +
'Gremlin -> Bowie: "Who-do?"\n' + 'Goblin -> Bowie: "Who-do?"\n' +
'Bowie -> Gremlin: You do!\n' + 'Bowie -> Goblin: You do!\n' +
'Gremlin -> Bowie: Do what?\n' + 'Goblin -> Bowie: Do what?\n' +
'Bowie -> Gremlin: Remind me of the babe!\n' + 'Bowie -> Goblin: Remind me of the babe!\n' +
'\n' + '\n' +
'Bowie -> Audience: Sings\n' + 'Bowie -> Audience: Sings\n' +
'\n' + '\n' +
@ -86,6 +86,16 @@
'end B' 'end B'
), ),
}, },
{
title: 'Numbered labels',
code: 'autolabel "[<inc>] <label>"',
preview: (
'autolabel "[<inc>] <label>"\n' +
'A -> B: Foo\n' +
'A <- B: Bar\n' +
'A -> B: Baz'
),
},
{ {
title: 'Conditional blocks', title: 'Conditional blocks',
code: ( code: (

View File

@ -207,6 +207,10 @@ define(['core/ArrayUtilities'], (array) => {
'left': makeSideNote('left'), 'left': makeSideNote('left'),
'right': makeSideNote('right'), 'right': makeSideNote('right'),
}}, }},
'autolabel': {type: 'keyword', suggest: true, then: {
'off': {type: 'keyword', suggest: true, then: {}},
'': textToEnd,
}},
'simultaneously': {type: 'keyword', suggest: true, then: { 'simultaneously': {type: 'keyword', suggest: true, then: {
':': {type: 'operator', suggest: true, then: {}}, ':': {type: 'operator', suggest: true, then: {}},
'with': {type: 'keyword', suggest: true, then: { 'with': {type: 'keyword', suggest: true, then: {

View File

@ -179,6 +179,7 @@ define(['core/ArrayUtilities'], (array) => {
this.agentStates = new Map(); this.agentStates = new Map();
this.agentAliases = new Map(); this.agentAliases = new Map();
this.agents = []; this.agents = [];
this.labelPattern = null;
this.blockCount = 0; this.blockCount = 0;
this.nesting = []; this.nesting = [];
this.markers = new Set(); this.markers = new Set();
@ -191,6 +192,7 @@ define(['core/ArrayUtilities'], (array) => {
'agent define': this.handleAgentDefine.bind(this), 'agent define': this.handleAgentDefine.bind(this),
'agent begin': this.handleAgentBegin.bind(this), 'agent begin': this.handleAgentBegin.bind(this),
'agent end': this.handleAgentEnd.bind(this), 'agent end': this.handleAgentEnd.bind(this),
'label pattern': this.handleLabelPattern.bind(this),
'connect': this.handleConnect.bind(this), 'connect': this.handleConnect.bind(this),
'note over': this.handleNote.bind(this), 'note over': this.handleNote.bind(this),
'note left': this.handleNote.bind(this), 'note left': this.handleNote.bind(this),
@ -373,6 +375,36 @@ define(['core/ArrayUtilities'], (array) => {
this.addStage({type: 'async', target}, false); this.addStage({type: 'async', target}, false);
} }
handleLabelPattern({pattern}) {
this.labelPattern = pattern.slice();
for(let i = 0; i < this.labelPattern.length; ++ i) {
const part = this.labelPattern[i];
if(typeof part === 'object' && part.start !== undefined) {
this.labelPattern[i] = Object.assign({
current: part.start,
}, part);
}
}
}
applyLabelPattern(label) {
let result = '';
const tokens = {
'label': label,
};
this.labelPattern.forEach((part) => {
if(typeof part === 'string') {
result += part;
} else if(part.token !== undefined) {
result += tokens[part.token];
} else if(part.current !== undefined) {
result += part.current.toFixed(part.dp);
part.current += part.inc;
}
});
return result;
}
handleConnect({agents, label, options}) { handleConnect({agents, label, options}) {
const beginAgents = (agents const beginAgents = (agents
.filter(agentHasFlag('begin')) .filter(agentHasFlag('begin'))
@ -412,7 +444,7 @@ define(['core/ArrayUtilities'], (array) => {
const connectStage = { const connectStage = {
type: 'connect', type: 'connect',
agentNames, agentNames,
label, label: this.applyLabelPattern(label),
options, options,
}; };
@ -528,6 +560,7 @@ define(['core/ArrayUtilities'], (array) => {
this.agents.length = 0; this.agents.length = 0;
this.blockCount = 0; this.blockCount = 0;
this.nesting.length = 0; this.nesting.length = 0;
this.labelPattern = [{token: 'label'}];
const globals = this.beginNested('global', '', '', 0); const globals = this.beginNested('global', '', '', 0);
stages.forEach(this.handleStage); stages.forEach(this.handleStage);

View File

@ -26,6 +26,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
return {type: 'block end'}; return {type: 'block end'};
}, },
labelPattern: (pattern, {ln = 0} = {}) => {
return {type: 'label pattern', pattern, ln};
},
defineAgents: (agentNames, {ln = 0} = {}) => { defineAgents: (agentNames, {ln = 0} = {}) => {
return { return {
type: 'agent define', type: 'agent define',
@ -325,6 +329,56 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]); ]);
}); });
it('uses label patterns for connections', () => {
const sequence = generator.generate({stages: [
PARSED.labelPattern(['foo ', {token: 'label'}, ' bar']),
PARSED.connect(['A', 'B'], {label: 'myLabel'}),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
GENERATED.connect(['A', 'B'], {
label: 'foo myLabel bar',
}),
jasmine.anything(),
]);
});
it('applies counters in label patterns', () => {
const sequence = generator.generate({stages: [
PARSED.labelPattern([{start: 3, inc: 2, dp: 0}, ' suffix']),
PARSED.connect(['A', 'B'], {label: 'foo'}),
PARSED.connect(['A', 'B'], {label: 'bar'}),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
GENERATED.connect(['A', 'B'], {
label: '3 suffix',
}),
GENERATED.connect(['A', 'B'], {
label: '5 suffix',
}),
jasmine.anything(),
]);
});
it('applies counter rounding in label patterns', () => {
const sequence = generator.generate({stages: [
PARSED.labelPattern([{start: 0.52, inc: 1, dp: 1}, ' suffix']),
PARSED.connect(['A', 'B'], {label: 'foo'}),
PARSED.connect(['A', 'B'], {label: 'bar'}),
]});
expect(sequence.stages).toEqual([
jasmine.anything(),
GENERATED.connect(['A', 'B'], {
label: '0.5 suffix',
}),
GENERATED.connect(['A', 'B'], {
label: '1.5 suffix',
}),
jasmine.anything(),
]);
});
it('creates implicit end stages for all remaining agents', () => { it('creates implicit end stages for all remaining agents', () => {
const sequence = generator.generate({ const sequence = generator.generate({
meta: { meta: {

View File

@ -0,0 +1,75 @@
define(() => {
'use strict';
const LABEL_PATTERN = /(.*?)<([^<>]*)>/g;
const DP_PATTERN = /\.([0-9]*)/;
function countDP(value) {
const match = DP_PATTERN.exec(value);
if(!match || !match[1]) {
return 0;
}
return match[1].length;
}
function parseCounter(args) {
let start = 1;
let inc = 1;
let dp = 0;
if(args[0]) {
start = Number(args[0]);
dp = Math.max(dp, countDP(args[0]));
}
if(args[1]) {
inc = Number(args[1]);
dp = Math.max(dp, countDP(args[1]));
}
return {start, inc, dp};
}
function parseToken(token) {
if(token === 'label') {
return {token: 'label'};
}
const p = token.indexOf(' ');
let type = null;
let args = null;
if(p === -1) {
type = token;
args = [];
} else {
type = token.substr(0, p);
args = token.substr(p + 1).split(',');
}
if(type === 'inc') {
return parseCounter(args);
}
return '<' + token + '>';
}
function parsePattern(raw) {
const pattern = [];
let match = null;
let end = 0;
LABEL_PATTERN.lastIndex = 0;
while((match = LABEL_PATTERN.exec(raw))) {
if(match[1]) {
pattern.push(match[1]);
}
if(match[2]) {
pattern.push(parseToken(match[2]));
}
end = LABEL_PATTERN.lastIndex;
}
const remainder = raw.substr(end);
if(remainder) {
pattern.push(remainder);
}
return pattern;
}
return parsePattern;
});

View File

@ -0,0 +1,83 @@
defineDescribe('Label Pattern Parser', ['./LabelPatternParser'], (parser) => {
'use strict';
it('converts simple text', () => {
const parsed = parser('hello everybody');
expect(parsed).toEqual([
'hello everybody',
]);
});
it('handles the empty case', () => {
const parsed = parser('');
expect(parsed).toEqual([]);
});
it('converts tokens', () => {
const parsed = parser('foo <label> bar');
expect(parsed).toEqual([
'foo ',
{token: 'label'},
' bar',
]);
});
it('converts multiple tokens', () => {
const parsed = parser('foo <label> bar <label> baz');
expect(parsed).toEqual([
'foo ',
{token: 'label'},
' bar ',
{token: 'label'},
' baz',
]);
});
it('ignores empty sequences', () => {
const parsed = parser('<label><label>');
expect(parsed).toEqual([
{token: 'label'},
{token: 'label'},
]);
});
it('passes unrecognised tokens through unchanged', () => {
const parsed = parser('foo <nope>');
expect(parsed).toEqual([
'foo ',
'<nope>',
]);
});
it('converts counters', () => {
const parsed = parser('<inc 5, 2>a<inc 2, 1>b');
expect(parsed).toEqual([
{start: 5, inc: 2, dp: 0},
'a',
{start: 2, inc: 1, dp: 0},
'b',
]);
});
it('defaults counters to increment = 1', () => {
const parsed = parser('<inc 5>');
expect(parsed).toEqual([
{start: 5, inc: 1, dp: 0},
]);
});
it('defaults counters to start = 1', () => {
const parsed = parser('<inc>');
expect(parsed).toEqual([
{start: 1, inc: 1, dp: 0},
]);
});
it('assigns decimal places to counters by their written precision', () => {
const parsed = parser('<inc 5.0, 2.00><inc 2.00, 1.0>');
expect(parsed).toEqual([
{start: jasmine.anything(), inc: jasmine.anything(), dp: 2},
{start: jasmine.anything(), inc: jasmine.anything(), dp: 2},
]);
});
});

View File

@ -1,10 +1,12 @@
define([ define([
'core/ArrayUtilities', 'core/ArrayUtilities',
'./Tokeniser', './Tokeniser',
'./LabelPatternParser',
'./CodeMirrorHints', './CodeMirrorHints',
], ( ], (
array, array,
Tokeniser, Tokeniser,
labelPatternParser,
CMHints CMHints
) => { ) => {
'use strict'; 'use strict';
@ -247,6 +249,23 @@ define([
return true; return true;
}, },
(line) => { // autolabel
if(tokenKeyword(line[0]) !== 'autolabel') {
return null;
}
let raw = null;
if(tokenKeyword(line[1]) === 'off') {
raw = '<label>';
} else {
raw = joinLabel(line, 1);
}
return {
type: 'label pattern',
pattern: labelPatternParser(raw),
};
},
(line) => { // block (line) => { // block
if(tokenKeyword(line[0]) === 'end' && line.length === 1) { if(tokenKeyword(line[0]) === 'end' && line.length === 1) {
return {type: 'block end'}; return {type: 'block end'};

View File

@ -412,6 +412,32 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}]); }]);
}); });
it('converts autolabel commands', () => {
const parsed = parser.parse('autolabel "foo <label> bar"');
expect(parsed.stages).toEqual([
{
type: 'label pattern',
ln: jasmine.anything(),
pattern: [
'foo ',
{token: 'label'},
' bar',
],
},
]);
});
it('converts autolabel off commands', () => {
const parsed = parser.parse('autolabel off');
expect(parsed.stages).toEqual([
{
type: 'label pattern',
ln: jasmine.anything(),
pattern: [{token: 'label'}],
},
]);
});
it('converts "simultaneously" flow commands', () => { it('converts "simultaneously" flow commands', () => {
const parsed = parser.parse('simultaneously:'); const parsed = parser.parse('simultaneously:');
expect(parsed.stages).toEqual([{ expect(parsed.stages).toEqual([{

View File

@ -7,6 +7,7 @@ define([
'interface/Interface_spec', 'interface/Interface_spec',
'sequence/Tokeniser_spec', 'sequence/Tokeniser_spec',
'sequence/Parser_spec', 'sequence/Parser_spec',
'sequence/LabelPatternParser_spec',
'sequence/Generator_spec', 'sequence/Generator_spec',
'sequence/Renderer_spec', 'sequence/Renderer_spec',
'sequence/themes/Basic_spec', 'sequence/themes/Basic_spec',