Add simple agent options (red, database) [#36]

This commit is contained in:
David Evans 2018-02-14 00:43:00 +00:00
parent ab3d67f313
commit 8397810c12
20 changed files with 1063 additions and 353 deletions

View File

@ -604,7 +604,15 @@ define('core/ArrayUtilities',[],() => {
define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
'use strict';
const CM_ERROR = {type: 'error line-error', then: {'': 0}};
const CM_ERROR = {type: 'error line-error', suggest: false, then: {'': 0}};
function textTo(exit, suggest = false) {
return {
type: 'string',
suggest,
then: Object.assign({'': 0}, exit),
};
}
function suggestionsEqual(a, b) {
return (
@ -615,6 +623,11 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
);
}
const AGENT_INFO_TYPES = [
'database',
'red',
];
const makeCommands = ((() => {
// The order of commands inside "then" blocks directly influences the
// order they are displayed to the user in autocomplete menus.
@ -623,36 +636,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
// to use Map objects instead for strict compliance, at the cost of
// extra syntax.
const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', then: {}};
function textTo(exit, suggest) {
return {
type: 'string',
suggest,
then: Object.assign({'': 0}, exit),
};
}
const textToEnd = textTo({'\n': end});
const aliasListToEnd = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
'\n': end,
',': {type: 'operator', suggest: true, then: {'': 1}},
'as': {type: 'keyword', suggest: true, then: {
'': {type: 'variable', suggest: {known: 'Agent'}, then: {
'': 0,
',': {type: 'operator', suggest: true, then: {'': 3}},
'\n': end,
}},
}},
},
};
function agentListTo(exit) {
function agentListTo(exit, next = 1) {
return {
type: 'variable',
suggest: {known: 'Agent'},
@ -660,46 +644,37 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
exit,
{
'': 0,
',': {type: 'operator', suggest: true, then: {'': 1}},
',': {type: 'operator', then: {'': next}},
}
),
};
}
const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', suggest: false, then: {}};
const textToEnd = textTo({'\n': end});
const colonTextToEnd = {
type: 'operator',
suggest: true,
then: {'': textToEnd, '\n': hiddenEnd},
};
const agentListToText = agentListTo({
':': colonTextToEnd,
});
const agentList2ToText = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
',': {type: 'operator', suggest: true, then: {
'': agentListToText,
const aliasListToEnd = agentListTo({
'\n': end,
'as': {type: 'keyword', then: {
'': {type: 'variable', suggest: {known: 'Agent'}, then: {
'': 0,
',': {type: 'operator', then: {'': 3}},
'\n': end,
}},
':': CM_ERROR,
},
};
const singleAgentToText = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
',': CM_ERROR,
':': colonTextToEnd,
},
};
}},
});
const agentListToText = agentListTo({':': colonTextToEnd});
const agentToOptText = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
@ -707,9 +682,9 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
},
};
const referenceName = {
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textTo({
'as': {type: 'keyword', suggest: true, then: {
'as': {type: 'keyword', then: {
'': {
type: 'variable',
suggest: {known: 'Agent'},
@ -722,23 +697,23 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}),
}},
};
const refDef = {type: 'keyword', suggest: true, then: Object.assign({
'over': {type: 'keyword', suggest: true, then: {
const refDef = {type: 'keyword', then: Object.assign({
'over': {type: 'keyword', then: {
'': agentListTo(referenceName),
}},
}, referenceName)};
const divider = {
'\n': end,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
'with': {type: 'keyword', suggest: ['with height '], then: {
'height': {type: 'keyword', suggest: true, then: {
'height': {type: 'keyword', then: {
'': {type: 'number', suggest: ['6 ', '30 '], then: {
'\n': end,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
@ -747,15 +722,41 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}},
};
function simpleList(type, keywords, exit) {
const first = {};
const recur = Object.assign({}, exit);
keywords.forEach((keyword) => {
first[keyword] = {type, then: recur};
recur[keyword] = 0;
});
return first;
}
function optionalKeywords(type, keywords, then) {
const result = Object.assign({}, then);
keywords.forEach((keyword) => {
result[keyword] = {type, then};
});
return result;
}
const agentInfoList = optionalKeywords(
'keyword',
['a', 'an'],
simpleList('keyword', AGENT_INFO_TYPES, {'\n': end})
);
function makeSideNote(side) {
return {
type: 'keyword',
suggest: [side + ' of ', side + ': '],
then: {
'of': {type: 'keyword', suggest: true, then: {
'of': {type: 'keyword', then: {
'': agentListToText,
}},
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
}},
'': agentListToText,
@ -763,8 +764,8 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
};
}
function makeOpBlock(exit, sourceExit) {
const op = {type: 'operator', suggest: true, then: {
function makeOpBlock({exit, sourceExit, blankExit}) {
const op = {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': CM_ERROR,
@ -772,14 +773,14 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
'': exit,
}};
return {
'+': {type: 'operator', suggest: true, then: {
'+': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': CM_ERROR,
'': exit,
}},
'-': {type: 'operator', suggest: true, then: {
'-': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
@ -792,27 +793,29 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}},
'': exit,
}},
'*': {type: 'operator', suggest: true, then: Object.assign({
'*': {type: 'operator', then: Object.assign({
'+': op,
'-': op,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}, sourceExit)},
}, sourceExit || exit)},
'!': op,
'': exit,
'': blankExit || exit,
};
}
function makeCMConnect(arrows) {
const connect = {
type: 'keyword',
suggest: true,
then: Object.assign({}, makeOpBlock(agentToOptText, {
':': colonTextToEnd,
'\n': hiddenEnd,
then: Object.assign({}, makeOpBlock({
exit: agentToOptText,
sourceExit: {
':': colonTextToEnd,
'\n': hiddenEnd,
},
}), {
'...': {type: 'operator', suggest: true, then: {
'...': {type: 'operator', then: {
'': {
type: 'variable',
suggest: {known: 'DelayedAgent'},
@ -831,7 +834,6 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
const labelIndicator = {
type: 'operator',
suggest: true,
override: 'Label',
then: {},
};
@ -848,8 +850,9 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
suggest: {known: 'Agent'},
then: Object.assign({
'': 0,
}, connectors, {
':': labelIndicator,
}, connectors),
}),
};
const firstAgentDelayed = {
@ -861,29 +864,39 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}, connectors),
};
const firstAgentNoFlags = Object.assign({}, firstAgent, {
then: Object.assign({}, firstAgent.then, {
'is': {type: 'keyword', then: agentInfoList},
}),
});
return Object.assign({
'...': {type: 'operator', suggest: true, then: {
'...': {type: 'operator', then: {
'': firstAgentDelayed,
}},
}, makeOpBlock(firstAgent, Object.assign({
'': firstAgent,
':': hiddenLabelIndicator,
}, connectors)));
}, makeOpBlock({
exit: firstAgent,
sourceExit: Object.assign({
'': firstAgent,
':': hiddenLabelIndicator,
}, connectors),
blankExit: firstAgentNoFlags,
}));
}
const group = {type: 'keyword', suggest: true, then: {
const group = {type: 'keyword', then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
}},
'\n': end,
}};
const BASE_THEN = {
'title': {type: 'keyword', suggest: true, then: {
'title': {type: 'keyword', then: {
'': textToEnd,
}},
'theme': {type: 'keyword', suggest: true, then: {
'theme': {type: 'keyword', then: {
'': {
type: 'string',
suggest: {
@ -896,36 +909,36 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
},
},
}},
'headers': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}},
'cross': {type: 'keyword', suggest: true, then: {}},
'box': {type: 'keyword', suggest: true, then: {}},
'fade': {type: 'keyword', suggest: true, then: {}},
'bar': {type: 'keyword', suggest: true, then: {}},
'headers': {type: 'keyword', then: {
'none': {type: 'keyword', then: {}},
'cross': {type: 'keyword', then: {}},
'box': {type: 'keyword', then: {}},
'fade': {type: 'keyword', then: {}},
'bar': {type: 'keyword', then: {}},
}},
'terminators': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}},
'cross': {type: 'keyword', suggest: true, then: {}},
'box': {type: 'keyword', suggest: true, then: {}},
'fade': {type: 'keyword', suggest: true, then: {}},
'bar': {type: 'keyword', suggest: true, then: {}},
'terminators': {type: 'keyword', then: {
'none': {type: 'keyword', then: {}},
'cross': {type: 'keyword', then: {}},
'box': {type: 'keyword', then: {}},
'fade': {type: 'keyword', then: {}},
'bar': {type: 'keyword', then: {}},
}},
'divider': {type: 'keyword', suggest: true, then: Object.assign({
'line': {type: 'keyword', suggest: true, then: divider},
'space': {type: 'keyword', suggest: true, then: divider},
'delay': {type: 'keyword', suggest: true, then: divider},
'tear': {type: 'keyword', suggest: true, then: divider},
'divider': {type: 'keyword', then: Object.assign({
'line': {type: 'keyword', then: divider},
'space': {type: 'keyword', then: divider},
'delay': {type: 'keyword', then: divider},
'tear': {type: 'keyword', then: divider},
}, divider)},
'define': {type: 'keyword', suggest: true, then: {
'define': {type: 'keyword', then: {
'': aliasListToEnd,
'as': CM_ERROR,
}},
'begin': {type: 'keyword', suggest: true, then: {
'begin': {type: 'keyword', then: {
'': aliasListToEnd,
'reference': refDef,
'as': CM_ERROR,
}},
'end': {type: 'keyword', suggest: true, then: {
'end': {type: 'keyword', then: {
'': aliasListToEnd,
'as': CM_ERROR,
'\n': end,
@ -934,7 +947,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
'if': {type: 'keyword', suggest: 'if: ', then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
}},
}},
@ -942,39 +955,47 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}},
'repeat': group,
'group': group,
'note': {type: 'keyword', suggest: true, then: {
'over': {type: 'keyword', suggest: true, then: {
'note': {type: 'keyword', then: {
'over': {type: 'keyword', then: {
'': agentListToText,
}},
'left': makeSideNote('left'),
'right': makeSideNote('right'),
'between': {type: 'keyword', suggest: true, then: {
'': agentList2ToText,
'between': {type: 'keyword', then: {
'': agentListTo({':': CM_ERROR}, agentListToText),
}},
}},
'state': {type: 'keyword', suggest: 'state over ', then: {
'over': {type: 'keyword', suggest: true, then: {
'': singleAgentToText,
'over': {type: 'keyword', then: {
'': {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
',': CM_ERROR,
':': colonTextToEnd,
},
},
}},
}},
'text': {type: 'keyword', suggest: true, then: {
'text': {type: 'keyword', then: {
'left': makeSideNote('left'),
'right': makeSideNote('right'),
}},
'autolabel': {type: 'keyword', suggest: true, then: {
'off': {type: 'keyword', suggest: true, then: {}},
'autolabel': {type: 'keyword', then: {
'off': {type: 'keyword', then: {}},
'': textTo({'\n': end}, [
{v: '<label>', suffix: '\n', q: true},
{v: '[<inc>] <label>', suffix: '\n', q: true},
{v: '[<inc 1,0.01>] <label>', suffix: '\n', q: true},
]),
}},
'simultaneously': {type: 'keyword', suggest: true, then: {
':': {type: 'operator', suggest: true, then: {}},
'with': {type: 'keyword', suggest: true, then: {
'simultaneously': {type: 'keyword', then: {
':': {type: 'operator', then: {}},
'with': {type: 'keyword', then: {
'': {type: 'variable', suggest: {known: 'Label'}, then: {
'': 0,
':': {type: 'operator', suggest: true, then: {}},
':': {type: 'operator', then: {}},
}},
}},
}},
@ -1003,8 +1024,8 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
}
return array.flatMap(suggestions, (suggest) => {
if(suggest === true) {
return [cmCappedToken(token, current)];
if(suggest === false) {
return [];
} else if(typeof suggest === 'object') {
if(suggest.known) {
return state['known' + suggest.known] || [];
@ -1014,7 +1035,7 @@ define('sequence/CodeMirrorMode',['core/ArrayUtilities'], (array) => {
} else if(typeof suggest === 'string' && suggest) {
return [{v: suggest, q: (token === '')}];
} else {
return [];
return [cmCappedToken(token, current)];
}
});
}
@ -2213,6 +2234,31 @@ define('sequence/Parser',[
name: joinLabel(line, 0, line.length - 1),
};
},
(line) => { // options
const sepPos = findToken(line, 'is');
if(sepPos < 1) {
return null;
}
const indefiniteArticles = ['a', 'an'];
let optionsBegin = sepPos + 1;
if(indefiniteArticles.includes(tokenKeyword(line[optionsBegin]))) {
++ optionsBegin;
}
if(optionsBegin === line.length) {
throw makeError('Empty agent options', {b: array.last(line).e});
}
const agent = readAgent(line, 0, sepPos);
const options = [];
for(let i = optionsBegin; i < line.length; ++ i) {
options.push(line[i].v);
}
return {
type: 'agent options',
agent,
options,
};
},
];
function parseLine(line, {meta, stages}) {
@ -2309,7 +2355,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
return a.id === b.id;
},
make: (id, {anchorRight = false, isVirtualSource = false} = {}) => {
return {id, anchorRight, isVirtualSource};
return {id, anchorRight, isVirtualSource, options: []};
},
indexOf: (list, gAgent) => {
return array.indexOf(list, gAgent, GAgent.equals);
@ -2523,6 +2569,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
'mark': this.handleMark.bind(this),
'async': this.handleAsync.bind(this),
'agent define': this.handleAgentDefine.bind(this),
'agent options': this.handleAgentOptions.bind(this),
'agent begin': this.handleAgentBegin.bind(this),
'agent end': this.handleAgentEnd.bind(this),
'divider': this.handleDivider.bind(this),
@ -2626,23 +2673,23 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
validateGAgents(gAgents, {
allowGrouped = false,
rejectGrouped = false,
allowCovered = false,
allowVirtual = false,
} = {}) {
/* jshint -W074 */ // agent validity checking requires several steps
gAgents.forEach((gAgent) => {
const state = this.getGAgentState(gAgent);
if(state.covered) {
if(state.blocked && state.group === null) {
// used to be a group alias; can never be reused
throw new Error('Duplicate agent name: ' + gAgent.id);
}
if(!allowCovered && state.covered) {
throw new Error(
'Agent ' + gAgent.id + ' is hidden behind group'
);
}
if(rejectGrouped && state.group !== null) {
if(!allowGrouped && state.group !== null) {
throw new Error('Agent ' + gAgent.id + ' is in a group');
}
if(state.blocked && (!allowGrouped || state.group === null)) {
throw new Error('Duplicate agent name: ' + gAgent.id);
}
if(!allowVirtual && gAgent.isVirtualSource) {
throw new Error('cannot use message source here');
}
@ -2833,7 +2880,7 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
makeGroupDetails(pAgents, alias) {
const gAgents = pAgents.map(this.toGAgent);
this.validateGAgents(gAgents, {rejectGrouped: true});
this.validateGAgents(gAgents);
if(this.agentStates.has(alias)) {
throw new Error('Duplicate agent name: ' + alias);
}
@ -3264,8 +3311,27 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => {
handleAgentDefine({agents}) {
const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents);
this.defineGAgents(gAgents);
this.validateGAgents(gAgents, {
allowGrouped: true,
allowCovered: true,
});
array.mergeSets(this.gAgents, gAgents, GAgent.equals);
}
handleAgentOptions({agent, options}) {
const gAgent = this.toGAgent(agent);
const gAgents = [gAgent];
this.validateGAgents(gAgents, {
allowGrouped: true,
allowCovered: true,
});
array.mergeSets(this.gAgents, gAgents, GAgent.equals);
this.gAgents
.filter(({id}) => (id === gAgent.id))
.forEach((storedGAgent) => {
array.mergeSets(storedGAgent.options, options);
});
}
handleAgentBegin({agents, mode}) {
@ -4433,8 +4499,16 @@ define('sequence/components/AgentCap',[
'use strict';
class CapBox {
separation({formattedLabel}, env) {
const config = env.theme.agentCap.box;
getConfig(options, env) {
let config = null;
if(options.includes('database')) {
config = env.theme.agentCap.database;
}
return config || env.theme.agentCap.box;
}
separation({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const width = (
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left +
@ -4448,8 +4522,8 @@ define('sequence/components/AgentCap',[
};
}
topShift({formattedLabel}, env) {
const config = env.theme.agentCap.box;
topShift({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const height = (
env.textSizer.measureHeight(config.labelAttrs, formattedLabel) +
config.padding.top +
@ -4458,8 +4532,8 @@ define('sequence/components/AgentCap',[
return Math.max(0, height - config.arrowBottom);
}
render(y, {x, formattedLabel}, env) {
const config = env.theme.agentCap.box;
render(y, {x, formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const clickable = env.makeRegion();
const text = SVGShapes.renderBoxedText(formattedLabel, {
x,
@ -4505,13 +4579,18 @@ define('sequence/components/AgentCap',[
return config.size / 2;
}
render(y, {x}, env) {
render(y, {x, options}, env) {
const config = env.theme.agentCap.cross;
const d = config.size / 2;
const clickable = env.makeRegion();
clickable.appendChild(config.render({x, y: y + d, radius: d}));
clickable.appendChild(config.render({
x,
y: y + d,
radius: d,
options,
}));
clickable.appendChild(svg.make('rect', {
'x': x - d,
'y': y,
@ -4550,7 +4629,7 @@ define('sequence/components/AgentCap',[
return config.height / 2;
}
render(y, {x, formattedLabel}, env) {
render(y, {x, formattedLabel, options}, env) {
const boxCfg = env.theme.agentCap.box;
const barCfg = env.theme.agentCap.bar;
const width = (
@ -4566,6 +4645,7 @@ define('sequence/components/AgentCap',[
y,
width,
height,
options,
}));
clickable.appendChild(svg.make('rect', {
'x': x - width / 2,
@ -4838,7 +4918,7 @@ define('sequence/components/Connect',[
const arrow = this.getConfig(theme);
const join = arrow.attrs['stroke-linejoin'] || 'miter';
const t = arrow.attrs['stroke-width'] * 0.5;
const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5;
const lineStroke = theme.agentLineAttrs['']['stroke-width'] * 0.5;
if(join === 'round') {
return lineStroke + t;
} else {
@ -6057,6 +6137,7 @@ define('sequence/Renderer',[
y1: toY,
width: agentInfo.currentRad * 2,
className: 'agent-' + agentInfo.index + '-line',
options: agentInfo.options,
}));
}
@ -6193,6 +6274,7 @@ define('sequence/Renderer',[
formattedLabel: agent.formattedLabel,
anchorRight: agent.anchorRight,
isVirtualSource: agent.isVirtualSource,
options: agent.options,
index,
x: null,
latestYStart: null,
@ -6784,6 +6866,14 @@ define('sequence/themes/BaseTheme',[
return r;
}
function optionsAttributes(attributes, options) {
let attrs = Object.assign({}, attributes['']);
options.forEach((opt) => {
Object.assign(attrs, attributes[opt] || {});
});
return attrs;
}
class BaseTheme {
constructor({name, settings, blocks, notes, dividers}) {
this.name = name;
@ -6811,7 +6901,12 @@ define('sequence/themes/BaseTheme',[
return this.dividers[type] || this.dividers[''];
}
renderAgentLine({x, y0, y1, width, className}) {
optionsAttributes(attributes, options) {
return optionsAttributes(attributes, options);
}
renderAgentLine({x, y0, y1, width, className, options}) {
const attrs = this.optionsAttributes(this.agentLineAttrs, options);
if(width > 0) {
return svg.make('rect', Object.assign({
'x': x - width / 2,
@ -6819,7 +6914,7 @@ define('sequence/themes/BaseTheme',[
'width': width,
'height': y1 - y0,
'class': className,
}, this.agentLineAttrs));
}, attrs));
} else {
return svg.make('line', Object.assign({
'x1': x,
@ -6827,7 +6922,7 @@ define('sequence/themes/BaseTheme',[
'x2': x,
'y2': y1,
'class': className,
}, this.agentLineAttrs));
}, attrs));
}
}
}
@ -6877,6 +6972,27 @@ define('sequence/themes/BaseTheme',[
return g;
};
BaseTheme.renderDB = (attrs, {x, y, width, height}) => {
const z = attrs['db-z'];
return svg.make('g', {}, [
svg.make('rect', Object.assign({
'x': x,
'y': y,
'width': width,
'height': height,
'rx': width / 2,
'ry': z,
}, attrs)),
svg.make('path', Object.assign({
'd': (
'M' + x + ' ' + (y + z) +
'a' + (width / 2) + ' ' + z +
' 0 0 0 ' + width + ' 0'
),
}, attrs, {'fill': 'none'})),
]);
};
BaseTheme.renderCross = (attrs, {x, y, radius}) => {
return svg.make('path', Object.assign({
'd': (
@ -7165,6 +7281,27 @@ define('sequence/themes/Basic',[
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 15,
left: 10,
right: 10,
bottom: 5,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'db-z': 5,
}),
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
render: BaseTheme.renderCross.bind(null, {
@ -7304,9 +7441,14 @@ define('sequence/themes/Basic',[
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
'red': {
'stroke': '#CC0000',
},
},
};
@ -7563,6 +7705,27 @@ define('sequence/themes/Monospace',[
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 12,
left: 8,
right: 8,
bottom: 4,
},
arrowBottom: 4 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'db-z': 4,
}),
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 16,
render: BaseTheme.renderCross.bind(null, {
@ -7700,9 +7863,14 @@ define('sequence/themes/Monospace',[
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
'red': {
'stroke': '#AA0000',
},
},
};
@ -7947,6 +8115,28 @@ define('sequence/themes/Chunky',[
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 5,
left: 3,
right: 3,
bottom: 1,
},
arrowBottom: 1 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'db-z': 2,
}),
labelAttrs: {
'font-family': FONT,
'font-weight': 'bold',
'font-size': 14,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
render: BaseTheme.renderCross.bind(null, {
@ -8094,9 +8284,14 @@ define('sequence/themes/Chunky',[
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
},
'red': {
'stroke': '#DD0000',
},
},
};
@ -8797,6 +8992,25 @@ define('sequence/themes/Sketch',[
},
boxRenderer: null,
},
database: {
padding: {
top: 15,
left: 10,
right: 10,
bottom: 5,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, Object.assign({
'fill': '#FFFFFF',
'db-z': 5,
}, PENCIL.normal)),
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 15,
render: null,
@ -8913,9 +9127,12 @@ define('sequence/themes/Sketch',[
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'': Object.assign({
'fill': 'none',
}, PENCIL.normal),
'red': {
'stroke': 'rgba(200,40,0,0.8)',
},
},
};
@ -9287,7 +9504,9 @@ define('sequence/themes/Sketch',[
'd': line.nodes,
'fill': 'none',
'stroke-dasharray': lineOptions.dash ? '6, 5' : 'none',
}, lineOptions.thick ? PENCIL.thick : PENCIL.normal));
}, lineOptions.attrs || (
lineOptions.thick ? PENCIL.thick : PENCIL.normal
)));
return shape;
}
@ -9316,11 +9535,11 @@ define('sequence/themes/Sketch',[
return lT.nodes + lR.nodes + lB.nodes + lL.nodes;
}
renderBox(position, {fill = null, thick = false} = {}) {
renderBox(position, {fill = null, thick = false, attrs = null} = {}) {
return svg.make('path', Object.assign({
'd': this.boxNodes(position),
'fill': fill || '#FFFFFF',
}, thick ? PENCIL.thick : PENCIL.normal));
}, attrs || (thick ? PENCIL.thick : PENCIL.normal)));
}
renderNote({x, y, width, height}) {
@ -9646,21 +9865,22 @@ define('sequence/themes/Sketch',[
}, PENCIL.normal));
}
renderAgentLine({x, y0, y1, width, className}) {
renderAgentLine({x, y0, y1, width, className, options}) {
const attrs = this.optionsAttributes(this.agentLineAttrs, options);
if(width > 0) {
const shape = this.renderBox({
x: x - width / 2,
y: y0,
width,
height: y1 - y0,
}, {fill: 'none'});
}, {fill: 'none', attrs});
shape.setAttribute('class', className);
return shape;
} else {
const shape = this.renderLine(
{x, y: y0},
{x, y: y1},
{varY: 0.3}
{varY: 0.3, attrs}
);
shape.setAttribute('class', className);
return shape;

File diff suppressed because one or more lines are too long

View File

@ -249,6 +249,16 @@ define([], [
code: '`{text}`',
preview: 'A -> B: `mono`',
},
{
title: 'Red agent line',
code: '{Agent} is red',
preview: 'headers box\nA is red\nbegin A',
},
{
title: 'Database indicator',
code: '{Agent} is a database',
preview: 'headers box\nA is a database\nbegin A',
},
{
title: 'Monospace theme',
code: 'theme monospace',

View File

@ -1,7 +1,15 @@
define(['core/ArrayUtilities'], (array) => {
'use strict';
const CM_ERROR = {type: 'error line-error', then: {'': 0}};
const CM_ERROR = {type: 'error line-error', suggest: false, then: {'': 0}};
function textTo(exit, suggest = false) {
return {
type: 'string',
suggest,
then: Object.assign({'': 0}, exit),
};
}
function suggestionsEqual(a, b) {
return (
@ -12,6 +20,11 @@ define(['core/ArrayUtilities'], (array) => {
);
}
const AGENT_INFO_TYPES = [
'database',
'red',
];
const makeCommands = ((() => {
// The order of commands inside "then" blocks directly influences the
// order they are displayed to the user in autocomplete menus.
@ -20,36 +33,7 @@ define(['core/ArrayUtilities'], (array) => {
// to use Map objects instead for strict compliance, at the cost of
// extra syntax.
const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', then: {}};
function textTo(exit, suggest) {
return {
type: 'string',
suggest,
then: Object.assign({'': 0}, exit),
};
}
const textToEnd = textTo({'\n': end});
const aliasListToEnd = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
'\n': end,
',': {type: 'operator', suggest: true, then: {'': 1}},
'as': {type: 'keyword', suggest: true, then: {
'': {type: 'variable', suggest: {known: 'Agent'}, then: {
'': 0,
',': {type: 'operator', suggest: true, then: {'': 3}},
'\n': end,
}},
}},
},
};
function agentListTo(exit) {
function agentListTo(exit, next = 1) {
return {
type: 'variable',
suggest: {known: 'Agent'},
@ -57,46 +41,37 @@ define(['core/ArrayUtilities'], (array) => {
exit,
{
'': 0,
',': {type: 'operator', suggest: true, then: {'': 1}},
',': {type: 'operator', then: {'': next}},
}
),
};
}
const end = {type: '', suggest: '\n', then: {}};
const hiddenEnd = {type: '', suggest: false, then: {}};
const textToEnd = textTo({'\n': end});
const colonTextToEnd = {
type: 'operator',
suggest: true,
then: {'': textToEnd, '\n': hiddenEnd},
};
const agentListToText = agentListTo({
':': colonTextToEnd,
});
const agentList2ToText = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
',': {type: 'operator', suggest: true, then: {
'': agentListToText,
const aliasListToEnd = agentListTo({
'\n': end,
'as': {type: 'keyword', then: {
'': {type: 'variable', suggest: {known: 'Agent'}, then: {
'': 0,
',': {type: 'operator', then: {'': 3}},
'\n': end,
}},
':': CM_ERROR,
},
};
const singleAgentToText = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
',': CM_ERROR,
':': colonTextToEnd,
},
};
}},
});
const agentListToText = agentListTo({':': colonTextToEnd});
const agentToOptText = {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
@ -104,9 +79,9 @@ define(['core/ArrayUtilities'], (array) => {
},
};
const referenceName = {
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textTo({
'as': {type: 'keyword', suggest: true, then: {
'as': {type: 'keyword', then: {
'': {
type: 'variable',
suggest: {known: 'Agent'},
@ -119,23 +94,23 @@ define(['core/ArrayUtilities'], (array) => {
}),
}},
};
const refDef = {type: 'keyword', suggest: true, then: Object.assign({
'over': {type: 'keyword', suggest: true, then: {
const refDef = {type: 'keyword', then: Object.assign({
'over': {type: 'keyword', then: {
'': agentListTo(referenceName),
}},
}, referenceName)};
const divider = {
'\n': end,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
'with': {type: 'keyword', suggest: ['with height '], then: {
'height': {type: 'keyword', suggest: true, then: {
'height': {type: 'keyword', then: {
'': {type: 'number', suggest: ['6 ', '30 '], then: {
'\n': end,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
@ -144,15 +119,41 @@ define(['core/ArrayUtilities'], (array) => {
}},
};
function simpleList(type, keywords, exit) {
const first = {};
const recur = Object.assign({}, exit);
keywords.forEach((keyword) => {
first[keyword] = {type, then: recur};
recur[keyword] = 0;
});
return first;
}
function optionalKeywords(type, keywords, then) {
const result = Object.assign({}, then);
keywords.forEach((keyword) => {
result[keyword] = {type, then};
});
return result;
}
const agentInfoList = optionalKeywords(
'keyword',
['a', 'an'],
simpleList('keyword', AGENT_INFO_TYPES, {'\n': end})
);
function makeSideNote(side) {
return {
type: 'keyword',
suggest: [side + ' of ', side + ': '],
then: {
'of': {type: 'keyword', suggest: true, then: {
'of': {type: 'keyword', then: {
'': agentListToText,
}},
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
}},
'': agentListToText,
@ -160,8 +161,8 @@ define(['core/ArrayUtilities'], (array) => {
};
}
function makeOpBlock(exit, sourceExit) {
const op = {type: 'operator', suggest: true, then: {
function makeOpBlock({exit, sourceExit, blankExit}) {
const op = {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': CM_ERROR,
@ -169,14 +170,14 @@ define(['core/ArrayUtilities'], (array) => {
'': exit,
}};
return {
'+': {type: 'operator', suggest: true, then: {
'+': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': CM_ERROR,
'': exit,
}},
'-': {type: 'operator', suggest: true, then: {
'-': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
@ -189,27 +190,29 @@ define(['core/ArrayUtilities'], (array) => {
}},
'': exit,
}},
'*': {type: 'operator', suggest: true, then: Object.assign({
'*': {type: 'operator', then: Object.assign({
'+': op,
'-': op,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}, sourceExit)},
}, sourceExit || exit)},
'!': op,
'': exit,
'': blankExit || exit,
};
}
function makeCMConnect(arrows) {
const connect = {
type: 'keyword',
suggest: true,
then: Object.assign({}, makeOpBlock(agentToOptText, {
':': colonTextToEnd,
'\n': hiddenEnd,
then: Object.assign({}, makeOpBlock({
exit: agentToOptText,
sourceExit: {
':': colonTextToEnd,
'\n': hiddenEnd,
},
}), {
'...': {type: 'operator', suggest: true, then: {
'...': {type: 'operator', then: {
'': {
type: 'variable',
suggest: {known: 'DelayedAgent'},
@ -228,7 +231,6 @@ define(['core/ArrayUtilities'], (array) => {
const labelIndicator = {
type: 'operator',
suggest: true,
override: 'Label',
then: {},
};
@ -245,8 +247,9 @@ define(['core/ArrayUtilities'], (array) => {
suggest: {known: 'Agent'},
then: Object.assign({
'': 0,
}, connectors, {
':': labelIndicator,
}, connectors),
}),
};
const firstAgentDelayed = {
@ -258,29 +261,39 @@ define(['core/ArrayUtilities'], (array) => {
}, connectors),
};
const firstAgentNoFlags = Object.assign({}, firstAgent, {
then: Object.assign({}, firstAgent.then, {
'is': {type: 'keyword', then: agentInfoList},
}),
});
return Object.assign({
'...': {type: 'operator', suggest: true, then: {
'...': {type: 'operator', then: {
'': firstAgentDelayed,
}},
}, makeOpBlock(firstAgent, Object.assign({
'': firstAgent,
':': hiddenLabelIndicator,
}, connectors)));
}, makeOpBlock({
exit: firstAgent,
sourceExit: Object.assign({
'': firstAgent,
':': hiddenLabelIndicator,
}, connectors),
blankExit: firstAgentNoFlags,
}));
}
const group = {type: 'keyword', suggest: true, then: {
const group = {type: 'keyword', then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
}},
'\n': end,
}};
const BASE_THEN = {
'title': {type: 'keyword', suggest: true, then: {
'title': {type: 'keyword', then: {
'': textToEnd,
}},
'theme': {type: 'keyword', suggest: true, then: {
'theme': {type: 'keyword', then: {
'': {
type: 'string',
suggest: {
@ -293,36 +306,36 @@ define(['core/ArrayUtilities'], (array) => {
},
},
}},
'headers': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}},
'cross': {type: 'keyword', suggest: true, then: {}},
'box': {type: 'keyword', suggest: true, then: {}},
'fade': {type: 'keyword', suggest: true, then: {}},
'bar': {type: 'keyword', suggest: true, then: {}},
'headers': {type: 'keyword', then: {
'none': {type: 'keyword', then: {}},
'cross': {type: 'keyword', then: {}},
'box': {type: 'keyword', then: {}},
'fade': {type: 'keyword', then: {}},
'bar': {type: 'keyword', then: {}},
}},
'terminators': {type: 'keyword', suggest: true, then: {
'none': {type: 'keyword', suggest: true, then: {}},
'cross': {type: 'keyword', suggest: true, then: {}},
'box': {type: 'keyword', suggest: true, then: {}},
'fade': {type: 'keyword', suggest: true, then: {}},
'bar': {type: 'keyword', suggest: true, then: {}},
'terminators': {type: 'keyword', then: {
'none': {type: 'keyword', then: {}},
'cross': {type: 'keyword', then: {}},
'box': {type: 'keyword', then: {}},
'fade': {type: 'keyword', then: {}},
'bar': {type: 'keyword', then: {}},
}},
'divider': {type: 'keyword', suggest: true, then: Object.assign({
'line': {type: 'keyword', suggest: true, then: divider},
'space': {type: 'keyword', suggest: true, then: divider},
'delay': {type: 'keyword', suggest: true, then: divider},
'tear': {type: 'keyword', suggest: true, then: divider},
'divider': {type: 'keyword', then: Object.assign({
'line': {type: 'keyword', then: divider},
'space': {type: 'keyword', then: divider},
'delay': {type: 'keyword', then: divider},
'tear': {type: 'keyword', then: divider},
}, divider)},
'define': {type: 'keyword', suggest: true, then: {
'define': {type: 'keyword', then: {
'': aliasListToEnd,
'as': CM_ERROR,
}},
'begin': {type: 'keyword', suggest: true, then: {
'begin': {type: 'keyword', then: {
'': aliasListToEnd,
'reference': refDef,
'as': CM_ERROR,
}},
'end': {type: 'keyword', suggest: true, then: {
'end': {type: 'keyword', then: {
'': aliasListToEnd,
'as': CM_ERROR,
'\n': end,
@ -331,7 +344,7 @@ define(['core/ArrayUtilities'], (array) => {
'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
'if': {type: 'keyword', suggest: 'if: ', then: {
'': textToEnd,
':': {type: 'operator', suggest: true, then: {
':': {type: 'operator', then: {
'': textToEnd,
}},
}},
@ -339,39 +352,47 @@ define(['core/ArrayUtilities'], (array) => {
}},
'repeat': group,
'group': group,
'note': {type: 'keyword', suggest: true, then: {
'over': {type: 'keyword', suggest: true, then: {
'note': {type: 'keyword', then: {
'over': {type: 'keyword', then: {
'': agentListToText,
}},
'left': makeSideNote('left'),
'right': makeSideNote('right'),
'between': {type: 'keyword', suggest: true, then: {
'': agentList2ToText,
'between': {type: 'keyword', then: {
'': agentListTo({':': CM_ERROR}, agentListToText),
}},
}},
'state': {type: 'keyword', suggest: 'state over ', then: {
'over': {type: 'keyword', suggest: true, then: {
'': singleAgentToText,
'over': {type: 'keyword', then: {
'': {
type: 'variable',
suggest: {known: 'Agent'},
then: {
'': 0,
',': CM_ERROR,
':': colonTextToEnd,
},
},
}},
}},
'text': {type: 'keyword', suggest: true, then: {
'text': {type: 'keyword', then: {
'left': makeSideNote('left'),
'right': makeSideNote('right'),
}},
'autolabel': {type: 'keyword', suggest: true, then: {
'off': {type: 'keyword', suggest: true, then: {}},
'autolabel': {type: 'keyword', then: {
'off': {type: 'keyword', then: {}},
'': textTo({'\n': end}, [
{v: '<label>', suffix: '\n', q: true},
{v: '[<inc>] <label>', suffix: '\n', q: true},
{v: '[<inc 1,0.01>] <label>', suffix: '\n', q: true},
]),
}},
'simultaneously': {type: 'keyword', suggest: true, then: {
':': {type: 'operator', suggest: true, then: {}},
'with': {type: 'keyword', suggest: true, then: {
'simultaneously': {type: 'keyword', then: {
':': {type: 'operator', then: {}},
'with': {type: 'keyword', then: {
'': {type: 'variable', suggest: {known: 'Label'}, then: {
'': 0,
':': {type: 'operator', suggest: true, then: {}},
':': {type: 'operator', then: {}},
}},
}},
}},
@ -400,8 +421,8 @@ define(['core/ArrayUtilities'], (array) => {
}
return array.flatMap(suggestions, (suggest) => {
if(suggest === true) {
return [cmCappedToken(token, current)];
if(suggest === false) {
return [];
} else if(typeof suggest === 'object') {
if(suggest.known) {
return state['known' + suggest.known] || [];
@ -411,7 +432,7 @@ define(['core/ArrayUtilities'], (array) => {
} else if(typeof suggest === 'string' && suggest) {
return [{v: suggest, q: (token === '')}];
} else {
return [];
return [cmCappedToken(token, current)];
}
});
}

View File

@ -405,6 +405,22 @@ defineDescribe('Code Mirror Mode', [
{v: ' stuff', type: 'string'},
]);
});
it('highlights agent info statements', () => {
cm.getDoc().setValue('A is a red database');
expect(getTokens(0)).toEqual([
{v: 'A', type: 'variable'},
{v: ' is', type: 'keyword'},
{v: ' a', type: 'keyword'},
{v: ' red', type: 'keyword'},
{v: ' database', type: 'keyword'},
]);
});
it('rejects unknown info statements', () => {
cm.getDoc().setValue('A is a foobar');
expect(getTokens(0)[3].type).toContain('line-error');
});
});
describe('autocomplete', () => {
@ -619,5 +635,31 @@ defineDescribe('Code Mirror Mode', [
const hints = getHintTexts({line: 1, ch: 4});
expect(hints).toEqual(['woo ']);
});
it('suggests agent properties', () => {
cm.getDoc().setValue('A is a ');
const hints = getHintTexts({line: 0, ch: 7});
expect(hints).toContain('database ');
expect(hints).toContain('red ');
expect(hints).not.toContain('\n');
});
it('suggests indefinite articles for agent properties', () => {
cm.getDoc().setValue('A is ');
const hints = getHintTexts({line: 0, ch: 5});
expect(hints).toContain('database ');
expect(hints).toContain('a ');
expect(hints).toContain('an ');
expect(hints).not.toContain('\n');
});
it('suggests more agent properties after the first', () => {
cm.getDoc().setValue('A is a red ');
const hints = getHintTexts({line: 0, ch: 11});
expect(hints).toContain('database ');
expect(hints).toContain('\n');
expect(hints).not.toContain('a ');
expect(hints).not.toContain('an ');
});
});
});

View File

@ -37,7 +37,7 @@ define(['core/ArrayUtilities'], (array) => {
return a.id === b.id;
},
make: (id, {anchorRight = false, isVirtualSource = false} = {}) => {
return {id, anchorRight, isVirtualSource};
return {id, anchorRight, isVirtualSource, options: []};
},
indexOf: (list, gAgent) => {
return array.indexOf(list, gAgent, GAgent.equals);
@ -251,6 +251,7 @@ define(['core/ArrayUtilities'], (array) => {
'mark': this.handleMark.bind(this),
'async': this.handleAsync.bind(this),
'agent define': this.handleAgentDefine.bind(this),
'agent options': this.handleAgentOptions.bind(this),
'agent begin': this.handleAgentBegin.bind(this),
'agent end': this.handleAgentEnd.bind(this),
'divider': this.handleDivider.bind(this),
@ -354,23 +355,23 @@ define(['core/ArrayUtilities'], (array) => {
validateGAgents(gAgents, {
allowGrouped = false,
rejectGrouped = false,
allowCovered = false,
allowVirtual = false,
} = {}) {
/* jshint -W074 */ // agent validity checking requires several steps
gAgents.forEach((gAgent) => {
const state = this.getGAgentState(gAgent);
if(state.covered) {
if(state.blocked && state.group === null) {
// used to be a group alias; can never be reused
throw new Error('Duplicate agent name: ' + gAgent.id);
}
if(!allowCovered && state.covered) {
throw new Error(
'Agent ' + gAgent.id + ' is hidden behind group'
);
}
if(rejectGrouped && state.group !== null) {
if(!allowGrouped && state.group !== null) {
throw new Error('Agent ' + gAgent.id + ' is in a group');
}
if(state.blocked && (!allowGrouped || state.group === null)) {
throw new Error('Duplicate agent name: ' + gAgent.id);
}
if(!allowVirtual && gAgent.isVirtualSource) {
throw new Error('cannot use message source here');
}
@ -561,7 +562,7 @@ define(['core/ArrayUtilities'], (array) => {
makeGroupDetails(pAgents, alias) {
const gAgents = pAgents.map(this.toGAgent);
this.validateGAgents(gAgents, {rejectGrouped: true});
this.validateGAgents(gAgents);
if(this.agentStates.has(alias)) {
throw new Error('Duplicate agent name: ' + alias);
}
@ -992,8 +993,27 @@ define(['core/ArrayUtilities'], (array) => {
handleAgentDefine({agents}) {
const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents);
this.defineGAgents(gAgents);
this.validateGAgents(gAgents, {
allowGrouped: true,
allowCovered: true,
});
array.mergeSets(this.gAgents, gAgents, GAgent.equals);
}
handleAgentOptions({agent, options}) {
const gAgent = this.toGAgent(agent);
const gAgents = [gAgent];
this.validateGAgents(gAgents, {
allowGrouped: true,
allowCovered: true,
});
array.mergeSets(this.gAgents, gAgents, GAgent.equals);
this.gAgents
.filter(({id}) => (id === gAgent.id))
.forEach((storedGAgent) => {
array.mergeSets(storedGAgent.options, options);
});
}
handleAgentBegin({agents, mode}) {

View File

@ -58,6 +58,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
};
},
agentOptions: (agentID, options, {ln = 0} = {}) => {
return {
type: 'agent options',
agent: makeParsedAgents([agentID])[0],
options,
ln,
};
},
beginAgents: (agentIDs, {mode = 'box', ln = 0} = {}) => {
return {
type: 'agent begin',
@ -159,8 +168,9 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
formattedLabel = any(),
anchorRight = any(),
isVirtualSource = any(),
options = any(),
} = {}) => {
return {id, formattedLabel, anchorRight, isVirtualSource};
return {id, formattedLabel, anchorRight, isVirtualSource, options};
},
beginAgents: (agentIDs, {
@ -452,6 +462,31 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('applies options to agents', () => {
const sequence = invoke([
PARSED.agentOptions('A', ['foo']),
]);
expect(sequence.agents).toEqual([
any(),
GENERATED.agent('A', {options: ['foo']}),
any(),
]);
});
it('combines agent options', () => {
const sequence = invoke([
PARSED.agentOptions('A', ['foo', 'bar']),
PARSED.agentOptions('B', ['zig']),
PARSED.agentOptions('A', ['zag', 'bar']),
]);
expect(sequence.agents).toEqual([
any(),
GENERATED.agent('A', {options: ['foo', 'bar', 'zag']}),
GENERATED.agent('B', {options: ['zig']}),
any(),
]);
});
it('converts aliases', () => {
const sequence = invoke([
PARSED.defineAgents([{name: 'Baz', alias: 'B', flags: []}]),
@ -1268,6 +1303,24 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('ignores defines when setting block bounds', () => {
const sequence = invoke([
PARSED.blockBegin('if', 'abc'),
PARSED.connect(['A', 'B']),
PARSED.defineAgents(['C']),
PARSED.blockEnd(),
]);
expect(sequence.agents).toEqual([
GENERATED.agent('['),
GENERATED.agent('__BLOCK0['),
GENERATED.agent('A'),
GENERATED.agent('B'),
GENERATED.agent('__BLOCK0]'),
GENERATED.agent('C'),
GENERATED.agent(']'),
]);
});
it('ignores side agents when calculating block bounds', () => {
const sequence = invoke([
PARSED.beginAgents(['A', 'B', 'C']),
@ -1755,6 +1808,24 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
]);
});
it('rejects interactions with agents involved in references', () => {
expect(() => invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.groupBegin('Bar', ['A', 'C']),
PARSED.endAgents(['A']),
PARSED.endAgents(['Bar']),
])).toThrow(new Error('Agent A is in a group at line 1'));
});
it('rejects flags on agents involved in references', () => {
expect(() => invoke([
PARSED.beginAgents(['A', 'B', 'C', 'D']),
PARSED.groupBegin('Bar', ['A', 'C']),
PARSED.connect([{name: 'A', alias: '', flags: ['start']}, 'D']),
PARSED.endAgents(['Bar']),
])).toThrow(new Error('Agent A is in a group at line 1'));
});
it('rejects interactions with agents hidden beneath references', () => {
expect(() => invoke([
PARSED.beginAgents(['A', 'B', 'C', 'D']),
@ -1762,6 +1833,20 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => {
PARSED.connect(['B', 'D']),
PARSED.endAgents(['AC']),
])).toThrow(new Error('Agent B is hidden behind group at line 1'));
expect(() => invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.groupBegin('Bar', ['A', 'C']),
PARSED.endAgents(['B']),
PARSED.endAgents(['Bar']),
])).toThrow(new Error('Agent B is hidden behind group at line 1'));
expect(() => invoke([
PARSED.beginAgents(['A', 'B', 'C']),
PARSED.groupBegin('Bar', ['A', 'C']),
PARSED.note('note over', ['B']),
PARSED.endAgents(['Bar']),
])).toThrow(new Error('Agent B is hidden behind group at line 1'));
});
it('encompasses entire reference boxes in block statements', () => {

View File

@ -562,6 +562,31 @@ define([
name: joinLabel(line, 0, line.length - 1),
};
},
(line) => { // options
const sepPos = findToken(line, 'is');
if(sepPos < 1) {
return null;
}
const indefiniteArticles = ['a', 'an'];
let optionsBegin = sepPos + 1;
if(indefiniteArticles.includes(tokenKeyword(line[optionsBegin]))) {
++ optionsBegin;
}
if(optionsBegin === line.length) {
throw makeError('Empty agent options', {b: array.last(line).e});
}
const agent = readAgent(line, 0, sepPos);
const options = [];
for(let i = optionsBegin; i < line.length; ++ i) {
options.push(line[i].v);
}
return {
type: 'agent options',
agent,
options,
};
},
];
function parseLine(line, {meta, stages}) {

View File

@ -142,6 +142,57 @@ defineDescribe('Sequence Parser', ['./Parser'], (Parser) => {
]);
});
it('propagates agent options', () => {
const parsed = parser.parse('Foo bar is zig zag');
expect(parsed.stages).toEqual([
{
type: 'agent options',
ln: jasmine.anything(),
agent: {
name: 'Foo bar',
alias: '',
flags: [],
},
options: ['zig', 'zag'],
},
]);
});
it('ignores indefinite articles in agent options', () => {
const parsed = parser.parse('Foo is a zig\nBar is an oom');
expect(parsed.stages).toEqual([
{
type: 'agent options',
ln: jasmine.anything(),
agent: {
name: 'Foo',
alias: '',
flags: [],
},
options: ['zig'],
},
{
type: 'agent options',
ln: jasmine.anything(),
agent: {
name: 'Bar',
alias: '',
flags: [],
},
options: ['oom'],
},
]);
});
it('rejects empty agent options', () => {
expect(() => parser.parse('Foo is')).toThrow(new Error(
'Empty agent options at line 1, character 6'
));
expect(() => parser.parse('Foo is a')).toThrow(new Error(
'Empty agent options at line 1, character 8'
));
});
it('respects spacing within agent names', () => {
const parsed = parser.parse('A+B -> C D');
expect(parsed.stages).toEqual([

View File

@ -338,6 +338,7 @@ define([
y1: toY,
width: agentInfo.currentRad * 2,
className: 'agent-' + agentInfo.index + '-line',
options: agentInfo.options,
}));
}
@ -474,6 +475,7 @@ define([
formattedLabel: agent.formattedLabel,
anchorRight: agent.anchorRight,
isVirtualSource: agent.isVirtualSource,
options: agent.options,
index,
x: null,
latestYStart: null,

View File

@ -56,18 +56,22 @@ defineDescribe('Sequence Renderer', [
id: '[',
formattedLabel: null,
anchorRight: true,
options: [],
}, {
id: 'Col 1',
formattedLabel: format('Col 1!'),
anchorRight: false,
options: [],
}, {
id: 'Col 2',
formattedLabel: format('Col 2!'),
anchorRight: false,
options: [],
}, {
id: ']',
formattedLabel: null,
anchorRight: false,
options: [],
},
],
stages: [],
@ -81,8 +85,17 @@ defineDescribe('Sequence Renderer', [
renderer.render({
meta: {title: [], code: 'hello'},
agents: [
{id: '[', formattedLabel: null, anchorRight: true},
{id: ']', formattedLabel: null, anchorRight: false},
{
id: '[',
formattedLabel: null,
anchorRight: true,
options: [],
}, {
id: ']',
formattedLabel: null,
anchorRight: false,
options: [],
},
],
stages: [],
});
@ -99,10 +112,27 @@ defineDescribe('Sequence Renderer', [
renderer.render({
meta: {title: []},
agents: [
{id: '[', formattedLabel: null, anchorRight: true},
{id: 'A', formattedLabel: format('A!'), anchorRight: false},
{id: 'B', formattedLabel: format('B!'), anchorRight: false},
{id: ']', formattedLabel: null, anchorRight: false},
{
id: '[',
formattedLabel: null,
anchorRight: true,
options: [],
}, {
id: 'A',
formattedLabel: format('A!'),
anchorRight: false,
options: [],
}, {
id: 'B',
formattedLabel: format('B!'),
anchorRight: false,
options: [],
}, {
id: ']',
formattedLabel: null,
anchorRight: false,
options: [],
},
],
stages: [
{type: 'agent begin', agentIDs: ['A', 'B'], mode: 'box'},
@ -129,11 +159,32 @@ defineDescribe('Sequence Renderer', [
renderer.render({
meta: {title: []},
agents: [
{id: '[', formattedLabel: null, anchorRight: true},
{id: 'A', formattedLabel: format('A!'), anchorRight: false},
{id: 'B', formattedLabel: format('B!'), anchorRight: false},
{id: 'C', formattedLabel: format('C!'), anchorRight: false},
{id: ']', formattedLabel: null, anchorRight: false},
{
id: '[',
formattedLabel: null,
anchorRight: true,
options: [],
}, {
id: 'A',
formattedLabel: format('A!'),
anchorRight: false,
options: [],
}, {
id: 'B',
formattedLabel: format('B!'),
anchorRight: false,
options: [],
}, {
id: 'C',
formattedLabel: format('C!'),
anchorRight: false,
options: [],
}, {
id: ']',
formattedLabel: null,
anchorRight: false,
options: [],
},
],
stages: [
{
@ -178,12 +229,37 @@ defineDescribe('Sequence Renderer', [
renderer.render({
meta: {title: []},
agents: [
{id: '[', formattedLabel: null, anchorRight: true},
{id: 'A', formattedLabel: format('A!'), anchorRight: false},
{id: 'B', formattedLabel: format('B!'), anchorRight: false},
{id: 'C', formattedLabel: format('C!'), anchorRight: false},
{id: 'D', formattedLabel: format('D!'), anchorRight: false},
{id: ']', formattedLabel: null, anchorRight: false},
{
id: '[',
formattedLabel: null,
anchorRight: true,
options: [],
}, {
id: 'A',
formattedLabel: format('A!'),
anchorRight: false,
options: [],
}, {
id: 'B',
formattedLabel: format('B!'),
anchorRight: false,
options: [],
}, {
id: 'C',
formattedLabel: format('C!'),
anchorRight: false,
options: [],
}, {
id: 'D',
formattedLabel: format('D!'),
anchorRight: false,
options: [],
}, {
id: ']',
formattedLabel: null,
anchorRight: false,
options: [],
},
],
stages: [
{type: 'agent begin', agentIDs: ['A', 'B'], mode: 'box'},

View File

@ -12,8 +12,16 @@ define([
'use strict';
class CapBox {
separation({formattedLabel}, env) {
const config = env.theme.agentCap.box;
getConfig(options, env) {
let config = null;
if(options.includes('database')) {
config = env.theme.agentCap.database;
}
return config || env.theme.agentCap.box;
}
separation({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const width = (
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left +
@ -27,8 +35,8 @@ define([
};
}
topShift({formattedLabel}, env) {
const config = env.theme.agentCap.box;
topShift({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const height = (
env.textSizer.measureHeight(config.labelAttrs, formattedLabel) +
config.padding.top +
@ -37,8 +45,8 @@ define([
return Math.max(0, height - config.arrowBottom);
}
render(y, {x, formattedLabel}, env) {
const config = env.theme.agentCap.box;
render(y, {x, formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const clickable = env.makeRegion();
const text = SVGShapes.renderBoxedText(formattedLabel, {
x,
@ -84,13 +92,18 @@ define([
return config.size / 2;
}
render(y, {x}, env) {
render(y, {x, options}, env) {
const config = env.theme.agentCap.cross;
const d = config.size / 2;
const clickable = env.makeRegion();
clickable.appendChild(config.render({x, y: y + d, radius: d}));
clickable.appendChild(config.render({
x,
y: y + d,
radius: d,
options,
}));
clickable.appendChild(svg.make('rect', {
'x': x - d,
'y': y,
@ -129,7 +142,7 @@ define([
return config.height / 2;
}
render(y, {x, formattedLabel}, env) {
render(y, {x, formattedLabel, options}, env) {
const boxCfg = env.theme.agentCap.box;
const barCfg = env.theme.agentCap.bar;
const width = (
@ -145,6 +158,7 @@ define([
y,
width,
height,
options,
}));
clickable.appendChild(svg.make('rect', {
'x': x - width / 2,

View File

@ -24,7 +24,7 @@ define([
const arrow = this.getConfig(theme);
const join = arrow.attrs['stroke-linejoin'] || 'miter';
const t = arrow.attrs['stroke-width'] * 0.5;
const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5;
const lineStroke = theme.agentLineAttrs['']['stroke-width'] * 0.5;
if(join === 'round') {
return lineStroke + t;
} else {

View File

@ -0,0 +1,5 @@
<svg width="134.673828125" height="65.6" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-5 -5 134.673828125 65.6"><metadata>begin A, B, C
A is a database
B is red
C is a red database
</metadata><defs></defs><defs><mask id="R0FullMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" x="-5" y="-5" width="134.673828125" height="65.6"></rect></mask><mask id="R0LineMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" x="-5" y="-5" width="134.673828125" height="65.6"></rect></mask></defs><g></g><g mask="url(#R0FullMask)"><g mask="url(#R0LineMask)"><line x1="24.001953125" y1="35.6" x2="24.001953125" y2="55.6" class="agent-1-line" fill="none" stroke="#000000" stroke-width="1"></line><line x1="62.005859375" y1="35.6" x2="62.005859375" y2="55.6" class="agent-2-line" fill="none" stroke="#CC0000" stroke-width="1"></line><line x1="100.3408203125" y1="35.6" x2="100.3408203125" y2="55.6" class="agent-3-line" fill="none" stroke="#CC0000" stroke-width="1"></line></g><g></g><g><g class="region"><g><rect x="10" y="0" width="28.00390625" height="35.6" rx="14.001953125" ry="5" fill="#FFFFFF" stroke="#000000" stroke-width="1" db-z="5"></rect><path d="M10 5a14.001953125 5 0 0 0 28.00390625 0" fill="none" stroke="#000000" stroke-width="1" db-z="5"></path></g><rect x="10" y="0" width="28.00390625" height="35.6" fill="transparent" class="outline"></rect><text x="24.001953125" font-family="sans-serif" font-size="12" line-height="1.3" text-anchor="middle" y="27">A</text></g><g class="region"><rect x="48.00390625" y="10" width="28.00390625" height="25.6" fill="#FFFFFF" stroke="#000000" stroke-width="1"></rect><rect x="48.00390625" y="10" width="28.00390625" height="25.6" fill="transparent" class="outline"></rect><text x="62.005859375" font-family="sans-serif" font-size="12" line-height="1.3" text-anchor="middle" y="27">B</text></g><g class="region"><g><rect x="86.0078125" y="0" width="28.666015625" height="35.6" rx="14.3330078125" ry="5" fill="#FFFFFF" stroke="#000000" stroke-width="1" db-z="5"></rect><path d="M86.0078125 5a14.3330078125 5 0 0 0 28.666015625 0" fill="none" stroke="#000000" stroke-width="1" db-z="5"></path></g><rect x="86.0078125" y="0" width="28.666015625" height="35.6" fill="transparent" class="outline"></rect><text x="100.3408203125" font-family="sans-serif" font-size="12" line-height="1.3" text-anchor="middle" y="27">C</text></g><g class="region"><rect x="19.001953125" y="45.6" width="10" height="10" fill="transparent" class="outline"></rect></g><g class="region"><rect x="57.005859375" y="45.6" width="10" height="10" fill="transparent" class="outline"></rect></g><g class="region"><rect x="95.3408203125" y="45.6" width="10" height="10" fill="transparent" class="outline"></rect></g></g></g><g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -9,4 +9,5 @@ define([], [
'Reference.svg',
'ReferenceLayering.svg',
'Markdown.svg',
'AgentOptions.svg',
]);

View File

@ -20,6 +20,14 @@ define([
return r;
}
function optionsAttributes(attributes, options) {
let attrs = Object.assign({}, attributes['']);
options.forEach((opt) => {
Object.assign(attrs, attributes[opt] || {});
});
return attrs;
}
class BaseTheme {
constructor({name, settings, blocks, notes, dividers}) {
this.name = name;
@ -47,7 +55,12 @@ define([
return this.dividers[type] || this.dividers[''];
}
renderAgentLine({x, y0, y1, width, className}) {
optionsAttributes(attributes, options) {
return optionsAttributes(attributes, options);
}
renderAgentLine({x, y0, y1, width, className, options}) {
const attrs = this.optionsAttributes(this.agentLineAttrs, options);
if(width > 0) {
return svg.make('rect', Object.assign({
'x': x - width / 2,
@ -55,7 +68,7 @@ define([
'width': width,
'height': y1 - y0,
'class': className,
}, this.agentLineAttrs));
}, attrs));
} else {
return svg.make('line', Object.assign({
'x1': x,
@ -63,7 +76,7 @@ define([
'x2': x,
'y2': y1,
'class': className,
}, this.agentLineAttrs));
}, attrs));
}
}
}
@ -113,6 +126,27 @@ define([
return g;
};
BaseTheme.renderDB = (attrs, {x, y, width, height}) => {
const z = attrs['db-z'];
return svg.make('g', {}, [
svg.make('rect', Object.assign({
'x': x,
'y': y,
'width': width,
'height': height,
'rx': width / 2,
'ry': z,
}, attrs)),
svg.make('path', Object.assign({
'd': (
'M' + x + ' ' + (y + z) +
'a' + (width / 2) + ' ' + z +
' 0 0 0 ' + width + ' 0'
),
}, attrs, {'fill': 'none'})),
]);
};
BaseTheme.renderCross = (attrs, {x, y, radius}) => {
return svg.make('path', Object.assign({
'd': (

View File

@ -43,6 +43,27 @@ define([
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 15,
left: 10,
right: 10,
bottom: 5,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'db-z': 5,
}),
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
render: BaseTheme.renderCross.bind(null, {
@ -182,9 +203,14 @@ define([
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
'red': {
'stroke': '#CC0000',
},
},
};

View File

@ -46,6 +46,28 @@ define([
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 5,
left: 3,
right: 3,
bottom: 1,
},
arrowBottom: 1 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'db-z': 2,
}),
labelAttrs: {
'font-family': FONT,
'font-weight': 'bold',
'font-size': 14,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
render: BaseTheme.renderCross.bind(null, {
@ -193,9 +215,14 @@ define([
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
},
'red': {
'stroke': '#DD0000',
},
},
};

View File

@ -52,6 +52,27 @@ define([
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 12,
left: 8,
right: 8,
bottom: 4,
},
arrowBottom: 4 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'db-z': 4,
}),
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 16,
render: BaseTheme.renderCross.bind(null, {
@ -189,9 +210,14 @@ define([
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
'red': {
'stroke': '#AA0000',
},
},
};

View File

@ -56,6 +56,25 @@ define([
},
boxRenderer: null,
},
database: {
padding: {
top: 15,
left: 10,
right: 10,
bottom: 5,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxRenderer: BaseTheme.renderDB.bind(null, Object.assign({
'fill': '#FFFFFF',
'db-z': 5,
}, PENCIL.normal)),
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 15,
render: null,
@ -172,9 +191,12 @@ define([
},
agentLineAttrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'': Object.assign({
'fill': 'none',
}, PENCIL.normal),
'red': {
'stroke': 'rgba(200,40,0,0.8)',
},
},
};
@ -546,7 +568,9 @@ define([
'd': line.nodes,
'fill': 'none',
'stroke-dasharray': lineOptions.dash ? '6, 5' : 'none',
}, lineOptions.thick ? PENCIL.thick : PENCIL.normal));
}, lineOptions.attrs || (
lineOptions.thick ? PENCIL.thick : PENCIL.normal
)));
return shape;
}
@ -575,11 +599,11 @@ define([
return lT.nodes + lR.nodes + lB.nodes + lL.nodes;
}
renderBox(position, {fill = null, thick = false} = {}) {
renderBox(position, {fill = null, thick = false, attrs = null} = {}) {
return svg.make('path', Object.assign({
'd': this.boxNodes(position),
'fill': fill || '#FFFFFF',
}, thick ? PENCIL.thick : PENCIL.normal));
}, attrs || (thick ? PENCIL.thick : PENCIL.normal)));
}
renderNote({x, y, width, height}) {
@ -905,21 +929,22 @@ define([
}, PENCIL.normal));
}
renderAgentLine({x, y0, y1, width, className}) {
renderAgentLine({x, y0, y1, width, className, options}) {
const attrs = this.optionsAttributes(this.agentLineAttrs, options);
if(width > 0) {
const shape = this.renderBox({
x: x - width / 2,
y: y0,
width,
height: y1 - y0,
}, {fill: 'none'});
}, {fill: 'none', attrs});
shape.setAttribute('class', className);
return shape;
} else {
const shape = this.renderLine(
{x, y: y0},
{x, y: y1},
{varY: 0.3}
{varY: 0.3, attrs}
);
shape.setAttribute('class', className);
return shape;