diff --git a/README.md b/README.md
index 2f41469..e7b6b57 100644
--- a/README.md
+++ b/README.md
@@ -17,16 +17,16 @@ other projects.
```
title Labyrinth
-Bowie -> Gremlin: You remind me of the babe
-Gremlin -> Bowie: What babe?
-Bowie -> Gremlin: The babe with the power
-Gremlin -> Bowie: What power?
-note right of Bowie, Gremlin: Most people get muddled here!
-Bowie -> Gremlin: 'The power of voodoo'
-Gremlin -> Bowie: "Who-do?"
-Bowie -> Gremlin: You do!
-Gremlin -> Bowie: Do what?
-Bowie -> Gremlin: Remind me of the babe!
+Bowie -> Goblin: You remind me of the babe
+Goblin -> Bowie: What babe?
+Bowie -> Goblin: The babe with the power
+Goblin -> Bowie: What power?
+note right of Bowie, Goblin: Most people get muddled here!
+Bowie -> Goblin: 'The power of voodoo'
+Goblin -> Bowie: "Who-do?"
+Bowie -> Goblin: You do!
+Goblin -> Bowie: Do what?
+Bowie -> Goblin: Remind me of the babe!
Bowie -> Audience: Sings
@@ -113,6 +113,19 @@ else
end
```
+### Label Templates
+
+
+
+```
+autolabel "[] "
+
+begin "Underpants\nGnomes" as A
+A <- ]: Collect underpants
+A <-> ]: ???
+A <- ]: Profit!
+```
+
### Multiline Text
diff --git a/screenshots/LabelTemplates.png b/screenshots/LabelTemplates.png
new file mode 100644
index 0000000..bbcdb6b
Binary files /dev/null and b/screenshots/LabelTemplates.png differ
diff --git a/screenshots/SimpleUsage.png b/screenshots/SimpleUsage.png
index cb2a986..c004107 100644
Binary files a/screenshots/SimpleUsage.png and b/screenshots/SimpleUsage.png differ
diff --git a/scripts/main.js b/scripts/main.js
index cc4661d..1b9f45f 100644
--- a/scripts/main.js
+++ b/scripts/main.js
@@ -25,16 +25,16 @@
const defaultCode = (
'title Labyrinth\n' +
'\n' +
- 'Bowie -> Gremlin: You remind me of the babe\n' +
- 'Gremlin -> Bowie: What babe?\n' +
- 'Bowie -> Gremlin: The babe with the power\n' +
- 'Gremlin -> Bowie: What power?\n' +
- 'note right of Bowie, Gremlin: Most people get muddled here!\n' +
- 'Bowie -> Gremlin: \'The power of voodoo\'\n' +
- 'Gremlin -> Bowie: "Who-do?"\n' +
- 'Bowie -> Gremlin: You do!\n' +
- 'Gremlin -> Bowie: Do what?\n' +
- 'Bowie -> Gremlin: Remind me of the babe!\n' +
+ 'Bowie -> Goblin: You remind me of the babe\n' +
+ 'Goblin -> Bowie: What babe?\n' +
+ 'Bowie -> Goblin: The babe with the power\n' +
+ 'Goblin -> Bowie: What power?\n' +
+ 'note right of Bowie, Goblin: Most people get muddled here!\n' +
+ 'Bowie -> Goblin: \'The power of voodoo\'\n' +
+ 'Goblin -> Bowie: "Who-do?"\n' +
+ 'Bowie -> Goblin: You do!\n' +
+ 'Goblin -> Bowie: Do what?\n' +
+ 'Bowie -> Goblin: Remind me of the babe!\n' +
'\n' +
'Bowie -> Audience: Sings\n' +
'\n' +
@@ -86,6 +86,16 @@
'end B'
),
},
+ {
+ title: 'Numbered labels',
+ code: 'autolabel "[] "',
+ preview: (
+ 'autolabel "[] "\n' +
+ 'A -> B: Foo\n' +
+ 'A <- B: Bar\n' +
+ 'A -> B: Baz'
+ ),
+ },
{
title: 'Conditional blocks',
code: (
diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js
index f16f68e..b4f21a2 100644
--- a/scripts/sequence/CodeMirrorMode.js
+++ b/scripts/sequence/CodeMirrorMode.js
@@ -207,6 +207,10 @@ define(['core/ArrayUtilities'], (array) => {
'left': makeSideNote('left'),
'right': makeSideNote('right'),
}},
+ 'autolabel': {type: 'keyword', suggest: true, then: {
+ 'off': {type: 'keyword', suggest: true, then: {}},
+ '': textToEnd,
+ }},
'simultaneously': {type: 'keyword', suggest: true, then: {
':': {type: 'operator', suggest: true, then: {}},
'with': {type: 'keyword', suggest: true, then: {
diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js
index b666490..fc7dc02 100644
--- a/scripts/sequence/Generator.js
+++ b/scripts/sequence/Generator.js
@@ -179,6 +179,7 @@ define(['core/ArrayUtilities'], (array) => {
this.agentStates = new Map();
this.agentAliases = new Map();
this.agents = [];
+ this.labelPattern = null;
this.blockCount = 0;
this.nesting = [];
this.markers = new Set();
@@ -191,6 +192,7 @@ define(['core/ArrayUtilities'], (array) => {
'agent define': this.handleAgentDefine.bind(this),
'agent begin': this.handleAgentBegin.bind(this),
'agent end': this.handleAgentEnd.bind(this),
+ 'label pattern': this.handleLabelPattern.bind(this),
'connect': this.handleConnect.bind(this),
'note over': this.handleNote.bind(this),
'note left': this.handleNote.bind(this),
@@ -373,6 +375,36 @@ define(['core/ArrayUtilities'], (array) => {
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}) {
const beginAgents = (agents
.filter(agentHasFlag('begin'))
@@ -412,7 +444,7 @@ define(['core/ArrayUtilities'], (array) => {
const connectStage = {
type: 'connect',
agentNames,
- label,
+ label: this.applyLabelPattern(label),
options,
};
@@ -528,6 +560,7 @@ define(['core/ArrayUtilities'], (array) => {
this.agents.length = 0;
this.blockCount = 0;
this.nesting.length = 0;
+ this.labelPattern = [{token: 'label'}];
const globals = this.beginNested('global', '', '', 0);
stages.forEach(this.handleStage);
diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js
index 00a3dd4..c957fbe 100644
--- a/scripts/sequence/Generator_spec.js
+++ b/scripts/sequence/Generator_spec.js
@@ -26,6 +26,10 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
return {type: 'block end'};
},
+ labelPattern: (pattern, {ln = 0} = {}) => {
+ return {type: 'label pattern', pattern, ln};
+ },
+
defineAgents: (agentNames, {ln = 0} = {}) => {
return {
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', () => {
const sequence = generator.generate({
meta: {
diff --git a/scripts/sequence/LabelPatternParser.js b/scripts/sequence/LabelPatternParser.js
new file mode 100644
index 0000000..1d35eb8
--- /dev/null
+++ b/scripts/sequence/LabelPatternParser.js
@@ -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;
+});
diff --git a/scripts/sequence/LabelPatternParser_spec.js b/scripts/sequence/LabelPatternParser_spec.js
new file mode 100644
index 0000000..dac1057
--- /dev/null
+++ b/scripts/sequence/LabelPatternParser_spec.js
@@ -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 bar');
+ expect(parsed).toEqual([
+ 'foo ',
+ {token: 'label'},
+ ' bar',
+ ]);
+ });
+
+ it('converts multiple tokens', () => {
+ const parsed = parser('foo bar baz');
+ expect(parsed).toEqual([
+ 'foo ',
+ {token: 'label'},
+ ' bar ',
+ {token: 'label'},
+ ' baz',
+ ]);
+ });
+
+ it('ignores empty sequences', () => {
+ const parsed = parser('');
+ expect(parsed).toEqual([
+ {token: 'label'},
+ {token: 'label'},
+ ]);
+ });
+
+ it('passes unrecognised tokens through unchanged', () => {
+ const parsed = parser('foo ');
+ expect(parsed).toEqual([
+ 'foo ',
+ '',
+ ]);
+ });
+
+ it('converts counters', () => {
+ const parsed = parser('ab');
+ 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('');
+ expect(parsed).toEqual([
+ {start: 5, inc: 1, dp: 0},
+ ]);
+ });
+
+ it('defaults counters to start = 1', () => {
+ const parsed = parser('');
+ expect(parsed).toEqual([
+ {start: 1, inc: 1, dp: 0},
+ ]);
+ });
+
+ it('assigns decimal places to counters by their written precision', () => {
+ const parsed = parser('');
+ expect(parsed).toEqual([
+ {start: jasmine.anything(), inc: jasmine.anything(), dp: 2},
+ {start: jasmine.anything(), inc: jasmine.anything(), dp: 2},
+ ]);
+ });
+});
diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js
index fd66c10..1f142f2 100644
--- a/scripts/sequence/Parser.js
+++ b/scripts/sequence/Parser.js
@@ -1,10 +1,12 @@
define([
'core/ArrayUtilities',
'./Tokeniser',
+ './LabelPatternParser',
'./CodeMirrorHints',
], (
array,
Tokeniser,
+ labelPatternParser,
CMHints
) => {
'use strict';
@@ -247,6 +249,23 @@ define([
return true;
},
+ (line) => { // autolabel
+ if(tokenKeyword(line[0]) !== 'autolabel') {
+ return null;
+ }
+
+ let raw = null;
+ if(tokenKeyword(line[1]) === 'off') {
+ raw = '';
+ } else {
+ raw = joinLabel(line, 1);
+ }
+ return {
+ type: 'label pattern',
+ pattern: labelPatternParser(raw),
+ };
+ },
+
(line) => { // block
if(tokenKeyword(line[0]) === 'end' && line.length === 1) {
return {type: 'block end'};
diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js
index b0e7c99..68f247a 100644
--- a/scripts/sequence/Parser_spec.js
+++ b/scripts/sequence/Parser_spec.js
@@ -412,6 +412,32 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
}]);
});
+ it('converts autolabel commands', () => {
+ const parsed = parser.parse('autolabel "foo 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', () => {
const parsed = parser.parse('simultaneously:');
expect(parsed.stages).toEqual([{
diff --git a/scripts/specs.js b/scripts/specs.js
index 115576b..5e8c060 100644
--- a/scripts/specs.js
+++ b/scripts/specs.js
@@ -7,6 +7,7 @@ define([
'interface/Interface_spec',
'sequence/Tokeniser_spec',
'sequence/Parser_spec',
+ 'sequence/LabelPatternParser_spec',
'sequence/Generator_spec',
'sequence/Renderer_spec',
'sequence/themes/Basic_spec',