Refactor to enable formatted text everywhere, and make 'agent' dichotomy in generator clearer through naming [#29]

This commit is contained in:
David Evans 2018-01-14 23:10:48 +00:00
parent 2b5f7eea2b
commit 0f22dc7f94
29 changed files with 2010 additions and 1636 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -21,35 +21,52 @@ define(['core/ArrayUtilities'], (array) => {
AgentState.LOCKED = new AgentState({locked: true}); AgentState.LOCKED = new AgentState({locked: true});
AgentState.DEFAULT = new AgentState(); AgentState.DEFAULT = new AgentState();
const Agent = { // Agent from Parser: {name, flags}
const PAgent = {
equals: (a, b) => { equals: (a, b) => {
return a.name === b.name; return a.name === b.name;
}, },
make: (name, {anchorRight = false} = {}) => {
return {name, anchorRight};
},
getName: (agent) => {
return agent.name;
},
hasFlag: (flag, has = true) => { 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 = { const MERGABLE = {
'agent begin': { 'agent begin': {
check: ['mode'], check: ['mode'],
merge: ['agentNames'], merge: ['agentIDs'],
siblings: new Set(['agent highlight']), siblings: new Set(['agent highlight']),
}, },
'agent end': { 'agent end': {
check: ['mode'], check: ['mode'],
merge: ['agentNames'], merge: ['agentIDs'],
siblings: new Set(['agent highlight']), siblings: new Set(['agent highlight']),
}, },
'agent highlight': { 'agent highlight': {
check: ['highlighted'], check: ['highlighted'],
merge: ['agentNames'], merge: ['agentIDs'],
siblings: new Set(['agent begin', 'agent end']), siblings: new Set(['agent begin', 'agent end']),
}, },
}; };
@ -183,39 +200,33 @@ define(['core/ArrayUtilities'], (array) => {
} }
} }
function addBounds(target, agentL, agentR, involvedAgents = null) { function addBounds(allGAgents, gAgentL, gAgentR, involvedGAgents = null) {
array.remove(target, agentL, Agent.equals); array.remove(allGAgents, gAgentL, GAgent.equals);
array.remove(target, agentR, Agent.equals); array.remove(allGAgents, gAgentR, GAgent.equals);
let indexL = 0; let indexL = 0;
let indexR = target.length; let indexR = allGAgents.length;
if(involvedAgents) { if(involvedGAgents) {
const found = (involvedAgents const found = (involvedGAgents
.map((agent) => array.indexOf(target, agent, Agent.equals)) .map((gAgent) => GAgent.indexOf(allGAgents, gAgent))
.filter((p) => (p !== -1)) .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; indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1;
} }
target.splice(indexL, 0, agentL); allGAgents.splice(indexL, 0, gAgentL);
target.splice(indexR + 1, 0, agentR); allGAgents.splice(indexR + 1, 0, gAgentR);
return {indexL, indexR: indexR + 1}; 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 { return class Generator {
constructor() { constructor() {
this.agentStates = new Map(); this.agentStates = new Map();
this.agentAliases = new Map(); this.agentAliases = new Map();
this.activeGroups = new Map(); this.activeGroups = new Map();
this.agents = []; this.gAgents = [];
this.labelPattern = null; this.labelPattern = null;
this.blockCount = 0; this.blockCount = 0;
this.nesting = []; this.nesting = [];
@ -240,13 +251,13 @@ define(['core/ArrayUtilities'], (array) => {
'note right': this.handleNote.bind(this), 'note right': this.handleNote.bind(this),
'note between': 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.handleStage = this.handleStage.bind(this);
this.convertAgent = this.convertAgent.bind(this); this.toGAgent = this.toGAgent.bind(this);
this.endGroup = this.endGroup.bind(this); this.endGroup = this.endGroup.bind(this);
} }
convertAgent({alias, name}) { toGAgent({alias, name}) {
if(alias) { if(alias) {
if(this.agentAliases.has(name)) { if(this.agentAliases.has(name)) {
throw new Error( throw new Error(
@ -256,7 +267,7 @@ define(['core/ArrayUtilities'], (array) => {
const old = this.agentAliases.get(alias); const old = this.agentAliases.get(alias);
if( if(
(old && old !== alias) || (old && old !== alias) ||
this.agents.some((agent) => (agent.name === alias)) this.gAgents.some((gAgent) => (gAgent.id === alias))
) { ) {
throw new Error( throw new Error(
'Cannot use ' + alias + 'Cannot use ' + alias +
@ -265,7 +276,7 @@ define(['core/ArrayUtilities'], (array) => {
} }
this.agentAliases.set(alias, name); 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) { addStage(stage, isVisible = true) {
@ -300,138 +311,139 @@ define(['core/ArrayUtilities'], (array) => {
}); });
} }
defineAgents(colAgents) { defineGAgents(gAgents) {
array.mergeSets(this.currentNest.agents, colAgents, Agent.equals); array.mergeSets(this.currentNest.gAgents, gAgents, GAgent.equals);
array.mergeSets(this.agents, colAgents, Agent.equals); array.mergeSets(this.gAgents, gAgents, GAgent.equals);
} }
getAgentState(agent) { getGAgentState(gAgent) {
return this.agentStates.get(agent.name) || AgentState.DEFAULT; return this.agentStates.get(gAgent.id) || AgentState.DEFAULT;
} }
updateAgentState(agent, change) { updateGAgentState(gAgent, change) {
const state = this.agentStates.get(agent.name); const state = this.agentStates.get(gAgent.id);
if(state) { if(state) {
Object.assign(state, change); Object.assign(state, change);
} else { } 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, allowGrouped = false,
rejectGrouped = false, rejectGrouped = false,
} = {}) { } = {}) {
agents.forEach((agent) => { gAgents.forEach((gAgent) => {
const state = this.getAgentState(agent); const state = this.getGAgentState(gAgent);
if(state.covered) { if(state.covered) {
throw new Error( throw new Error(
'Agent ' + agent.name + ' is hidden behind group' 'Agent ' + gAgent.id + ' is hidden behind group'
); );
} }
if(rejectGrouped && state.group !== null) { 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)) { 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('__')) { if(gAgent.id.startsWith('__')) {
throw new Error(agent.name + ' is a reserved name'); 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 seen = new Set();
const filteredAgents = colAgents.filter((agent) => { const filteredGAgents = gAgents.filter((gAgent) => {
if(seen.has(agent.name)) { if(seen.has(gAgent.id)) {
return false; return false;
} }
seen.add(agent.name); seen.add(gAgent.id);
const state = this.getAgentState(agent); const state = this.getGAgentState(gAgent);
if(state.locked || state.blocked) { if(state.locked || state.blocked) {
if(checked) { if(checked) {
throw new Error( throw new Error('Cannot begin/end agent: ' + gAgent.id);
'Cannot begin/end agent: ' + agent.name
);
} else { } else {
return false; return false;
} }
} }
return state.visible !== visible; return state.visible !== visible;
}); });
if(filteredAgents.length === 0) { if(filteredGAgents.length === 0) {
return null; return null;
} }
filteredAgents.forEach((agent) => { filteredGAgents.forEach((gAgent) => {
this.updateAgentState(agent, {visible}); this.updateGAgentState(gAgent, {visible});
}); });
this.defineAgents(filteredAgents); this.defineGAgents(filteredGAgents);
return { return {
type: (visible ? 'agent begin' : 'agent end'), type: (visible ? 'agent begin' : 'agent end'),
agentNames: filteredAgents.map(Agent.getName), agentIDs: filteredGAgents.map((gAgent) => gAgent.id),
mode, mode,
}; };
} }
setAgentHighlight(colAgents, highlighted, checked = false) { setGAgentHighlight(gAgents, highlighted, checked = false) {
const filteredAgents = colAgents.filter((agent) => { const filteredGAgents = gAgents.filter((gAgent) => {
const state = this.getAgentState(agent); const state = this.getGAgentState(gAgent);
if(state.locked || state.blocked) { if(state.locked || state.blocked) {
if(checked) { if(checked) {
throw new Error( throw new Error('Cannot highlight agent: ' + gAgent.id);
'Cannot highlight agent: ' + agent.name
);
} else { } else {
return false; return false;
} }
} }
return state.visible && (state.highlighted !== highlighted); return state.visible && (state.highlighted !== highlighted);
}); });
if(filteredAgents.length === 0) { if(filteredGAgents.length === 0) {
return null; return null;
} }
filteredAgents.forEach((agent) => { filteredGAgents.forEach((gAgent) => {
this.updateAgentState(agent, {highlighted}); this.updateGAgentState(gAgent, {highlighted});
}); });
return { return {
type: 'agent highlight', type: 'agent highlight',
agentNames: filteredAgents.map(Agent.getName), agentIDs: filteredGAgents.map((gAgent) => gAgent.id),
highlighted, highlighted,
}; };
} }
beginNested(mode, label, name, ln) { beginNested(blockType, {tag, label, name, ln}) {
const leftAgent = Agent.make(name + '[', {anchorRight: true}); const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
const rightAgent = Agent.make(name + ']'); const rightGAgent = GAgent.make(name + ']');
const agents = [leftAgent, rightAgent]; const gAgents = [leftGAgent, rightGAgent];
const stages = []; const stages = [];
this.currentSection = { this.currentSection = {
header: { header: {
type: 'block begin', type: 'block begin',
mode, blockType,
label, tag: this.textFormatter(tag),
left: leftAgent.name, label: this.textFormatter(label),
right: rightAgent.name, left: leftGAgent.id,
right: rightGAgent.id,
ln, ln,
}, },
stages, stages,
}; };
this.currentNest = { this.currentNest = {
mode, blockType,
agents, gAgents,
leftAgent, leftGAgent,
rightAgent, rightGAgent,
hasContent: false, hasContent: false,
sections: [this.currentSection], sections: [this.currentSection],
}; };
this.agentStates.set(leftAgent.name, AgentState.LOCKED); this.replaceGAgentState(leftGAgent, AgentState.LOCKED);
this.agentStates.set(rightAgent.name, AgentState.LOCKED); this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
this.nesting.push(this.currentNest); this.nesting.push(this.currentNest);
return {agents, stages}; return {gAgents, stages};
} }
nextBlockName() { nextBlockName() {
@ -440,25 +452,31 @@ define(['core/ArrayUtilities'], (array) => {
return name; return name;
} }
handleBlockBegin({ln, mode, label}) { handleBlockBegin({ln, blockType, tag, label}) {
this.beginNested(mode, label, this.nextBlockName(), ln); this.beginNested(blockType, {
tag,
label,
name: this.nextBlockName(),
ln,
});
} }
handleBlockSplit({ln, mode, label}) { handleBlockSplit({ln, blockType, tag, label}) {
if(this.currentNest.mode !== 'if') { if(this.currentNest.blockType !== 'if') {
throw new Error( throw new Error(
'Invalid block nesting ("else" inside ' + 'Invalid block nesting ("else" inside ' +
this.currentNest.mode + ')' this.currentNest.blockType + ')'
); );
} }
optimiseStages(this.currentSection.stages); optimiseStages(this.currentSection.stages);
this.currentSection = { this.currentSection = {
header: { header: {
type: 'block split', type: 'block split',
mode, blockType,
label, tag: this.textFormatter(tag),
left: this.currentNest.leftAgent.name, label: this.textFormatter(label),
right: this.currentNest.rightAgent.name, left: this.currentNest.leftGAgent.id,
right: this.currentNest.rightGAgent.id,
ln, ln,
}, },
stages: [], stages: [],
@ -476,12 +494,12 @@ define(['core/ArrayUtilities'], (array) => {
this.currentSection = array.last(this.currentNest.sections); this.currentSection = array.last(this.currentNest.sections);
if(nested.hasContent) { if(nested.hasContent) {
this.defineAgents(nested.agents); this.defineGAgents(nested.gAgents);
addBounds( addBounds(
this.agents, this.gAgents,
nested.leftAgent, nested.leftGAgent,
nested.rightAgent, nested.rightGAgent,
nested.agents nested.gAgents
); );
nested.sections.forEach((section) => { nested.sections.forEach((section) => {
this.currentSection.stages.push(section.header); this.currentSection.stages.push(section.header);
@ -489,70 +507,71 @@ define(['core/ArrayUtilities'], (array) => {
}); });
this.addStage({ this.addStage({
type: 'block end', type: 'block end',
left: nested.leftAgent.name, left: nested.leftGAgent.id,
right: nested.rightAgent.name, right: nested.rightGAgent.id,
}); });
} else { } else {
throw new Error('Empty block'); throw new Error('Empty block');
} }
} }
makeGroupDetails(agents, alias) { makeGroupDetails(pAgents, alias) {
const colAgents = agents.map(this.convertAgent); const gAgents = pAgents.map(this.toGAgent);
this.validateAgents(colAgents, {rejectGrouped: true}); this.validateGAgents(gAgents, {rejectGrouped: true});
if(this.agentStates.has(alias)) { if(this.agentStates.has(alias)) {
throw new Error('Duplicate agent name: ' + alias); throw new Error('Duplicate agent name: ' + alias);
} }
const name = this.nextBlockName(); const name = this.nextBlockName();
const leftAgent = Agent.make(name + '[', {anchorRight: true}); const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
const rightAgent = Agent.make(name + ']'); const rightGAgent = GAgent.make(name + ']');
this.agentStates.set(leftAgent.name, AgentState.LOCKED); this.replaceGAgentState(leftGAgent, AgentState.LOCKED);
this.agentStates.set(rightAgent.name, AgentState.LOCKED); this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
this.updateAgentState( this.updateGAgentState(
{name: alias}, GAgent.make(alias),
{blocked: true, group: alias} {blocked: true, group: alias}
); );
this.defineAgents(colAgents); this.defineGAgents(gAgents);
const {indexL, indexR} = addBounds( const {indexL, indexR} = addBounds(
this.agents, this.gAgents,
leftAgent, leftGAgent,
rightAgent, rightGAgent,
colAgents gAgents
); );
const agentsCovered = []; const gAgentsCovered = [];
const agentsContained = colAgents.slice(); const gAgentsContained = gAgents.slice();
for(let i = indexL + 1; i < indexR; ++ i) { 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 { return {
colAgents, gAgents,
leftAgent, leftGAgent,
rightAgent, rightGAgent,
agentsContained, gAgentsContained,
agentsCovered, gAgentsCovered,
}; };
} }
handleGroupBegin({agents, mode, label, alias}) { handleGroupBegin({agents, blockType, tag, label, alias}) {
const details = this.makeGroupDetails(agents, alias); const details = this.makeGroupDetails(agents, alias);
details.agentsContained.forEach((agent) => { details.gAgentsContained.forEach((gAgent) => {
this.updateAgentState(agent, {group: alias}); this.updateGAgentState(gAgent, {group: alias});
}); });
details.agentsCovered.forEach((agent) => { details.gAgentsCovered.forEach((gAgent) => {
this.updateAgentState(agent, {covered: true}); this.updateGAgentState(gAgent, {covered: true});
}); });
this.activeGroups.set(alias, details); this.activeGroups.set(alias, details);
this.addStage(this.setAgentVis(details.colAgents, true, 'box')); this.addStage(this.setGAgentVis(details.gAgents, true, 'box'));
this.addStage({ this.addStage({
type: 'block begin', type: 'block begin',
mode, blockType,
label, tag: this.textFormatter(tag),
left: details.leftAgent.name, label: this.textFormatter(label),
right: details.rightAgent.name, left: details.leftGAgent.id,
right: details.rightGAgent.id,
}); });
} }
@ -563,18 +582,18 @@ define(['core/ArrayUtilities'], (array) => {
} }
this.activeGroups.delete(name); this.activeGroups.delete(name);
details.agentsContained.forEach((agent) => { details.gAgentsContained.forEach((gAgent) => {
this.updateAgentState(agent, {group: null}); this.updateGAgentState(gAgent, {group: null});
}); });
details.agentsCovered.forEach((agent) => { details.gAgentsCovered.forEach((gAgent) => {
this.updateAgentState(agent, {covered: false}); this.updateGAgentState(gAgent, {covered: false});
}); });
this.updateAgentState({name}, {group: null}); this.updateGAgentState(GAgent.make(name), {group: null});
return { return {
type: 'block end', type: 'block end',
left: details.leftAgent.name, left: details.leftGAgent.id,
right: details.rightAgent.name, right: details.rightGAgent.id,
}; };
} }
@ -620,156 +639,156 @@ define(['core/ArrayUtilities'], (array) => {
return result; return result;
} }
expandGroupedAgent(agent) { expandGroupedGAgent(gAgent) {
const group = this.getAgentState(agent).group; const group = this.getGAgentState(gAgent).group;
if(!group) { if(!group) {
return [agent]; return [gAgent];
} }
const details = this.activeGroups.get(group); const details = this.activeGroups.get(group);
return [details.leftAgent, details.rightAgent]; return [details.leftGAgent, details.rightGAgent];
} }
expandGroupedAgentConnection(agents) { expandGroupedGAgentConnection(gAgents) {
const agents1 = this.expandGroupedAgent(agents[0]); const gAgents1 = this.expandGroupedGAgent(gAgents[0]);
const agents2 = this.expandGroupedAgent(agents[1]); const gAgents2 = this.expandGroupedGAgent(gAgents[1]);
let ind1 = array.indexOf(this.agents, agents1[0], Agent.equals); let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]);
let ind2 = array.indexOf(this.agents, agents2[0], Agent.equals); let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]);
if(ind1 === -1) { if(ind1 === -1) {
ind1 = this.agents.length; ind1 = this.gAgents.length;
} }
if(ind2 === -1) { if(ind2 === -1) {
ind2 = this.agents.length; ind2 = this.gAgents.length;
} }
if(ind1 === ind2) { if(ind1 === ind2) {
// Self-connection // Self-connection
return [array.last(agents1), array.last(agents2)]; return [array.last(gAgents1), array.last(gAgents2)];
} else if(ind1 < ind2) { } else if(ind1 < ind2) {
return [array.last(agents1), agents2[0]]; return [array.last(gAgents1), gAgents2[0]];
} else { } else {
return [agents1[0], array.last(agents2)]; return [gAgents1[0], array.last(gAgents2)];
} }
} }
filterConnectFlags(agents) { filterConnectFlags(pAgents) {
const beginAgents = (agents const beginGAgents = (pAgents
.filter(Agent.hasFlag('begin')) .filter(PAgent.hasFlag('begin'))
.map(this.convertAgent) .map(this.toGAgent)
); );
const endAgents = (agents const endGAgents = (pAgents
.filter(Agent.hasFlag('end')) .filter(PAgent.hasFlag('end'))
.map(this.convertAgent) .map(this.toGAgent)
); );
if(array.hasIntersection(beginAgents, endAgents, Agent.equals)) { if(GAgent.hasIntersection(beginGAgents, endGAgents)) {
throw new Error('Cannot set agent visibility multiple times'); throw new Error('Cannot set agent visibility multiple times');
} }
const startAgents = (agents const startGAgents = (pAgents
.filter(Agent.hasFlag('start')) .filter(PAgent.hasFlag('start'))
.map(this.convertAgent) .map(this.toGAgent)
); );
const stopAgents = (agents const stopGAgents = (pAgents
.filter(Agent.hasFlag('stop')) .filter(PAgent.hasFlag('stop'))
.map(this.convertAgent) .map(this.toGAgent)
); );
array.mergeSets(stopAgents, endAgents); array.mergeSets(stopGAgents, endGAgents);
if(array.hasIntersection(startAgents, stopAgents, Agent.equals)) { if(GAgent.hasIntersection(startGAgents, stopGAgents)) {
throw new Error('Cannot set agent highlighting multiple times'); throw new Error('Cannot set agent highlighting multiple times');
} }
this.validateAgents(beginAgents); this.validateGAgents(beginGAgents);
this.validateAgents(endAgents); this.validateGAgents(endGAgents);
this.validateAgents(startAgents); this.validateGAgents(startGAgents);
this.validateAgents(stopAgents); this.validateGAgents(stopGAgents);
return {beginAgents, endAgents, startAgents, stopAgents}; return {beginGAgents, endGAgents, startGAgents, stopGAgents};
} }
handleConnect({agents, label, options}) { handleConnect({agents, label, options}) {
const flags = this.filterConnectFlags(agents); const flags = this.filterConnectFlags(agents);
let colAgents = agents.map(this.convertAgent); let gAgents = agents.map(this.toGAgent);
this.validateAgents(colAgents, {allowGrouped: true}); this.validateGAgents(gAgents, {allowGrouped: true});
const allAgents = array.flatMap(colAgents, this.expandGroupedAgent); const allGAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
this.defineAgents(allAgents); this.defineGAgents(allGAgents);
colAgents = this.expandGroupedAgentConnection(colAgents); gAgents = this.expandGroupedGAgentConnection(gAgents);
const agentNames = colAgents.map(Agent.getName); const agentIDs = gAgents.map((gAgent) => gAgent.id);
const implicitBegin = (agents const implicitBeginGAgents = (agents
.filter(Agent.hasFlag('begin', false)) .filter(PAgent.hasFlag('begin', false))
.map(this.convertAgent) .map(this.toGAgent)
); );
this.addStage(this.setAgentVis(implicitBegin, true, 'box')); this.addStage(this.setGAgentVis(implicitBeginGAgents, true, 'box'));
const connectStage = { const connectStage = {
type: 'connect', type: 'connect',
agentNames, agentIDs,
label: this.applyLabelPattern(label), label: this.textFormatter(this.applyLabelPattern(label)),
options, options,
}; };
this.addParallelStages([ this.addParallelStages([
this.setAgentVis(flags.beginAgents, true, 'box', true), this.setGAgentVis(flags.beginGAgents, true, 'box', true),
this.setAgentHighlight(flags.startAgents, true, true), this.setGAgentHighlight(flags.startGAgents, true, true),
connectStage, connectStage,
this.setAgentHighlight(flags.stopAgents, false, true), this.setGAgentHighlight(flags.stopGAgents, false, true),
this.setAgentVis(flags.endAgents, false, 'cross', true), this.setGAgentVis(flags.endGAgents, false, 'cross', true),
]); ]);
} }
handleNote({type, agents, mode, label}) { handleNote({type, agents, mode, label}) {
let colAgents = null; let gAgents = null;
if(agents.length === 0) { if(agents.length === 0) {
colAgents = NOTE_DEFAULT_AGENTS[type] || []; gAgents = NOTE_DEFAULT_G_AGENTS[type] || [];
} else { } else {
colAgents = agents.map(this.convertAgent); gAgents = agents.map(this.toGAgent);
} }
this.validateAgents(colAgents, {allowGrouped: true}); this.validateGAgents(gAgents, {allowGrouped: true});
colAgents = array.flatMap(colAgents, this.expandGroupedAgent); gAgents = array.flatMap(gAgents, this.expandGroupedGAgent);
const agentNames = colAgents.map(Agent.getName); const agentIDs = gAgents.map((gAgent) => gAgent.id);
const uniqueAgents = new Set(agentNames).size; const uniqueAgents = new Set(agentIDs).size;
if(type === 'note between' && uniqueAgents < 2) { if(type === 'note between' && uniqueAgents < 2) {
throw new Error('note between requires at least 2 agents'); throw new Error('note between requires at least 2 agents');
} }
this.addStage(this.setAgentVis(colAgents, true, 'box')); this.addStage(this.setGAgentVis(gAgents, true, 'box'));
this.defineAgents(colAgents); this.defineGAgents(gAgents);
this.addStage({ this.addStage({
type, type,
agentNames, agentIDs,
mode, mode,
label, label: this.textFormatter(label),
}); });
} }
handleAgentDefine({agents}) { handleAgentDefine({agents}) {
const colAgents = agents.map(this.convertAgent); const gAgents = agents.map(this.toGAgent);
this.validateAgents(colAgents); this.validateGAgents(gAgents);
this.defineAgents(colAgents); this.defineGAgents(gAgents);
} }
handleAgentBegin({agents, mode}) { handleAgentBegin({agents, mode}) {
const colAgents = agents.map(this.convertAgent); const gAgents = agents.map(this.toGAgent);
this.validateAgents(colAgents); this.validateGAgents(gAgents);
this.addStage(this.setAgentVis(colAgents, true, mode, true)); this.addStage(this.setGAgentVis(gAgents, true, mode, true));
} }
handleAgentEnd({agents, mode}) { handleAgentEnd({agents, mode}) {
const groupAgents = (agents const groupPAgents = (agents
.filter((agent) => this.activeGroups.has(agent.name)) .filter((pAgent) => this.activeGroups.has(pAgent.name))
); );
const colAgents = (agents const gAgents = (agents
.filter((agent) => !this.activeGroups.has(agent.name)) .filter((pAgent) => !this.activeGroups.has(pAgent.name))
.map(this.convertAgent) .map(this.toGAgent)
); );
this.validateAgents(colAgents); this.validateGAgents(gAgents);
this.addParallelStages([ this.addParallelStages([
this.setAgentHighlight(colAgents, false), this.setGAgentHighlight(gAgents, false),
this.setAgentVis(colAgents, false, mode, true), this.setGAgentVis(gAgents, false, mode, true),
...groupAgents.map(this.endGroup), ...groupPAgents.map(this.endGroup),
]); ]);
} }
@ -783,21 +802,46 @@ define(['core/ArrayUtilities'], (array) => {
handler(stage); handler(stage);
} catch(e) { } catch(e) {
if(typeof e === 'object' && e.message) { if(typeof e === 'object' && e.message) {
throw new Error(e.message + ' at line ' + (stage.ln + 1)); e.message += ' at line ' + (stage.ln + 1);
throw e;
} }
} }
} }
generate({stages, meta = {}}) { _reset() {
this.agentStates.clear(); this.agentStates.clear();
this.markers.clear(); this.markers.clear();
this.agentAliases.clear(); this.agentAliases.clear();
this.activeGroups.clear(); this.activeGroups.clear();
this.agents.length = 0; this.gAgents.length = 0;
this.blockCount = 0; this.blockCount = 0;
this.nesting.length = 0; this.nesting.length = 0;
this.labelPattern = [{token: 'label'}]; 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); stages.forEach(this.handleStage);
@ -812,26 +856,21 @@ define(['core/ArrayUtilities'], (array) => {
} }
const terminators = meta.terminators || 'none'; const terminators = meta.terminators || 'none';
this.addParallelStages([ this.addParallelStages([
this.setAgentHighlight(this.agents, false), this.setGAgentHighlight(this.gAgents, false),
this.setAgentVis(this.agents, false, terminators), this.setGAgentVis(this.gAgents, false, terminators),
]); ]);
addBounds( this._finalise(globals);
this.agents,
this.currentNest.leftAgent,
this.currentNest.rightAgent
);
optimiseStages(globals.stages);
swapFirstBegin(globals.stages, meta.headers || 'box'); swapFirstBegin(globals.stages, meta.headers || 'box');
return { return {
meta: { meta: {
title: meta.title, title: this.textFormatter(meta.title),
theme: meta.theme, theme: meta.theme,
}, },
agents: this.agents.slice(), agents: this.gAgents.slice(),
stages: globals.stages, stages: globals.stages,
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
define(() => {
'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;
});

View File

@ -0,0 +1,51 @@
defineDescribe('Markdown Parser', [
'./MarkdownParser',
'svg/SVGTextBlock',
'svg/SVGUtilities',
], (
parser,
SVGTextBlock,
svg
) => {
'use strict';
it('converts simple text', () => {
const formatted = parser('hello everybody');
expect(formatted).toEqual([[{text: 'hello everybody', attrs: null}]]);
});
it('produces an empty array given an empty input', () => {
const formatted = parser('');
expect(formatted).toEqual([]);
});
it('converts multiline text', () => {
const formatted = parser('hello\neverybody');
expect(formatted).toEqual([
[{text: 'hello', attrs: null}],
[{text: 'everybody', attrs: null}],
]);
});
describe('SVGTextBlock interaction', () => {
let hold = null;
let block = null;
beforeEach(() => {
hold = svg.makeContainer();
document.body.appendChild(hold);
block = new SVGTextBlock(hold, {attrs: {'font-size': 12}});
});
afterEach(() => {
document.body.removeChild(hold);
});
it('produces a format compatible with SVGTextBlock', () => {
const formatted = parser('hello everybody');
block.set({formatted});
expect(hold.children.length).toEqual(1);
expect(hold.children[0].innerHTML).toEqual('hello everybody');
});
});
});

View File

@ -1,20 +1,35 @@
define([ define([
'core/ArrayUtilities', 'core/ArrayUtilities',
'./Tokeniser', './Tokeniser',
'./MarkdownParser',
'./LabelPatternParser', './LabelPatternParser',
'./CodeMirrorHints',
], ( ], (
array, array,
Tokeniser, Tokeniser,
labelPatternParser, markdownParser,
CMHints labelPatternParser
) => { ) => {
'use strict'; 'use strict';
const BLOCK_TYPES = { const BLOCK_TYPES = {
'if': {type: 'block begin', mode: 'if', skip: []}, 'if': {
'else': {type: 'block split', mode: 'else', skip: ['if']}, type: 'block begin',
'repeat': {type: 'block begin', mode: 'repeat', skip: []}, 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 = ((() => { const CONNECT_TYPES = ((() => {
@ -314,7 +329,8 @@ define([
skip = skipOver(line, skip, [':']); skip = skipOver(line, skip, [':']);
return { return {
type: type.type, type: type.type,
mode: type.mode, blockType: type.blockType,
tag: type.tag,
label: joinLabel(line, skip), label: joinLabel(line, skip),
}; };
}, },
@ -345,7 +361,8 @@ define([
return { return {
type: 'group begin', type: 'group begin',
agents, agents,
mode: 'ref', blockType: 'ref',
tag: 'ref',
label: def.name, label: def.name,
alias: def.alias, alias: def.alias,
}; };
@ -480,10 +497,6 @@ define([
); );
} }
getCodeMirrorHints() {
return CMHints.getHints;
}
parseLines(lines) { parseLines(lines) {
const result = { const result = {
meta: { meta: {
@ -491,6 +504,7 @@ define([
theme: '', theme: '',
terminators: 'none', terminators: 'none',
headers: 'box', headers: 'box',
textFormatter: markdownParser,
}, },
stages: [], stages: [],
}; };

View File

@ -6,26 +6,30 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
const PARSED = { const PARSED = {
blockBegin: ({ blockBegin: ({
ln = jasmine.anything(), ln = jasmine.anything(),
mode = jasmine.anything(), blockType = jasmine.anything(),
tag = jasmine.anything(),
label = jasmine.anything(), label = jasmine.anything(),
} = {}) => { } = {}) => {
return { return {
type: 'block begin', type: 'block begin',
ln, ln,
mode, blockType,
tag,
label, label,
}; };
}, },
blockSplit: ({ blockSplit: ({
ln = jasmine.anything(), ln = jasmine.anything(),
mode = jasmine.anything(), blockType = jasmine.anything(),
tag = jasmine.anything(),
label = jasmine.anything(), label = jasmine.anything(),
} = {}) => { } = {}) => {
return { return {
type: 'block split', type: 'block split',
ln, ln,
mode, blockType,
tag,
label, label,
}; };
}, },
@ -73,6 +77,7 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
theme: '', theme: '',
terminators: 'none', terminators: 'none',
headers: 'box', headers: 'box',
textFormatter: jasmine.anything(),
}, },
stages: [], stages: [],
}); });
@ -98,6 +103,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
expect(parsed.meta.headers).toEqual('bar'); expect(parsed.meta.headers).toEqual('bar');
}); });
it('propagates a function which can be used to format text', () => {
const parsed = parser.parse('title foo');
expect(parsed.meta.textFormatter).toEqual(jasmine.any(Function));
});
it('reads multiple tokens as one when reading values', () => { it('reads multiple tokens as one when reading values', () => {
const parsed = parser.parse('title foo bar'); const parsed = parser.parse('title foo bar');
expect(parsed.meta.title).toEqual('foo bar'); expect(parsed.meta.title).toEqual('foo bar');
@ -435,7 +445,8 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
type: 'group begin', type: 'group begin',
ln: jasmine.anything(), ln: jasmine.anything(),
agents: [], agents: [],
mode: 'ref', blockType: 'ref',
tag: 'ref',
label: 'Foo bar', label: 'Foo bar',
alias: 'baz', alias: 'baz',
}, },
@ -446,7 +457,8 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
{name: 'A', alias: '', flags: []}, {name: 'A', alias: '', flags: []},
{name: 'B', alias: '', flags: []}, {name: 'B', alias: '', flags: []},
], ],
mode: 'ref', blockType: 'ref',
tag: 'ref',
label: 'Foo bar', label: 'Foo bar',
alias: 'baz', alias: 'baz',
}, },
@ -518,12 +530,24 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
'end\n' 'end\n'
); );
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.blockBegin({mode: 'if', label: 'something happens'}), PARSED.blockBegin({
blockType: 'if',
tag: 'if',
label: 'something happens',
}),
PARSED.connect(['A', 'B']), PARSED.connect(['A', 'B']),
PARSED.blockSplit({mode: 'else', label: 'something else'}), PARSED.blockSplit({
blockType: 'else',
tag: 'else',
label: 'something else',
}),
PARSED.connect(['A', 'C']), PARSED.connect(['A', 'C']),
PARSED.connect(['C', 'B']), PARSED.connect(['C', 'B']),
PARSED.blockSplit({mode: 'else', label: ''}), PARSED.blockSplit({
blockType: 'else',
tag: 'else',
label: '',
}),
PARSED.connect(['A', 'D']), PARSED.connect(['A', 'D']),
PARSED.blockEnd(), PARSED.blockEnd(),
]); ]);
@ -532,7 +556,11 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
it('converts loop blocks', () => { it('converts loop blocks', () => {
const parsed = parser.parse('repeat until something'); const parsed = parser.parse('repeat until something');
expect(parsed.stages).toEqual([ expect(parsed.stages).toEqual([
PARSED.blockBegin({mode: 'repeat', label: 'until something'}), PARSED.blockBegin({
blockType: 'repeat',
tag: 'repeat',
label: 'until something',
}),
]); ]);
}); });

View File

@ -22,11 +22,11 @@ define([
/* jshint +W072 */ /* jshint +W072 */
'use strict'; 'use strict';
function findExtremes(agentInfos, agentNames) { function findExtremes(agentInfos, agentIDs) {
let min = null; let min = null;
let max = null; let max = null;
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const info = agentInfos.get(name); const info = agentInfos.get(id);
if(min === null || info.index < min.index) { if(min === null || info.index < min.index) {
min = info; min = info;
} }
@ -35,8 +35,8 @@ define([
} }
}); });
return { return {
left: min.label, left: min.id,
right: max.label, right: max.id,
}; };
} }
@ -158,23 +158,23 @@ define([
return namespacedName; return namespacedName;
} }
addSeparation(agentName1, agentName2, dist) { addSeparation(agentID1, agentID2, dist) {
const info1 = this.agentInfos.get(agentName1); const info1 = this.agentInfos.get(agentID1);
const info2 = this.agentInfos.get(agentName2); const info2 = this.agentInfos.get(agentID2);
const d1 = info1.separations.get(agentName2) || 0; const d1 = info1.separations.get(agentID2) || 0;
info1.separations.set(agentName2, Math.max(d1, dist)); info1.separations.set(agentID2, Math.max(d1, dist));
const d2 = info2.separations.get(agentName1) || 0; const d2 = info2.separations.get(agentID1) || 0;
info2.separations.set(agentName1, Math.max(d2, dist)); info2.separations.set(agentID1, Math.max(d2, dist));
} }
separationStage(stage) { separationStage(stage) {
const agentSpaces = new Map(); const agentSpaces = new Map();
const agentNames = this.visibleAgents.slice(); const agentIDs = this.visibleAgentIDs.slice();
const addSpacing = (agentName, {left, right}) => { const addSpacing = (agentID, {left, right}) => {
const current = agentSpaces.get(agentName); const current = agentSpaces.get(agentID);
current.left = Math.max(current.left, left); current.left = Math.max(current.left, left);
current.right = Math.max(current.right, right); current.right = Math.max(current.right, right);
}; };
@ -182,12 +182,12 @@ define([
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad; const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad; agentInfo.currentMaxRad = rad;
agentSpaces.set(agentInfo.label, {left: rad, right: rad}); agentSpaces.set(agentInfo.id, {left: rad, right: rad});
}); });
const env = { const env = {
theme: this.theme, theme: this.theme,
agentInfos: this.agentInfos, agentInfos: this.agentInfos,
visibleAgents: this.visibleAgents, visibleAgentIDs: this.visibleAgentIDs,
textSizer: this.sizer, textSizer: this.sizer,
addSpacing, addSpacing,
addSeparation: this.addSeparation, addSeparation: this.addSeparation,
@ -200,33 +200,33 @@ define([
} }
component.separationPre(stage, env); component.separationPre(stage, env);
component.separation(stage, env); component.separation(stage, env);
array.mergeSets(agentNames, this.visibleAgents); array.mergeSets(agentIDs, this.visibleAgentIDs);
agentNames.forEach((agentNameR) => { agentIDs.forEach((agentIDR) => {
const infoR = this.agentInfos.get(agentNameR); const infoR = this.agentInfos.get(agentIDR);
const sepR = agentSpaces.get(agentNameR); const sepR = agentSpaces.get(agentIDR);
infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right); infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right);
infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left); infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left);
agentNames.forEach((agentNameL) => { agentIDs.forEach((agentIDL) => {
const infoL = this.agentInfos.get(agentNameL); const infoL = this.agentInfos.get(agentIDL);
if(infoL.index >= infoR.index) { if(infoL.index >= infoR.index) {
return; return;
} }
const sepL = agentSpaces.get(agentNameL); const sepL = agentSpaces.get(agentIDL);
this.addSeparation( this.addSeparation(
agentNameR, agentIDR,
agentNameL, agentIDL,
sepR.left + sepL.right + this.theme.agentMargin sepR.left + sepL.right + this.theme.agentMargin
); );
}); });
}); });
} }
checkAgentRange(agentNames, topY = 0) { checkAgentRange(agentIDs, topY = 0) {
if(agentNames.length === 0) { if(agentIDs.length === 0) {
return topY; return topY;
} }
const {left, right} = findExtremes(this.agentInfos, agentNames); const {left, right} = findExtremes(this.agentInfos, agentIDs);
const leftX = this.agentInfos.get(left).x; const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x; const rightX = this.agentInfos.get(right).x;
let baseY = topY; let baseY = topY;
@ -238,11 +238,11 @@ define([
return baseY; return baseY;
} }
markAgentRange(agentNames, y) { markAgentRange(agentIDs, y) {
if(agentNames.length === 0) { if(agentIDs.length === 0) {
return; return;
} }
const {left, right} = findExtremes(this.agentInfos, agentNames); const {left, right} = findExtremes(this.agentInfos, agentIDs);
const leftX = this.agentInfos.get(left).x; const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x; const rightX = this.agentInfos.get(right).x;
this.agentInfos.forEach((agentInfo) => { this.agentInfos.forEach((agentInfo) => {
@ -293,10 +293,10 @@ define([
}; };
const component = this.components.get(stage.type); const component = this.components.get(stage.type);
const result = component.renderPre(stage, envPre); const result = component.renderPre(stage, envPre);
const {topShift, agentNames, asynchronousY} = const {topShift, agentIDs, asynchronousY} =
BaseComponent.cleanRenderPreResult(result, this.currentY); BaseComponent.cleanRenderPreResult(result, this.currentY);
const topY = this.checkAgentRange(agentNames, asynchronousY); const topY = this.checkAgentRange(agentIDs, asynchronousY);
const eventOut = () => { const eventOut = () => {
this.trigger('mouseout'); this.trigger('mouseout');
@ -332,8 +332,8 @@ define([
textSizer: this.sizer, textSizer: this.sizer,
SVGTextBlockClass: this.SVGTextBlockClass, SVGTextBlockClass: this.SVGTextBlockClass,
state: this.state, state: this.state,
drawAgentLine: (agentName, toY, andStop = false) => { drawAgentLine: (agentID, toY, andStop = false) => {
const agentInfo = this.agentInfos.get(agentName); const agentInfo = this.agentInfos.get(agentID);
this.drawAgentLine(agentInfo, toY); this.drawAgentLine(agentInfo, toY);
agentInfo.latestYStart = andStop ? null : toY; agentInfo.latestYStart = andStop ? null : toY;
}, },
@ -343,7 +343,7 @@ define([
}; };
const bottomY = Math.max(topY, component.render(stage, env) || 0); const bottomY = Math.max(topY, component.render(stage, env) || 0);
this.markAgentRange(agentNames, bottomY); this.markAgentRange(agentIDs, bottomY);
this.currentY = bottomY; this.currentY = bottomY;
} }
@ -379,7 +379,7 @@ define([
agentInfo.x = currentX; 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.minX = Math.min(this.minX, x - maxLPad);
this.maxX = Math.max(this.maxX, x + maxRPad); this.maxX = Math.max(this.maxX, x + maxRPad);
}); });
@ -388,8 +388,9 @@ define([
buildAgentInfos(agents, stages) { buildAgentInfos(agents, stages) {
this.agentInfos = new Map(); this.agentInfos = new Map();
agents.forEach((agent, index) => { agents.forEach((agent, index) => {
this.agentInfos.set(agent.name, { this.agentInfos.set(agent.id, {
label: agent.name, id: agent.id,
formattedLabel: agent.formattedLabel,
anchorRight: agent.anchorRight, anchorRight: agent.anchorRight,
index, index,
x: null, x: null,
@ -403,7 +404,7 @@ define([
}); });
}); });
this.visibleAgents = ['[', ']']; this.visibleAgentIDs = ['[', ']'];
stages.forEach(this.separationStage); stages.forEach(this.separationStage);
this.positionAgents(); this.positionAgents();
@ -497,7 +498,7 @@ define([
this.title.set({ this.title.set({
attrs: this.theme.titleAttrs, attrs: this.theme.titleAttrs,
text: sequence.meta.title, formatted: sequence.meta.title,
}); });
this.minX = 0; this.minX = 0;
@ -534,8 +535,8 @@ define([
return this.themes.get(''); return this.themes.get('');
} }
getAgentX(name) { getAgentX(id) {
return this.agentInfos.get(name).x; return this.agentInfos.get(id).x;
} }
svg() { svg() {

View File

@ -26,10 +26,10 @@ defineDescribe('Sequence Renderer', [
}); });
const GENERATED = { const GENERATED = {
connect: (agentNames, label = '') => { connect: (agentIDs, label = []) => {
return { return {
type: 'connect', type: 'connect',
agentNames, agentIDs,
label, label,
options: { options: {
line: 'solid', line: 'solid',
@ -40,15 +40,35 @@ defineDescribe('Sequence Renderer', [
}, },
}; };
function format(text) {
if(!text) {
return [];
}
return [[{text}]];
}
describe('.render', () => { describe('.render', () => {
it('populates the SVG with content', () => { it('populates the SVG with content', () => {
renderer.render({ renderer.render({
meta: {title: 'Title'}, meta: {title: format('Title')},
agents: [ agents: [
{name: '[', anchorRight: true}, {
{name: 'Col 1', anchorRight: false}, id: '[',
{name: 'Col 2', anchorRight: false}, formattedLabel: null,
{name: ']', anchorRight: false}, anchorRight: true,
}, {
id: 'Col 1',
formattedLabel: format('Col 1!'),
anchorRight: false,
}, {
id: 'Col 2',
formattedLabel: format('Col 2!'),
anchorRight: false,
}, {
id: ']',
formattedLabel: null,
anchorRight: false,
},
], ],
stages: [], stages: [],
}); });
@ -63,17 +83,17 @@ defineDescribe('Sequence Renderer', [
*/ */
renderer.render({ renderer.render({
meta: {title: ''}, meta: {title: []},
agents: [ agents: [
{name: '[', anchorRight: true}, {id: '[', formattedLabel: null, anchorRight: true},
{name: 'A', anchorRight: false}, {id: 'A', formattedLabel: format('A!'), anchorRight: false},
{name: 'B', anchorRight: false}, {id: 'B', formattedLabel: format('B!'), anchorRight: false},
{name: ']', anchorRight: false}, {id: ']', formattedLabel: null, anchorRight: false},
], ],
stages: [ stages: [
{type: 'agent begin', agentNames: ['A', 'B'], mode: 'box'}, {type: 'agent begin', agentIDs: ['A', 'B'], mode: 'box'},
GENERATED.connect(['A', 'B']), GENERATED.connect(['A', 'B']),
{type: 'agent end', agentNames: ['A', 'B'], mode: 'none'}, {type: 'agent end', agentIDs: ['A', 'B'], mode: 'none'},
], ],
}); });
@ -93,18 +113,18 @@ defineDescribe('Sequence Renderer', [
*/ */
renderer.render({ renderer.render({
meta: {title: ''}, meta: {title: []},
agents: [ agents: [
{name: '[', anchorRight: true}, {id: '[', formattedLabel: null, anchorRight: true},
{name: 'A', anchorRight: false}, {id: 'A', formattedLabel: format('A!'), anchorRight: false},
{name: 'B', anchorRight: false}, {id: 'B', formattedLabel: format('B!'), anchorRight: false},
{name: 'C', anchorRight: false}, {id: 'C', formattedLabel: format('C!'), anchorRight: false},
{name: ']', anchorRight: false}, {id: ']', formattedLabel: null, anchorRight: false},
], ],
stages: [ stages: [
{ {
type: 'agent begin', type: 'agent begin',
agentNames: ['A', 'B', 'C'], agentIDs: ['A', 'B', 'C'],
mode: 'box', mode: 'box',
}, },
GENERATED.connect(['[', 'A']), GENERATED.connect(['[', 'A']),
@ -113,7 +133,7 @@ defineDescribe('Sequence Renderer', [
GENERATED.connect(['C', ']']), GENERATED.connect(['C', ']']),
{ {
type: 'agent end', type: 'agent end',
agentNames: ['A', 'B', 'C'], agentIDs: ['A', 'B', 'C'],
mode: 'none', mode: 'none',
}, },
], ],
@ -142,25 +162,25 @@ defineDescribe('Sequence Renderer', [
*/ */
renderer.render({ renderer.render({
meta: {title: ''}, meta: {title: []},
agents: [ agents: [
{name: '[', anchorRight: true}, {id: '[', formattedLabel: null, anchorRight: true},
{name: 'A', anchorRight: false}, {id: 'A', formattedLabel: format('A!'), anchorRight: false},
{name: 'B', anchorRight: false}, {id: 'B', formattedLabel: format('B!'), anchorRight: false},
{name: 'C', anchorRight: false}, {id: 'C', formattedLabel: format('C!'), anchorRight: false},
{name: 'D', anchorRight: false}, {id: 'D', formattedLabel: format('D!'), anchorRight: false},
{name: ']', anchorRight: false}, {id: ']', formattedLabel: null, anchorRight: false},
], ],
stages: [ stages: [
{type: 'agent begin', agentNames: ['A', 'B'], mode: 'box'}, {type: 'agent begin', agentIDs: ['A', 'B'], mode: 'box'},
GENERATED.connect(['A', 'B'], 'short'), GENERATED.connect(['A', 'B'], format('short')),
{type: 'agent end', agentNames: ['B'], mode: 'cross'}, {type: 'agent end', agentIDs: ['B'], mode: 'cross'},
{type: 'agent begin', agentNames: ['C'], mode: 'box'}, {type: 'agent begin', agentIDs: ['C'], mode: 'box'},
GENERATED.connect(['A', 'C'], 'long description here'), GENERATED.connect(['A', 'C'], format('long description')),
{type: 'agent end', agentNames: ['C'], mode: 'cross'}, {type: 'agent end', agentIDs: ['C'], mode: 'cross'},
{type: 'agent begin', agentNames: ['D'], mode: 'box'}, {type: 'agent begin', agentIDs: ['D'], mode: 'box'},
GENERATED.connect(['A', 'D'], 'short again'), GENERATED.connect(['A', 'D'], format('short again')),
{type: 'agent end', agentNames: ['A', 'D'], mode: 'cross'}, {type: 'agent end', agentIDs: ['A', 'D'], mode: 'cross'},
], ],
}); });

View File

@ -5,6 +5,7 @@ define([
'./Generator', './Generator',
'./Renderer', './Renderer',
'./Exporter', './Exporter',
'./CodeMirrorHints',
'./themes/BaseTheme', './themes/BaseTheme',
'./themes/Basic', './themes/Basic',
'./themes/Monospace', './themes/Monospace',
@ -16,6 +17,7 @@ define([
Generator, Generator,
Renderer, Renderer,
Exporter, Exporter,
CMHints,
BaseTheme, BaseTheme,
BasicTheme, BasicTheme,
MonospaceTheme, MonospaceTheme,
@ -36,14 +38,13 @@ define([
const SharedParser = new Parser(); const SharedParser = new Parser();
const SharedGenerator = new Generator(); const SharedGenerator = new Generator();
const CMMode = SharedParser.getCodeMirrorMode(); const CMMode = SharedParser.getCodeMirrorMode();
const CMHints = SharedParser.getCodeMirrorHints();
function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') { function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') {
if(!CodeMirror) { if(!CodeMirror) {
CodeMirror = window.CodeMirror; CodeMirror = window.CodeMirror;
} }
CodeMirror.defineMode(modeName, () => CMMode); CodeMirror.defineMode(modeName, () => CMMode);
CodeMirror.registerHelper('hint', modeName, CMHints); CodeMirror.registerHelper('hint', modeName, CMHints.getHints);
} }
function addTheme(theme) { function addTheme(theme) {

View File

@ -12,10 +12,10 @@ define([
'use strict'; 'use strict';
class CapBox { class CapBox {
separation({label}, env) { separation({formattedLabel}, env) {
const config = env.theme.agentCap.box; const config = env.theme.agentCap.box;
const width = ( const width = (
env.textSizer.measure(config.labelAttrs, label).width + env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left + config.padding.left +
config.padding.right config.padding.right
); );
@ -27,20 +27,20 @@ define([
}; };
} }
topShift({label}, env) { topShift({formattedLabel}, env) {
const config = env.theme.agentCap.box; const config = env.theme.agentCap.box;
const height = ( const height = (
env.textSizer.measureHeight(config.labelAttrs, label) + env.textSizer.measureHeight(config.labelAttrs, formattedLabel) +
config.padding.top + config.padding.top +
config.padding.bottom config.padding.bottom
); );
return Math.max(0, height - config.arrowBottom); return Math.max(0, height - config.arrowBottom);
} }
render(y, {x, label}, env) { render(y, {x, formattedLabel}, env) {
const config = env.theme.agentCap.box; const config = env.theme.agentCap.box;
const clickable = env.makeRegion(); const clickable = env.makeRegion();
const {width, height} = SVGShapes.renderBoxedText(label, { const {width, height} = SVGShapes.renderBoxedText(formattedLabel, {
x, x,
y, y,
padding: config.padding, padding: config.padding,
@ -105,10 +105,10 @@ define([
} }
class CapBar { class CapBar {
separation({label}, env) { separation({formattedLabel}, env) {
const config = env.theme.agentCap.box; const config = env.theme.agentCap.box;
const width = ( const width = (
env.textSizer.measure(config.labelAttrs, label).width + env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left + config.padding.left +
config.padding.right config.padding.right
); );
@ -125,17 +125,17 @@ define([
return config.height / 2; return config.height / 2;
} }
render(y, {x, label}, env) { render(y, {x, formattedLabel}, env) {
const configB = env.theme.agentCap.box; const boxCfg = env.theme.agentCap.box;
const config = env.theme.agentCap.bar; const barCfg = env.theme.agentCap.bar;
const width = ( const width = (
env.textSizer.measure(configB.labelAttrs, label).width + env.textSizer.measure(boxCfg.labelAttrs, formattedLabel).width +
configB.padding.left + boxCfg.padding.left +
configB.padding.right 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, x: x - width / 2,
y, y,
width, width,
@ -172,7 +172,7 @@ define([
return isBegin ? config.height : 0; return isBegin ? config.height : 0;
} }
render(y, {x, label}, env, isBegin) { render(y, {x}, env, isBegin) {
const config = env.theme.agentCap.fade; const config = env.theme.agentCap.fade;
const ratio = config.height / (config.height + config.extend); const ratio = config.height / (config.height + config.extend);
@ -266,12 +266,12 @@ define([
this.begin = begin; this.begin = begin;
} }
separationPre({mode, agentNames}, env) { separationPre({mode, agentIDs}, env) {
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode]; const cap = AGENT_CAPS[mode];
const sep = cap.separation(agentInfo, env, this.begin); const sep = cap.separation(agentInfo, env, this.begin);
env.addSpacing(name, sep); env.addSpacing(id, sep);
agentInfo.currentMaxRad = Math.max( agentInfo.currentMaxRad = Math.max(
agentInfo.currentMaxRad, agentInfo.currentMaxRad,
sep.radius sep.radius
@ -279,18 +279,18 @@ define([
}); });
} }
separation({mode, agentNames}, env) { separation({mode, agentIDs}, env) {
if(this.begin) { if(this.begin) {
array.mergeSets(env.visibleAgents, agentNames); array.mergeSets(env.visibleAgentIDs, agentIDs);
} else { } else {
array.removeAll(env.visibleAgents, agentNames); array.removeAll(env.visibleAgentIDs, agentIDs);
} }
} }
renderPre({mode, agentNames}, env) { renderPre({mode, agentIDs}, env) {
let maxTopShift = 0; let maxTopShift = 0;
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode]; const cap = AGENT_CAPS[mode];
const topShift = cap.topShift(agentInfo, env, this.begin); const topShift = cap.topShift(agentInfo, env, this.begin);
maxTopShift = Math.max(maxTopShift, topShift); maxTopShift = Math.max(maxTopShift, topShift);
@ -299,15 +299,15 @@ define([
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
}); });
return { return {
agentNames, agentIDs,
topShift: maxTopShift, topShift: maxTopShift,
}; };
} }
render({mode, agentNames}, env) { render({mode, agentIDs}, env) {
let maxEnd = 0; let maxEnd = 0;
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode]; const cap = AGENT_CAPS[mode];
const topShift = cap.topShift(agentInfo, env, this.begin); const topShift = cap.topShift(agentInfo, env, this.begin);
const y0 = env.primaryY - topShift; const y0 = env.primaryY - topShift;
@ -319,9 +319,9 @@ define([
); );
maxEnd = Math.max(maxEnd, y0 + shifts.height); maxEnd = Math.max(maxEnd, y0 + shifts.height);
if(this.begin) { if(this.begin) {
env.drawAgentLine(name, y0 + shifts.lineBottom); env.drawAgentLine(id, y0 + shifts.lineBottom);
} else { } else {
env.drawAgentLine(name, y0 + shifts.lineTop, true); env.drawAgentLine(id, y0 + shifts.lineTop, true);
} }
}); });
return maxEnd + env.theme.actionMargin; return maxEnd + env.theme.actionMargin;

View File

@ -6,28 +6,28 @@ define(['./BaseComponent'], (BaseComponent) => {
return highlighted ? env.theme.agentLineHighlightRadius : 0; return highlighted ? env.theme.agentLineHighlightRadius : 0;
} }
separationPre({agentNames, highlighted}, env) { separationPre({agentIDs, highlighted}, env) {
const r = this.radius(highlighted, env); const r = this.radius(highlighted, env);
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(id);
agentInfo.currentRad = r; agentInfo.currentRad = r;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
}); });
} }
renderPre({agentNames, highlighted}, env) { renderPre({agentIDs, highlighted}, env) {
const r = this.radius(highlighted, env); const r = this.radius(highlighted, env);
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(name); const agentInfo = env.agentInfos.get(id);
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r); agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
}); });
} }
render({agentNames, highlighted}, env) { render({agentIDs, highlighted}, env) {
const r = this.radius(highlighted, env); const r = this.radius(highlighted, env);
agentNames.forEach((name) => { agentIDs.forEach((id) => {
env.drawAgentLine(name, env.primaryY); env.drawAgentLine(id, env.primaryY);
env.agentInfos.get(name).currentRad = r; env.agentInfos.get(id).currentRad = r;
}); });
return env.primaryY + env.theme.actionMargin; return env.primaryY + env.theme.actionMargin;
} }

View File

@ -28,7 +28,7 @@ defineDescribe('AgentHighlight', [
theme, theme,
agentInfos, agentInfos,
}; };
highlight.separationPre({agentNames: ['foo'], highlighted: true}, env); highlight.separationPre({agentIDs: ['foo'], highlighted: true}, env);
expect(agentInfo.currentRad).toEqual(2); expect(agentInfo.currentRad).toEqual(2);
expect(agentInfo.currentMaxRad).toEqual(2); expect(agentInfo.currentMaxRad).toEqual(2);
}); });
@ -41,7 +41,7 @@ defineDescribe('AgentHighlight', [
theme, theme,
agentInfos, agentInfos,
}; };
highlight.separationPre({agentNames: ['foo'], highlighted: true}, env); highlight.separationPre({agentIDs: ['foo'], highlighted: true}, env);
expect(agentInfo.currentRad).toEqual(2); expect(agentInfo.currentRad).toEqual(2);
expect(agentInfo.currentMaxRad).toEqual(3); expect(agentInfo.currentMaxRad).toEqual(3);
}); });
@ -54,7 +54,7 @@ defineDescribe('AgentHighlight', [
theme, theme,
agentInfos, agentInfos,
}; };
highlight.separationPre({agentNames: ['foo'], highlighted: false}, env); highlight.separationPre({agentIDs: ['foo'], highlighted: false}, env);
expect(agentInfo.currentRad).toEqual(0); expect(agentInfo.currentRad).toEqual(0);
expect(agentInfo.currentMaxRad).toEqual(1); expect(agentInfo.currentMaxRad).toEqual(1);
}); });

View File

@ -12,7 +12,7 @@ define(() => {
separationPre(/*stage, { separationPre(/*stage, {
theme, theme,
agentInfos, agentInfos,
visibleAgents, visibleAgentIDs,
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
@ -24,7 +24,7 @@ define(() => {
separation(/*stage, { separation(/*stage, {
theme, theme,
agentInfos, agentInfos,
visibleAgents, visibleAgentIDs,
textSizer, textSizer,
addSpacing, addSpacing,
addSeparation, addSeparation,
@ -40,7 +40,7 @@ define(() => {
state, state,
components, components,
}*/) { }*/) {
// return {topShift, agentNames, asynchronousY} // return {topShift, agentIDs, asynchronousY}
} }
render(/*stage, { render(/*stage, {
@ -64,12 +64,12 @@ define(() => {
BaseComponent.cleanRenderPreResult = ({ BaseComponent.cleanRenderPreResult = ({
topShift = 0, topShift = 0,
agentNames = [], agentIDs = [],
asynchronousY = null, asynchronousY = null,
} = {}, currentY = null) => { } = {}, currentY = null) => {
return { return {
topShift, topShift,
agentNames, agentIDs,
asynchronousY: (asynchronousY !== null) ? asynchronousY : currentY, asynchronousY: (asynchronousY !== null) ? asynchronousY : currentY,
}; };
}; };

View File

@ -12,13 +12,13 @@ define([
'use strict'; 'use strict';
class BlockSplit extends BaseComponent { class BlockSplit extends BaseComponent {
separation({left, right, mode, label}, env) { separation({left, right, tag, label}, env) {
const blockInfo = env.state.blocks.get(left); 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 = ( const width = (
env.textSizer.measure(config.mode.labelAttrs, mode).width + env.textSizer.measure(config.tag.labelAttrs, tag).width +
config.mode.padding.left + config.tag.padding.left +
config.mode.padding.right + config.tag.padding.right +
env.textSizer.measure(config.label.labelAttrs, label).width + env.textSizer.measure(config.label.labelAttrs, label).width +
config.label.padding.left + config.label.padding.left +
config.label.padding.right config.label.padding.right
@ -28,13 +28,13 @@ define([
renderPre({left, right}) { renderPre({left, right}) {
return { 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 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 agentInfoL = env.agentInfos.get(left);
const agentInfoR = env.agentInfos.get(right); const agentInfoR = env.agentInfos.get(right);
@ -46,20 +46,20 @@ define([
const clickable = env.makeRegion(); const clickable = env.makeRegion();
const modeRender = SVGShapes.renderBoxedText(mode, { const tagRender = SVGShapes.renderBoxedText(tag, {
x: agentInfoL.x, x: agentInfoL.x,
y, y,
padding: config.section.mode.padding, padding: config.section.tag.padding,
boxAttrs: config.section.mode.boxAttrs, boxAttrs: config.section.tag.boxAttrs,
boxRenderer: config.section.mode.boxRenderer, boxRenderer: config.section.tag.boxRenderer,
labelAttrs: config.section.mode.labelAttrs, labelAttrs: config.section.tag.labelAttrs,
boxLayer: blockInfo.hold, boxLayer: blockInfo.hold,
labelLayer: clickable, labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass, SVGTextBlockClass: env.SVGTextBlockClass,
}); });
const labelRender = SVGShapes.renderBoxedText(label, { const labelRender = SVGShapes.renderBoxedText(label, {
x: agentInfoL.x + modeRender.width, x: agentInfoL.x + tagRender.width,
y, y,
padding: config.section.label.padding, padding: config.section.label.padding,
boxAttrs: {'fill': '#000000'}, boxAttrs: {'fill': '#000000'},
@ -69,7 +69,7 @@ define([
SVGTextBlockClass: env.SVGTextBlockClass, SVGTextBlockClass: env.SVGTextBlockClass,
}); });
const labelHeight = Math.max(modeRender.height, labelRender.height); const labelHeight = Math.max(tagRender.height, labelRender.height);
clickable.insertBefore(svg.make('rect', { clickable.insertBefore(svg.make('rect', {
'x': agentInfoL.x, 'x': agentInfoL.x,
@ -102,11 +102,13 @@ define([
} }
storeBlockInfo(stage, env) { storeBlockInfo(stage, env) {
env.state.blocks.set(stage.left, { const blockInfo = {
mode: stage.mode, type: stage.blockType,
hold: null, hold: null,
startY: null, startY: null,
}); };
env.state.blocks.set(stage.left, blockInfo);
return blockInfo;
} }
separationPre(stage, env) { separationPre(stage, env) {
@ -114,17 +116,17 @@ define([
} }
separation(stage, env) { separation(stage, env) {
array.mergeSets(env.visibleAgents, [stage.left, stage.right]); array.mergeSets(env.visibleAgentIDs, [stage.left, stage.right]);
super.separation(stage, env); super.separation(stage, env);
} }
renderPre(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 { return {
agentNames: [stage.left, stage.right], agentIDs: [stage.left, stage.right],
topShift: config.margin.top, topShift: config.margin.top,
}; };
} }
@ -143,22 +145,22 @@ define([
class BlockEnd extends BaseComponent { class BlockEnd extends BaseComponent {
separation({left, right}, env) { separation({left, right}, env) {
array.removeAll(env.visibleAgents, [left, right]); array.removeAll(env.visibleAgentIDs, [left, right]);
} }
renderPre({left, right}, env) { renderPre({left, right}, env) {
const blockInfo = env.state.blocks.get(left); const blockInfo = env.state.blocks.get(left);
const config = env.theme.getBlock(blockInfo.mode); const config = env.theme.getBlock(blockInfo.type);
return { return {
agentNames: [left, right], agentIDs: [left, right],
topShift: config.section.padding.bottom, topShift: config.section.padding.bottom,
}; };
} }
render({left, right}, env) { render({left, right}, env) {
const blockInfo = env.state.blocks.get(left); 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 agentInfoL = env.agentInfos.get(left);
const agentInfoR = env.agentInfos.get(right); const agentInfoR = env.agentInfos.get(right);

View File

@ -77,7 +77,7 @@ define([
]; ];
class Connect extends BaseComponent { class Connect extends BaseComponent {
separation({label, agentNames, options}, env) { separation({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
const lArrow = ARROWHEADS[options.left]; const lArrow = ARROWHEADS[options.left];
@ -90,9 +90,9 @@ define([
labelWidth += config.label.padding * 2; labelWidth += config.label.padding * 2;
} }
const info1 = env.agentInfos.get(agentNames[0]); const info1 = env.agentInfos.get(agentIDs[0]);
if(agentNames[0] === agentNames[1]) { if(agentIDs[0] === agentIDs[1]) {
env.addSpacing(agentNames[0], { env.addSpacing(agentIDs[0], {
left: 0, left: 0,
right: ( right: (
info1.currentMaxRad + info1.currentMaxRad +
@ -104,10 +104,10 @@ define([
), ),
}); });
} else { } else {
const info2 = env.agentInfos.get(agentNames[1]); const info2 = env.agentInfos.get(agentIDs[1]);
env.addSeparation( env.addSeparation(
agentNames[0], agentIDs[0],
agentNames[1], agentIDs[1],
info1.currentMaxRad + info1.currentMaxRad +
info2.currentMaxRad + info2.currentMaxRad +
@ -120,10 +120,10 @@ define([
} }
} }
renderSelfConnect({label, agentNames, options}, env) { renderSelfConnect({label, agentIDs, options}, env) {
/* jshint -W071 */ // TODO: find appropriate abstractions /* jshint -W071 */ // TODO: find appropriate abstractions
const config = env.theme.connect; 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 lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right]; const rArrow = ARROWHEADS[options.right];
@ -195,10 +195,10 @@ define([
); );
} }
renderSimpleConnect({label, agentNames, options}, env) { renderSimpleConnect({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
const from = env.agentInfos.get(agentNames[0]); const from = env.agentInfos.get(agentIDs[0]);
const to = env.agentInfos.get(agentNames[1]); const to = env.agentInfos.get(agentIDs[1]);
const lArrow = ARROWHEADS[options.left]; const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right]; const rArrow = ARROWHEADS[options.right];
@ -260,7 +260,7 @@ define([
); );
} }
renderPre({label, agentNames, options}, env) { renderPre({label, agentIDs, options}, env) {
const config = env.theme.connect; const config = env.theme.connect;
const lArrow = ARROWHEADS[options.left]; const lArrow = ARROWHEADS[options.left];
@ -273,18 +273,18 @@ define([
); );
let arrowH = lArrow.height(env.theme); let arrowH = lArrow.height(env.theme);
if(agentNames[0] !== agentNames[1]) { if(agentIDs[0] !== agentIDs[1]) {
arrowH = Math.max(arrowH, rArrow.height(env.theme)); arrowH = Math.max(arrowH, rArrow.height(env.theme));
} }
return { return {
agentNames, agentIDs,
topShift: Math.max(arrowH / 2, height), topShift: Math.max(arrowH / 2, height),
}; };
} }
render(stage, env) { render(stage, env) {
if(stage.agentNames[0] === stage.agentNames[1]) { if(stage.agentIDs[0] === stage.agentIDs[1]) {
return this.renderSelfConnect(stage, env); return this.renderSelfConnect(stage, env);
} else { } else {
return this.renderSimpleConnect(stage, env); return this.renderSimpleConnect(stage, env);

View File

@ -1,11 +1,11 @@
define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => { define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
'use strict'; 'use strict';
function findExtremes(agentInfos, agentNames) { function findExtremes(agentInfos, agentIDs) {
let min = null; let min = null;
let max = null; let max = null;
agentNames.forEach((name) => { agentIDs.forEach((id) => {
const info = agentInfos.get(name); const info = agentInfos.get(id);
if(min === null || info.index < min.index) { if(min === null || info.index < min.index) {
min = info; min = info;
} }
@ -14,14 +14,14 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
} }
}); });
return { return {
left: min.label, left: min.id,
right: max.label, right: max.id,
}; };
} }
class NoteComponent extends BaseComponent { class NoteComponent extends BaseComponent {
renderPre({agentNames}) { renderPre({agentIDs}) {
return {agentNames}; return {agentIDs};
} }
renderNote({ renderNote({
@ -39,7 +39,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
const y = env.topY + config.margin.top + config.padding.top; const y = env.topY + config.margin.top + config.padding.top;
const labelNode = new env.SVGTextBlockClass(clickable, { const labelNode = new env.SVGTextBlockClass(clickable, {
attrs: config.labelAttrs, attrs: config.labelAttrs,
text: label, formatted: label,
y, y,
}); });
@ -105,7 +105,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
} }
class NoteOver extends NoteComponent { class NoteOver extends NoteComponent {
separation({agentNames, mode, label}, env) { separation({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode); const config = env.theme.getNote(mode);
const width = ( const width = (
env.textSizer.measure(config.labelAttrs, label).width + env.textSizer.measure(config.labelAttrs, label).width +
@ -113,7 +113,7 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
config.padding.right config.padding.right
); );
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentIDs);
const infoL = env.agentInfos.get(left); const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
if(infoL !== infoR) { if(infoL !== infoR) {
@ -132,10 +132,10 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
} }
} }
render({agentNames, mode, label}, env) { render({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode); 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 infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
if(infoL !== infoR) { if(infoL !== infoR) {
@ -164,9 +164,9 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
this.isRight = isRight; this.isRight = isRight;
} }
separation({agentNames, mode, label}, env) { separation({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode); const config = env.theme.getNote(mode);
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentIDs);
const width = ( const width = (
env.textSizer.measure(config.labelAttrs, label).width + env.textSizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
@ -190,9 +190,9 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
} }
} }
render({agentNames, mode, label}, env) { render({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode); const config = env.theme.getNote(mode);
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentIDs);
if(this.isRight) { if(this.isRight) {
const info = env.agentInfos.get(right); const info = env.agentInfos.get(right);
const x0 = info.x + info.currentMaxRad + config.margin.left; const x0 = info.x + info.currentMaxRad + config.margin.left;
@ -216,9 +216,9 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
} }
class NoteBetween extends NoteComponent { class NoteBetween extends NoteComponent {
separation({agentNames, mode, label}, env) { separation({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode); 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 infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
@ -236,8 +236,8 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
); );
} }
render({agentNames, mode, label}, env) { render({agentIDs, mode, label}, env) {
const {left, right} = findExtremes(env.agentInfos, agentNames); const {left, right} = findExtremes(env.agentInfos, agentIDs);
const infoL = env.agentInfos.get(left); const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right); const infoR = env.agentInfos.get(right);
const xMid = ( const xMid = (

View File

@ -18,10 +18,10 @@ define([
} }
function mergeResults(a, b) { function mergeResults(a, b) {
array.mergeSets(a.agentNames, b.agentNames); array.mergeSets(a.agentIDs, b.agentIDs);
return { return {
topShift: Math.max(a.topShift, b.topShift), topShift: Math.max(a.topShift, b.topShift),
agentNames: a.agentNames, agentIDs: a.agentIDs,
asynchronousY: nullableMax(a.asynchronousY, b.asynchronousY), asynchronousY: nullableMax(a.asynchronousY, b.asynchronousY),
}; };
} }
@ -42,7 +42,7 @@ define([
renderPre(stage, env) { renderPre(stage, env) {
const baseResults = { const baseResults = {
topShift: 0, topShift: 0,
agentNames: [], agentIDs: [],
asynchronousY: null, asynchronousY: null,
}; };

View File

@ -171,7 +171,7 @@ define([
top: 3, top: 3,
bottom: 2, bottom: 2,
}, },
mode: { tag: {
padding: { padding: {
top: 1, top: 1,
left: 3, left: 3,

View File

@ -180,7 +180,7 @@ define([
top: 3, top: 3,
bottom: 4, bottom: 4,
}, },
mode: { tag: {
padding: { padding: {
top: 2, top: 2,
left: 5, left: 5,

View File

@ -178,7 +178,7 @@ define([
top: 3, top: 3,
bottom: 2, bottom: 2,
}, },
mode: { tag: {
padding: { padding: {
top: 2, top: 2,
left: 4, left: 4,

View File

@ -164,7 +164,7 @@ define([
top: 3, top: 3,
bottom: 2, bottom: 2,
}, },
mode: { tag: {
padding: { padding: {
top: 2, top: 2,
left: 3, left: 3,
@ -376,8 +376,8 @@ define([
this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this); this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this);
this.blocks[''].boxRenderer = this.renderBlock.bind(this); this.blocks[''].boxRenderer = this.renderBlock.bind(this);
this.blocks.ref.section.mode.boxRenderer = this.renderTag; this.blocks.ref.section.tag.boxRenderer = this.renderTag;
this.blocks[''].section.mode.boxRenderer = this.renderTag; this.blocks[''].section.tag.boxRenderer = this.renderTag;
this.blocks[''].sepRenderer = this.renderSeparator.bind(this); this.blocks[''].sepRenderer = this.renderSeparator.bind(this);
} }

View File

@ -9,6 +9,7 @@ define([
'sequence/SequenceDiagram_spec', 'sequence/SequenceDiagram_spec',
'sequence/Tokeniser_spec', 'sequence/Tokeniser_spec',
'sequence/Parser_spec', 'sequence/Parser_spec',
'sequence/MarkdownParser_spec',
'sequence/LabelPatternParser_spec', 'sequence/LabelPatternParser_spec',
'sequence/Generator_spec', 'sequence/Generator_spec',
'sequence/Renderer_spec', 'sequence/Renderer_spec',

View File

@ -14,12 +14,14 @@ define(['svg/SVGUtilities'], (svg) => {
} }
} }
const EMPTY = [];
class SVGTextBlock { class SVGTextBlock {
constructor(container, initialState = {}) { constructor(container, initialState = {}) {
this.container = container; this.container = container;
this.state = { this.state = {
attrs: {}, attrs: {},
text: '', formatted: EMPTY,
x: 0, x: 0,
y: 0, y: 0,
}; };
@ -57,18 +59,19 @@ define(['svg/SVGUtilities'], (svg) => {
} }
_renderText() { _renderText() {
if(!this.state.text) { if(!this.state.formatted) {
this._reset(); this._reset();
return; return;
} }
const lines = this.state.text.split('\n'); const formatted = this.state.formatted;
this._rebuildNodes(lines.length); this._rebuildNodes(formatted.length);
let maxWidth = 0; let maxWidth = 0;
this.nodes.forEach(({text, element}, i) => { this.nodes.forEach(({text, element}, i) => {
text.nodeValue = lines[i]; const ln = formatted[i].reduce((v, pt) => v + pt.text, '');
maxWidth = Math.max(maxWidth, lines[i].length); text.nodeValue = ln;
maxWidth = Math.max(maxWidth, ln.length);
}); });
this.width = maxWidth; this.width = maxWidth;
} }
@ -100,12 +103,12 @@ define(['svg/SVGUtilities'], (svg) => {
if(this.state.attrs !== oldState.attrs) { if(this.state.attrs !== oldState.attrs) {
this._reset(); this._reset();
oldState.text = ''; oldState.formatted = EMPTY;
} }
const oldNodes = this.nodes.length; const oldNodes = this.nodes.length;
if(this.state.text !== oldState.text) { if(this.state.formatted !== oldState.formatted) {
this._renderText(); this._renderText();
} }
@ -120,29 +123,29 @@ define(['svg/SVGUtilities'], (svg) => {
} }
class SizeTester { class SizeTester {
measure(attrs, content) { measure(attrs, formatted) {
if(!content) { if(!formatted || !formatted.length) {
return {width: 0, height: 0}; return {width: 0, height: 0};
} }
const lines = content.split('\n');
let width = 0; let width = 0;
lines.forEach((line) => { formatted.forEach((line) => {
width = Math.max(width, line.length); const length = line.reduce((v, pt) => v + pt.text.length, 0);
width = Math.max(width, length);
}); });
return { return {
width, width,
height: lines.length, height: formatted.length,
}; };
} }
measureHeight(attrs, content) { measureHeight(attrs, formatted) {
if(!content) { if(!formatted) {
return 0; return 0;
} }
return content.split('\n').length; return formatted.length;
} }
resetCache() { resetCache() {

View File

@ -46,24 +46,10 @@ define([
return g; return g;
} }
function renderBoxedText(text, { function calculateAnchor(x, attrs, padding) {
x,
y,
padding,
boxAttrs,
labelAttrs,
boxLayer,
labelLayer,
boxRenderer = null,
SVGTextBlockClass = SVGTextBlock,
}) {
if(!text) {
return {width: 0, height: 0, label: null, box: null};
}
let shift = 0; let shift = 0;
let anchorX = x; let anchorX = x;
switch(labelAttrs['text-anchor']) { switch(attrs['text-anchor']) {
case 'middle': case 'middle':
shift = 0.5; shift = 0.5;
anchorX += (padding.left - padding.right) / 2; anchorX += (padding.left - padding.right) / 2;
@ -77,10 +63,29 @@ define([
anchorX += padding.left; anchorX += padding.left;
break; 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, { const label = new SVGTextBlockClass(labelLayer, {
attrs: labelAttrs, attrs: labelAttrs,
text, formatted,
x: anchorX, x: anchorX,
y: y + padding.top, y: y + padding.top,
}); });

View File

@ -56,7 +56,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
describe('renderBoxedText', () => { describe('renderBoxedText', () => {
it('renders a label', () => { it('renders a label', () => {
const o = document.createElement('p'); const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText('foo', { const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
x: 1, x: 1,
y: 2, y: 2,
padding: {left: 4, top: 8, right: 16, bottom: 32}, padding: {left: 4, top: 8, right: 16, bottom: 32},
@ -65,7 +65,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
boxLayer: o, boxLayer: o,
labelLayer: o, labelLayer: o,
}); });
expect(rendered.label.state.text).toEqual('foo'); expect(rendered.label.state.formatted).toEqual([[{text: 'foo'}]]);
expect(rendered.label.state.x).toEqual(5); expect(rendered.label.state.x).toEqual(5);
expect(rendered.label.state.y).toEqual(10); expect(rendered.label.state.y).toEqual(10);
expect(rendered.label.firstLine().parentNode).toEqual(o); expect(rendered.label.firstLine().parentNode).toEqual(o);
@ -73,7 +73,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
it('positions a box beneath the rendered label', () => { it('positions a box beneath the rendered label', () => {
const o = document.createElement('p'); const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText('foo', { const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
x: 1, x: 1,
y: 2, y: 2,
padding: {left: 4, top: 8, right: 16, bottom: 32}, padding: {left: 4, top: 8, right: 16, bottom: 32},
@ -91,7 +91,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
it('returns the size of the rendered box', () => { it('returns the size of the rendered box', () => {
const o = document.createElement('p'); const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText('foo', { const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
x: 1, x: 1,
y: 2, y: 2,
padding: {left: 4, top: 8, right: 16, bottom: 32}, padding: {left: 4, top: 8, right: 16, bottom: 32},

View File

@ -23,84 +23,107 @@ define(['./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 { class SVGTextBlock {
constructor(container, initialState = {}) { constructor(container, initialState = {}) {
this.container = container; this.container = container;
this.state = { this.state = {
attrs: {}, attrs: {},
text: '', formatted: EMPTY,
x: 0, x: 0,
y: 0, y: 0,
}; };
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
this.nodes = []; this.lines = [];
this.set(initialState); this.set(initialState);
} }
_rebuildNodes(count) { _rebuildLines(count) {
if(count > this.nodes.length) { if(count > this.lines.length) {
const attrs = Object.assign({ const attrs = Object.assign({
'x': this.state.x, 'x': this.state.x,
}, this.state.attrs); }, this.state.attrs);
while(this.nodes.length < count) { while(this.lines.length < count) {
const element = svg.make('text', attrs); const node = svg.make('text', attrs);
const text = svg.makeText(); this.container.appendChild(node);
element.appendChild(text); this.lines.push({node, latest: ''});
this.container.appendChild(element);
this.nodes.push({element, text});
} }
} else { } else {
while(this.nodes.length > count) { while(this.lines.length > count) {
const {element} = this.nodes.pop(); const {node} = this.lines.pop();
this.container.removeChild(element); this.container.removeChild(node);
} }
} }
} }
_reset() { _reset() {
this._rebuildNodes(0); this._rebuildLines(0);
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
} }
_renderText() { _renderText() {
if(!this.state.text) { const {formatted} = this.state;
if(!formatted || !formatted.length) {
this._reset(); this._reset();
return; return;
} }
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
const lines = this.state.text.split('\n'); this._rebuildLines(formatted.length);
this._rebuildNodes(lines.length);
let maxWidth = 0; let maxWidth = 0;
this.nodes.forEach(({text, element}, i) => { this.lines.forEach((ln, i) => {
if(text.nodeValue !== lines[i]) { const id = JSON.stringify(formatted[i]);
text.nodeValue = lines[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; this.width = maxWidth;
} }
_updateX() { _updateX() {
this.nodes.forEach(({element}) => { this.lines.forEach(({node}) => {
element.setAttribute('x', this.state.x); node.setAttribute('x', this.state.x);
}); });
} }
_updateY() { _updateY() {
const {size, lineHeight} = fontDetails(this.state.attrs); const {size, lineHeight} = fontDetails(this.state.attrs);
this.nodes.forEach(({element}, i) => { this.lines.forEach(({node}, i) => {
element.setAttribute('y', this.state.y + i * lineHeight + size); node.setAttribute('y', this.state.y + i * lineHeight + size);
}); });
this.height = lineHeight * this.nodes.length; this.height = lineHeight * this.lines.length;
} }
firstLine() { firstLine() {
if(this.nodes.length > 0) { if(this.lines.length > 0) {
return this.nodes[0].element; return this.lines[0].node;
} else { } else {
return null; return null;
} }
@ -112,12 +135,12 @@ define(['./SVGUtilities'], (svg) => {
if(this.state.attrs !== oldState.attrs) { if(this.state.attrs !== oldState.attrs) {
this._reset(); 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(); this._renderText();
} }
@ -125,7 +148,7 @@ define(['./SVGUtilities'], (svg) => {
this._updateX(); this._updateX();
} }
if(this.state.y !== oldState.y || this.nodes.length !== oldNodes) { if(this.state.y !== oldState.y || this.lines.length !== oldLines) {
this._updateY(); this._updateY();
} }
} }
@ -142,18 +165,18 @@ define(['./SVGUtilities'], (svg) => {
this.cache = new Map(); this.cache = new Map();
} }
measure(attrs, content) { measure(attrs, formatted) {
if(!content) { if(!formatted || !formatted.length) {
return {width: 0, height: 0}; return {width: 0, height: 0};
} }
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
let tester = this.cache.get(attrs); let tester = this.cache.get(attrs);
if(!tester) { if(!tester) {
const text = svg.makeText(); tester = svg.make('text', attrs);
const node = svg.make('text', attrs); this.testers.appendChild(tester);
node.appendChild(text);
this.testers.appendChild(node);
tester = {text, node};
this.cache.set(attrs, tester); this.cache.set(attrs, tester);
} }
@ -161,26 +184,28 @@ define(['./SVGUtilities'], (svg) => {
this.container.appendChild(this.testers); this.container.appendChild(this.testers);
} }
const lines = content.split('\n');
let width = 0; let width = 0;
lines.forEach((line) => { formatted.forEach((line) => {
tester.text.nodeValue = line; svg.empty(tester);
width = Math.max(width, tester.node.getComputedTextLength()); populateSvgTextLine(tester, line);
width = Math.max(width, tester.getComputedTextLength());
}); });
return { return {
width, width,
height: lines.length * fontDetails(attrs).lineHeight, height: formatted.length * fontDetails(attrs).lineHeight,
}; };
} }
measureHeight(attrs, content) { measureHeight(attrs, formatted) {
if(!content) { if(!formatted) {
return 0; return 0;
} }
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
const lines = content.split('\n'); return formatted.length * fontDetails(attrs).lineHeight;
return lines.length * fontDetails(attrs).lineHeight;
} }
resetCache() { resetCache() {

View File

@ -23,7 +23,7 @@ defineDescribe('SVGTextBlock', [
describe('constructor', () => { describe('constructor', () => {
it('defaults to blank text at 0, 0', () => { it('defaults to blank text at 0, 0', () => {
expect(block.state.text).toEqual(''); expect(block.state.formatted).toEqual([]);
expect(block.state.x).toEqual(0); expect(block.state.x).toEqual(0);
expect(block.state.y).toEqual(0); expect(block.state.y).toEqual(0);
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
@ -31,15 +31,18 @@ defineDescribe('SVGTextBlock', [
it('does not explode if given no setup', () => { it('does not explode if given no setup', () => {
block = new SVGTextBlock(hold); block = new SVGTextBlock(hold);
expect(block.state.text).toEqual(''); expect(block.state.formatted).toEqual([]);
expect(block.state.x).toEqual(0); expect(block.state.x).toEqual(0);
expect(block.state.y).toEqual(0); expect(block.state.y).toEqual(0);
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
}); });
it('adds the given text if specified', () => { it('adds the given formatted text if specified', () => {
block = new SVGTextBlock(hold, {attrs, text: 'abc'}); block = new SVGTextBlock(hold, {
expect(block.state.text).toEqual('abc'); attrs,
formatted: [[{text: 'abc'}]],
});
expect(block.state.formatted).toEqual([[{text: 'abc'}]]);
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
}); });
@ -52,31 +55,35 @@ defineDescribe('SVGTextBlock', [
describe('.set', () => { describe('.set', () => {
it('sets the text to the given content', () => { it('sets the text to the given content', () => {
block.set({text: 'foo'}); block.set({formatted: [[{text: 'foo'}]]});
expect(block.state.text).toEqual('foo'); expect(block.state.formatted).toEqual([[{text: 'foo'}]]);
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
expect(hold.children[0].innerHTML).toEqual('foo'); expect(hold.children[0].innerHTML).toEqual('foo');
}); });
it('renders multiline text', () => { it('renders multiline text', () => {
block.set({text: 'foo\nbar'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
expect(hold.children.length).toEqual(2); expect(hold.children.length).toEqual(2);
expect(hold.children[0].innerHTML).toEqual('foo'); expect(hold.children[0].innerHTML).toEqual('foo');
expect(hold.children[1].innerHTML).toEqual('bar'); expect(hold.children[1].innerHTML).toEqual('bar');
}); });
it('populates width and height with the size of the text', () => { it('populates width and height with the size of the text', () => {
block.set({text: 'foo\nbar'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
expect(block.width).toBeGreaterThan(0); expect(block.width).toBeGreaterThan(0);
expect(block.height).toEqual(30); expect(block.height).toEqual(30);
}); });
it('re-uses text nodes when possible, adding more if needed', () => { it('re-uses text nodes when possible, adding more if needed', () => {
block.set({text: 'foo\nbar'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
const line0 = hold.children[0]; const line0 = hold.children[0];
const line1 = hold.children[1]; const line1 = hold.children[1];
block.set({text: 'zig\nzag\nbaz'}); block.set({formatted: [
[{text: 'zig'}],
[{text: 'zag'}],
[{text: 'baz'}],
]});
expect(hold.children.length).toEqual(3); expect(hold.children.length).toEqual(3);
expect(hold.children[0]).toEqual(line0); expect(hold.children[0]).toEqual(line0);
@ -87,10 +94,10 @@ defineDescribe('SVGTextBlock', [
}); });
it('re-uses text nodes when possible, removing extra if needed', () => { it('re-uses text nodes when possible, removing extra if needed', () => {
block.set({text: 'foo\nbar'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
const line0 = hold.children[0]; const line0 = hold.children[0];
block.set({text: 'zig'}); block.set({formatted: [[{text: 'zig'}]]});
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
expect(hold.children[0]).toEqual(line0); expect(hold.children[0]).toEqual(line0);
@ -98,7 +105,7 @@ defineDescribe('SVGTextBlock', [
}); });
it('positions text nodes and applies attributes', () => { it('positions text nodes and applies attributes', () => {
block.set({text: 'foo\nbar'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
expect(hold.children.length).toEqual(2); expect(hold.children.length).toEqual(2);
expect(hold.children[0].getAttribute('x')).toEqual('0'); expect(hold.children[0].getAttribute('x')).toEqual('0');
expect(hold.children[0].getAttribute('y')).toEqual('10'); expect(hold.children[0].getAttribute('y')).toEqual('10');
@ -109,7 +116,7 @@ defineDescribe('SVGTextBlock', [
}); });
it('moves all nodes', () => { it('moves all nodes', () => {
block.set({text: 'foo\nbaz'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
block.set({x: 5, y: 7}); block.set({x: 5, y: 7});
expect(hold.children[0].getAttribute('x')).toEqual('5'); expect(hold.children[0].getAttribute('x')).toEqual('5');
expect(hold.children[0].getAttribute('y')).toEqual('17'); expect(hold.children[0].getAttribute('y')).toEqual('17');
@ -118,10 +125,10 @@ defineDescribe('SVGTextBlock', [
}); });
it('clears if the text is empty', () => { it('clears if the text is empty', () => {
block.set({text: 'foo\nbaz'}); block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
block.set({text: ''}); block.set({formatted: []});
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
expect(block.state.text).toEqual(''); expect(block.state.formatted).toEqual([]);
expect(block.width).toEqual(0); expect(block.width).toEqual(0);
expect(block.height).toEqual(0); expect(block.height).toEqual(0);
}); });
@ -135,28 +142,39 @@ defineDescribe('SVGTextBlock', [
}); });
describe('.measure', () => { describe('.measure', () => {
it('calculates the size of the rendered text', () => { it('calculates the size of the formatted text', () => {
const size = tester.measure(attrs, 'foo'); const size = tester.measure(attrs, [[{text: 'foo'}]]);
expect(size.width).toBeGreaterThan(0); expect(size.width).toBeGreaterThan(0);
expect(size.height).toEqual(15); expect(size.height).toEqual(15);
}); });
it('measures multiline text', () => { it('measures multiline text', () => {
const size = tester.measure(attrs, 'foo\nbar'); const size = tester.measure(attrs, [
[{text: 'foo'}],
[{text: 'bar'}],
]);
expect(size.width).toBeGreaterThan(0); expect(size.width).toBeGreaterThan(0);
expect(size.height).toEqual(30); expect(size.height).toEqual(30);
}); });
it('returns 0, 0 for empty content', () => { it('returns 0, 0 for empty content', () => {
const size = tester.measure(attrs, ''); const size = tester.measure(attrs, []);
expect(size.width).toEqual(0); expect(size.width).toEqual(0);
expect(size.height).toEqual(0); expect(size.height).toEqual(0);
}); });
it('returns the maximum width for multiline text', () => { it('returns the maximum width for multiline text', () => {
const size0 = tester.measure(attrs, 'foo'); const size0 = tester.measure(attrs, [
const size1 = tester.measure(attrs, 'longline'); [{text: 'foo'}],
const size = tester.measure(attrs, 'foo\nlongline\nfoo'); ]);
const size1 = tester.measure(attrs, [
[{text: 'longline'}],
]);
const size = tester.measure(attrs, [
[{text: 'foo'}],
[{text: 'longline'}],
[{text: 'foo'}],
]);
expect(size1.width).toBeGreaterThan(size0.width); expect(size1.width).toBeGreaterThan(size0.width);
expect(size.width).toEqual(size1.width); expect(size.width).toEqual(size1.width);
}); });
@ -164,39 +182,44 @@ defineDescribe('SVGTextBlock', [
describe('.measureHeight', () => { describe('.measureHeight', () => {
it('calculates the height of the rendered text', () => { it('calculates the height of the rendered text', () => {
const height = tester.measureHeight(attrs, 'foo'); const height = tester.measureHeight(attrs, [
[{text: 'foo'}],
]);
expect(height).toEqual(15); expect(height).toEqual(15);
}); });
it('measures multiline text', () => { it('measures multiline text', () => {
const height = tester.measureHeight(attrs, 'foo\nbar'); const height = tester.measureHeight(attrs, [
[{text: 'foo'}],
[{text: 'bar'}],
]);
expect(height).toEqual(30); expect(height).toEqual(30);
}); });
it('returns 0 for empty content', () => { it('returns 0 for empty content', () => {
const height = tester.measureHeight(attrs, ''); const height = tester.measureHeight(attrs, []);
expect(height).toEqual(0); expect(height).toEqual(0);
}); });
it('does not require the container', () => { it('does not require the container', () => {
tester.measureHeight(attrs, 'foo'); tester.measureHeight(attrs, [[{text: 'foo'}]]);
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
}); });
}); });
describe('.detach', () => { describe('.detach', () => {
it('removes the test node from the DOM', () => { it('removes the test node from the DOM', () => {
tester.measure(attrs, 'foo'); tester.measure(attrs, [[{text: 'foo'}]]);
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
tester.detach(); tester.detach();
expect(hold.children.length).toEqual(0); expect(hold.children.length).toEqual(0);
}); });
it('does not prevent using the tester again later', () => { it('does not prevent using the tester again later', () => {
tester.measure(attrs, 'foo'); tester.measure(attrs, [[{text: 'foo'}]]);
tester.detach(); tester.detach();
const size = tester.measure(attrs, 'foo'); const size = tester.measure(attrs, [[{text: 'foo'}]]);
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
expect(size.width).toBeGreaterThan(0); expect(size.width).toBeGreaterThan(0);
}); });