diff --git a/README.md b/README.md index 62c1c60..bbc6b43 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ Foo -> Foo: Foo talks to itself Foo -> +Bar: Foo asks Bar -Bar --> Foo: and Bar replies +# * and ! cause agents to be created and destroyed inline +Bar -> *Baz +Bar <- !Baz + # Arrows leaving on the left and right of the diagram [ -> Foo: From the left [ <- Foo: To the left @@ -258,7 +262,7 @@ Note: the linter can't run from the local filesystem, so you'll need to run a local HTTP server to ensure linting is successful. One option if you have NPM installed is: -``` +```shell # Setup npm install http-server -g; diff --git a/screenshots/ConnectionTypes.png b/screenshots/ConnectionTypes.png index 428b4f8..a57f090 100644 Binary files a/screenshots/ConnectionTypes.png and b/screenshots/ConnectionTypes.png differ diff --git a/scripts/readme_images.js b/scripts/readme_images.js index 43379a8..6ab372e 100644 --- a/scripts/readme_images.js +++ b/scripts/readme_images.js @@ -18,7 +18,7 @@ } const SAMPLE_REGEX = new RegExp( - /]*>[\s]*```([^]+?)```/g + /]*>[\s]*```(?!shell).*\n([^]+?)```/g ); function findSamples(content) { diff --git a/scripts/sequence/CodeMirrorMode.js b/scripts/sequence/CodeMirrorMode.js index defdc58..169af6b 100644 --- a/scripts/sequence/CodeMirrorMode.js +++ b/scripts/sequence/CodeMirrorMode.js @@ -46,24 +46,57 @@ define(['core/ArrayUtilities'], (array) => { }; } - const CM_CONNECT = {type: 'keyword', suggest: true, then: { - '+': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, - '-': {type: 'operator', suggest: true, then: {'': CM_AGENT_TO_OPTTEXT}}, - '': CM_AGENT_TO_OPTTEXT, - }}; + function makeCMOperatorBlock(exit) { + const op = {type: 'operator', suggest: true, then: { + '+': CM_ERROR, + '-': CM_ERROR, + '*': CM_ERROR, + '!': CM_ERROR, + '': exit, + }}; + const pm = {type: 'operator', suggest: true, then: { + '+': CM_ERROR, + '-': CM_ERROR, + '*': op, + '!': op, + '': exit, + }}; + const se = {type: 'operator', suggest: true, then: { + '+': op, + '-': op, + '*': CM_ERROR, + '!': CM_ERROR, + '': exit, + }}; + return { + '+': pm, + '-': pm, + '*': se, + '!': se, + '': exit, + }; + } - const CM_CONNECT_FULL = {type: 'variable', suggest: 'Agent', then: { - '->': CM_CONNECT, - '-->': CM_CONNECT, - '<-': CM_CONNECT, - '<--': CM_CONNECT, - '<->': CM_CONNECT, - '<-->': CM_CONNECT, - ':': {type: 'operator', suggest: true, override: 'Label', then: {}}, - '': 0, - }}; + function makeCMConnect() { + const connect = { + type: 'keyword', + suggest: true, + then: makeCMOperatorBlock(CM_AGENT_TO_OPTTEXT), + }; - const CM_COMMANDS = {type: 'error line-error', then: { + return makeCMOperatorBlock({type: 'variable', suggest: 'Agent', then: { + '->': connect, + '-->': connect, + '<-': connect, + '<--': connect, + '<->': connect, + '<-->': connect, + ':': {type: 'operator', suggest: true, override: 'Label', then: {}}, + '': 0, + }}); + } + + const CM_COMMANDS = {type: 'error line-error', then: Object.assign({ 'title': {type: 'keyword', suggest: true, then: { '': CM_TEXT_TO_END, }}, @@ -134,10 +167,7 @@ define(['core/ArrayUtilities'], (array) => { }}, }}, }}, - '+': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, - '-': {type: 'operator', suggest: true, then: {'': CM_CONNECT_FULL}}, - '': CM_CONNECT_FULL, - }}; + }, makeCMConnect())}; function cmCappedToken(token, current) { if(Object.keys(current.then).length > 0) { diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js index f951689..791c301 100644 --- a/scripts/sequence/Generator.js +++ b/scripts/sequence/Generator.js @@ -25,8 +25,8 @@ define(['core/ArrayUtilities'], (array) => { return agent.name; } - function agentHasFlag(flag) { - return (agent) => agent.flags.includes(flag); + function agentHasFlag(flag, has = true) { + return (agent) => (agent.flags.includes(flag) === has); } const MERGABLE = { @@ -235,7 +235,7 @@ define(['core/ArrayUtilities'], (array) => { array.mergeSets(this.agents, agents, agentEqCheck); } - setAgentVis(agents, visible, mode, checked = false) { + setAgentVisRaw(agents, visible, mode, checked = false) { const filteredAgents = agents.filter((agent) => { const state = this.agentStates.get(agent.name) || DEFAULT_AGENT; if(state.locked) { @@ -269,6 +269,15 @@ define(['core/ArrayUtilities'], (array) => { }; } + setAgentVis(agents, visible, mode, checked = false) { + return this.setAgentVisRaw( + agents.map(convertAgent), + visible, + mode, + checked + ); + } + setAgentHighlight(agents, highlighted, checked = false) { const filteredAgents = agents.filter((agent) => { const state = this.agentStates.get(agent.name) || DEFAULT_AGENT; @@ -340,15 +349,24 @@ define(['core/ArrayUtilities'], (array) => { } handleConnect({agents, label, options}) { - const colAgents = agents.map(convertAgent); - this.addStage(this.setAgentVis(colAgents, true, 'box')); - this.defineAgents(colAgents); + const beginAgents = agents.filter(agentHasFlag('begin')); + const endAgents = agents.filter(agentHasFlag('end')); + if(array.hasIntersection(beginAgents, endAgents, agentEqCheck)) { + throw new Error('Cannot set agent visibility multiple times'); + } const startAgents = agents.filter(agentHasFlag('start')); const stopAgents = agents.filter(agentHasFlag('stop')); + array.mergeSets(stopAgents, endAgents); if(array.hasIntersection(startAgents, stopAgents, agentEqCheck)) { throw new Error('Cannot set agent highlighting multiple times'); } + + this.defineAgents(agents.map(convertAgent)); + + const implicitBegin = agents.filter(agentHasFlag('begin', false)); + this.addStage(this.setAgentVis(implicitBegin, true, 'box')); + const connectStage = { type: 'connect', agentNames: agents.map(getAgentName), @@ -357,9 +375,11 @@ define(['core/ArrayUtilities'], (array) => { }; this.addParallelStages([ + this.setAgentVis(beginAgents, true, 'box', true), this.setAgentHighlight(startAgents, true, true), connectStage, this.setAgentHighlight(stopAgents, false, true), + this.setAgentVis(endAgents, false, 'cross', true), ]); } @@ -371,7 +391,7 @@ define(['core/ArrayUtilities'], (array) => { colAgents = agents.map(convertAgent); } - this.addStage(this.setAgentVis(colAgents, true, 'box')); + this.addStage(this.setAgentVisRaw(colAgents, true, 'box')); this.defineAgents(colAgents); this.addStage({ @@ -383,23 +403,17 @@ define(['core/ArrayUtilities'], (array) => { } handleAgentDefine({agents}) { - const colAgents = agents.map(convertAgent); - this.defineAgents(colAgents); + this.defineAgents(agents.map(convertAgent)); } handleAgentBegin({agents, mode}) { - this.addStage(this.setAgentVis( - agents.map(convertAgent), - true, - mode, - true - )); + this.addStage(this.setAgentVis(agents, true, mode, true)); } handleAgentEnd({agents, mode}) { this.addParallelStages([ this.setAgentHighlight(agents, false), - this.setAgentVis(agents.map(convertAgent), false, mode, true), + this.setAgentVis(agents, false, mode, true), ]); } @@ -471,7 +485,7 @@ define(['core/ArrayUtilities'], (array) => { this.addParallelStages([ this.setAgentHighlight(this.agents, false), - this.setAgentVis(this.agents, false, terminators), + this.setAgentVisRaw(this.agents, false, terminators), ]); addBounds( diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index ca1cf6e..4f4ed59 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -426,6 +426,51 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { ]); }); + it('adds parallel begin stages', () => { + const sequence = generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['begin']}]), + ]}); + expect(sequence.stages).toEqual([ + GENERATED.beginAgents(['A']), + GENERATED.parallel([ + GENERATED.beginAgents(['B']), + GENERATED.connect(['A', 'B']), + ]), + GENERATED.endAgents(['A', 'B']), + ]); + }); + + it('adds parallel end stages', () => { + const sequence = generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['end']}]), + ]}); + expect(sequence.stages).toEqual([ + GENERATED.beginAgents(['A', 'B']), + GENERATED.parallel([ + GENERATED.connect(['A', 'B']), + GENERATED.endAgents(['B']), + ]), + GENERATED.endAgents(['A']), + ]); + }); + + it('implicitly ends highlighting when ending a stage', () => { + const sequence = generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['start']}]), + PARSED.connect(['A', {name: 'B', flags: ['end']}]), + ]}); + expect(sequence.stages).toEqual([ + jasmine.anything(), + jasmine.anything(), + GENERATED.parallel([ + GENERATED.connect(['A', 'B']), + GENERATED.highlight(['B'], false), + GENERATED.endAgents(['B']), + ]), + GENERATED.endAgents(['A']), + ]); + }); + it('rejects conflicting flags', () => { expect(() => generator.generate({stages: [ PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]), @@ -437,6 +482,17 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { {name: 'A', flags: ['stop']}, ]), ]})).toThrow(); + + expect(() => generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['begin', 'end']}]), + ]})).toThrow(); + + expect(() => generator.generate({stages: [ + PARSED.connect([ + {name: 'A', flags: ['begin']}, + {name: 'A', flags: ['end']}, + ]), + ]})).toThrow(); }); it('adds implicit highlight end with implicit terminator', () => { diff --git a/scripts/sequence/Parser.js b/scripts/sequence/Parser.js index 6ee861d..2aaaf69 100644 --- a/scripts/sequence/Parser.js +++ b/scripts/sequence/Parser.js @@ -26,8 +26,10 @@ define([ }; const CONNECT_AGENT_FLAGS = { + '*': 'begin', '+': 'start', '-': 'stop', + '!': 'end', }; const TERMINATOR_TYPES = [ @@ -116,13 +118,20 @@ define([ const flags = []; let p = start; for(; p < end; ++ p) { - const flag = flagTypes[tokenKeyword(line[p])]; + const rawFlag = tokenKeyword(line[p]); + const flag = flagTypes[rawFlag]; if(flag) { + if(flags.includes(flag)) { + throw new Error('Duplicate agent flag: ' + rawFlag); + } flags.push(flag); } else { break; } } + if(p >= end) { + throw new Error('Missing agent name'); + } return { name: joinLabel(line, p, end), flags, diff --git a/scripts/sequence/Parser_spec.js b/scripts/sequence/Parser_spec.js index 5b06a15..503428d 100644 --- a/scripts/sequence/Parser_spec.js +++ b/scripts/sequence/Parser_spec.js @@ -79,13 +79,13 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { }); it('parses optional flags', () => { - const parsed = parser.parse('+A -> -B'); + const parsed = parser.parse('+A -> -*!B'); expect(parsed.stages).toEqual([ { type: 'connect', agents: [ {name: 'A', flags: ['start']}, - {name: 'B', flags: ['stop']}, + {name: 'B', flags: ['stop', 'begin', 'end']}, ], label: jasmine.anything(), options: jasmine.anything(), @@ -93,6 +93,15 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => { ]); }); + it('rejects duplicate flags', () => { + expect(() => parser.parse('A -> +*+B')).toThrow(); + expect(() => parser.parse('A -> **B')).toThrow(); + }); + + it('rejects missing agent names', () => { + expect(() => parser.parse('A -> +')).toThrow(); + }); + it('converts multiple entries', () => { const parsed = parser.parse('A -> B\nB -> A'); expect(parsed.stages).toEqual([ diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 942ec85..5f189dc 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -161,10 +161,10 @@ define([ const agentSpaces = new Map(); const agentNames = this.visibleAgents.slice(); - const addSpacing = (agentName, spacing) => { + const addSpacing = (agentName, {left, right}) => { const current = agentSpaces.get(agentName); - current.left = Math.max(current.left, spacing.left); - current.right = Math.max(current.right, spacing.right); + current.left = Math.max(current.left, left); + current.right = Math.max(current.right, right); }; this.agentInfos.forEach((agentInfo) => { @@ -366,7 +366,7 @@ define([ const touchedAgentNames = []; stages.forEach((stage) => { const component = this.components.get(stage.type); - const r = component.renderPre(stage, envPre); + const r = component.renderPre(stage, envPre) || {}; if(r.topShift !== undefined) { maxTopShift = Math.max(maxTopShift, r.topShift); } @@ -460,6 +460,7 @@ define([ x: null, latestYStart: null, currentRad: 0, + currentMaxRad: 0, latestY: 0, maxRPad: 0, maxLPad: 0, diff --git a/scripts/sequence/Tokeniser.js b/scripts/sequence/Tokeniser.js index 4114459..7105d12 100644 --- a/scripts/sequence/Tokeniser.js +++ b/scripts/sequence/Tokeniser.js @@ -31,10 +31,13 @@ define(['./CodeMirrorMode'], (CMMode) => { unescape, baseToken: {q: true}, }, - {start: /(?=[^ \t\r\n:+\-<>,])/y, end: /(?=[ \t\r\n:+\-<>,])|$/y}, - {start: /(?=[+\-<>])/y, end: /(?=[^+\-<>])|$/y}, + {start: /(?=[^ \t\r\n:+\-*!<>,])/y, end: /(?=[ \t\r\n:+\-*!<>,])|$/y}, + {start: /(?=[\-<>])/y, end: /(?=[^\-<>])|$/y}, {start: /,/y, baseToken: {v: ','}}, {start: /:/y, baseToken: {v: ':'}}, + {start: /!/y, baseToken: {v: '!'}}, + {start: /\+/y, baseToken: {v: '+'}}, + {start: /\*/y, baseToken: {v: '*'}}, {start: /\n/y, baseToken: {v: '\n'}}, ]; diff --git a/scripts/sequence/components/AgentCap.js b/scripts/sequence/components/AgentCap.js index c492cf5..95e0a13 100644 --- a/scripts/sequence/components/AgentCap.js +++ b/scripts/sequence/components/AgentCap.js @@ -23,11 +23,18 @@ define([ return { left: width / 2, right: width / 2, + radius: width / 2, }; } - topShift() { - return 0; + topShift({label}, env) { + const config = env.theme.agentCap.box; + const height = ( + env.textSizer.measureHeight(config.labelAttrs, label) + + config.padding.top + + config.padding.bottom + ); + return Math.max(0, height - config.arrowBottom); } render(y, {x, label}, env) { @@ -57,6 +64,7 @@ define([ return { left: config.size / 2, right: config.size / 2, + radius: 0, }; } @@ -98,6 +106,7 @@ define([ return { left: width / 2, right: width / 2, + radius: width / 2, }; } @@ -131,7 +140,11 @@ define([ class CapNone { separation({currentRad}) { - return {left: currentRad, right: currentRad}; + return { + left: currentRad, + right: currentRad, + radius: currentRad, + }; } topShift(agentInfo, env) { @@ -162,18 +175,24 @@ define([ this.begin = begin; } + separationPre({mode, agentNames}, env) { + agentNames.forEach((name) => { + const agentInfo = env.agentInfos.get(name); + const sep = AGENT_CAPS[mode].separation(agentInfo, env); + env.addSpacing(name, sep); + agentInfo.currentMaxRad = Math.max( + agentInfo.currentMaxRad, + sep.radius + ); + }); + } + separation({mode, agentNames}, env) { if(this.begin) { array.mergeSets(env.visibleAgents, agentNames); } else { array.removeAll(env.visibleAgents, agentNames); } - - agentNames.forEach((name) => { - const agentInfo = env.agentInfos.get(name); - const separationFn = AGENT_CAPS[mode].separation; - env.addSpacing(name, separationFn(agentInfo, env)); - }); } renderPre({mode, agentNames}, env) { @@ -182,6 +201,9 @@ define([ const agentInfo = env.agentInfos.get(name); const topShift = AGENT_CAPS[mode].topShift(agentInfo, env); maxTopShift = Math.max(maxTopShift, topShift); + + const r = AGENT_CAPS[mode].separation(agentInfo, env).radius; + agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); }); return { agentNames, diff --git a/scripts/sequence/components/AgentHighlight.js b/scripts/sequence/components/AgentHighlight.js index b379283..314fd94 100644 --- a/scripts/sequence/components/AgentHighlight.js +++ b/scripts/sequence/components/AgentHighlight.js @@ -2,21 +2,32 @@ define(['./BaseComponent'], (BaseComponent) => { 'use strict'; class AgentHighlight extends BaseComponent { + radius(highlighted, env) { + return highlighted ? env.theme.agentLineHighlightRadius : 0; + } + separationPre({agentNames, highlighted}, env) { - const rad = highlighted ? env.theme.agentLineHighlightRadius : 0; + const r = this.radius(highlighted, env); agentNames.forEach((name) => { const agentInfo = env.agentInfos.get(name); - const maxRad = Math.max(agentInfo.currentMaxRad, rad); - agentInfo.currentRad = rad; - agentInfo.currentMaxRad = maxRad; + agentInfo.currentRad = r; + agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); + }); + } + + renderPre({agentNames, highlighted}, env) { + const r = this.radius(highlighted, env); + agentNames.forEach((name) => { + const agentInfo = env.agentInfos.get(name); + agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); }); } render({agentNames, highlighted}, env) { - const rad = highlighted ? env.theme.agentLineHighlightRadius : 0; + const r = this.radius(highlighted, env); agentNames.forEach((name) => { env.drawAgentLine(name, env.primaryY); - env.agentInfos.get(name).currentRad = rad; + env.agentInfos.get(name).currentRad = r; }); } } diff --git a/scripts/sequence/components/BaseComponent.js b/scripts/sequence/components/BaseComponent.js index 89ebab3..6223e84 100644 --- a/scripts/sequence/components/BaseComponent.js +++ b/scripts/sequence/components/BaseComponent.js @@ -35,7 +35,6 @@ define(() => { textSizer, state, }*/) { - return {}; } render(/*stage, { @@ -49,7 +48,6 @@ define(() => { SVGTextBlockClass, state, }*/) { - return 0; } } diff --git a/scripts/sequence/components/Connect.js b/scripts/sequence/components/Connect.js index 1af5be6..ab5d4c5 100644 --- a/scripts/sequence/components/Connect.js +++ b/scripts/sequence/components/Connect.js @@ -83,7 +83,7 @@ define([ config.label.margin.bottom ); - const lineX = from.x + from.currentRad; + const lineX = from.x + from.currentMaxRad; const y0 = env.primaryY; const x0 = ( lineX + @@ -159,8 +159,8 @@ define([ config.label.margin.bottom ); - const x0 = from.x + from.currentRad * dir; - const x1 = to.x - to.currentRad * dir; + const x0 = from.x + from.currentMaxRad * dir; + const x1 = to.x - to.currentMaxRad * dir; let y = env.primaryY; SVGShapes.renderBoxedText(label, { diff --git a/scripts/sequence/components/Note.js b/scripts/sequence/components/Note.js index 4f41388..bc705f9 100644 --- a/scripts/sequence/components/Note.js +++ b/scripts/sequence/components/Note.js @@ -131,8 +131,8 @@ define(['./BaseComponent'], (BaseComponent) => { const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); return this.renderNote({ - x0: infoL.x - infoL.currentRad - config.overlap.left, - x1: infoR.x + infoR.currentRad + config.overlap.right, + x0: infoL.x - infoL.currentMaxRad - config.overlap.left, + x1: infoR.x + infoR.currentMaxRad + config.overlap.right, anchor: 'middle', mode, label, @@ -186,7 +186,7 @@ define(['./BaseComponent'], (BaseComponent) => { const {left, right} = findExtremes(env.agentInfos, agentNames); if(this.isRight) { const info = env.agentInfos.get(right); - const x0 = info.x + info.currentRad + config.margin.left; + const x0 = info.x + info.currentMaxRad + config.margin.left; return this.renderNote({ x0, anchor: 'start', @@ -195,7 +195,7 @@ define(['./BaseComponent'], (BaseComponent) => { }, env); } else { const info = env.agentInfos.get(left); - const x1 = info.x - info.currentRad - config.margin.right; + const x1 = info.x - info.currentMaxRad - config.margin.right; return this.renderNote({ x1, anchor: 'end', @@ -232,8 +232,8 @@ define(['./BaseComponent'], (BaseComponent) => { const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); const xMid = ( - infoL.x + infoL.currentRad + - infoR.x - infoR.currentRad + infoL.x + infoL.currentMaxRad + + infoR.x - infoR.currentMaxRad ) / 2; return this.renderNote({ diff --git a/scripts/sequence/sequence_integration_spec.js b/scripts/sequence/sequence_integration_spec.js index bbb8667..0a772c5 100644 --- a/scripts/sequence/sequence_integration_spec.js +++ b/scripts/sequence/sequence_integration_spec.js @@ -108,176 +108,32 @@ defineDescribe('Sequence Integration', [ ); }); - it('Renders the "Simple Usage" example without error', () => { - const parsed = parser.parse( - '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' + - '\n' + - 'Bowie -> Audience: Sings\n' + - '\n' + - 'terminators box\n' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); + const SAMPLE_REGEX = new RegExp( + /```(?!shell).*\n([^]+?)```/g + ); - it('Renders the "Connection Types" example without error', () => { - const parsed = parser.parse( - 'title Connection Types\n' + - '\n' + - 'Foo -> Bar: Simple arrow\n' + - 'Foo --> Bar: Dashed arrow\n' + - 'Foo <- Bar: Reversed arrow\n' + - 'Foo <-- Bar: Reversed dashed arrow\n' + - 'Foo <-> Bar: Double arrow\n' + - 'Foo <--> Bar: Double dashed arrow\n' + - '\n' + - '# An arrow with no label:\n' + - 'Foo -> Bar\n' + - '\n' + - 'Foo -> Foo: Foo talks to itself\n' + - '\n' + - '# Arrows leaving on the left and right of the diagram\n' + - '[ -> Foo: From the left\n' + - '[ <- Foo: To the left\n' + - 'Foo -> ]: To the right\n' + - 'Foo <- ]: From the right\n' + - '[ -> ]: Left to right!\n' + - '# (etc.)\n' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); + function findSamples(content) { + SAMPLE_REGEX.lastIndex = 0; + const results = []; + while(true) { + const match = SAMPLE_REGEX.exec(content); + if(!match) { + break; + } + results.push(match[1]); + } + return results; + } - it('Renders the "Notes & State" example without error', () => { - const parsed = parser.parse( - 'title Note Placements\n' + - '\n' + - 'note over Foo: Foo says something\n' + - 'note left of Foo: Stuff\n' + - 'note right of Bar: More stuff\n' + - 'note over Foo, Bar: "Foo and Bar\n' + - 'on multiple lines"\n' + - 'note between Foo, Bar: Link\n' + - '\n' + - 'text right: \'Comments\\nOver here\!\'\n' + - '\n' + - 'state over Foo: Foo is ponderous' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); - - it('Renders the "Logic" example without error', () => { - const parsed = parser.parse( - 'title At the Bank\n' + - '\n' + - 'begin Person, ATM, Bank\n' + - 'Person -> ATM: Request money\n' + - 'ATM -> Bank: Check funds\n' + - 'if fraud detected\n' + - ' Bank -> Police: "Get \'em!"\n' + - ' Police -> Person: "You\'re nicked"\n' + - ' end Police\n' + - 'else if sufficient funds\n' + - ' ATM -> Bank: Withdraw funds\n' + - ' repeat until "all requested money\n' + - ' has been handed over"\n' + - ' ATM -> Person: Dispense note\n' + - ' end\n' + - 'else\n' + - ' ATM -> Person: Error\n' + - 'end' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); - - it('Renders the "Multiline Text" example without error', () => { - const parsed = parser.parse( - 'title \'My Multiline\n' + - 'Title\'\n' + - '\n' + - 'note over Foo: \'Also possible\\nwith escapes\'\n' + - '\n' + - 'Foo -> Bar: \'Lines of text\\non this arrow\'\n' + - '\n' + - 'if \'Even multiline\\ninside conditions like this\'\n' + - ' Foo -> \'Multiline\\nagent\'\n' + - 'end\n' + - '\n' + - 'state over Foo: \'Newlines here,\\ntoo!\'' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); - - it('Renders the "Short-Lived Agents" example without error', () => { - const parsed = parser.parse( - 'title "Baz doesn\'t live long"\n' + - '\n' + - 'Foo -> Bar\n' + - 'begin Baz\n' + - 'Bar -> Baz\n' + - 'Baz -> Foo\n' + - 'end Baz\n' + - 'Foo -> Bar\n' + - '\n' + - '# Foo and Bar end with black bars\n' + - 'terminators bar\n' + - '# (options are: box, bar, cross, none)' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); - - it('Renders the "Alternative Agent Ordering" example without error', () => { - const parsed = parser.parse( - 'define Baz, Foo\n' + - 'Foo -> Bar\n' + - 'Bar -> Baz\n' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); - - it('Renders the "Simultaneous Actions" example without error', () => { - const parsed = parser.parse( - 'begin A, B, C, D\n' + - 'A -> C\n' + - '\n' + - '# Define a marker which can be returned to later\n' + - '\n' + - 'some primary process:\n' + - 'A -> B\n' + - 'B -> A\n' + - 'A -> B\n' + - 'B -> A\n' + - '\n' + - '# Return to the defined marker\n' + - '# (should be interpreted as no-higher-then the marker; may be\n' + - '# pushed down to keep relative action ordering consistent)\n' + - '\n' + - 'simultaneously with some primary process:\n' + - 'C -> D\n' + - 'D -> C\n' + - 'end D\n' + - 'C -> A\n' + - '\n' + - '# The marker name is optional; using "simultaneously:" with no\n' + - '# marker will jump to the top of the entire sequence.' - ); - const sequence = generator.generate(parsed); - expect(() => renderer.render(sequence)).not.toThrow(); - }); + return (fetch('README.md') + .then((response) => response.text()) + .then(findSamples) + .then((samples) => samples.forEach((code, i) => { + it('Renders readme example #' + (i + 1) + ' without error', () => { + const parsed = parser.parse(code); + const sequence = generator.generate(parsed); + expect(() => renderer.render(sequence)).not.toThrow(); + }); + })) + ); }); diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js index 12efaa6..856ab6b 100644 --- a/scripts/sequence/themes/Basic.js +++ b/scripts/sequence/themes/Basic.js @@ -18,6 +18,7 @@ define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => { right: 10, bottom: 5, }, + arrowBottom: 5 + 12 * 1.3 / 2, boxAttrs: { 'fill': '#FFFFFF', 'stroke': '#000000',