From 0f22dc7f94525eb30937240784820e7f123d054a Mon Sep 17 00:00:00 2001 From: David Evans Date: Sun, 14 Jan 2018 23:10:48 +0000 Subject: [PATCH] Refactor to enable formatted text everywhere, and make 'agent' dichotomy in generator clearer through naming [#29] --- lib/sequence-diagram.js | 1271 +++++++++-------- lib/sequence-diagram.min.js | 2 +- scripts/sequence/Generator.js | 509 ++++--- scripts/sequence/Generator_spec.js | 979 +++++++------ scripts/sequence/MarkdownParser.js | 19 + scripts/sequence/MarkdownParser_spec.js | 51 + scripts/sequence/Parser.js | 38 +- scripts/sequence/Parser_spec.js | 48 +- scripts/sequence/Renderer.js | 89 +- scripts/sequence/Renderer_spec.js | 96 +- scripts/sequence/SequenceDiagram.js | 5 +- scripts/sequence/components/AgentCap.js | 66 +- scripts/sequence/components/AgentHighlight.js | 20 +- .../components/AgentHighlight_spec.js | 6 +- scripts/sequence/components/BaseComponent.js | 10 +- scripts/sequence/components/Block.js | 54 +- scripts/sequence/components/Connect.js | 32 +- scripts/sequence/components/Note.js | 40 +- scripts/sequence/components/Parallel.js | 6 +- scripts/sequence/themes/Basic.js | 2 +- scripts/sequence/themes/Chunky.js | 2 +- scripts/sequence/themes/Monospace.js | 2 +- scripts/sequence/themes/Sketch.js | 6 +- scripts/specs.js | 1 + scripts/stubs/SVGTextBlock.js | 37 +- scripts/svg/SVGShapes.js | 39 +- scripts/svg/SVGShapes_spec.js | 8 +- scripts/svg/SVGTextBlock.js | 121 +- scripts/svg/SVGTextBlock_spec.js | 87 +- 29 files changed, 2010 insertions(+), 1636 deletions(-) create mode 100644 scripts/sequence/MarkdownParser.js create mode 100644 scripts/sequence/MarkdownParser_spec.js diff --git a/lib/sequence-diagram.js b/lib/sequence-diagram.js index e144da9..26f7743 100644 --- a/lib/sequence-diagram.js +++ b/lib/sequence-diagram.js @@ -1331,6 +1331,26 @@ define('sequence/Tokeniser',['./CodeMirrorMode'], (CMMode) => { }; }); +define('sequence/MarkdownParser',[],() => { + 'use strict'; + + function parseMarkdown(text) { + if(!text) { + return []; + } + + const lines = text.split('\n'); + const result = []; + const attrs = null; + lines.forEach((line) => { + result.push([{text: line, attrs}]); + }); + return result; + } + + return parseMarkdown; +}); + define('sequence/LabelPatternParser',[],() => { 'use strict'; @@ -1407,128 +1427,38 @@ define('sequence/LabelPatternParser',[],() => { return parsePattern; }); -define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => { - 'use strict'; - - const TRIMMER = /^([ \t]*)(.*)$/; - const SQUASH_START = /^[ \t\r\n:,]/; - const SQUASH_END = /[ \t\r\n]$/; - - function makeRanges(cm, line, chFrom, chTo) { - const ln = cm.getLine(line); - const ranges = { - wordFrom: {line: line, ch: chFrom}, - squashFrom: {line: line, ch: chFrom}, - wordTo: {line: line, ch: chTo}, - squashTo: {line: line, ch: chTo}, - }; - if(chFrom > 0 && ln[chFrom - 1] === ' ') { - ranges.squashFrom.ch --; - } - if(ln[chTo] === ' ') { - ranges.squashTo.ch ++; - } - return ranges; - } - - function makeHintItem(text, ranges) { - return { - text: text, - displayText: (text === '\n') ? '' : text.trim(), - className: (text === '\n') ? 'pick-virtual' : null, - from: SQUASH_START.test(text) ? ranges.squashFrom : ranges.wordFrom, - to: SQUASH_END.test(text) ? ranges.squashTo : ranges.wordTo, - }; - } - - function getGlobals({global, prefix = '', suffix = ''}, globals) { - const identified = globals[global]; - if(!identified) { - return []; - } - return identified.map((item) => (prefix + item + suffix)); - } - - function populateGlobals(suggestions, globals = {}) { - for(let i = 0; i < suggestions.length;) { - if(typeof suggestions[i] === 'object') { - const identified = getGlobals(suggestions[i], globals); - array.mergeSets(suggestions, identified); - suggestions.splice(i, 1); - } else { - ++ i; - } - } - } - - function getHints(cm, options) { - const cur = cm.getCursor(); - const token = cm.getTokenAt(cur); - let partial = token.string; - if(token.end > cur.ch) { - partial = partial.substr(0, cur.ch - token.start); - } - const parts = TRIMMER.exec(partial); - partial = parts[2]; - const from = token.start + parts[1].length; - - const continuation = (cur.ch > 0 && token.state.line.length > 0); - let comp = (continuation ? - token.state.completions : - token.state.beginCompletions - ); - if(!continuation) { - comp = comp.concat(token.state.knownAgent); - } - - populateGlobals(comp, cm.options.globals); - - const ranges = makeRanges(cm, cur.line, from, token.end); - let selfValid = false; - const list = (comp - .filter((opt) => opt.startsWith(partial)) - .map((opt) => { - if(opt === partial + ' ' && !options.completeSingle) { - selfValid = true; - return null; - } - return makeHintItem(opt, ranges); - }) - .filter((opt) => (opt !== null)) - ); - if(selfValid && list.length > 0) { - list.unshift(makeHintItem(partial + ' ', ranges)); - } - - return { - list, - from: ranges.wordFrom, - to: ranges.wordTo, - }; - } - - return { - getHints, - }; -}); - define('sequence/Parser',[ 'core/ArrayUtilities', './Tokeniser', + './MarkdownParser', './LabelPatternParser', - './CodeMirrorHints', ], ( array, Tokeniser, - labelPatternParser, - CMHints + markdownParser, + labelPatternParser ) => { 'use strict'; const BLOCK_TYPES = { - 'if': {type: 'block begin', mode: 'if', skip: []}, - 'else': {type: 'block split', mode: 'else', skip: ['if']}, - 'repeat': {type: 'block begin', mode: 'repeat', skip: []}, + 'if': { + type: 'block begin', + blockType: 'if', + tag: 'if', + skip: [], + }, + 'else': { + type: 'block split', + blockType: 'else', + tag: 'else', + skip: ['if'], + }, + 'repeat': { + type: 'block begin', + blockType: 'repeat', + tag: 'repeat', + skip: [], + }, }; const CONNECT_TYPES = ((() => { @@ -1828,7 +1758,8 @@ define('sequence/Parser',[ skip = skipOver(line, skip, [':']); return { type: type.type, - mode: type.mode, + blockType: type.blockType, + tag: type.tag, label: joinLabel(line, skip), }; }, @@ -1859,7 +1790,8 @@ define('sequence/Parser',[ return { type: 'group begin', agents, - mode: 'ref', + blockType: 'ref', + tag: 'ref', label: def.name, alias: def.alias, }; @@ -1994,10 +1926,6 @@ define('sequence/Parser',[ ); } - getCodeMirrorHints() { - return CMHints.getHints; - } - parseLines(lines) { const result = { meta: { @@ -2005,6 +1933,7 @@ define('sequence/Parser',[ theme: '', terminators: 'none', headers: 'box', + textFormatter: markdownParser, }, stages: [], }; @@ -2045,35 +1974,52 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { AgentState.LOCKED = new AgentState({locked: true}); AgentState.DEFAULT = new AgentState(); - const Agent = { + // Agent from Parser: {name, flags} + const PAgent = { equals: (a, b) => { return a.name === b.name; }, - make: (name, {anchorRight = false} = {}) => { - return {name, anchorRight}; - }, - getName: (agent) => { - return agent.name; - }, hasFlag: (flag, has = true) => { - return (agent) => (agent.flags.includes(flag) === has); + return (pAgent) => (pAgent.flags.includes(flag) === has); }, }; + // Agent from Generator: {id, formattedLabel, anchorRight} + const GAgent = { + equals: (a, b) => { + return a.id === b.id; + }, + make: (id, {anchorRight = false} = {}) => { + return {id, anchorRight}; + }, + indexOf: (list, gAgent) => { + return array.indexOf(list, gAgent, GAgent.equals); + }, + hasIntersection: (a, b) => { + return array.hasIntersection(a, b, GAgent.equals); + }, + }; + + const NOTE_DEFAULT_G_AGENTS = { + 'note over': [GAgent.make('['), GAgent.make(']')], + 'note left': [GAgent.make('[')], + 'note right': [GAgent.make(']')], + }; + const MERGABLE = { 'agent begin': { check: ['mode'], - merge: ['agentNames'], + merge: ['agentIDs'], siblings: new Set(['agent highlight']), }, 'agent end': { check: ['mode'], - merge: ['agentNames'], + merge: ['agentIDs'], siblings: new Set(['agent highlight']), }, 'agent highlight': { check: ['highlighted'], - merge: ['agentNames'], + merge: ['agentIDs'], siblings: new Set(['agent begin', 'agent end']), }, }; @@ -2207,39 +2153,33 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { } } - function addBounds(target, agentL, agentR, involvedAgents = null) { - array.remove(target, agentL, Agent.equals); - array.remove(target, agentR, Agent.equals); + function addBounds(allGAgents, gAgentL, gAgentR, involvedGAgents = null) { + array.remove(allGAgents, gAgentL, GAgent.equals); + array.remove(allGAgents, gAgentR, GAgent.equals); let indexL = 0; - let indexR = target.length; - if(involvedAgents) { - const found = (involvedAgents - .map((agent) => array.indexOf(target, agent, Agent.equals)) + let indexR = allGAgents.length; + if(involvedGAgents) { + const found = (involvedGAgents + .map((gAgent) => GAgent.indexOf(allGAgents, gAgent)) .filter((p) => (p !== -1)) ); - indexL = found.reduce((a, b) => Math.min(a, b), target.length); + indexL = found.reduce((a, b) => Math.min(a, b), allGAgents.length); indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1; } - target.splice(indexL, 0, agentL); - target.splice(indexR + 1, 0, agentR); + allGAgents.splice(indexL, 0, gAgentL); + allGAgents.splice(indexR + 1, 0, gAgentR); return {indexL, indexR: indexR + 1}; } - const NOTE_DEFAULT_AGENTS = { - 'note over': [{name: '[', flags: []}, {name: ']', flags: []}], - 'note left': [{name: '[', flags: []}], - 'note right': [{name: ']', flags: []}], - }; - return class Generator { constructor() { this.agentStates = new Map(); this.agentAliases = new Map(); this.activeGroups = new Map(); - this.agents = []; + this.gAgents = []; this.labelPattern = null; this.blockCount = 0; this.nesting = []; @@ -2264,13 +2204,13 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { 'note right': this.handleNote.bind(this), 'note between': this.handleNote.bind(this), }; - this.expandGroupedAgent = this.expandGroupedAgent.bind(this); + this.expandGroupedGAgent = this.expandGroupedGAgent.bind(this); this.handleStage = this.handleStage.bind(this); - this.convertAgent = this.convertAgent.bind(this); + this.toGAgent = this.toGAgent.bind(this); this.endGroup = this.endGroup.bind(this); } - convertAgent({alias, name}) { + toGAgent({alias, name}) { if(alias) { if(this.agentAliases.has(name)) { throw new Error( @@ -2280,7 +2220,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { const old = this.agentAliases.get(alias); if( (old && old !== alias) || - this.agents.some((agent) => (agent.name === alias)) + this.gAgents.some((gAgent) => (gAgent.id === alias)) ) { throw new Error( 'Cannot use ' + alias + @@ -2289,7 +2229,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { } this.agentAliases.set(alias, name); } - return Agent.make(this.agentAliases.get(name) || name); + return GAgent.make(this.agentAliases.get(name) || name); } addStage(stage, isVisible = true) { @@ -2324,138 +2264,139 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { }); } - defineAgents(colAgents) { - array.mergeSets(this.currentNest.agents, colAgents, Agent.equals); - array.mergeSets(this.agents, colAgents, Agent.equals); + defineGAgents(gAgents) { + array.mergeSets(this.currentNest.gAgents, gAgents, GAgent.equals); + array.mergeSets(this.gAgents, gAgents, GAgent.equals); } - getAgentState(agent) { - return this.agentStates.get(agent.name) || AgentState.DEFAULT; + getGAgentState(gAgent) { + return this.agentStates.get(gAgent.id) || AgentState.DEFAULT; } - updateAgentState(agent, change) { - const state = this.agentStates.get(agent.name); + updateGAgentState(gAgent, change) { + const state = this.agentStates.get(gAgent.id); if(state) { Object.assign(state, change); } else { - this.agentStates.set(agent.name, new AgentState(change)); + this.agentStates.set(gAgent.id, new AgentState(change)); } } - validateAgents(agents, { + replaceGAgentState(gAgent, state) { + this.agentStates.set(gAgent.id, state); + } + + validateGAgents(gAgents, { allowGrouped = false, rejectGrouped = false, } = {}) { - agents.forEach((agent) => { - const state = this.getAgentState(agent); + gAgents.forEach((gAgent) => { + const state = this.getGAgentState(gAgent); if(state.covered) { throw new Error( - 'Agent ' + agent.name + ' is hidden behind group' + 'Agent ' + gAgent.id + ' is hidden behind group' ); } if(rejectGrouped && state.group !== null) { - throw new Error('Agent ' + agent.name + ' is in a group'); + throw new Error('Agent ' + gAgent.id + ' is in a group'); } if(state.blocked && (!allowGrouped || state.group === null)) { - throw new Error('Duplicate agent name: ' + agent.name); + throw new Error('Duplicate agent name: ' + gAgent.id); } - if(agent.name.startsWith('__')) { - throw new Error(agent.name + ' is a reserved name'); + if(gAgent.id.startsWith('__')) { + throw new Error(gAgent.id + ' is a reserved name'); } }); } - setAgentVis(colAgents, visible, mode, checked = false) { + setGAgentVis(gAgents, visible, mode, checked = false) { const seen = new Set(); - const filteredAgents = colAgents.filter((agent) => { - if(seen.has(agent.name)) { + const filteredGAgents = gAgents.filter((gAgent) => { + if(seen.has(gAgent.id)) { return false; } - seen.add(agent.name); - const state = this.getAgentState(agent); + seen.add(gAgent.id); + const state = this.getGAgentState(gAgent); if(state.locked || state.blocked) { if(checked) { - throw new Error( - 'Cannot begin/end agent: ' + agent.name - ); + throw new Error('Cannot begin/end agent: ' + gAgent.id); } else { return false; } } return state.visible !== visible; }); - if(filteredAgents.length === 0) { + if(filteredGAgents.length === 0) { return null; } - filteredAgents.forEach((agent) => { - this.updateAgentState(agent, {visible}); + filteredGAgents.forEach((gAgent) => { + this.updateGAgentState(gAgent, {visible}); }); - this.defineAgents(filteredAgents); + this.defineGAgents(filteredGAgents); return { type: (visible ? 'agent begin' : 'agent end'), - agentNames: filteredAgents.map(Agent.getName), + agentIDs: filteredGAgents.map((gAgent) => gAgent.id), mode, }; } - setAgentHighlight(colAgents, highlighted, checked = false) { - const filteredAgents = colAgents.filter((agent) => { - const state = this.getAgentState(agent); + setGAgentHighlight(gAgents, highlighted, checked = false) { + const filteredGAgents = gAgents.filter((gAgent) => { + const state = this.getGAgentState(gAgent); if(state.locked || state.blocked) { if(checked) { - throw new Error( - 'Cannot highlight agent: ' + agent.name - ); + throw new Error('Cannot highlight agent: ' + gAgent.id); } else { return false; } } return state.visible && (state.highlighted !== highlighted); }); - if(filteredAgents.length === 0) { + if(filteredGAgents.length === 0) { return null; } - filteredAgents.forEach((agent) => { - this.updateAgentState(agent, {highlighted}); + filteredGAgents.forEach((gAgent) => { + this.updateGAgentState(gAgent, {highlighted}); }); return { type: 'agent highlight', - agentNames: filteredAgents.map(Agent.getName), + agentIDs: filteredGAgents.map((gAgent) => gAgent.id), highlighted, }; } - beginNested(mode, label, name, ln) { - const leftAgent = Agent.make(name + '[', {anchorRight: true}); - const rightAgent = Agent.make(name + ']'); - const agents = [leftAgent, rightAgent]; + beginNested(blockType, {tag, label, name, ln}) { + const leftGAgent = GAgent.make(name + '[', {anchorRight: true}); + const rightGAgent = GAgent.make(name + ']'); + const gAgents = [leftGAgent, rightGAgent]; const stages = []; this.currentSection = { header: { type: 'block begin', - mode, - label, - left: leftAgent.name, - right: rightAgent.name, + blockType, + tag: this.textFormatter(tag), + label: this.textFormatter(label), + left: leftGAgent.id, + right: rightGAgent.id, ln, }, stages, }; this.currentNest = { - mode, - agents, - leftAgent, - rightAgent, + blockType, + gAgents, + leftGAgent, + rightGAgent, hasContent: false, sections: [this.currentSection], }; - this.agentStates.set(leftAgent.name, AgentState.LOCKED); - this.agentStates.set(rightAgent.name, AgentState.LOCKED); + this.replaceGAgentState(leftGAgent, AgentState.LOCKED); + this.replaceGAgentState(rightGAgent, AgentState.LOCKED); this.nesting.push(this.currentNest); - return {agents, stages}; + return {gAgents, stages}; } nextBlockName() { @@ -2464,25 +2405,31 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { return name; } - handleBlockBegin({ln, mode, label}) { - this.beginNested(mode, label, this.nextBlockName(), ln); + handleBlockBegin({ln, blockType, tag, label}) { + this.beginNested(blockType, { + tag, + label, + name: this.nextBlockName(), + ln, + }); } - handleBlockSplit({ln, mode, label}) { - if(this.currentNest.mode !== 'if') { + handleBlockSplit({ln, blockType, tag, label}) { + if(this.currentNest.blockType !== 'if') { throw new Error( 'Invalid block nesting ("else" inside ' + - this.currentNest.mode + ')' + this.currentNest.blockType + ')' ); } optimiseStages(this.currentSection.stages); this.currentSection = { header: { type: 'block split', - mode, - label, - left: this.currentNest.leftAgent.name, - right: this.currentNest.rightAgent.name, + blockType, + tag: this.textFormatter(tag), + label: this.textFormatter(label), + left: this.currentNest.leftGAgent.id, + right: this.currentNest.rightGAgent.id, ln, }, stages: [], @@ -2500,12 +2447,12 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { this.currentSection = array.last(this.currentNest.sections); if(nested.hasContent) { - this.defineAgents(nested.agents); + this.defineGAgents(nested.gAgents); addBounds( - this.agents, - nested.leftAgent, - nested.rightAgent, - nested.agents + this.gAgents, + nested.leftGAgent, + nested.rightGAgent, + nested.gAgents ); nested.sections.forEach((section) => { this.currentSection.stages.push(section.header); @@ -2513,70 +2460,71 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { }); this.addStage({ type: 'block end', - left: nested.leftAgent.name, - right: nested.rightAgent.name, + left: nested.leftGAgent.id, + right: nested.rightGAgent.id, }); } else { throw new Error('Empty block'); } } - makeGroupDetails(agents, alias) { - const colAgents = agents.map(this.convertAgent); - this.validateAgents(colAgents, {rejectGrouped: true}); + makeGroupDetails(pAgents, alias) { + const gAgents = pAgents.map(this.toGAgent); + this.validateGAgents(gAgents, {rejectGrouped: true}); if(this.agentStates.has(alias)) { throw new Error('Duplicate agent name: ' + alias); } const name = this.nextBlockName(); - const leftAgent = Agent.make(name + '[', {anchorRight: true}); - const rightAgent = Agent.make(name + ']'); - this.agentStates.set(leftAgent.name, AgentState.LOCKED); - this.agentStates.set(rightAgent.name, AgentState.LOCKED); - this.updateAgentState( - {name: alias}, + const leftGAgent = GAgent.make(name + '[', {anchorRight: true}); + const rightGAgent = GAgent.make(name + ']'); + this.replaceGAgentState(leftGAgent, AgentState.LOCKED); + this.replaceGAgentState(rightGAgent, AgentState.LOCKED); + this.updateGAgentState( + GAgent.make(alias), {blocked: true, group: alias} ); - this.defineAgents(colAgents); + this.defineGAgents(gAgents); const {indexL, indexR} = addBounds( - this.agents, - leftAgent, - rightAgent, - colAgents + this.gAgents, + leftGAgent, + rightGAgent, + gAgents ); - const agentsCovered = []; - const agentsContained = colAgents.slice(); + const gAgentsCovered = []; + const gAgentsContained = gAgents.slice(); for(let i = indexL + 1; i < indexR; ++ i) { - agentsCovered.push(this.agents[i]); + gAgentsCovered.push(this.gAgents[i]); } - array.removeAll(agentsCovered, agentsContained, Agent.equals); + array.removeAll(gAgentsCovered, gAgentsContained, GAgent.equals); return { - colAgents, - leftAgent, - rightAgent, - agentsContained, - agentsCovered, + gAgents, + leftGAgent, + rightGAgent, + gAgentsContained, + gAgentsCovered, }; } - handleGroupBegin({agents, mode, label, alias}) { + handleGroupBegin({agents, blockType, tag, label, alias}) { const details = this.makeGroupDetails(agents, alias); - details.agentsContained.forEach((agent) => { - this.updateAgentState(agent, {group: alias}); + details.gAgentsContained.forEach((gAgent) => { + this.updateGAgentState(gAgent, {group: alias}); }); - details.agentsCovered.forEach((agent) => { - this.updateAgentState(agent, {covered: true}); + details.gAgentsCovered.forEach((gAgent) => { + this.updateGAgentState(gAgent, {covered: true}); }); this.activeGroups.set(alias, details); - this.addStage(this.setAgentVis(details.colAgents, true, 'box')); + this.addStage(this.setGAgentVis(details.gAgents, true, 'box')); this.addStage({ type: 'block begin', - mode, - label, - left: details.leftAgent.name, - right: details.rightAgent.name, + blockType, + tag: this.textFormatter(tag), + label: this.textFormatter(label), + left: details.leftGAgent.id, + right: details.rightGAgent.id, }); } @@ -2587,18 +2535,18 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { } this.activeGroups.delete(name); - details.agentsContained.forEach((agent) => { - this.updateAgentState(agent, {group: null}); + details.gAgentsContained.forEach((gAgent) => { + this.updateGAgentState(gAgent, {group: null}); }); - details.agentsCovered.forEach((agent) => { - this.updateAgentState(agent, {covered: false}); + details.gAgentsCovered.forEach((gAgent) => { + this.updateGAgentState(gAgent, {covered: false}); }); - this.updateAgentState({name}, {group: null}); + this.updateGAgentState(GAgent.make(name), {group: null}); return { type: 'block end', - left: details.leftAgent.name, - right: details.rightAgent.name, + left: details.leftGAgent.id, + right: details.rightGAgent.id, }; } @@ -2644,156 +2592,156 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { return result; } - expandGroupedAgent(agent) { - const group = this.getAgentState(agent).group; + expandGroupedGAgent(gAgent) { + const group = this.getGAgentState(gAgent).group; if(!group) { - return [agent]; + return [gAgent]; } const details = this.activeGroups.get(group); - return [details.leftAgent, details.rightAgent]; + return [details.leftGAgent, details.rightGAgent]; } - expandGroupedAgentConnection(agents) { - const agents1 = this.expandGroupedAgent(agents[0]); - const agents2 = this.expandGroupedAgent(agents[1]); - let ind1 = array.indexOf(this.agents, agents1[0], Agent.equals); - let ind2 = array.indexOf(this.agents, agents2[0], Agent.equals); + expandGroupedGAgentConnection(gAgents) { + const gAgents1 = this.expandGroupedGAgent(gAgents[0]); + const gAgents2 = this.expandGroupedGAgent(gAgents[1]); + let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]); + let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]); if(ind1 === -1) { - ind1 = this.agents.length; + ind1 = this.gAgents.length; } if(ind2 === -1) { - ind2 = this.agents.length; + ind2 = this.gAgents.length; } if(ind1 === ind2) { // Self-connection - return [array.last(agents1), array.last(agents2)]; + return [array.last(gAgents1), array.last(gAgents2)]; } else if(ind1 < ind2) { - return [array.last(agents1), agents2[0]]; + return [array.last(gAgents1), gAgents2[0]]; } else { - return [agents1[0], array.last(agents2)]; + return [gAgents1[0], array.last(gAgents2)]; } } - filterConnectFlags(agents) { - const beginAgents = (agents - .filter(Agent.hasFlag('begin')) - .map(this.convertAgent) + filterConnectFlags(pAgents) { + const beginGAgents = (pAgents + .filter(PAgent.hasFlag('begin')) + .map(this.toGAgent) ); - const endAgents = (agents - .filter(Agent.hasFlag('end')) - .map(this.convertAgent) + const endGAgents = (pAgents + .filter(PAgent.hasFlag('end')) + .map(this.toGAgent) ); - if(array.hasIntersection(beginAgents, endAgents, Agent.equals)) { + if(GAgent.hasIntersection(beginGAgents, endGAgents)) { throw new Error('Cannot set agent visibility multiple times'); } - const startAgents = (agents - .filter(Agent.hasFlag('start')) - .map(this.convertAgent) + const startGAgents = (pAgents + .filter(PAgent.hasFlag('start')) + .map(this.toGAgent) ); - const stopAgents = (agents - .filter(Agent.hasFlag('stop')) - .map(this.convertAgent) + const stopGAgents = (pAgents + .filter(PAgent.hasFlag('stop')) + .map(this.toGAgent) ); - array.mergeSets(stopAgents, endAgents); - if(array.hasIntersection(startAgents, stopAgents, Agent.equals)) { + array.mergeSets(stopGAgents, endGAgents); + if(GAgent.hasIntersection(startGAgents, stopGAgents)) { throw new Error('Cannot set agent highlighting multiple times'); } - this.validateAgents(beginAgents); - this.validateAgents(endAgents); - this.validateAgents(startAgents); - this.validateAgents(stopAgents); + this.validateGAgents(beginGAgents); + this.validateGAgents(endGAgents); + this.validateGAgents(startGAgents); + this.validateGAgents(stopGAgents); - return {beginAgents, endAgents, startAgents, stopAgents}; + return {beginGAgents, endGAgents, startGAgents, stopGAgents}; } handleConnect({agents, label, options}) { const flags = this.filterConnectFlags(agents); - let colAgents = agents.map(this.convertAgent); - this.validateAgents(colAgents, {allowGrouped: true}); + let gAgents = agents.map(this.toGAgent); + this.validateGAgents(gAgents, {allowGrouped: true}); - const allAgents = array.flatMap(colAgents, this.expandGroupedAgent); - this.defineAgents(allAgents); + const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent); + this.defineGAgents(allGAgents); - colAgents = this.expandGroupedAgentConnection(colAgents); - const agentNames = colAgents.map(Agent.getName); + gAgents = this.expandGroupedGAgentConnection(gAgents); + const agentIDs = gAgents.map((gAgent) => gAgent.id); - const implicitBegin = (agents - .filter(Agent.hasFlag('begin', false)) - .map(this.convertAgent) + const implicitBeginGAgents = (agents + .filter(PAgent.hasFlag('begin', false)) + .map(this.toGAgent) ); - this.addStage(this.setAgentVis(implicitBegin, true, 'box')); + this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box')); const connectStage = { type: 'connect', - agentNames, - label: this.applyLabelPattern(label), + agentIDs, + label: this.textFormatter(this.applyLabelPattern(label)), options, }; this.addParallelStages([ - this.setAgentVis(flags.beginAgents, true, 'box', true), - this.setAgentHighlight(flags.startAgents, true, true), + this.setGAgentVis(flags.beginGAgents, true, 'box', true), + this.setGAgentHighlight(flags.startGAgents, true, true), connectStage, - this.setAgentHighlight(flags.stopAgents, false, true), - this.setAgentVis(flags.endAgents, false, 'cross', true), + this.setGAgentHighlight(flags.stopGAgents, false, true), + this.setGAgentVis(flags.endGAgents, false, 'cross', true), ]); } handleNote({type, agents, mode, label}) { - let colAgents = null; + let gAgents = null; if(agents.length === 0) { - colAgents = NOTE_DEFAULT_AGENTS[type] || []; + gAgents = NOTE_DEFAULT_G_AGENTS[type] || []; } else { - colAgents = agents.map(this.convertAgent); + gAgents = agents.map(this.toGAgent); } - this.validateAgents(colAgents, {allowGrouped: true}); - colAgents = array.flatMap(colAgents, this.expandGroupedAgent); - const agentNames = colAgents.map(Agent.getName); - const uniqueAgents = new Set(agentNames).size; + this.validateGAgents(gAgents, {allowGrouped: true}); + gAgents = array.flatMap(gAgents, this.expandGroupedGAgent); + const agentIDs = gAgents.map((gAgent) => gAgent.id); + const uniqueAgents = new Set(agentIDs).size; if(type === 'note between' && uniqueAgents < 2) { throw new Error('note between requires at least 2 agents'); } - this.addStage(this.setAgentVis(colAgents, true, 'box')); - this.defineAgents(colAgents); + this.addStage(this.setGAgentVis(gAgents, true, 'box')); + this.defineGAgents(gAgents); this.addStage({ type, - agentNames, + agentIDs, mode, - label, + label: this.textFormatter(label), }); } handleAgentDefine({agents}) { - const colAgents = agents.map(this.convertAgent); - this.validateAgents(colAgents); - this.defineAgents(colAgents); + const gAgents = agents.map(this.toGAgent); + this.validateGAgents(gAgents); + this.defineGAgents(gAgents); } handleAgentBegin({agents, mode}) { - const colAgents = agents.map(this.convertAgent); - this.validateAgents(colAgents); - this.addStage(this.setAgentVis(colAgents, true, mode, true)); + const gAgents = agents.map(this.toGAgent); + this.validateGAgents(gAgents); + this.addStage(this.setGAgentVis(gAgents, true, mode, true)); } handleAgentEnd({agents, mode}) { - const groupAgents = (agents - .filter((agent) => this.activeGroups.has(agent.name)) + const groupPAgents = (agents + .filter((pAgent) => this.activeGroups.has(pAgent.name)) ); - const colAgents = (agents - .filter((agent) => !this.activeGroups.has(agent.name)) - .map(this.convertAgent) + const gAgents = (agents + .filter((pAgent) => !this.activeGroups.has(pAgent.name)) + .map(this.toGAgent) ); - this.validateAgents(colAgents); + this.validateGAgents(gAgents); this.addParallelStages([ - this.setAgentHighlight(colAgents, false), - this.setAgentVis(colAgents, false, mode, true), - ...groupAgents.map(this.endGroup), + this.setGAgentHighlight(gAgents, false), + this.setGAgentVis(gAgents, false, mode, true), + ...groupPAgents.map(this.endGroup), ]); } @@ -2807,21 +2755,46 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { handler(stage); } catch(e) { if(typeof e === 'object' && e.message) { - throw new Error(e.message + ' at line ' + (stage.ln + 1)); + e.message += ' at line ' + (stage.ln + 1); + throw e; } } } - generate({stages, meta = {}}) { + _reset() { this.agentStates.clear(); this.markers.clear(); this.agentAliases.clear(); this.activeGroups.clear(); - this.agents.length = 0; + this.gAgents.length = 0; this.blockCount = 0; this.nesting.length = 0; this.labelPattern = [{token: 'label'}]; - const globals = this.beginNested('global', '', '', 0); + } + + _finalise(globals) { + addBounds( + this.gAgents, + this.currentNest.leftGAgent, + this.currentNest.rightGAgent + ); + optimiseStages(globals.stages); + + this.gAgents.forEach((gAgent) => { + gAgent.formattedLabel = this.textFormatter(gAgent.id); + }); + } + + generate({stages, meta = {}}) { + this._reset(); + + this.textFormatter = meta.textFormatter; + const globals = this.beginNested('global', { + tag: '', + label: '', + name: '', + ln: 0, + }); stages.forEach(this.handleStage); @@ -2836,26 +2809,21 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { } const terminators = meta.terminators || 'none'; - this.addParallelStages([ - this.setAgentHighlight(this.agents, false), - this.setAgentVis(this.agents, false, terminators), + this.setGAgentHighlight(this.gAgents, false), + this.setGAgentVis(this.gAgents, false, terminators), ]); - addBounds( - this.agents, - this.currentNest.leftAgent, - this.currentNest.rightAgent - ); - optimiseStages(globals.stages); + this._finalise(globals); + swapFirstBegin(globals.stages, meta.headers || 'box'); return { meta: { - title: meta.title, + title: this.textFormatter(meta.title), theme: meta.theme, }, - agents: this.agents.slice(), + agents: this.gAgents.slice(), stages: globals.stages, }; } @@ -2927,84 +2895,107 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { } } + function populateSvgTextLine(node, formattedLine) { + if(!Array.isArray(formattedLine)) { + throw new Error('Invalid formatted text line: ' + formattedLine); + } + formattedLine.forEach(({text, attrs}) => { + const textNode = svg.makeText(text); + if(attrs) { + const span = svg.make('tspan', attrs); + span.appendChild(textNode); + node.appendChild(span); + } else { + node.appendChild(textNode); + } + }); + } + + const EMPTY = []; + class SVGTextBlock { constructor(container, initialState = {}) { this.container = container; this.state = { attrs: {}, - text: '', + formatted: EMPTY, x: 0, y: 0, }; this.width = 0; this.height = 0; - this.nodes = []; + this.lines = []; this.set(initialState); } - _rebuildNodes(count) { - if(count > this.nodes.length) { + _rebuildLines(count) { + if(count > this.lines.length) { const attrs = Object.assign({ 'x': this.state.x, }, this.state.attrs); - while(this.nodes.length < count) { - const element = svg.make('text', attrs); - const text = svg.makeText(); - element.appendChild(text); - this.container.appendChild(element); - this.nodes.push({element, text}); + while(this.lines.length < count) { + const node = svg.make('text', attrs); + this.container.appendChild(node); + this.lines.push({node, latest: ''}); } } else { - while(this.nodes.length > count) { - const {element} = this.nodes.pop(); - this.container.removeChild(element); + while(this.lines.length > count) { + const {node} = this.lines.pop(); + this.container.removeChild(node); } } } _reset() { - this._rebuildNodes(0); + this._rebuildLines(0); this.width = 0; this.height = 0; } _renderText() { - if(!this.state.text) { + const {formatted} = this.state; + + if(!formatted || !formatted.length) { this._reset(); return; } + if(!Array.isArray(formatted)) { + throw new Error('Invalid formatted text: ' + formatted); + } - const lines = this.state.text.split('\n'); - this._rebuildNodes(lines.length); + this._rebuildLines(formatted.length); let maxWidth = 0; - this.nodes.forEach(({text, element}, i) => { - if(text.nodeValue !== lines[i]) { - text.nodeValue = lines[i]; + this.lines.forEach((ln, i) => { + const id = JSON.stringify(formatted[i]); + if(id !== ln.latest) { + svg.empty(ln.node); + populateSvgTextLine(ln.node, formatted[i]); + ln.latest = id; } - maxWidth = Math.max(maxWidth, element.getComputedTextLength()); + maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength()); }); this.width = maxWidth; } _updateX() { - this.nodes.forEach(({element}) => { - element.setAttribute('x', this.state.x); + this.lines.forEach(({node}) => { + node.setAttribute('x', this.state.x); }); } _updateY() { const {size, lineHeight} = fontDetails(this.state.attrs); - this.nodes.forEach(({element}, i) => { - element.setAttribute('y', this.state.y + i * lineHeight + size); + this.lines.forEach(({node}, i) => { + node.setAttribute('y', this.state.y + i * lineHeight + size); }); - this.height = lineHeight * this.nodes.length; + this.height = lineHeight * this.lines.length; } firstLine() { - if(this.nodes.length > 0) { - return this.nodes[0].element; + if(this.lines.length > 0) { + return this.lines[0].node; } else { return null; } @@ -3016,12 +3007,12 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { if(this.state.attrs !== oldState.attrs) { this._reset(); - oldState.text = ''; + oldState.formatted = EMPTY; } - const oldNodes = this.nodes.length; + const oldLines = this.lines.length; - if(this.state.text !== oldState.text) { + if(this.state.formatted !== oldState.formatted) { this._renderText(); } @@ -3029,7 +3020,7 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this._updateX(); } - if(this.state.y !== oldState.y || this.nodes.length !== oldNodes) { + if(this.state.y !== oldState.y || this.lines.length !== oldLines) { this._updateY(); } } @@ -3046,18 +3037,18 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this.cache = new Map(); } - measure(attrs, content) { - if(!content) { + measure(attrs, formatted) { + if(!formatted || !formatted.length) { return {width: 0, height: 0}; } + if(!Array.isArray(formatted)) { + throw new Error('Invalid formatted text: ' + formatted); + } let tester = this.cache.get(attrs); if(!tester) { - const text = svg.makeText(); - const node = svg.make('text', attrs); - node.appendChild(text); - this.testers.appendChild(node); - tester = {text, node}; + tester = svg.make('text', attrs); + this.testers.appendChild(tester); this.cache.set(attrs, tester); } @@ -3065,26 +3056,28 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this.container.appendChild(this.testers); } - const lines = content.split('\n'); let width = 0; - lines.forEach((line) => { - tester.text.nodeValue = line; - width = Math.max(width, tester.node.getComputedTextLength()); + formatted.forEach((line) => { + svg.empty(tester); + populateSvgTextLine(tester, line); + width = Math.max(width, tester.getComputedTextLength()); }); return { width, - height: lines.length * fontDetails(attrs).lineHeight, + height: formatted.length * fontDetails(attrs).lineHeight, }; } - measureHeight(attrs, content) { - if(!content) { + measureHeight(attrs, formatted) { + if(!formatted) { return 0; } + if(!Array.isArray(formatted)) { + throw new Error('Invalid formatted text: ' + formatted); + } - const lines = content.split('\n'); - return lines.length * fontDetails(attrs).lineHeight; + return formatted.length * fontDetails(attrs).lineHeight; } resetCache() { @@ -3267,24 +3260,10 @@ define('svg/SVGShapes',[ return g; } - function renderBoxedText(text, { - x, - y, - padding, - boxAttrs, - labelAttrs, - boxLayer, - labelLayer, - boxRenderer = null, - SVGTextBlockClass = SVGTextBlock, - }) { - if(!text) { - return {width: 0, height: 0, label: null, box: null}; - } - + function calculateAnchor(x, attrs, padding) { let shift = 0; let anchorX = x; - switch(labelAttrs['text-anchor']) { + switch(attrs['text-anchor']) { case 'middle': shift = 0.5; anchorX += (padding.left - padding.right) / 2; @@ -3298,10 +3277,29 @@ define('svg/SVGShapes',[ anchorX += padding.left; break; } + return {shift, anchorX}; + } + + function renderBoxedText(formatted, { + x, + y, + padding, + boxAttrs, + labelAttrs, + boxLayer, + labelLayer, + boxRenderer = null, + SVGTextBlockClass = SVGTextBlock, + }) { + if(!formatted || !formatted.length) { + return {width: 0, height: 0, label: null, box: null}; + } + + const {shift, anchorX} = calculateAnchor(x, labelAttrs, padding); const label = new SVGTextBlockClass(labelLayer, { attrs: labelAttrs, - text, + formatted, x: anchorX, y: y + padding.top, }); @@ -3359,7 +3357,7 @@ define('sequence/components/BaseComponent',[],() => { separationPre(/*stage, { theme, agentInfos, - visibleAgents, + visibleAgentIDs, textSizer, addSpacing, addSeparation, @@ -3371,7 +3369,7 @@ define('sequence/components/BaseComponent',[],() => { separation(/*stage, { theme, agentInfos, - visibleAgents, + visibleAgentIDs, textSizer, addSpacing, addSeparation, @@ -3387,7 +3385,7 @@ define('sequence/components/BaseComponent',[],() => { state, components, }*/) { - // return {topShift, agentNames, asynchronousY} + // return {topShift, agentIDs, asynchronousY} } render(/*stage, { @@ -3411,12 +3409,12 @@ define('sequence/components/BaseComponent',[],() => { BaseComponent.cleanRenderPreResult = ({ topShift = 0, - agentNames = [], + agentIDs = [], asynchronousY = null, } = {}, currentY = null) => { return { topShift, - agentNames, + agentIDs, asynchronousY: (asynchronousY !== null) ? asynchronousY : currentY, }; }; @@ -3448,13 +3446,13 @@ define('sequence/components/Block',[ 'use strict'; class BlockSplit extends BaseComponent { - separation({left, right, mode, label}, env) { + separation({left, right, tag, label}, env) { const blockInfo = env.state.blocks.get(left); - const config = env.theme.getBlock(blockInfo.mode).section; + const config = env.theme.getBlock(blockInfo.type).section; const width = ( - env.textSizer.measure(config.mode.labelAttrs, mode).width + - config.mode.padding.left + - config.mode.padding.right + + env.textSizer.measure(config.tag.labelAttrs, tag).width + + config.tag.padding.left + + config.tag.padding.right + env.textSizer.measure(config.label.labelAttrs, label).width + config.label.padding.left + config.label.padding.right @@ -3464,13 +3462,13 @@ define('sequence/components/Block',[ renderPre({left, right}) { return { - agentNames: [left, right], + agentIDs: [left, right], }; } - render({left, right, mode, label}, env, first = false) { + render({left, right, tag, label}, env, first = false) { const blockInfo = env.state.blocks.get(left); - const config = env.theme.getBlock(blockInfo.mode); + const config = env.theme.getBlock(blockInfo.type); const agentInfoL = env.agentInfos.get(left); const agentInfoR = env.agentInfos.get(right); @@ -3482,20 +3480,20 @@ define('sequence/components/Block',[ const clickable = env.makeRegion(); - const modeRender = SVGShapes.renderBoxedText(mode, { + const tagRender = SVGShapes.renderBoxedText(tag, { x: agentInfoL.x, y, - padding: config.section.mode.padding, - boxAttrs: config.section.mode.boxAttrs, - boxRenderer: config.section.mode.boxRenderer, - labelAttrs: config.section.mode.labelAttrs, + padding: config.section.tag.padding, + boxAttrs: config.section.tag.boxAttrs, + boxRenderer: config.section.tag.boxRenderer, + labelAttrs: config.section.tag.labelAttrs, boxLayer: blockInfo.hold, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, }); const labelRender = SVGShapes.renderBoxedText(label, { - x: agentInfoL.x + modeRender.width, + x: agentInfoL.x + tagRender.width, y, padding: config.section.label.padding, boxAttrs: {'fill': '#000000'}, @@ -3505,7 +3503,7 @@ define('sequence/components/Block',[ SVGTextBlockClass: env.SVGTextBlockClass, }); - const labelHeight = Math.max(modeRender.height, labelRender.height); + const labelHeight = Math.max(tagRender.height, labelRender.height); clickable.insertBefore(svg.make('rect', { 'x': agentInfoL.x, @@ -3538,11 +3536,13 @@ define('sequence/components/Block',[ } storeBlockInfo(stage, env) { - env.state.blocks.set(stage.left, { - mode: stage.mode, + const blockInfo = { + type: stage.blockType, hold: null, startY: null, - }); + }; + env.state.blocks.set(stage.left, blockInfo); + return blockInfo; } separationPre(stage, env) { @@ -3550,17 +3550,17 @@ define('sequence/components/Block',[ } separation(stage, env) { - array.mergeSets(env.visibleAgents, [stage.left, stage.right]); + array.mergeSets(env.visibleAgentIDs, [stage.left, stage.right]); super.separation(stage, env); } renderPre(stage, env) { - this.storeBlockInfo(stage, env); + const blockInfo = this.storeBlockInfo(stage, env); - const config = env.theme.getBlock(stage.mode); + const config = env.theme.getBlock(blockInfo.type); return { - agentNames: [stage.left, stage.right], + agentIDs: [stage.left, stage.right], topShift: config.margin.top, }; } @@ -3579,22 +3579,22 @@ define('sequence/components/Block',[ class BlockEnd extends BaseComponent { separation({left, right}, env) { - array.removeAll(env.visibleAgents, [left, right]); + array.removeAll(env.visibleAgentIDs, [left, right]); } renderPre({left, right}, env) { const blockInfo = env.state.blocks.get(left); - const config = env.theme.getBlock(blockInfo.mode); + const config = env.theme.getBlock(blockInfo.type); return { - agentNames: [left, right], + agentIDs: [left, right], topShift: config.section.padding.bottom, }; } render({left, right}, env) { const blockInfo = env.state.blocks.get(left); - const config = env.theme.getBlock(blockInfo.mode); + const config = env.theme.getBlock(blockInfo.type); const agentInfoL = env.agentInfos.get(left); const agentInfoR = env.agentInfos.get(right); @@ -3640,10 +3640,10 @@ define('sequence/components/Parallel',[ } function mergeResults(a, b) { - array.mergeSets(a.agentNames, b.agentNames); + array.mergeSets(a.agentIDs, b.agentIDs); return { topShift: Math.max(a.topShift, b.topShift), - agentNames: a.agentNames, + agentIDs: a.agentIDs, asynchronousY: nullableMax(a.asynchronousY, b.asynchronousY), }; } @@ -3664,7 +3664,7 @@ define('sequence/components/Parallel',[ renderPre(stage, env) { const baseResults = { topShift: 0, - agentNames: [], + agentIDs: [], asynchronousY: null, }; @@ -3749,10 +3749,10 @@ define('sequence/components/AgentCap',[ 'use strict'; class CapBox { - separation({label}, env) { + separation({formattedLabel}, env) { const config = env.theme.agentCap.box; const width = ( - env.textSizer.measure(config.labelAttrs, label).width + + env.textSizer.measure(config.labelAttrs, formattedLabel).width + config.padding.left + config.padding.right ); @@ -3764,20 +3764,20 @@ define('sequence/components/AgentCap',[ }; } - topShift({label}, env) { + topShift({formattedLabel}, env) { const config = env.theme.agentCap.box; const height = ( - env.textSizer.measureHeight(config.labelAttrs, label) + + env.textSizer.measureHeight(config.labelAttrs, formattedLabel) + config.padding.top + config.padding.bottom ); return Math.max(0, height - config.arrowBottom); } - render(y, {x, label}, env) { + render(y, {x, formattedLabel}, env) { const config = env.theme.agentCap.box; const clickable = env.makeRegion(); - const {width, height} = SVGShapes.renderBoxedText(label, { + const {width, height} = SVGShapes.renderBoxedText(formattedLabel, { x, y, padding: config.padding, @@ -3842,10 +3842,10 @@ define('sequence/components/AgentCap',[ } class CapBar { - separation({label}, env) { + separation({formattedLabel}, env) { const config = env.theme.agentCap.box; const width = ( - env.textSizer.measure(config.labelAttrs, label).width + + env.textSizer.measure(config.labelAttrs, formattedLabel).width + config.padding.left + config.padding.right ); @@ -3862,17 +3862,17 @@ define('sequence/components/AgentCap',[ return config.height / 2; } - render(y, {x, label}, env) { - const configB = env.theme.agentCap.box; - const config = env.theme.agentCap.bar; + render(y, {x, formattedLabel}, env) { + const boxCfg = env.theme.agentCap.box; + const barCfg = env.theme.agentCap.bar; const width = ( - env.textSizer.measure(configB.labelAttrs, label).width + - configB.padding.left + - configB.padding.right + env.textSizer.measure(boxCfg.labelAttrs, formattedLabel).width + + boxCfg.padding.left + + boxCfg.padding.right ); - const height = config.height; + const height = barCfg.height; - env.shapeLayer.appendChild(config.render({ + env.shapeLayer.appendChild(barCfg.render({ x: x - width / 2, y, width, @@ -3909,7 +3909,7 @@ define('sequence/components/AgentCap',[ return isBegin ? config.height : 0; } - render(y, {x, label}, env, isBegin) { + render(y, {x}, env, isBegin) { const config = env.theme.agentCap.fade; const ratio = config.height / (config.height + config.extend); @@ -4003,12 +4003,12 @@ define('sequence/components/AgentCap',[ this.begin = begin; } - separationPre({mode, agentNames}, env) { - agentNames.forEach((name) => { - const agentInfo = env.agentInfos.get(name); + separationPre({mode, agentIDs}, env) { + agentIDs.forEach((id) => { + const agentInfo = env.agentInfos.get(id); const cap = AGENT_CAPS[mode]; const sep = cap.separation(agentInfo, env, this.begin); - env.addSpacing(name, sep); + env.addSpacing(id, sep); agentInfo.currentMaxRad = Math.max( agentInfo.currentMaxRad, sep.radius @@ -4016,18 +4016,18 @@ define('sequence/components/AgentCap',[ }); } - separation({mode, agentNames}, env) { + separation({mode, agentIDs}, env) { if(this.begin) { - array.mergeSets(env.visibleAgents, agentNames); + array.mergeSets(env.visibleAgentIDs, agentIDs); } else { - array.removeAll(env.visibleAgents, agentNames); + array.removeAll(env.visibleAgentIDs, agentIDs); } } - renderPre({mode, agentNames}, env) { + renderPre({mode, agentIDs}, env) { let maxTopShift = 0; - agentNames.forEach((name) => { - const agentInfo = env.agentInfos.get(name); + agentIDs.forEach((id) => { + const agentInfo = env.agentInfos.get(id); const cap = AGENT_CAPS[mode]; const topShift = cap.topShift(agentInfo, env, this.begin); maxTopShift = Math.max(maxTopShift, topShift); @@ -4036,15 +4036,15 @@ define('sequence/components/AgentCap',[ agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); }); return { - agentNames, + agentIDs, topShift: maxTopShift, }; } - render({mode, agentNames}, env) { + render({mode, agentIDs}, env) { let maxEnd = 0; - agentNames.forEach((name) => { - const agentInfo = env.agentInfos.get(name); + agentIDs.forEach((id) => { + const agentInfo = env.agentInfos.get(id); const cap = AGENT_CAPS[mode]; const topShift = cap.topShift(agentInfo, env, this.begin); const y0 = env.primaryY - topShift; @@ -4056,9 +4056,9 @@ define('sequence/components/AgentCap',[ ); maxEnd = Math.max(maxEnd, y0 + shifts.height); if(this.begin) { - env.drawAgentLine(name, y0 + shifts.lineBottom); + env.drawAgentLine(id, y0 + shifts.lineBottom); } else { - env.drawAgentLine(name, y0 + shifts.lineTop, true); + env.drawAgentLine(id, y0 + shifts.lineTop, true); } }); return maxEnd + env.theme.actionMargin; @@ -4079,28 +4079,28 @@ define('sequence/components/AgentHighlight',['./BaseComponent'], (BaseComponent) return highlighted ? env.theme.agentLineHighlightRadius : 0; } - separationPre({agentNames, highlighted}, env) { + separationPre({agentIDs, highlighted}, env) { const r = this.radius(highlighted, env); - agentNames.forEach((name) => { - const agentInfo = env.agentInfos.get(name); + agentIDs.forEach((id) => { + const agentInfo = env.agentInfos.get(id); agentInfo.currentRad = r; agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); }); } - renderPre({agentNames, highlighted}, env) { + renderPre({agentIDs, highlighted}, env) { const r = this.radius(highlighted, env); - agentNames.forEach((name) => { - const agentInfo = env.agentInfos.get(name); + agentIDs.forEach((id) => { + const agentInfo = env.agentInfos.get(id); agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); }); } - render({agentNames, highlighted}, env) { + render({agentIDs, highlighted}, env) { const r = this.radius(highlighted, env); - agentNames.forEach((name) => { - env.drawAgentLine(name, env.primaryY); - env.agentInfos.get(name).currentRad = r; + agentIDs.forEach((id) => { + env.drawAgentLine(id, env.primaryY); + env.agentInfos.get(id).currentRad = r; }); return env.primaryY + env.theme.actionMargin; } @@ -4190,7 +4190,7 @@ define('sequence/components/Connect',[ ]; class Connect extends BaseComponent { - separation({label, agentNames, options}, env) { + separation({label, agentIDs, options}, env) { const config = env.theme.connect; const lArrow = ARROWHEADS[options.left]; @@ -4203,9 +4203,9 @@ define('sequence/components/Connect',[ labelWidth += config.label.padding * 2; } - const info1 = env.agentInfos.get(agentNames[0]); - if(agentNames[0] === agentNames[1]) { - env.addSpacing(agentNames[0], { + const info1 = env.agentInfos.get(agentIDs[0]); + if(agentIDs[0] === agentIDs[1]) { + env.addSpacing(agentIDs[0], { left: 0, right: ( info1.currentMaxRad + @@ -4217,10 +4217,10 @@ define('sequence/components/Connect',[ ), }); } else { - const info2 = env.agentInfos.get(agentNames[1]); + const info2 = env.agentInfos.get(agentIDs[1]); env.addSeparation( - agentNames[0], - agentNames[1], + agentIDs[0], + agentIDs[1], info1.currentMaxRad + info2.currentMaxRad + @@ -4233,10 +4233,10 @@ define('sequence/components/Connect',[ } } - renderSelfConnect({label, agentNames, options}, env) { + renderSelfConnect({label, agentIDs, options}, env) { /* jshint -W071 */ // TODO: find appropriate abstractions const config = env.theme.connect; - const from = env.agentInfos.get(agentNames[0]); + const from = env.agentInfos.get(agentIDs[0]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; @@ -4308,10 +4308,10 @@ define('sequence/components/Connect',[ ); } - renderSimpleConnect({label, agentNames, options}, env) { + renderSimpleConnect({label, agentIDs, options}, env) { const config = env.theme.connect; - const from = env.agentInfos.get(agentNames[0]); - const to = env.agentInfos.get(agentNames[1]); + const from = env.agentInfos.get(agentIDs[0]); + const to = env.agentInfos.get(agentIDs[1]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; @@ -4373,7 +4373,7 @@ define('sequence/components/Connect',[ ); } - renderPre({label, agentNames, options}, env) { + renderPre({label, agentIDs, options}, env) { const config = env.theme.connect; const lArrow = ARROWHEADS[options.left]; @@ -4386,18 +4386,18 @@ define('sequence/components/Connect',[ ); let arrowH = lArrow.height(env.theme); - if(agentNames[0] !== agentNames[1]) { + if(agentIDs[0] !== agentIDs[1]) { arrowH = Math.max(arrowH, rArrow.height(env.theme)); } return { - agentNames, + agentIDs, topShift: Math.max(arrowH / 2, height), }; } render(stage, env) { - if(stage.agentNames[0] === stage.agentNames[1]) { + if(stage.agentIDs[0] === stage.agentIDs[1]) { return this.renderSelfConnect(stage, env); } else { return this.renderSimpleConnect(stage, env); @@ -4413,11 +4413,11 @@ define('sequence/components/Connect',[ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => { 'use strict'; - function findExtremes(agentInfos, agentNames) { + function findExtremes(agentInfos, agentIDs) { let min = null; let max = null; - agentNames.forEach((name) => { - const info = agentInfos.get(name); + agentIDs.forEach((id) => { + const info = agentInfos.get(id); if(min === null || info.index < min.index) { min = info; } @@ -4426,14 +4426,14 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base } }); return { - left: min.label, - right: max.label, + left: min.id, + right: max.id, }; } class NoteComponent extends BaseComponent { - renderPre({agentNames}) { - return {agentNames}; + renderPre({agentIDs}) { + return {agentIDs}; } renderNote({ @@ -4451,7 +4451,7 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base const y = env.topY + config.margin.top + config.padding.top; const labelNode = new env.SVGTextBlockClass(clickable, { attrs: config.labelAttrs, - text: label, + formatted: label, y, }); @@ -4517,7 +4517,7 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base } class NoteOver extends NoteComponent { - separation({agentNames, mode, label}, env) { + separation({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); const width = ( env.textSizer.measure(config.labelAttrs, label).width + @@ -4525,7 +4525,7 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base config.padding.right ); - const {left, right} = findExtremes(env.agentInfos, agentNames); + const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); if(infoL !== infoR) { @@ -4544,10 +4544,10 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base } } - render({agentNames, mode, label}, env) { + render({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); - const {left, right} = findExtremes(env.agentInfos, agentNames); + const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); if(infoL !== infoR) { @@ -4576,9 +4576,9 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base this.isRight = isRight; } - separation({agentNames, mode, label}, env) { + separation({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); - const {left, right} = findExtremes(env.agentInfos, agentNames); + const {left, right} = findExtremes(env.agentInfos, agentIDs); const width = ( env.textSizer.measure(config.labelAttrs, label).width + config.padding.left + @@ -4602,9 +4602,9 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base } } - render({agentNames, mode, label}, env) { + render({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); - const {left, right} = findExtremes(env.agentInfos, agentNames); + const {left, right} = findExtremes(env.agentInfos, agentIDs); if(this.isRight) { const info = env.agentInfos.get(right); const x0 = info.x + info.currentMaxRad + config.margin.left; @@ -4628,9 +4628,9 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base } class NoteBetween extends NoteComponent { - separation({agentNames, mode, label}, env) { + separation({agentIDs, mode, label}, env) { const config = env.theme.getNote(mode); - const {left, right} = findExtremes(env.agentInfos, agentNames); + const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); @@ -4648,8 +4648,8 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base ); } - render({agentNames, mode, label}, env) { - const {left, right} = findExtremes(env.agentInfos, agentNames); + render({agentIDs, mode, label}, env) { + const {left, right} = findExtremes(env.agentInfos, agentIDs); const infoL = env.agentInfos.get(left); const infoR = env.agentInfos.get(right); const xMid = ( @@ -4702,11 +4702,11 @@ define('sequence/Renderer',[ /* jshint +W072 */ 'use strict'; - function findExtremes(agentInfos, agentNames) { + function findExtremes(agentInfos, agentIDs) { let min = null; let max = null; - agentNames.forEach((name) => { - const info = agentInfos.get(name); + agentIDs.forEach((id) => { + const info = agentInfos.get(id); if(min === null || info.index < min.index) { min = info; } @@ -4715,8 +4715,8 @@ define('sequence/Renderer',[ } }); return { - left: min.label, - right: max.label, + left: min.id, + right: max.id, }; } @@ -4838,23 +4838,23 @@ define('sequence/Renderer',[ return namespacedName; } - addSeparation(agentName1, agentName2, dist) { - const info1 = this.agentInfos.get(agentName1); - const info2 = this.agentInfos.get(agentName2); + addSeparation(agentID1, agentID2, dist) { + const info1 = this.agentInfos.get(agentID1); + const info2 = this.agentInfos.get(agentID2); - const d1 = info1.separations.get(agentName2) || 0; - info1.separations.set(agentName2, Math.max(d1, dist)); + const d1 = info1.separations.get(agentID2) || 0; + info1.separations.set(agentID2, Math.max(d1, dist)); - const d2 = info2.separations.get(agentName1) || 0; - info2.separations.set(agentName1, Math.max(d2, dist)); + const d2 = info2.separations.get(agentID1) || 0; + info2.separations.set(agentID1, Math.max(d2, dist)); } separationStage(stage) { const agentSpaces = new Map(); - const agentNames = this.visibleAgents.slice(); + const agentIDs = this.visibleAgentIDs.slice(); - const addSpacing = (agentName, {left, right}) => { - const current = agentSpaces.get(agentName); + const addSpacing = (agentID, {left, right}) => { + const current = agentSpaces.get(agentID); current.left = Math.max(current.left, left); current.right = Math.max(current.right, right); }; @@ -4862,12 +4862,12 @@ define('sequence/Renderer',[ this.agentInfos.forEach((agentInfo) => { const rad = agentInfo.currentRad; agentInfo.currentMaxRad = rad; - agentSpaces.set(agentInfo.label, {left: rad, right: rad}); + agentSpaces.set(agentInfo.id, {left: rad, right: rad}); }); const env = { theme: this.theme, agentInfos: this.agentInfos, - visibleAgents: this.visibleAgents, + visibleAgentIDs: this.visibleAgentIDs, textSizer: this.sizer, addSpacing, addSeparation: this.addSeparation, @@ -4880,33 +4880,33 @@ define('sequence/Renderer',[ } component.separationPre(stage, env); component.separation(stage, env); - array.mergeSets(agentNames, this.visibleAgents); + array.mergeSets(agentIDs, this.visibleAgentIDs); - agentNames.forEach((agentNameR) => { - const infoR = this.agentInfos.get(agentNameR); - const sepR = agentSpaces.get(agentNameR); + agentIDs.forEach((agentIDR) => { + const infoR = this.agentInfos.get(agentIDR); + const sepR = agentSpaces.get(agentIDR); infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right); infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left); - agentNames.forEach((agentNameL) => { - const infoL = this.agentInfos.get(agentNameL); + agentIDs.forEach((agentIDL) => { + const infoL = this.agentInfos.get(agentIDL); if(infoL.index >= infoR.index) { return; } - const sepL = agentSpaces.get(agentNameL); + const sepL = agentSpaces.get(agentIDL); this.addSeparation( - agentNameR, - agentNameL, + agentIDR, + agentIDL, sepR.left + sepL.right + this.theme.agentMargin ); }); }); } - checkAgentRange(agentNames, topY = 0) { - if(agentNames.length === 0) { + checkAgentRange(agentIDs, topY = 0) { + if(agentIDs.length === 0) { return topY; } - const {left, right} = findExtremes(this.agentInfos, agentNames); + const {left, right} = findExtremes(this.agentInfos, agentIDs); const leftX = this.agentInfos.get(left).x; const rightX = this.agentInfos.get(right).x; let baseY = topY; @@ -4918,11 +4918,11 @@ define('sequence/Renderer',[ return baseY; } - markAgentRange(agentNames, y) { - if(agentNames.length === 0) { + markAgentRange(agentIDs, y) { + if(agentIDs.length === 0) { return; } - const {left, right} = findExtremes(this.agentInfos, agentNames); + const {left, right} = findExtremes(this.agentInfos, agentIDs); const leftX = this.agentInfos.get(left).x; const rightX = this.agentInfos.get(right).x; this.agentInfos.forEach((agentInfo) => { @@ -4973,10 +4973,10 @@ define('sequence/Renderer',[ }; const component = this.components.get(stage.type); const result = component.renderPre(stage, envPre); - const {topShift, agentNames, asynchronousY} = + const {topShift, agentIDs, asynchronousY} = BaseComponent.cleanRenderPreResult(result, this.currentY); - const topY = this.checkAgentRange(agentNames, asynchronousY); + const topY = this.checkAgentRange(agentIDs, asynchronousY); const eventOut = () => { this.trigger('mouseout'); @@ -5012,8 +5012,8 @@ define('sequence/Renderer',[ textSizer: this.sizer, SVGTextBlockClass: this.SVGTextBlockClass, state: this.state, - drawAgentLine: (agentName, toY, andStop = false) => { - const agentInfo = this.agentInfos.get(agentName); + drawAgentLine: (agentID, toY, andStop = false) => { + const agentInfo = this.agentInfos.get(agentID); this.drawAgentLine(agentInfo, toY); agentInfo.latestYStart = andStop ? null : toY; }, @@ -5023,7 +5023,7 @@ define('sequence/Renderer',[ }; const bottomY = Math.max(topY, component.render(stage, env) || 0); - this.markAgentRange(agentNames, bottomY); + this.markAgentRange(agentIDs, bottomY); this.currentY = bottomY; } @@ -5059,7 +5059,7 @@ define('sequence/Renderer',[ agentInfo.x = currentX; }); - this.agentInfos.forEach(({label, x, maxRPad, maxLPad}) => { + this.agentInfos.forEach(({x, maxRPad, maxLPad}) => { this.minX = Math.min(this.minX, x - maxLPad); this.maxX = Math.max(this.maxX, x + maxRPad); }); @@ -5068,8 +5068,9 @@ define('sequence/Renderer',[ buildAgentInfos(agents, stages) { this.agentInfos = new Map(); agents.forEach((agent, index) => { - this.agentInfos.set(agent.name, { - label: agent.name, + this.agentInfos.set(agent.id, { + id: agent.id, + formattedLabel: agent.formattedLabel, anchorRight: agent.anchorRight, index, x: null, @@ -5083,7 +5084,7 @@ define('sequence/Renderer',[ }); }); - this.visibleAgents = ['[', ']']; + this.visibleAgentIDs = ['[', ']']; stages.forEach(this.separationStage); this.positionAgents(); @@ -5177,7 +5178,7 @@ define('sequence/Renderer',[ this.title.set({ attrs: this.theme.titleAttrs, - text: sequence.meta.title, + formatted: sequence.meta.title, }); this.minX = 0; @@ -5214,8 +5215,8 @@ define('sequence/Renderer',[ return this.themes.get(''); } - getAgentX(name) { - return this.agentInfos.get(name).x; + getAgentX(id) { + return this.agentInfos.get(id).x; } svg() { @@ -5342,6 +5343,111 @@ define('sequence/Exporter',[],() => { }; }); +define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => { + 'use strict'; + + const TRIMMER = /^([ \t]*)(.*)$/; + const SQUASH_START = /^[ \t\r\n:,]/; + const SQUASH_END = /[ \t\r\n]$/; + + function makeRanges(cm, line, chFrom, chTo) { + const ln = cm.getLine(line); + const ranges = { + wordFrom: {line: line, ch: chFrom}, + squashFrom: {line: line, ch: chFrom}, + wordTo: {line: line, ch: chTo}, + squashTo: {line: line, ch: chTo}, + }; + if(chFrom > 0 && ln[chFrom - 1] === ' ') { + ranges.squashFrom.ch --; + } + if(ln[chTo] === ' ') { + ranges.squashTo.ch ++; + } + return ranges; + } + + function makeHintItem(text, ranges) { + return { + text: text, + displayText: (text === '\n') ? '' : text.trim(), + className: (text === '\n') ? 'pick-virtual' : null, + from: SQUASH_START.test(text) ? ranges.squashFrom : ranges.wordFrom, + to: SQUASH_END.test(text) ? ranges.squashTo : ranges.wordTo, + }; + } + + function getGlobals({global, prefix = '', suffix = ''}, globals) { + const identified = globals[global]; + if(!identified) { + return []; + } + return identified.map((item) => (prefix + item + suffix)); + } + + function populateGlobals(suggestions, globals = {}) { + for(let i = 0; i < suggestions.length;) { + if(typeof suggestions[i] === 'object') { + const identified = getGlobals(suggestions[i], globals); + array.mergeSets(suggestions, identified); + suggestions.splice(i, 1); + } else { + ++ i; + } + } + } + + function getHints(cm, options) { + const cur = cm.getCursor(); + const token = cm.getTokenAt(cur); + let partial = token.string; + if(token.end > cur.ch) { + partial = partial.substr(0, cur.ch - token.start); + } + const parts = TRIMMER.exec(partial); + partial = parts[2]; + const from = token.start + parts[1].length; + + const continuation = (cur.ch > 0 && token.state.line.length > 0); + let comp = (continuation ? + token.state.completions : + token.state.beginCompletions + ); + if(!continuation) { + comp = comp.concat(token.state.knownAgent); + } + + populateGlobals(comp, cm.options.globals); + + const ranges = makeRanges(cm, cur.line, from, token.end); + let selfValid = false; + const list = (comp + .filter((opt) => opt.startsWith(partial)) + .map((opt) => { + if(opt === partial + ' ' && !options.completeSingle) { + selfValid = true; + return null; + } + return makeHintItem(opt, ranges); + }) + .filter((opt) => (opt !== null)) + ); + if(selfValid && list.length > 0) { + list.unshift(makeHintItem(partial + ' ', ranges)); + } + + return { + list, + from: ranges.wordFrom, + to: ranges.wordTo, + }; + } + + return { + getHints, + }; +}); + define('sequence/themes/BaseTheme',[ 'svg/SVGUtilities', 'svg/SVGShapes', @@ -5693,7 +5799,7 @@ define('sequence/themes/Basic',[ top: 3, bottom: 2, }, - mode: { + tag: { padding: { top: 1, left: 3, @@ -6005,7 +6111,7 @@ define('sequence/themes/Monospace',[ top: 3, bottom: 2, }, - mode: { + tag: { padding: { top: 2, left: 4, @@ -6315,7 +6421,7 @@ define('sequence/themes/Chunky',[ top: 3, bottom: 4, }, - mode: { + tag: { padding: { top: 2, left: 5, @@ -7049,7 +7155,7 @@ define('sequence/themes/Sketch',[ top: 3, bottom: 2, }, - mode: { + tag: { padding: { top: 2, left: 3, @@ -7261,8 +7367,8 @@ define('sequence/themes/Sketch',[ this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this); this.blocks[''].boxRenderer = this.renderBlock.bind(this); - this.blocks.ref.section.mode.boxRenderer = this.renderTag; - this.blocks[''].section.mode.boxRenderer = this.renderTag; + this.blocks.ref.section.tag.boxRenderer = this.renderTag; + this.blocks[''].section.tag.boxRenderer = this.renderTag; this.blocks[''].sepRenderer = this.renderSeparator.bind(this); } @@ -7708,6 +7814,7 @@ define('sequence/SequenceDiagram',[ './Generator', './Renderer', './Exporter', + './CodeMirrorHints', './themes/BaseTheme', './themes/Basic', './themes/Monospace', @@ -7719,6 +7826,7 @@ define('sequence/SequenceDiagram',[ Generator, Renderer, Exporter, + CMHints, BaseTheme, BasicTheme, MonospaceTheme, @@ -7739,14 +7847,13 @@ define('sequence/SequenceDiagram',[ const SharedParser = new Parser(); const SharedGenerator = new Generator(); const CMMode = SharedParser.getCodeMirrorMode(); - const CMHints = SharedParser.getCodeMirrorHints(); function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') { if(!CodeMirror) { CodeMirror = window.CodeMirror; } CodeMirror.defineMode(modeName, () => CMMode); - CodeMirror.registerHelper('hint', modeName, CMHints); + CodeMirror.registerHelper('hint', modeName, CMHints.getHints); } function addTheme(theme) { diff --git a/lib/sequence-diagram.min.js b/lib/sequence-diagram.min.js index 52767b8..e9ef049 100644 --- a/lib/sequence-diagram.min.js +++ b/lib/sequence-diagram.min.js @@ -1 +1 @@ -!function(){var e,t,n;!function(s){function r(e,t){return y.call(e,t)}function i(e,t){var n,s,r,i,a,o,h,l,d,g,c,u=t&&t.split("/"),p=b.map,m=p&&p["*"]||{};if(e){for(a=(e=e.split("/")).length-1,b.nodeIdCompat&&w.test(e[a])&&(e[a]=e[a].replace(w,"")),"."===e[0].charAt(0)&&u&&(e=u.slice(0,u.length-1).concat(e)),d=0;d0&&(e.splice(d-1,2),d-=2)}e=e.join("/")}if((u||m)&&p){for(d=(n=e.split("/")).length;d>0;d-=1){if(s=n.slice(0,d).join("/"),u)for(g=u.length;g>0;g-=1)if((r=p[u.slice(0,g).join("/")])&&(r=r[s])){i=r,o=d;break}if(i)break;!h&&m&&m[s]&&(h=m[s],l=d)}!i&&h&&(i=h,o=l),i&&(n.splice(0,o,i),e=n.join("/"))}return e}function a(e,t){return function(){var n=k.call(arguments,0);return"string"!=typeof n[0]&&1===n.length&&n.push(null),c.apply(s,n.concat([e,t]))}}function o(e){return function(t){m[e]=t}}function h(e){if(r(f,e)){var t=f[e];delete f[e],x[e]=!0,g.apply(s,t)}if(!r(m,e)&&!r(x,e))throw new Error("No "+e);return m[e]}function l(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function d(e){return e?l(e):[]}var g,c,u,p,m={},f={},b={},x={},y=Object.prototype.hasOwnProperty,k=[].slice,w=/\.js$/;u=function(e,t){var n,s=l(e),r=s[0],a=t[1];return e=s[1],r&&(n=h(r=i(r,a))),r?e=n&&n.normalize?n.normalize(e,function(e){return function(t){return i(t,e)}}(a)):i(e,a):(r=(s=l(e=i(e,a)))[0],e=s[1],r&&(n=h(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},p={require:function(e){return a(e)},exports:function(e){var t=m[e];return void 0!==t?t:m[e]={}},module:function(e){return{id:e,uri:"",exports:m[e],config:function(e){return function(){return b&&b.config&&b.config[e]||{}}}(e)}}},g=function(e,t,n,i){var l,g,c,b,y,k,w,v=[],A=typeof n;if(i=i||e,k=d(i),"undefined"===A||"function"===A){for(t=!t.length&&n.length?["require","exports","module"]:t,y=0;y{"use strict";return class{constructor(){this.listeners=new Map,this.forwards=new Set}addEventListener(e,t){const n=this.listeners.get(e);n?n.push(t):this.listeners.set(e,[t])}removeEventListener(e,t){const n=this.listeners.get(e);if(!n)return;const s=n.indexOf(t);-1!==s&&n.splice(s,1)}countEventListeners(e){return(this.listeners.get(e)||[]).length}removeAllEventListeners(e){e?this.listeners.delete(e):this.listeners.clear()}addEventForwarding(e){this.forwards.add(e)}removeEventForwarding(e){this.forwards.delete(e)}removeAllEventForwardings(){this.forwards.clear()}trigger(e,t=[]){(this.listeners.get(e)||[]).forEach(e=>e.apply(null,t)),this.forwards.forEach(n=>n.trigger(e,t))}}}),n("core/ArrayUtilities",[],()=>{"use strict";function e(e,t,n=null){if(null===n)return e.indexOf(t);for(let s=0;s=e.length)return void r.push(s.slice());const i=e[n];if(!Array.isArray(i))return s.push(i),t(e,n+1,s,r),void s.pop();for(let a=0;a{n.push(...t(e))}),n}}}),n("sequence/CodeMirrorMode",["core/ArrayUtilities"],e=>{"use strict";function t(e,t,n,s){return""===t?function(e,t,n){return"object"==typeof n.suggest&&n.suggest.global?[n.suggest]:"string"!=typeof n.suggest||t.suggest===n.suggest?null:e["known"+n.suggest]}(e,n,s):!0===s.suggest?[function(e,t){return Object.keys(t.then).length>0?e+" ":e+"\n"}(t,s)]:Array.isArray(s.suggest)?s.suggest:s.suggest?[s.suggest]:null}function n(n,s){const r=[],i=e.last(s);return Object.keys(i.then).forEach(a=>{let o=i.then[a];"number"==typeof o&&(o=s[s.length-o-1]),e.mergeSets(r,t(n,a,i,o))}),r}function s(t,n,s,{suggest:r,override:i}){n.type&&r!==n.type&&(i&&(n.type=i),e.mergeSets(t["known"+n.type],[n.value+" "]),n.type="",n.value=""),"string"==typeof r&&t["known"+r]&&(n.type=r,n.value&&(n.value+=s.s),n.value+=s.v)}function r(t,r,i){const o={type:"",value:""};let h=i;const l=[h];return t.line.forEach((r,i)=>{i===t.line.length-1&&(t.completions=n(t,l));const d=r.q?"":r.v,g=h.then[d]||h.then[""];"number"==typeof g?l.length-=g:l.push(g||a),h=e.last(l),s(t,o,r,h)}),r&&s(t,o,null,{}),t.nextCompletions=n(t,l),t.valid=Boolean(h.then["\n"])||0===Object.keys(h.then).length,h.type}function i(e){const t=e.baseToken||{};return{value:t.v||"",quoted:t.q||!1}}const a={type:"error line-error",then:{"":0}},o=(()=>{function e(e){return{type:"string",then:Object.assign({"":0},e)}}function t(e){return{type:"variable",suggest:"Agent",then:Object.assign({},e,{"":0,",":{type:"operator",suggest:!0,then:{"":1}}})}}function n(e){return{type:"keyword",suggest:[e+" of ",e+": "],then:{of:{type:"keyword",suggest:!0,then:{"":h}},":":{type:"operator",suggest:!0,then:{"":i}},"":h}}}function s(e){const t={type:"operator",suggest:!0,then:{"+":a,"-":a,"*":a,"!":a,"":e}};return{"+":{type:"operator",suggest:!0,then:{"+":a,"-":a,"*":t,"!":a,"":e}},"-":{type:"operator",suggest:!0,then:{"+":a,"-":a,"*":t,"!":{type:"operator",then:{"+":a,"-":a,"*":a,"!":a,"":e}},"":e}},"*":{type:"operator",suggest:!0,then:{"+":t,"-":t,"*":a,"!":a,"":e}},"!":t,"":e}}const r={type:"",suggest:"\n",then:{}},i=e({"\n":r}),o={type:"variable",suggest:"Agent",then:{"":0,"\n":r,",":{type:"operator",suggest:!0,then:{"":1}},as:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:"Agent",then:{"":0,",":{type:"operator",suggest:!0,then:{"":3}},"\n":r}}}}}},h=t({":":{type:"operator",suggest:!0,then:{"":i}}}),l={type:"variable",suggest:"Agent",then:{"":0,",":{type:"operator",suggest:!0,then:{"":h}},":":a}},d={type:"variable",suggest:"Agent",then:{"":0,",":a,":":{type:"operator",suggest:!0,then:{"":i}}}},g={type:"variable",suggest:"Agent",then:{"":0,":":{type:"operator",suggest:!0,then:{"":i,"\n":{type:"",then:{}}}},"\n":r}},c={":":{type:"operator",suggest:!0,then:{"":e({as:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:"Agent",then:{"":0,"\n":r}}}}})}}},u={type:"keyword",suggest:!0,then:Object.assign({over:{type:"keyword",suggest:!0,then:{"":t(c)}}},c)},p={title:{type:"keyword",suggest:!0,then:{"":i}},theme:{type:"keyword",suggest:!0,then:{"":{type:"string",suggest:{global:"themes",suffix:"\n"},then:{"":0,"\n":r}}}},headers:{type:"keyword",suggest:!0,then:{none:{type:"keyword",suggest:!0,then:{}},cross:{type:"keyword",suggest:!0,then:{}},box:{type:"keyword",suggest:!0,then:{}},fade:{type:"keyword",suggest:!0,then:{}},bar:{type:"keyword",suggest:!0,then:{}}}},terminators:{type:"keyword",suggest:!0,then:{none:{type:"keyword",suggest:!0,then:{}},cross:{type:"keyword",suggest:!0,then:{}},box:{type:"keyword",suggest:!0,then:{}},fade:{type:"keyword",suggest:!0,then:{}},bar:{type:"keyword",suggest:!0,then:{}}}},define:{type:"keyword",suggest:!0,then:{"":o,as:a}},begin:{type:"keyword",suggest:!0,then:{"":o,reference:u,as:a}},end:{type:"keyword",suggest:!0,then:{"":o,as:a,"\n":r}},if:{type:"keyword",suggest:!0,then:{"":i,":":{type:"operator",suggest:!0,then:{"":i}},"\n":r}},else:{type:"keyword",suggest:["else\n","else if: "],then:{if:{type:"keyword",suggest:"if: ",then:{"":i,":":{type:"operator",suggest:!0,then:{"":i}}}},"\n":r}},repeat:{type:"keyword",suggest:!0,then:{"":i,":":{type:"operator",suggest:!0,then:{"":i}},"\n":r}},note:{type:"keyword",suggest:!0,then:{over:{type:"keyword",suggest:!0,then:{"":h}},left:n("left"),right:n("right"),between:{type:"keyword",suggest:!0,then:{"":l}}}},state:{type:"keyword",suggest:"state over ",then:{over:{type:"keyword",suggest:!0,then:{"":d}}}},text:{type:"keyword",suggest:!0,then:{left:n("left"),right:n("right")}},autolabel:{type:"keyword",suggest:!0,then:{off:{type:"keyword",suggest:!0,then:{}},"":i}},simultaneously:{type:"keyword",suggest:!0,then:{":":{type:"operator",suggest:!0,then:{}},with:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:"Label",then:{"":0,":":{type:"operator",suggest:!0,then:{}}}}}}}}};return e=>({type:"error line-error",then:Object.assign({},p,function(e){const t={type:"keyword",suggest:!0,then:s(g)},n={"":0};return e.forEach(e=>n[e]=t),n[":"]={type:"operator",suggest:!0,override:"Label",then:{}},s({type:"variable",suggest:"Agent",then:n})}(e))})})();return class{constructor(e,t){this.tokenDefinitions=e,this.commands=o(t),this.lineComment="#"}startState(){return{currentType:-1,current:"",currentSpace:"",currentQuoted:!1,knownAgent:[],knownLabel:[],beginCompletions:n({},[this.commands]),completions:[],nextCompletions:[],valid:!0,line:[],indent:0}}_matchPattern(e,t,n){return t?(t.lastIndex=0,e.match(t,n)):null}_tokenBegin(e,t){t.currentSpace="";let n="";for(;;){if(e.eol())return!1;t.currentSpace+=n;for(let n=0;n{"use strict";function t(e,t,n){return t.lastIndex=n,t.exec(e)}function n(e){return"n"===e[1]?"\n":e[1]}function s(e,n,s){return s?function(e,n,s){if(s.escape){const r=t(e,s.escape,n);if(r)return{newBlock:null,end:!1,appendSpace:"",appendValue:s.escapeWith(r),skip:r[0].length}}const r=t(e,s.end,n);return r?{newBlock:null,end:!0,appendSpace:"",appendValue:"",skip:r[0].length}:{newBlock:null,end:!1,appendSpace:"",appendValue:e[n],skip:1}}(e,n,s):function(e,n){for(let s=0;s,])/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"}}];class a{constructor(e){this.src=e,this.block=null,this.token=null,this.pos={i:0,ln:0,ch:0},this.reset()}isOver(){return this.pos.i>this.src.length}reset(){this.token={s:"",v:"",q:!1,b:null,e:null},this.block=null}beginToken(e){this.block=e.newBlock,Object.assign(this.token,this.block.baseToken),this.token.b=r(this.pos)}endToken(){let e=null;return this.block.omit||(this.token.e=r(this.pos),e=this.token),this.reset(),e}advance(){const e=s(this.src,this.pos.i,this.block);return e.newBlock&&this.beginToken(e),this.token.s+=e.appendSpace,this.token.v+=e.appendValue,function(e,t,n){for(let s=0;s{e.q||"\n"!==e.v?n.push(e):n.length>0&&(t.push(n),n=[])}),n.length>0&&t.push(n),t}}}),n("sequence/LabelPatternParser",[],()=>{"use strict";function e(e){const t=s.exec(e);return t&&t[1]?t[1].length:0}function t(t){if("label"===t)return{token:"label"};const n=t.indexOf(" ");let s=null,r=null;return-1===n?(s=t,r=[]):(s=t.substr(0,n),r=t.substr(n+1).split(",")),"inc"===s?function(t){let n=1,s=1,r=0;return t[0]&&(n=Number(t[0]),r=Math.max(r,e(t[0]))),t[1]&&(s=Number(t[1]),r=Math.max(r,e(t[1]))),{start:n,inc:s,dp:r}}(r):"<"+t+">"}const n=/(.*?)<([^<>]*)>/g,s=/\.([0-9]*)/;return function(e){const s=[];let r=null,i=0;for(n.lastIndex=0;r=n.exec(e);)r[1]&&s.push(r[1]),r[2]&&s.push(t(r[2])),i=n.lastIndex;const a=e.substr(i);return a&&s.push(a),s}}),n("sequence/CodeMirrorHints",["core/ArrayUtilities"],e=>{"use strict";function t(e,t){return{text:e,displayText:"\n"===e?"":e.trim(),className:"\n"===e?"pick-virtual":null,from:r.test(e)?t.squashFrom:t.wordFrom,to:i.test(e)?t.squashTo:t.wordTo}}function n({global:e,prefix:t="",suffix:n=""},s){const r=s[e];return r?r.map(e=>t+e+n):[]}const s=/^([ \t]*)(.*)$/,r=/^[ \t\r\n:,]/,i=/[ \t\r\n]$/;return{getHints:function(r,i){const a=r.getCursor(),o=r.getTokenAt(a);let h=o.string;o.end>a.ch&&(h=h.substr(0,a.ch-o.start));const l=s.exec(h);h=l[2];const d=o.start+l[1].length,g=a.ch>0&&o.state.line.length>0;let c=g?o.state.completions:o.state.beginCompletions;g||(c=c.concat(o.state.knownAgent)),function(t,s={}){for(let r=0;r0&&" "===r[n-1]&&i.squashFrom.ch--," "===r[s]&&i.squashTo.ch++,i}(r,a.line,d,o.end);let p=!1;const m=c.filter(e=>e.startsWith(h)).map(e=>e!==h+" "||i.completeSingle?t(e,u):(p=!0,null)).filter(e=>null!==e);return p&&m.length>0&&m.unshift(t(h+" ",u)),{list:m,from:u.wordFrom,to:u.wordTo}}}}),n("sequence/Parser",["core/ArrayUtilities","./Tokeniser","./LabelPatternParser","./CodeMirrorHints"],(e,t,n,s)=>{"use strict";function r(e,t=null){let n="";return t&&(n=" at line "+(t.b.ln+1)+", character "+t.b.ch),new Error(e+n)}function i(e,t=0,n=null){if(null===n&&(n=e.length),n<=t)return"";let s=e[t].v;for(let r=t+1;r=s)&&(o=s),n>=o)throw r("Missing agent name",function(t,n){if(n{const t=e.combine([[{tok:"",type:0},{tok:"<",type:1},{tok:"<<",type:2}],[{tok:"-",type:"solid"},{tok:"--",type:"dash"},{tok:"~",type:"wave"}],[{tok:"",type:0},{tok:">",type:1},{tok:">>",type:2}]]).filter(e=>0!==e[0].type||0!==e[2].type),n=new Map;return t.forEach(e=>{n.set(e.map(e=>e.tok).join(""),{line:e[1].type,left:e[0].type,right:e[2].type})}),n})(),p={"*":"begin","+":"start","-":"stop","!":"end"},m=["none","box","cross","fade","bar"],f={text:{mode:"text",types:{left:{type:"note left",skip:["of"],min:0,max:null},right:{type:"note right",skip:["of"],min:0,max:null}}},note:{mode:"note",types:{over:{type:"note over",skip:[],min:0,max:null},left:{type:"note left",skip:["of"],min:0,max:null},right:{type:"note right",skip:["of"],min:0,max:null},between:{type:"note between",skip:[],min:2,max:null}}},state:{mode:"state",types:{over:{type:"note over",skip:[],min:1,max:1}}}},b={define:{type:"agent define"},begin:{type:"agent begin",mode:"box"},end:{type:"agent end",mode:"cross"}},x=[(e,t)=>"title"!==a(e[0])?null:(t.title=i(e,1),!0),(e,t)=>"theme"!==a(e[0])?null:(t.theme=i(e,1),!0),(e,t)=>{if("terminators"!==a(e[0]))return null;const n=a(e[1]);if(!n)throw r("Unspecified termination",e[0]);if(-1===m.indexOf(n))throw r('Unknown termination "'+n+'"',e[1]);return t.terminators=n,!0},(e,t)=>{if("headers"!==a(e[0]))return null;const n=a(e[1]);if(!n)throw r("Unspecified header",e[0]);if(-1===m.indexOf(n))throw r('Unknown header "'+n+'"',e[1]);return t.headers=n,!0},e=>{if("autolabel"!==a(e[0]))return null;let t=null;return t="off"===a(e[1])?"