SequenceDiagram/lib/sequence-diagram.js

10758 lines
258 KiB
JavaScript

(function () {
'use strict';
function optionsAttributes(attributes, options) {
const attrs = Object.assign({}, attributes['']);
options.forEach((opt) => {
Object.assign(attrs, attributes[opt] || {});
});
return attrs;
}
class WavePattern {
constructor(width, height) {
if(Array.isArray(height)) {
this.deltas = height;
} else {
this.deltas = [
0,
-height * 2 / 3,
-height,
-height * 2 / 3,
0,
height * 2 / 3,
height,
height * 2 / 3,
];
}
this.partWidth = width / this.deltas.length;
}
getDelta(p) {
return this.deltas[p % this.deltas.length];
}
}
class BaseTheme {
constructor(svg, baseFontAttrs) {
this.svg = svg;
this.baseFontAttrs = baseFontAttrs;
this.fontSize = this.baseFontAttrs['font-size'];
this.connectLines = new Map();
}
addConnectLine(type, {
attrs = {},
pattern = null,
} = {}) {
const base = this.connectLines.get('solid') || {attrs: {}};
const fullAttrs = Object.assign({'fill': 'none'}, base.attrs, attrs);
this.connectLines.set(type, {
attrs: fullAttrs,
renderFlat: this.renderFlatConnect.bind(this, pattern, fullAttrs),
renderRev: this.renderRevConnect.bind(this, pattern, fullAttrs),
});
}
// PUBLIC API
reset() {
// No-op
}
addDefs(builder, textBuilder) {
// Thanks, https://stackoverflow.com/a/12263962/1180785
// https://bugs.chromium.org/p/chromium/issues/detail?id=603157
// https://bugzilla.mozilla.org/show_bug.cgi?id=917766
textBuilder('highlight', () => this.svg.el('filter')
.add(
// Morph makes characters consistent
this.svg.el('feMorphology').attrs({
'in': 'SourceAlpha',
'operator': 'dilate',
'radius': '4',
}),
// Blur+thresh makes edges smooth
this.svg.el('feGaussianBlur').attrs({
'edgeMode': 'none',
'stdDeviation': '3, 1.5',
}),
this.svg.el('feComponentTransfer').add(
this.svg.el('feFuncA').attrs({
'intercept': -70,
'slope': 100,
'type': 'linear',
})
),
// Add colour
this.svg.el('feComponentTransfer').add(
this.svg.el('feFuncR').attrs({
'intercept': 1,
'slope': 0,
'type': 'linear',
}),
this.svg.el('feFuncG').attrs({
'intercept': 0.875,
'slope': 0,
'type': 'linear',
}),
this.svg.el('feFuncB').attrs({
'intercept': 0,
'slope': 0,
'type': 'linear',
}),
this.svg.el('feFuncA').attrs({
'slope': 0.8,
'type': 'linear',
})
),
// Draw text on top
this.svg.el('feMerge').add(
this.svg.el('feMergeNode'),
this.svg.el('feMergeNode').attr('in', 'SourceGraphic')
)
));
}
getTitleAttrs() {
return Object.assign({}, this.baseFontAttrs, {
'font-size': this.fontSize * 2.5,
'text-anchor': 'middle',
});
}
getConnectLine(type) {
const lines = this.connectLines;
return lines.get(type) || lines.get('solid');
}
getBlock(type) {
return this.blocks[type] || this.blocks[''];
}
getNote(type) {
return this.notes[type] || this.notes[''];
}
getDivider(type) {
return this.dividers[type] || this.dividers[''];
}
optionsAttributes(attributes, options) {
return optionsAttributes(attributes, options);
}
renderAgentLine({className, options, width, x, y0, y1}) {
const attrs = this.optionsAttributes(this.agentLineAttrs, options);
if(width > 0) {
return this.svg.box(attrs, {
height: y1 - y0,
width,
x: x - width / 2,
y: y0,
}).addClass(className);
} else {
return this.svg.line(attrs, {
'x1': x,
'x2': x,
'y1': y0,
'y2': y1,
}).addClass(className);
}
}
// INTERNAL HELPERS
renderArrowHead(attrs, {dir, height, width, x, y}) {
const wx = width * dir.dx;
const wy = width * dir.dy;
const hy = height * 0.5 * dir.dx;
const hx = -height * 0.5 * dir.dy;
return this.svg.el(attrs.fill === 'none' ? 'polyline' : 'polygon')
.attr('points', (
(x + wx - hx) + ' ' + (y + wy - hy) + ' ' +
x + ' ' + y + ' ' +
(x + wx + hx) + ' ' + (y + wy + hy)
))
.attrs(attrs);
}
renderTag(attrs, {height, width, x, y}) {
const {rx, ry} = attrs;
const x2 = x + width;
const y2 = y + height;
const line = (
'M' + x2 + ' ' + y +
'L' + x2 + ' ' + (y2 - ry) +
'L' + (x2 - rx) + ' ' + y2 +
'L' + x + ' ' + y2
);
const g = this.svg.el('g');
if(attrs.fill !== 'none') {
g.add(this.svg.el('path')
.attr('d', line + 'L' + x + ' ' + y)
.attrs(attrs)
.attr('stroke', 'none'));
}
if(attrs.stroke !== 'none') {
g.add(this.svg.el('path')
.attr('d', line)
.attrs(attrs)
.attr('fill', 'none'));
}
return g;
}
renderDB(attrs, position) {
const z = attrs['db-z'];
return this.svg.el('g').add(
this.svg.box({
'rx': position.width / 2,
'ry': z,
}, position).attrs(attrs),
this.svg.el('path')
.attr('d', (
'M' + position.x + ' ' + (position.y + z) +
'a' + (position.width / 2) + ' ' + z +
' 0 0 0 ' + position.width + ' 0'
))
.attrs(attrs)
.attr('fill', 'none')
);
}
renderRef(options, position) {
return {
fill: this.svg.box(options, position).attrs({'stroke': 'none'}),
mask: this.svg.box(options, position).attrs({
'fill': '#000000',
'stroke': 'none',
}),
shape: this.svg.box(options, position).attrs({'fill': 'none'}),
};
}
renderFlatConnect(
pattern,
attrs,
{x1, y1, x2, y2}
) {
return {
p1: {x: x1, y: y1},
p2: {x: x2, y: y2},
shape: this.svg.el('path')
.attr('d', this.svg.patternedLine(pattern)
.move(x1, y1)
.line(x2, y2)
.cap()
.asPath())
.attrs(attrs),
};
}
renderRevConnect(
pattern,
attrs,
{rad, x1, x2, xR, y1, y2}
) {
const maxRad = (y2 - y1) / 2;
const line = this.svg.patternedLine(pattern)
.move(x1, y1)
.line(xR, y1);
if(rad < maxRad) {
line
.arc(xR, y1 + rad, Math.PI / 2)
.line(xR + rad, y2 - rad)
.arc(xR, y2 - rad, Math.PI / 2);
} else {
line.arc(xR, (y1 + y2) / 2, Math.PI);
}
return {
p1: {x: x1, y: y1},
p2: {x: x2, y: y2},
shape: this.svg.el('path')
.attr('d', line
.line(x2, y2)
.cap()
.asPath())
.attrs(attrs),
};
}
renderLineDivider(
{lineAttrs},
{height, labelWidth, width, x, y}
) {
let shape = null;
const yPos = y + height / 2;
if(labelWidth > 0) {
shape = this.svg.el('g').add(
this.svg.line({'fill': 'none'}, {
'x1': x,
'x2': x + (width - labelWidth) / 2,
'y1': yPos,
'y2': yPos,
}).attrs(lineAttrs),
this.svg.line({'fill': 'none'}, {
'x1': x + (width + labelWidth) / 2,
'x2': x + width,
'y1': yPos,
'y2': yPos,
}).attrs(lineAttrs)
);
} else {
shape = this.svg.line({'fill': 'none'}, {
'x1': x,
'x2': x + width,
'y1': yPos,
'y2': yPos,
}).attrs(lineAttrs);
}
return {shape};
}
renderDelayDivider(
{dotSize, gapSize},
{height, width, x, y}
) {
const mask = this.svg.el('g');
for(let i = 0; i + gapSize <= height; i += dotSize + gapSize) {
mask.add(this.svg.box({
'fill': '#000000',
}, {
height: gapSize,
width,
x,
y: y + i,
}));
}
return {mask};
}
renderTearDivider(
{fadeBegin, fadeSize, lineAttrs, pattern, zigHeight, zigWidth},
{env, height, labelHeight, labelWidth, width, x, y}
) {
const maskGradID = env.addDef('tear-grad', () => {
const px = 100 / width;
return this.svg.linearGradient({}, [
{
'offset': (fadeBegin * px) + '%',
'stop-color': '#000000',
},
{
'offset': ((fadeBegin + fadeSize) * px) + '%',
'stop-color': '#FFFFFF',
},
{
'offset': (100 - (fadeBegin + fadeSize) * px) + '%',
'stop-color': '#FFFFFF',
},
{
'offset': (100 - fadeBegin * px) + '%',
'stop-color': '#000000',
},
]);
});
const shapeMask = this.svg.el('mask')
.attr('maskUnits', 'userSpaceOnUse')
.add(
this.svg.box({
'fill': 'url(#' + maskGradID + ')',
}, {
height: height + 10,
width,
x,
y: y - 5,
})
);
const shapeMaskID = env.addDef(shapeMask);
if(labelWidth > 0) {
shapeMask.add(this.svg.box({
'fill': '#000000',
'rx': 2,
'ry': 2,
}, {
'height': labelHeight + 2,
'width': labelWidth,
'x': x + (width - labelWidth) / 2,
'y': y + (height - labelHeight) / 2 - 1,
}));
}
const p = pattern || new WavePattern(zigWidth, [zigHeight, -zigHeight]);
let mask = null;
const pathTop = this.svg.patternedLine(p)
.move(x, y)
.line(x + width, y);
const shape = this.svg.el('g')
.attr('mask', 'url(#' + shapeMaskID + ')')
.add(
this.svg.el('path')
.attrs({
'd': pathTop.asPath(),
'fill': 'none',
})
.attrs(lineAttrs)
);
if(height > 0) {
const pathBase = this.svg.patternedLine(p)
.move(x, y + height)
.line(x + width, y + height);
shape.add(
this.svg.el('path')
.attrs({
'd': pathBase.asPath(),
'fill': 'none',
})
.attrs(lineAttrs)
);
pathTop
.line(pathBase.x, pathBase.y, {patterned: false})
.cap();
pathTop.points.push(...pathBase.points.reverse());
mask = this.svg.el('path').attrs({
'd': pathTop.asPath(),
'fill': '#000000',
});
}
return {mask, shape};
}
}
/* eslint-disable sort-keys */ // Maybe later
const FONT = 'Helvetica,Arial,Liberation Sans,sans-serif';
const LINE_HEIGHT = 1.3;
const NOTE_ATTRS = {
'font-family': FONT,
'font-size': 8,
'line-height': LINE_HEIGHT,
};
const DIVIDER_LABEL_ATTRS = {
'font-family': FONT,
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
};
class BasicTheme extends BaseTheme {
constructor(svg) {
super(svg, {
'font-family': FONT,
'font-size': 8,
'line-height': LINE_HEIGHT,
});
const sharedBlockSection = {
padding: {
top: 3,
bottom: 2,
},
tag: {
padding: {
top: 1,
left: 3,
right: 3,
bottom: 0,
},
boxRenderer: this.renderTag.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 2,
'ry': 2,
}),
labelAttrs: {
'font-family': FONT,
'font-weight': 'bold',
'font-size': 9,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
label: {
minHeight: 4,
padding: {
top: 1,
left: 5,
right: 3,
bottom: 1,
},
labelAttrs: {
'font-family': FONT,
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'left',
},
},
};
Object.assign(this, {
titleMargin: 10,
outerMargin: 5,
agentMargin: 10,
actionMargin: 10,
minActionMargin: 3,
agentLineHighlightRadius: 4,
agentCap: {
box: {
padding: {
top: 5,
left: 10,
right: 10,
bottom: 5,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
},
labelAttrs: {
'font-family': FONT,
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 12,
left: 10,
right: 10,
bottom: 3,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxRenderer: this.renderDB.bind(this, {
'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: svg.crossFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
},
bar: {
height: 4,
render: svg.boxFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
}),
},
fade: {
width: 5,
height: 6,
extend: 1,
},
none: {
height: 10,
},
},
connect: {
loopbackRadius: 6,
arrow: {
'single': {
width: 5,
height: 10,
render: this.renderArrowHead.bind(this),
attrs: {
'fill': '#000000',
'stroke-width': 0,
'stroke-linejoin': 'miter',
},
},
'double': {
width: 4,
height: 6,
render: this.renderArrowHead.bind(this),
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'stroke-linejoin': 'miter',
},
},
'cross': {
short: 7,
radius: 3,
render: svg.crossFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
},
},
label: {
padding: 6,
margin: {top: 2, bottom: 1},
attrs: {
'font-family': FONT,
'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
loopbackAttrs: {
'font-family': FONT,
'font-size': 8,
'line-height': LINE_HEIGHT,
},
},
source: {
radius: 2,
render: svg.circleFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
}),
},
mask: {
padding: {
top: 0,
left: 3,
right: 3,
bottom: 1,
},
},
},
agentLineAttrs: {
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
'red': {
'stroke': '#CC0000',
},
},
blocks: {
'ref': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: this.renderRef.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
}),
section: sharedBlockSection,
},
'': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: svg.boxFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
}),
collapsedBoxRenderer: this.renderRef.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1.5,
'rx': 2,
'ry': 2,
}),
section: sharedBlockSection,
sepRenderer: svg.lineFactory({
'stroke': '#000000',
'stroke-width': 1.5,
'stroke-dasharray': '4, 2',
}),
},
},
notes: {
'text': {
margin: {top: 0, left: 2, right: 2, bottom: 0},
padding: {top: 2, left: 2, right: 2, bottom: 2},
overlap: {left: 10, right: 10},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
}),
labelAttrs: NOTE_ATTRS,
},
'note': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 5, left: 5, right: 10, bottom: 5},
overlap: {left: 10, right: 10},
boxRenderer: svg.noteFactory({
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
}, {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
labelAttrs: NOTE_ATTRS,
},
'state': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 7, left: 7, right: 7, bottom: 7},
overlap: {left: 10, right: 10},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 10,
'ry': 10,
}),
labelAttrs: NOTE_ATTRS,
},
},
dividers: {
'': {
labelAttrs: DIVIDER_LABEL_ATTRS,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: () => ({}),
},
'line': {
labelAttrs: DIVIDER_LABEL_ATTRS,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 10,
margin: 0,
render: this.renderLineDivider.bind(this, {
lineAttrs: {
'stroke': '#000000',
},
}),
},
'delay': {
labelAttrs: DIVIDER_LABEL_ATTRS,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: this.renderDelayDivider.bind(this, {
dotSize: 1,
gapSize: 2,
}),
},
'tear': {
labelAttrs: DIVIDER_LABEL_ATTRS,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 10,
margin: 10,
render: this.renderTearDivider.bind(this, {
fadeBegin: 5,
fadeSize: 10,
zigWidth: 6,
zigHeight: 1,
lineAttrs: {
'stroke': '#000000',
},
}),
},
},
});
this.addConnectLine('solid', {attrs: {
'stroke': '#000000',
'stroke-width': 1,
}});
this.addConnectLine('dash', {attrs: {
'stroke-dasharray': '4, 2',
}});
this.addConnectLine('wave', {
attrs: {
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
},
pattern: new WavePattern(6, 0.5),
});
}
}
class Factory {
constructor() {
this.name = 'basic';
}
build(svg) {
return new BasicTheme(svg);
}
}
/* eslint-disable sort-keys */ // Maybe later
const FONT$1 = 'Helvetica,Arial,Liberation Sans,sans-serif';
const LINE_HEIGHT$1 = 1.3;
const NOTE_ATTRS$1 = {
'font-family': FONT$1,
'font-size': 8,
'line-height': LINE_HEIGHT$1,
};
const DIVIDER_LABEL_ATTRS$1 = {
'font-family': FONT$1,
'font-size': 8,
'line-height': LINE_HEIGHT$1,
'text-anchor': 'middle',
};
class ChunkyTheme extends BaseTheme {
constructor(svg) {
super(svg, {
'font-family': FONT$1,
'font-size': 8,
'line-height': LINE_HEIGHT$1,
});
const sharedBlockSection = {
padding: {
top: 3,
bottom: 4,
},
tag: {
padding: {
top: 2,
left: 5,
right: 5,
bottom: 1,
},
boxRenderer: this.renderTag.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 2,
'rx': 3,
'ry': 3,
}),
labelAttrs: {
'font-family': FONT$1,
'font-weight': 'bold',
'font-size': 9,
'line-height': LINE_HEIGHT$1,
'text-anchor': 'left',
},
},
label: {
minHeight: 5,
padding: {
top: 2,
left: 5,
right: 3,
bottom: 1,
},
labelAttrs: {
'font-family': FONT$1,
'font-size': 8,
'line-height': LINE_HEIGHT$1,
'text-anchor': 'left',
},
},
};
Object.assign(this, {
titleMargin: 12,
outerMargin: 5,
agentMargin: 8,
actionMargin: 5,
minActionMargin: 5,
agentLineHighlightRadius: 4,
agentCap: {
box: {
padding: {
top: 1,
left: 3,
right: 3,
bottom: 1,
},
arrowBottom: 2 + 14 * 1.3 / 2,
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'rx': 4,
'ry': 4,
},
labelAttrs: {
'font-family': FONT$1,
'font-weight': 'bold',
'font-size': 14,
'line-height': LINE_HEIGHT$1,
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 4,
left: 3,
right: 3,
bottom: 0,
},
arrowBottom: 2 + 14 * 1.3 / 2,
boxRenderer: this.renderDB.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'db-z': 2,
}),
labelAttrs: {
'font-family': FONT$1,
'font-weight': 'bold',
'font-size': 14,
'line-height': LINE_HEIGHT$1,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
render: svg.crossFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'stroke-linecap': 'round',
}),
},
bar: {
height: 4,
render: svg.boxFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 3,
'rx': 2,
'ry': 2,
}),
},
fade: {
width: 5,
height: 10,
extend: 1,
},
none: {
height: 10,
},
},
connect: {
loopbackRadius: 8,
arrow: {
'single': {
width: 10,
height: 12,
render: this.renderArrowHead.bind(this),
attrs: {
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 3,
'stroke-linejoin': 'round',
},
},
'double': {
width: 10,
height: 12,
render: this.renderArrowHead.bind(this),
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
},
},
'cross': {
short: 10,
radius: 5,
render: svg.crossFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
}),
},
},
label: {
padding: 7,
margin: {top: 2, bottom: 3},
attrs: {
'font-family': FONT$1,
'font-size': 8,
'line-height': LINE_HEIGHT$1,
'text-anchor': 'middle',
},
loopbackAttrs: {
'font-family': FONT$1,
'font-size': 8,
'line-height': LINE_HEIGHT$1,
},
},
source: {
radius: 5,
render: svg.circleFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 3,
}),
},
mask: {
padding: {
top: 1,
left: 5,
right: 5,
bottom: 3,
},
},
},
agentLineAttrs: {
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 3,
},
'red': {
'stroke': '#DD0000',
},
},
blocks: {
'ref': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: this.renderRef.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 4,
'rx': 5,
'ry': 5,
}),
section: sharedBlockSection,
},
'': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: svg.boxFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 4,
'rx': 5,
'ry': 5,
}),
collapsedBoxRenderer: this.renderRef.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 4,
'rx': 5,
'ry': 5,
}),
section: sharedBlockSection,
sepRenderer: svg.lineFactory({
'stroke': '#000000',
'stroke-width': 2,
'stroke-dasharray': '5, 3',
}),
},
},
notes: {
'text': {
margin: {top: 0, left: 2, right: 2, bottom: 0},
padding: {top: 2, left: 2, right: 2, bottom: 2},
overlap: {left: 10, right: 10},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
}),
labelAttrs: NOTE_ATTRS$1,
},
'note': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 3, left: 3, right: 10, bottom: 3},
overlap: {left: 10, right: 10},
boxRenderer: svg.noteFactory({
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 2,
'stroke-linejoin': 'round',
}, {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
labelAttrs: NOTE_ATTRS$1,
},
'state': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 5, left: 7, right: 7, bottom: 5},
overlap: {left: 10, right: 10},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 3,
'rx': 10,
'ry': 10,
}),
labelAttrs: NOTE_ATTRS$1,
},
},
dividers: {
'': {
labelAttrs: DIVIDER_LABEL_ATTRS$1,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: () => ({}),
},
'line': {
labelAttrs: DIVIDER_LABEL_ATTRS$1,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 10,
margin: 0,
render: this.renderLineDivider.bind(this, {
lineAttrs: {
'stroke': '#000000',
'stroke-width': 2,
'stroke-linecap': 'round',
},
}),
},
'delay': {
labelAttrs: DIVIDER_LABEL_ATTRS$1,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: this.renderDelayDivider.bind(this, {
dotSize: 3,
gapSize: 3,
}),
},
'tear': {
labelAttrs: DIVIDER_LABEL_ATTRS$1,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 10,
margin: 10,
render: this.renderTearDivider.bind(this, {
fadeBegin: 5,
fadeSize: 10,
zigWidth: 6,
zigHeight: 1,
lineAttrs: {
'stroke': '#000000',
'stroke-width': 2,
'stroke-linejoin': 'round',
},
}),
},
},
});
this.addConnectLine('solid', {attrs: {
'stroke': '#000000',
'stroke-width': 3,
}});
this.addConnectLine('dash', {attrs: {
'stroke-dasharray': '10, 4',
}});
this.addConnectLine('wave', {
attrs: {
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
},
pattern: new WavePattern(10, 1),
});
}
getTitleAttrs() {
return Object.assign(super.getTitleAttrs(), {
'font-weight': 'bolder',
});
}
}
class Factory$1 {
constructor() {
this.name = 'chunky';
}
build(svg) {
return new ChunkyTheme(svg);
}
}
class EventObject {
constructor() {
this.listeners = new Map();
this.forwards = new Set();
}
addEventListener(type, callback) {
const l = this.listeners.get(type);
if(l) {
l.push(callback);
} else {
this.listeners.set(type, [callback]);
}
}
removeEventListener(type, fn) {
const l = this.listeners.get(type);
if(!l) {
return;
}
const i = l.indexOf(fn);
if(i !== -1) {
l.splice(i, 1);
}
}
on(type, fn) {
this.addEventListener(type, fn);
return this;
}
off(type, fn) {
this.removeEventListener(type, fn);
return this;
}
countEventListeners(type) {
return (this.listeners.get(type) || []).length;
}
removeAllEventListeners(type) {
if(type) {
this.listeners.delete(type);
} else {
this.listeners.clear();
}
}
addEventForwarding(target) {
this.forwards.add(target);
}
removeEventForwarding(target) {
this.forwards.delete(target);
}
removeAllEventForwardings() {
this.forwards.clear();
}
trigger(type, params = []) {
(this.listeners.get(type) || []).forEach(
(listener) => listener(...params)
);
this.forwards.forEach((fwd) => fwd.trigger(type, params));
}
}
const nodejs = (typeof window === 'undefined');
// Thanks, https://stackoverflow.com/a/23522755/1180785
const safari = (
!nodejs &&
(/^((?!chrome|android).)*safari/i).test(window.navigator.userAgent)
);
// Thanks, https://stackoverflow.com/a/9851769/1180785
const firefox = (
!nodejs &&
typeof window.InstallTrigger !== 'undefined'
);
class Exporter {
constructor() {
this.latestSVG = null;
this.latestInternalSVG = null;
this.canvas = null;
this.context = null;
this.indexPNG = 0;
this.latestPNGIndex = 0;
this.latestPNG = null;
}
getSVGContent(renderer) {
let code = renderer.dom().outerHTML;
/*
* Firefox fails to render SVGs as <img> unless they have size
* attributes on the <svg> tag, so we must set this when
* exporting from any environment, in case it is opened in FireFox
*/
code = code.replace(
(/^<svg ?/),
'<svg width="' + (renderer.width || 1) +
'" height="' + (renderer.height || 1) + '" '
);
return code;
}
getSVGBlob(renderer) {
return new Blob(
[this.getSVGContent(renderer)],
{type: 'image/svg+xml'}
);
}
getSVGURL(renderer) {
const blob = this.getSVGBlob(renderer);
if(this.latestSVG) {
URL.revokeObjectURL(this.latestSVG);
}
this.latestSVG = URL.createObjectURL(blob);
return this.latestSVG;
}
getCanvas(renderer, resolution, callback) {
if(!this.canvas) {
window.devicePixelRatio = 1;
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
}
const width = (renderer.width || 1) * resolution;
const height = (renderer.height || 1) * resolution;
const img = new Image(width, height);
let safariHackaround = null;
if(safari) {
/*
* Safari fails to resize SVG images unless they are displayed
* on the page somewhere, so we must add it before drawing the
* image. For some reason, doing this inside the load listener
* is too late, so we do it here and do our best to ensure it
* doesn't change the page rendering (display:none fails too)
*/
safariHackaround = document.createElement('div');
safariHackaround.style.position = 'absolute';
safariHackaround.style.visibility = 'hidden';
safariHackaround.appendChild(img);
document.body.appendChild(safariHackaround);
}
const render = () => {
this.canvas.width = width;
this.canvas.height = height;
this.context.drawImage(img, 0, 0, width, height);
if(safariHackaround) {
document.body.removeChild(safariHackaround);
}
callback(this.canvas);
};
img.addEventListener('load', () => {
if(safariHackaround) {
// Wait for custom fonts to load (Safari takes a moment)
setTimeout(render, 50);
} else {
render();
}
}, {once: true});
img.src = this.getSVGURL(renderer);
}
getPNGBlob(renderer, resolution, callback) {
this.getCanvas(renderer, resolution, (canvas) => {
canvas.toBlob(callback, 'image/png');
});
}
getPNGURL(renderer, resolution, callback) {
++ this.indexPNG;
const index = this.indexPNG;
this.getPNGBlob(renderer, resolution, (blob) => {
const url = URL.createObjectURL(blob);
const isLatest = index >= this.latestPNGIndex;
if(isLatest) {
if(this.latestPNG) {
URL.revokeObjectURL(this.latestPNG);
}
this.latestPNG = url;
this.latestPNGIndex = index;
callback(url, true);
} else {
callback(url, false);
URL.revokeObjectURL(url);
}
});
}
}
function indexOf(list, element, equalityCheck = null) {
if(equalityCheck === null) {
return list.indexOf(element);
}
for(let i = 0; i < list.length; ++ i) {
if(equalityCheck(list[i], element)) {
return i;
}
}
return -1;
}
function mergeSets(target, b = null, equalityCheck = null) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
if(indexOf(target, b[i], equalityCheck) === -1) {
target.push(b[i]);
}
}
}
function hasIntersection(a, b, equalityCheck = null) {
for(let i = 0; i < b.length; ++ i) {
if(indexOf(a, b[i], equalityCheck) !== -1) {
return true;
}
}
return false;
}
function removeAll(target, b = null, equalityCheck = null) {
if(!b) {
return;
}
for(let i = 0; i < b.length; ++ i) {
const p = indexOf(target, b[i], equalityCheck);
if(p !== -1) {
target.splice(p, 1);
}
}
}
function remove(list, item, equalityCheck = null) {
const p = indexOf(list, item, equalityCheck);
if(p !== -1) {
list.splice(p, 1);
}
}
function last(list) {
return list[list.length - 1];
}
function combineRecur(parts, position, current, target) {
if(position >= parts.length) {
target.push(current.slice());
return;
}
const choices = parts[position];
if(!Array.isArray(choices)) {
current.push(choices);
combineRecur(parts, position + 1, current, target);
current.pop();
return;
}
for(let i = 0; i < choices.length; ++ i) {
current.push(choices[i]);
combineRecur(parts, position + 1, current, target);
current.pop();
}
}
function combine(parts) {
const target = [];
combineRecur(parts, 0, [], target);
return target;
}
function flatMap(list, fn) {
const result = [];
list.forEach((item) => {
result.push(...fn(item));
});
return result;
}
/* eslint-disable max-lines */
class AgentState {
constructor({
blocked = false,
covered = false,
group = null,
highlighted = false,
locked = false,
visible = false,
} = {}) {
this.blocked = blocked;
this.covered = covered;
this.group = group;
this.highlighted = highlighted;
this.locked = locked;
this.visible = visible;
}
}
AgentState.LOCKED = new AgentState({locked: true});
AgentState.DEFAULT = new AgentState();
// Agent from Parser: {name, flags}
const PAgent = {
equals: (a, b) => (a.name === b.name),
hasFlag: (flag, has = true) => (pAgent) => (
pAgent.flags.includes(flag) === has),
};
// Agent from Generator: {id, formattedLabel, anchorRight}
const GAgent = {
addNearby: (target, reference, item, offset) => {
const p = indexOf(target, reference, GAgent.equals);
if(p === -1) {
target.push(item);
} else {
target.splice(p + offset, 0, item);
}
},
equals: (a, b) => (a.id === b.id),
hasIntersection: (a, b) => hasIntersection(a, b, GAgent.equals),
indexOf: (list, gAgent) => indexOf(list, gAgent, GAgent.equals),
make: (id, {anchorRight = false, isVirtualSource = false} = {}) => ({
anchorRight,
id,
isVirtualSource,
options: [],
}),
};
function isExpiredGroupAlias(state) {
return state.blocked && state.group === null;
}
function isReservedAgentName(name) {
return name.startsWith('__');
}
const NOTE_DEFAULT_G_AGENTS = {
'note left': [GAgent.make('[')],
'note over': [GAgent.make('['), GAgent.make(']')],
'note right': [GAgent.make(']')],
};
const SPECIAL_AGENT_IDS = ['[', ']'];
const MERGABLE = {
'agent begin': {
check: ['mode'],
merge: ['agentIDs'],
siblings: new Set(['agent highlight']),
},
'agent end': {
check: ['mode'],
merge: ['agentIDs'],
siblings: new Set(['agent highlight']),
},
'agent highlight': {
check: ['highlighted'],
merge: ['agentIDs'],
siblings: new Set(['agent begin', 'agent end']),
},
};
function mergableParallel(target, copy) {
const info = MERGABLE[target.type];
if(!info || target.type !== copy.type) {
return false;
}
if(info.check.some((c) => target[c] !== copy[c])) {
return false;
}
return true;
}
function performMerge(target, copy) {
const info = MERGABLE[target.type];
info.merge.forEach((m) => {
mergeSets(target[m], copy[m]);
});
}
function iterateRemoval(list, fn) {
for(let i = 0; i < list.length;) {
const rm = fn(list[i], i);
if(rm) {
list.splice(i, 1);
} else {
++ i;
}
}
}
function performParallelMergers(stages) {
iterateRemoval(stages, (stage, i) => {
for(let j = 0; j < i; ++ j) {
if(mergableParallel(stages[j], stage)) {
performMerge(stages[j], stage);
return true;
}
}
return false;
});
}
function findViableSequentialMergers(stages) {
const mergers = new Set();
const types = stages.map(({type}) => type);
types.forEach((type) => {
const info = MERGABLE[type];
if(!info) {
return;
}
if(types.every((t) => (type === t || info.siblings.has(t)))) {
mergers.add(type);
}
});
return mergers;
}
function performSequentialMergers(lastViable, viable, lastStages, stages) {
iterateRemoval(stages, (stage) => {
if(!lastViable.has(stage.type) || !viable.has(stage.type)) {
return false;
}
for(let j = 0; j < lastStages.length; ++ j) {
if(mergableParallel(lastStages[j], stage)) {
performMerge(lastStages[j], stage);
return true;
}
}
return false;
});
}
function optimiseStages(stages) {
let lastStages = [];
let lastViable = new Set();
for(let i = 0; i < stages.length;) {
const stage = stages[i];
let subStages = null;
if(stage.type === 'parallel') {
subStages = stage.stages;
} else {
subStages = [stage];
}
performParallelMergers(subStages);
const viable = findViableSequentialMergers(subStages);
performSequentialMergers(lastViable, viable, lastStages, subStages);
if(subStages.length === 0) {
stages.splice(i, 1);
} else {
if(stage.type === 'parallel' && subStages.length === 1) {
stages.splice(i, 1, subStages[0]);
}
lastViable = viable;
lastStages = subStages;
++ i;
}
}
}
function extractParallel(target, stages) {
for(const stage of stages) {
if(!stage) {
continue;
}
if(stage.type === 'parallel') {
extractParallel(target, stage.stages);
} else {
target.push(stage);
}
}
}
function checkAgentConflicts(allStages) {
const createIDs = flatMap(
allStages
.filter((stage) => (stage.type === 'agent begin')),
(stage) => stage.agentIDs
);
for(const stage of allStages) {
if(stage.type !== 'agent end') {
continue;
}
for(const id of stage.agentIDs) {
if(createIDs.indexOf(id) !== -1) {
return 'Cannot create and destroy ' + id + ' simultaneously';
}
}
}
return null;
}
function checkReferenceConflicts(allStages) {
const count = allStages
.filter((stage) => (
stage.type === 'block begin' ||
stage.type === 'block end'
))
.length;
if(!count) {
return null;
} else if(count !== allStages.length) {
return 'Cannot use parallel here';
}
const leftIDs = allStages
.filter((stage) => (stage.type === 'block begin'))
.map((stage) => stage.left);
for(const stage of allStages) {
if(stage.type !== 'block end') {
continue;
}
if(leftIDs.indexOf(stage.left) !== -1) {
return 'Cannot create and destroy reference simultaneously';
}
}
return null;
}
function checkDelayedConflicts(allStages) {
const tags = allStages
.filter((stage) => (stage.type === 'connect-delay-begin'))
.map((stage) => stage.tag);
for(const stage of allStages) {
if(stage.type !== 'connect-delay-end') {
continue;
}
if(tags.indexOf(stage.tag) !== -1) {
return 'Cannot start and finish delayed connection simultaneously';
}
}
return null;
}
const PARALLEL_STAGES = [
'agent begin',
'agent end',
'agent highlight',
'block begin',
'block end',
'connect',
'connect-delay-begin',
'connect-delay-end',
'note over',
'note right',
'note left',
'note between',
];
function errorForParallel(existing, latest) {
if(!existing) {
return 'Nothing to run statement in parallel with';
}
const allStages = [];
extractParallel(allStages, [existing]);
extractParallel(allStages, [latest]);
if(allStages.some((stage) => !PARALLEL_STAGES.includes(stage.type))) {
return 'Cannot use parallel here';
}
return (
checkAgentConflicts(allStages) ||
checkReferenceConflicts(allStages) ||
checkDelayedConflicts(allStages)
);
}
function swapBegin(stage, mode) {
if(stage.type === 'agent begin') {
stage.mode = mode;
return true;
}
if(stage.type === 'parallel') {
let any = false;
stage.stages.forEach((subStage) => {
if(subStage.type === 'agent begin') {
subStage.mode = mode;
any = true;
}
});
return any;
}
return false;
}
function swapFirstBegin(stages, mode) {
for(let i = 0; i < stages.length; ++ i) {
if(swapBegin(stages[i], mode)) {
break;
}
}
}
function addBounds(allGAgents, gAgentL, gAgentR, involvedGAgents = null) {
remove(allGAgents, gAgentL, GAgent.equals);
remove(allGAgents, gAgentR, GAgent.equals);
let indexL = 0;
let indexR = allGAgents.length;
if(involvedGAgents) {
const found = (involvedGAgents
.map((gAgent) => GAgent.indexOf(allGAgents, gAgent))
.filter((p) => (p !== -1))
);
indexL = found.reduce((a, b) => Math.min(a, b), allGAgents.length);
indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1;
}
allGAgents.splice(indexL, 0, gAgentL);
allGAgents.splice(indexR + 1, 0, gAgentR);
return {indexL, indexR: indexR + 1};
}
class Generator {
constructor() {
this.agentStates = new Map();
this.agentAliases = new Map();
this.activeGroups = new Map();
this.gAgents = [];
this.labelPattern = null;
this.nextID = 0;
this.nesting = [];
this.markers = new Set();
this.currentSection = null;
this.currentNest = null;
this.stageHandlers = {
'agent begin': this.handleAgentBegin.bind(this),
'agent define': this.handleAgentDefine.bind(this),
'agent end': this.handleAgentEnd.bind(this),
'agent options': this.handleAgentOptions.bind(this),
'async': this.handleAsync.bind(this),
'block begin': this.handleBlockBegin.bind(this),
'block end': this.handleBlockEnd.bind(this),
'block split': this.handleBlockSplit.bind(this),
'connect': this.handleConnect.bind(this),
'connect-delay-begin': this.handleConnectDelayBegin.bind(this),
'connect-delay-end': this.handleConnectDelayEnd.bind(this),
'divider': this.handleDivider.bind(this),
'group begin': this.handleGroupBegin.bind(this),
'label pattern': this.handleLabelPattern.bind(this),
'mark': this.handleMark.bind(this),
'note between': this.handleNote.bind(this),
'note left': this.handleNote.bind(this),
'note over': this.handleNote.bind(this),
'note right': this.handleNote.bind(this),
};
this.expandGroupedGAgent = this.expandGroupedGAgent.bind(this);
this.handleStage = this.handleStage.bind(this);
this.toGAgent = this.toGAgent.bind(this);
this.endGroup = this.endGroup.bind(this);
}
_aliasInUse(alias) {
const old = this.agentAliases.get(alias);
if(old && old !== alias) {
return true;
}
return this.gAgents.some((gAgent) => (gAgent.id === alias));
}
toGAgent({name, alias, flags}) {
if(alias) {
if(this.agentAliases.has(name)) {
throw new Error(
'Cannot alias ' + name + '; it is already an alias'
);
}
if(this._aliasInUse(alias)) {
throw new Error(
'Cannot use ' + alias +
' as an alias; it is already in use'
);
}
this.agentAliases.set(alias, name);
}
return GAgent.make(this.agentAliases.get(name) || name, {
isVirtualSource: flags.includes('source'),
});
}
addStage(stage, {isVisible = true, parallel = false} = {}) {
if(!stage) {
return;
}
if(isVisible) {
this.currentNest.hasContent = true;
}
if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine;
}
const {stages} = this.currentSection;
if(parallel) {
const target = last(stages);
const err = errorForParallel(target, stage);
if(err) {
throw new Error(err);
}
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
-- stages.length;
stages.push(pstage);
} else {
stages.push(stage);
}
}
addImpStage(stage, {parallel = false} = {}) {
if(!stage) {
return;
}
if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine;
}
const {stages} = this.currentSection;
if(parallel) {
const target = stages[stages.length - 2];
if(stages.length === 0) {
throw new Error('Nothing to run statement in parallel with');
}
if(errorForParallel(target, stage)) {
stages.splice(stages.length - 1, 0, stage);
} else {
const pstage = this.makeParallel([target, stage]);
pstage.ln = stage.ln;
stages.splice(stages.length - 2, 1, pstage);
}
} else {
stages.push(stage);
}
}
makeParallel(stages) {
const viableStages = [];
extractParallel(viableStages, stages);
if(viableStages.length === 0) {
return null;
}
if(viableStages.length === 1) {
return viableStages[0];
}
viableStages.forEach((stage) => {
if(typeof stage.ln === 'undefined') {
stage.ln = this.latestLine;
}
});
return {
stages: viableStages,
type: 'parallel',
};
}
defineGAgents(gAgents) {
mergeSets(
this.currentNest.gAgents,
gAgents.filter((gAgent) => !SPECIAL_AGENT_IDS.includes(gAgent.id)),
GAgent.equals
);
mergeSets(this.gAgents, gAgents, GAgent.equals);
}
getGAgentState(gAgent) {
return this.agentStates.get(gAgent.id) || AgentState.DEFAULT;
}
updateGAgentState(gAgent, change) {
const state = this.agentStates.get(gAgent.id);
if(state) {
Object.assign(state, change);
} else {
this.agentStates.set(gAgent.id, new AgentState(change));
}
}
replaceGAgentState(gAgent, state) {
this.agentStates.set(gAgent.id, state);
}
validateGAgents(gAgents, {
allowGrouped = false,
allowCovered = false,
allowVirtual = false,
} = {}) {
/* eslint-disable complexity */ // The checks are quite simple
gAgents.forEach((gAgent) => {
/* eslint-enable complexity */
const state = this.getGAgentState(gAgent);
const name = gAgent.id;
if(isExpiredGroupAlias(state)) {
// Used to be a group alias; can never be reused
throw new Error('Duplicate agent name: ' + name);
}
if(!allowCovered && state.covered) {
throw new Error('Agent ' + name + ' is hidden behind group');
}
if(!allowGrouped && state.group !== null) {
throw new Error('Agent ' + name + ' is in a group');
}
if(!allowVirtual && gAgent.isVirtualSource) {
throw new Error('Cannot use message source here');
}
if(isReservedAgentName(name)) {
throw new Error(name + ' is a reserved name');
}
});
}
setGAgentVis(gAgents, visible, mode, checked = false) {
const seen = new Set();
const filteredGAgents = gAgents.filter((gAgent) => {
if(seen.has(gAgent.id)) {
return false;
}
seen.add(gAgent.id);
const state = this.getGAgentState(gAgent);
if(state.locked || state.blocked) {
if(checked) {
throw new Error('Cannot begin/end agent: ' + gAgent.id);
} else {
return false;
}
}
return state.visible !== visible;
});
if(filteredGAgents.length === 0) {
return null;
}
filteredGAgents.forEach((gAgent) => {
this.updateGAgentState(gAgent, {visible});
});
this.defineGAgents(filteredGAgents);
return {
agentIDs: filteredGAgents.map((gAgent) => gAgent.id),
mode,
type: (visible ? 'agent begin' : 'agent end'),
};
}
setGAgentHighlight(gAgents, highlighted, checked = false) {
const filteredGAgents = gAgents.filter((gAgent) => {
const state = this.getGAgentState(gAgent);
if(state.locked || state.blocked) {
if(checked) {
throw new Error('Cannot highlight agent: ' + gAgent.id);
} else {
return false;
}
}
return state.visible && (state.highlighted !== highlighted);
});
if(filteredGAgents.length === 0) {
return null;
}
filteredGAgents.forEach((gAgent) => {
this.updateGAgentState(gAgent, {highlighted});
});
return {
agentIDs: filteredGAgents.map((gAgent) => gAgent.id),
highlighted,
type: 'agent highlight',
};
}
_makeSection(header, stages) {
return {
delayedConnections: new Map(),
header,
stages,
};
}
_checkSectionEnd() {
const dcs = this.currentSection.delayedConnections;
if(dcs.size > 0) {
const dc = dcs.values().next().value;
throw new Error(
'Unused delayed connection "' + dc.tag +
'" at line ' + (dc.ln + 1)
);
}
}
beginNested(blockType, {tag, label, name, ln}) {
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
const rightGAgent = GAgent.make(name + ']');
const gAgents = [leftGAgent, rightGAgent];
const stages = [];
this.currentSection = this._makeSection({
blockType,
canHide: true,
label: this.textFormatter(label),
left: leftGAgent.id,
ln,
right: rightGAgent.id,
tag: this.textFormatter(tag),
type: 'block begin',
}, stages);
this.currentNest = {
blockType,
gAgents,
hasContent: false,
leftGAgent,
rightGAgent,
sections: [this.currentSection],
};
this.replaceGAgentState(leftGAgent, AgentState.LOCKED);
this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
this.nesting.push(this.currentNest);
return {stages};
}
nextBlockName() {
const name = '__BLOCK' + this.nextID;
++ this.nextID;
return name;
}
nextVirtualAgentName() {
const name = '__' + this.nextID;
++ this.nextID;
return name;
}
handleBlockBegin({ln, blockType, tag, label, parallel}) {
if(parallel) {
throw new Error('Cannot use parallel here');
}
this.beginNested(blockType, {
label,
ln,
name: this.nextBlockName(),
tag,
});
}
handleBlockSplit({ln, blockType, tag, label, parallel}) {
if(parallel) {
throw new Error('Cannot use parallel here');
}
if(this.currentNest.blockType !== 'if') {
throw new Error(
'Invalid block nesting ("else" inside ' +
this.currentNest.blockType + ')'
);
}
this._checkSectionEnd();
this.currentSection = this._makeSection({
blockType,
label: this.textFormatter(label),
left: this.currentNest.leftGAgent.id,
ln,
right: this.currentNest.rightGAgent.id,
tag: this.textFormatter(tag),
type: 'block split',
}, []);
this.currentNest.sections.push(this.currentSection);
}
handleBlockEnd({parallel}) {
if(this.nesting.length <= 1) {
throw new Error('Invalid block nesting (too many "end"s)');
}
this._checkSectionEnd();
const nested = this.nesting.pop();
this.currentNest = last(this.nesting);
this.currentSection = last(this.currentNest.sections);
if(!nested.hasContent) {
throw new Error('Empty block');
}
this.defineGAgents(nested.gAgents);
addBounds(
this.gAgents,
nested.leftGAgent,
nested.rightGAgent,
nested.gAgents
);
nested.sections.forEach((section) => {
this.currentSection.stages.push(section.header);
this.currentSection.stages.push(...section.stages);
});
this.addStage({
left: nested.leftGAgent.id,
right: nested.rightGAgent.id,
type: 'block end',
}, {parallel});
}
makeGroupDetails(pAgents, alias) {
const gAgents = pAgents.map(this.toGAgent);
this.validateGAgents(gAgents);
if(this.agentStates.has(alias)) {
throw new Error('Duplicate agent name: ' + alias);
}
const name = this.nextBlockName();
const leftGAgent = GAgent.make(name + '[', {anchorRight: true});
const rightGAgent = GAgent.make(name + ']');
this.replaceGAgentState(leftGAgent, AgentState.LOCKED);
this.replaceGAgentState(rightGAgent, AgentState.LOCKED);
this.updateGAgentState(
GAgent.make(alias),
{blocked: true, group: alias}
);
this.defineGAgents([...gAgents, leftGAgent, rightGAgent]);
const {indexL, indexR} = addBounds(
this.gAgents,
leftGAgent,
rightGAgent,
gAgents
);
const gAgentsCovered = [];
const gAgentsContained = gAgents.slice();
for(let i = indexL + 1; i < indexR; ++ i) {
gAgentsCovered.push(this.gAgents[i]);
}
removeAll(gAgentsCovered, gAgentsContained, GAgent.equals);
return {
gAgents,
gAgentsContained,
gAgentsCovered,
leftGAgent,
rightGAgent,
};
}
handleGroupBegin({agents, blockType, tag, label, alias, parallel}) {
const details = this.makeGroupDetails(agents, alias);
details.gAgentsContained.forEach((gAgent) => {
this.updateGAgentState(gAgent, {group: alias});
});
details.gAgentsCovered.forEach((gAgent) => {
this.updateGAgentState(gAgent, {covered: true});
});
this.activeGroups.set(alias, details);
this.addImpStage(
this.setGAgentVis(details.gAgents, true, 'box'),
{parallel}
);
this.addStage({
blockType,
canHide: false,
label: this.textFormatter(label),
left: details.leftGAgent.id,
right: details.rightGAgent.id,
tag: this.textFormatter(tag),
type: 'block begin',
}, {parallel});
}
endGroup({name}) {
const details = this.activeGroups.get(name);
if(!details) {
return null;
}
this.activeGroups.delete(name);
details.gAgentsContained.forEach((gAgent) => {
this.updateGAgentState(gAgent, {group: null});
});
details.gAgentsCovered.forEach((gAgent) => {
this.updateGAgentState(gAgent, {covered: false});
});
this.updateGAgentState(GAgent.make(name), {group: null});
return {
left: details.leftGAgent.id,
right: details.rightGAgent.id,
type: 'block end',
};
}
handleMark({name, parallel}) {
this.markers.add(name);
this.addStage({name, type: 'mark'}, {isVisible: false, parallel});
}
handleDivider({mode, height, label, parallel}) {
this.addStage({
formattedLabel: this.textFormatter(label),
height,
mode,
type: 'divider',
}, {isVisible: false, parallel});
}
handleAsync({target, parallel}) {
if(target !== '' && !this.markers.has(target)) {
throw new Error('Unknown marker: ' + target);
}
this.addStage({target, type: 'async'}, {isVisible: false, parallel});
}
handleLabelPattern({pattern}) {
this.labelPattern = pattern.slice();
for(let i = 0; i < this.labelPattern.length; ++ i) {
const part = this.labelPattern[i];
if(typeof part === 'object' && typeof part.start !== 'undefined') {
this.labelPattern[i] = Object.assign({
current: part.start,
}, part);
}
}
}
applyLabelPattern(label) {
let result = '';
const tokens = {label};
this.labelPattern.forEach((part) => {
if(typeof part === 'string') {
result += part;
} else if(typeof part.token !== 'undefined') {
result += tokens[part.token];
} else if(typeof part.current !== 'undefined') {
result += part.current.toFixed(part.dp);
part.current += part.inc;
}
});
return result;
}
expandGroupedGAgent(gAgent) {
const {group} = this.getGAgentState(gAgent);
if(!group) {
return [gAgent];
}
const details = this.activeGroups.get(group);
return [details.leftGAgent, details.rightGAgent];
}
expandGroupedGAgentConnection(gAgents) {
const gAgents1 = this.expandGroupedGAgent(gAgents[0]);
const gAgents2 = this.expandGroupedGAgent(gAgents[1]);
let ind1 = GAgent.indexOf(this.gAgents, gAgents1[0]);
let ind2 = GAgent.indexOf(this.gAgents, gAgents2[0]);
if(ind1 === -1) {
/*
* Virtual sources written as '* -> Ref' will spawn to the left,
* not the right (as non-virtual agents would)
*/
ind1 = gAgents1[0].isVirtualSource ? -1 : this.gAgents.length;
}
if(ind2 === -1) {
/*
* Virtual and non-virtual agents written as 'Ref -> *' will
* spawn to the right
*/
ind2 = this.gAgents.length;
}
if(ind1 === ind2) {
// Self-connection
return [last(gAgents1), last(gAgents2)];
} else if(ind1 < ind2) {
return [last(gAgents1), gAgents2[0]];
} else {
return [gAgents1[0], last(gAgents2)];
}
}
filterConnectFlags(pAgents) {
const beginGAgents = (pAgents
.filter(PAgent.hasFlag('begin'))
.map(this.toGAgent)
);
const endGAgents = (pAgents
.filter(PAgent.hasFlag('end'))
.map(this.toGAgent)
);
if(GAgent.hasIntersection(beginGAgents, endGAgents)) {
throw new Error('Cannot set agent visibility multiple times');
}
const startGAgents = (pAgents
.filter(PAgent.hasFlag('start'))
.map(this.toGAgent)
);
const stopGAgents = (pAgents
.filter(PAgent.hasFlag('stop'))
.map(this.toGAgent)
);
mergeSets(stopGAgents, endGAgents);
if(GAgent.hasIntersection(startGAgents, stopGAgents)) {
throw new Error('Cannot set agent highlighting multiple times');
}
this.validateGAgents(beginGAgents);
this.validateGAgents(endGAgents);
this.validateGAgents(startGAgents);
this.validateGAgents(stopGAgents);
return {beginGAgents, endGAgents, startGAgents, stopGAgents};
}
makeVirtualAgent(anchorRight) {
const virtualGAgent = GAgent.make(this.nextVirtualAgentName(), {
anchorRight,
isVirtualSource: true,
});
this.replaceGAgentState(virtualGAgent, AgentState.LOCKED);
return virtualGAgent;
}
addNearbyAgent(gAgentReference, gAgent, offset) {
GAgent.addNearby(
this.currentNest.gAgents,
gAgentReference,
gAgent,
offset
);
GAgent.addNearby(
this.gAgents,
gAgentReference,
gAgent,
offset
);
}
expandVirtualSourceAgents(gAgents) {
if(gAgents[0].isVirtualSource) {
if(gAgents[1].isVirtualSource) {
throw new Error('Cannot connect found messages');
}
if(SPECIAL_AGENT_IDS.includes(gAgents[1].id)) {
throw new Error(
'Cannot connect found messages to special agents'
);
}
const virtualGAgent = this.makeVirtualAgent(true);
this.addNearbyAgent(gAgents[1], virtualGAgent, 0);
return [virtualGAgent, gAgents[1]];
}
if(gAgents[1].isVirtualSource) {
if(SPECIAL_AGENT_IDS.includes(gAgents[0].id)) {
throw new Error(
'Cannot connect found messages to special agents'
);
}
const virtualGAgent = this.makeVirtualAgent(false);
this.addNearbyAgent(gAgents[0], virtualGAgent, 1);
return [gAgents[0], virtualGAgent];
}
return gAgents;
}
_handlePartialConnect(agents, parallel) {
const flags = this.filterConnectFlags(agents);
const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents, {
allowGrouped: true,
allowVirtual: true,
});
this.defineGAgents(flatMap(gAgents, this.expandGroupedGAgent)
.filter((gAgent) => !gAgent.isVirtualSource));
const implicitBeginGAgents = (agents
.filter(PAgent.hasFlag('begin', false))
.map(this.toGAgent)
.filter((gAgent) => !gAgent.isVirtualSource)
);
this.addImpStage(
this.setGAgentVis(implicitBeginGAgents, true, 'box'),
{parallel}
);
return {flags, gAgents};
}
_makeConnectParallelStages(flags, connectStage) {
return this.makeParallel([
this.setGAgentVis(flags.beginGAgents, true, 'box', true),
this.setGAgentHighlight(flags.startGAgents, true, true),
connectStage,
this.setGAgentHighlight(flags.stopGAgents, false, true),
this.setGAgentVis(flags.endGAgents, false, 'cross', true),
]);
}
_isSelfConnect(agents) {
const gAgents = agents.map(this.toGAgent);
const expandedGAgents = this.expandGroupedGAgentConnection(gAgents);
if(expandedGAgents[0].id !== expandedGAgents[1].id) {
return false;
}
if(expandedGAgents.some((gAgent) => gAgent.isVirtualSource)) {
return false;
}
return true;
}
handleConnect({agents, label, options, parallel}) {
if(this._isSelfConnect(agents)) {
const tag = {};
this.handleConnectDelayBegin({
agent: agents[0],
ln: 0,
options,
parallel,
tag,
});
this.handleConnectDelayEnd({
agent: agents[1],
label,
options,
tag,
});
return;
}
let {flags, gAgents} = this._handlePartialConnect(agents, parallel);
gAgents = this.expandGroupedGAgentConnection(gAgents);
gAgents = this.expandVirtualSourceAgents(gAgents);
const connectStage = {
agentIDs: gAgents.map((gAgent) => gAgent.id),
label: this.textFormatter(this.applyLabelPattern(label)),
options,
type: 'connect',
};
this.addStage(
this._makeConnectParallelStages(flags, connectStage),
{parallel}
);
}
handleConnectDelayBegin({agent, tag, options, ln, parallel}) {
const dcs = this.currentSection.delayedConnections;
if(dcs.has(tag)) {
throw new Error('Duplicate delayed connection "' + tag + '"');
}
const {flags, gAgents} = this._handlePartialConnect([agent], parallel);
const uniqueTag = this.nextVirtualAgentName();
const connectStage = {
agentIDs: null,
label: null,
options,
tag: uniqueTag,
type: 'connect-delay-begin',
};
dcs.set(tag, {connectStage, gAgents, ln, tag, uniqueTag});
this.addStage(
this._makeConnectParallelStages(flags, connectStage),
{parallel}
);
}
handleConnectDelayEnd({agent, tag, label, options, parallel}) {
const dcs = this.currentSection.delayedConnections;
const dcInfo = dcs.get(tag);
if(!dcInfo) {
throw new Error('Unknown delayed connection "' + tag + '"');
}
let {flags, gAgents} = this._handlePartialConnect([agent], parallel);
gAgents = this.expandGroupedGAgentConnection([
...dcInfo.gAgents,
...gAgents,
]);
gAgents = this.expandVirtualSourceAgents(gAgents);
let combinedOptions = dcInfo.connectStage.options;
if(combinedOptions.line !== options.line) {
throw new Error('Mismatched delayed connection arrows');
}
if(options.right) {
combinedOptions = Object.assign({}, combinedOptions, {
right: options.right,
});
}
Object.assign(dcInfo.connectStage, {
agentIDs: gAgents.map((gAgent) => gAgent.id),
label: this.textFormatter(this.applyLabelPattern(label)),
options: combinedOptions,
});
const connectEndStage = {
tag: dcInfo.uniqueTag,
type: 'connect-delay-end',
};
this.addStage(
this._makeConnectParallelStages(flags, connectEndStage),
{parallel}
);
dcs.delete(tag);
}
handleNote({type, agents, mode, label, parallel}) {
let gAgents = null;
if(agents.length === 0) {
gAgents = NOTE_DEFAULT_G_AGENTS[type] || [];
} else {
gAgents = agents.map(this.toGAgent);
}
this.validateGAgents(gAgents, {allowGrouped: true});
gAgents = flatMap(gAgents, this.expandGroupedGAgent);
const agentIDs = gAgents.map((gAgent) => gAgent.id);
const uniqueAgents = new Set(agentIDs).size;
if(type === 'note between' && uniqueAgents < 2) {
throw new Error('note between requires at least 2 agents');
}
this.defineGAgents(gAgents);
this.addImpStage(this.setGAgentVis(gAgents, true, 'box'), {parallel});
this.addStage({
agentIDs,
label: this.textFormatter(label),
mode,
type,
}, {parallel});
}
handleAgentDefine({agents}) {
const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents, {
allowCovered: true,
allowGrouped: true,
});
mergeSets(this.gAgents, gAgents, GAgent.equals);
}
handleAgentOptions({agent, options}) {
const gAgent = this.toGAgent(agent);
const gAgents = [gAgent];
this.validateGAgents(gAgents, {
allowCovered: true,
allowGrouped: true,
});
mergeSets(this.gAgents, gAgents, GAgent.equals);
this.gAgents
.filter(({id}) => (id === gAgent.id))
.forEach((storedGAgent) => {
mergeSets(storedGAgent.options, options);
});
}
handleAgentBegin({agents, mode, parallel}) {
const gAgents = agents.map(this.toGAgent);
this.validateGAgents(gAgents);
this.addStage(this.setGAgentVis(gAgents, true, mode, true), {parallel});
}
handleAgentEnd({agents, mode, parallel}) {
const groupPAgents = (agents
.filter((pAgent) => this.activeGroups.has(pAgent.name))
);
const gAgents = (agents
.filter((pAgent) => !this.activeGroups.has(pAgent.name))
.map(this.toGAgent)
);
this.validateGAgents(gAgents);
this.addStage(this.makeParallel([
this.setGAgentHighlight(gAgents, false),
this.setGAgentVis(gAgents, false, mode, true),
...groupPAgents.map(this.endGroup),
]), {parallel});
}
handleStage(stage) {
this.latestLine = stage.ln;
try {
const handler = this.stageHandlers[stage.type];
if(!handler) {
throw new Error('Unknown command: ' + stage.type);
}
handler(stage);
} catch(e) {
if(typeof e === 'object' && e.message) {
e.message += ' at line ' + (stage.ln + 1);
throw e;
}
}
}
_reset() {
this.agentStates.clear();
this.markers.clear();
this.agentAliases.clear();
this.activeGroups.clear();
this.gAgents.length = 0;
this.nextID = 0;
this.nesting.length = 0;
this.labelPattern = [{token: 'label'}];
}
_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', {
label: '',
ln: 0,
name: '',
tag: '',
});
stages.forEach(this.handleStage);
if(this.nesting.length !== 1) {
throw new Error(
'Unterminated section at line ' +
(this.currentSection.header.ln + 1)
);
}
if(this.activeGroups.size > 0) {
throw new Error('Unterminated group');
}
this._checkSectionEnd();
const terminators = meta.terminators || 'none';
this.addStage(this.makeParallel([
this.setGAgentHighlight(this.gAgents, false),
this.setGAgentVis(this.gAgents, false, terminators),
]));
this._finalise(globals);
swapFirstBegin(globals.stages, meta.headers || 'box');
return {
agents: this.gAgents.slice(),
meta: {
code: meta.code,
theme: meta.theme,
title: this.textFormatter(meta.title),
},
stages: globals.stages,
};
}
}
/* eslint-disable sort-keys */ // Maybe later
const FONT$2 = 'Courier New,Liberation Mono,monospace';
const LINE_HEIGHT$2 = 1.3;
const NOTE_ATTRS$2 = {
'font-family': FONT$2,
'font-size': 8,
'line-height': LINE_HEIGHT$2,
};
const DIVIDER_LABEL_ATTRS$2 = {
'font-family': FONT$2,
'font-size': 8,
'line-height': LINE_HEIGHT$2,
'text-anchor': 'middle',
};
class MonospaceTheme extends BaseTheme {
constructor(svg) {
super(svg, {
'font-family': FONT$2,
'font-size': 8,
'line-height': LINE_HEIGHT$2,
});
const sharedBlockSection = {
padding: {
top: 3,
bottom: 2,
},
tag: {
padding: {
top: 2,
left: 4,
right: 4,
bottom: 2,
},
boxRenderer: this.renderTag.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 3,
'ry': 3,
}),
labelAttrs: {
'font-family': FONT$2,
'font-weight': 'bold',
'font-size': 9,
'line-height': LINE_HEIGHT$2,
'text-anchor': 'left',
},
},
label: {
minHeight: 8,
padding: {
top: 2,
left: 8,
right: 8,
bottom: 2,
},
labelAttrs: {
'font-family': FONT$2,
'font-size': 8,
'line-height': LINE_HEIGHT$2,
'text-anchor': 'left',
},
},
};
Object.assign(this, {
titleMargin: 8,
outerMargin: 4,
agentMargin: 12,
actionMargin: 12,
minActionMargin: 4,
agentLineHighlightRadius: 4,
agentCap: {
box: {
padding: {
top: 4,
left: 8,
right: 8,
bottom: 4,
},
arrowBottom: 12,
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
},
labelAttrs: {
'font-family': FONT$2,
'font-size': 12,
'line-height': LINE_HEIGHT$2,
'text-anchor': 'middle',
},
},
database: {
padding: {
top: 9,
left: 8,
right: 8,
bottom: 3,
},
arrowBottom: 12,
boxRenderer: this.renderDB.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'db-z': 4,
}),
labelAttrs: {
'font-family': FONT$2,
'font-size': 12,
'line-height': LINE_HEIGHT$2,
'text-anchor': 'middle',
},
},
cross: {
size: 16,
render: svg.crossFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
},
bar: {
height: 4,
render: svg.boxFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
}),
},
fade: {
width: 5,
height: 8,
extend: 1,
},
none: {
height: 8,
},
},
connect: {
loopbackRadius: 4,
arrow: {
'single': {
width: 4,
height: 8,
render: this.renderArrowHead.bind(this),
attrs: {
'fill': '#000000',
'stroke-width': 0,
'stroke-linejoin': 'miter',
},
},
'double': {
width: 3,
height: 6,
render: this.renderArrowHead.bind(this),
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
'stroke-linejoin': 'miter',
},
},
'cross': {
short: 8,
radius: 4,
render: svg.crossFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
},
},
label: {
padding: 4,
margin: {top: 2, bottom: 1},
attrs: {
'font-family': FONT$2,
'font-size': 8,
'line-height': LINE_HEIGHT$2,
'text-anchor': 'middle',
},
loopbackAttrs: {
'font-family': FONT$2,
'font-size': 8,
'line-height': LINE_HEIGHT$2,
},
},
source: {
radius: 2,
render: svg.circleFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
}),
},
mask: {
padding: {
top: 0,
left: 3,
right: 3,
bottom: 1,
},
},
},
agentLineAttrs: {
'': {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
'red': {
'stroke': '#AA0000',
},
},
blocks: {
'ref': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: this.renderRef.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 2,
}),
section: sharedBlockSection,
},
'': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: svg.boxFactory({
'fill': 'none',
'stroke': '#000000',
'stroke-width': 2,
}),
collapsedBoxRenderer: this.renderRef.bind(this, {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 2,
}),
section: sharedBlockSection,
sepRenderer: svg.lineFactory({
'stroke': '#000000',
'stroke-width': 2,
'stroke-dasharray': '8, 4',
}),
},
},
notes: {
'text': {
margin: {top: 0, left: 8, right: 8, bottom: 0},
padding: {top: 4, left: 4, right: 4, bottom: 4},
overlap: {left: 8, right: 8},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
}),
labelAttrs: NOTE_ATTRS$2,
},
'note': {
margin: {top: 0, left: 8, right: 8, bottom: 0},
padding: {top: 8, left: 8, right: 8, bottom: 8},
overlap: {left: 8, right: 8},
boxRenderer: svg.noteFactory({
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
}, {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
}),
labelAttrs: NOTE_ATTRS$2,
},
'state': {
margin: {top: 0, left: 8, right: 8, bottom: 0},
padding: {top: 8, left: 8, right: 8, bottom: 8},
overlap: {left: 8, right: 8},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'rx': 8,
'ry': 8,
}),
labelAttrs: NOTE_ATTRS$2,
},
},
dividers: {
'': {
labelAttrs: DIVIDER_LABEL_ATTRS$2,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: () => ({}),
},
'line': {
labelAttrs: DIVIDER_LABEL_ATTRS$2,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 8,
margin: 0,
render: this.renderLineDivider.bind(this, {
lineAttrs: {
'stroke': '#000000',
},
}),
},
'delay': {
labelAttrs: DIVIDER_LABEL_ATTRS$2,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: this.renderDelayDivider.bind(this, {
dotSize: 2,
gapSize: 2,
}),
},
'tear': {
labelAttrs: DIVIDER_LABEL_ATTRS$2,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 8,
margin: 8,
render: this.renderTearDivider.bind(this, {
fadeBegin: 4,
fadeSize: 4,
zigWidth: 4,
zigHeight: 1,
lineAttrs: {
'stroke': '#000000',
},
}),
},
},
});
this.addConnectLine('solid', {attrs: {
'stroke': '#000000',
'stroke-width': 1,
}});
this.addConnectLine('dash', {attrs: {
'stroke-dasharray': '4, 4',
}});
this.addConnectLine('wave', {
pattern: new WavePattern(6, [
+0,
-0.25,
-0.5,
-0.25,
+0,
+0.25,
+0.5,
+0.25,
]),
});
}
}
class Factory$2 {
constructor() {
this.name = 'monospace';
}
build(svg) {
return new MonospaceTheme(svg);
}
}
/*
* The order of commands inside "then" blocks directly influences the
* order they are displayed to the user in autocomplete menus.
* This relies on the fact that common JS engines maintain insertion
* order in objects, though this is not guaranteed. It could be switched
* to use Map objects instead for strict compliance, at the cost of
* extra syntax.
*/
const CM_ERROR = {type: 'error line-error', suggest: [], then: {'': 0}};
function textTo(exit, suggest = []) {
return {
type: 'string',
suggest,
then: Object.assign({'': 0}, exit),
};
}
function suggestionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
const AGENT_INFO_TYPES = [
'database',
'red',
];
const PARALLEL_TASKS = [
'begin',
'end',
'note',
'state',
'text',
];
const makeCommands = ((() => {
function agentListTo(exit, next = 1) {
return {
type: 'variable',
suggest: [{known: 'Agent'}],
then: Object.assign({}, exit, {
'': 0,
',': {type: 'operator', then: {'': next}},
}),
};
}
const end = {type: '', suggest: ['\n'], then: {}};
const hiddenEnd = {type: '', suggest: [], then: {}};
const textToEnd = textTo({'\n': end});
const colonTextToEnd = {
type: 'operator',
then: {'': textToEnd, '\n': hiddenEnd},
};
const aliasListToEnd = agentListTo({
'\n': end,
'as': {type: 'keyword', then: {
'': {type: 'variable', suggest: [{known: 'Agent'}], then: {
'': 0,
',': {type: 'operator', then: {'': 3}},
'\n': end,
}},
}},
});
const agentListToText = agentListTo({':': colonTextToEnd});
const agentToOptText = {
type: 'variable',
suggest: [{known: 'Agent'}],
then: {
'': 0,
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
'\n': end,
},
};
const referenceName = {
':': {type: 'operator', then: {
'': textTo({
'as': {type: 'keyword', then: {
'': {
type: 'variable',
suggest: [{known: 'Agent'}],
then: {
'': 0,
'\n': end,
},
},
}},
}),
}},
};
const refDef = {type: 'keyword', then: Object.assign({
'over': {type: 'keyword', then: {
'': agentListTo(referenceName),
}},
}, referenceName)};
const divider = {
'\n': end,
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
'with': {type: 'keyword', suggest: ['with height '], then: {
'height': {type: 'keyword', then: {
'': {type: 'number', suggest: ['6 ', '30 '], then: {
'\n': end,
':': {type: 'operator', then: {
'': textToEnd,
'\n': hiddenEnd,
}},
}},
}},
}},
};
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', then: {
'': agentListToText,
}},
':': {type: 'operator', then: {
'': textToEnd,
}},
'': agentListToText,
},
};
}
function makeOpBlock({exit, sourceExit, blankExit}) {
const op = {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}};
return {
'+': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': CM_ERROR,
'': exit,
}},
'-': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': op,
'!': {type: 'operator', then: {
'+': CM_ERROR,
'-': CM_ERROR,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}},
'': exit,
}},
'*': {type: 'operator', then: Object.assign({
'+': op,
'-': op,
'*': CM_ERROR,
'!': CM_ERROR,
'': exit,
}, sourceExit || exit)},
'!': op,
'': blankExit || exit,
};
}
function makeCMConnect(arrows) {
const connect = {
type: 'keyword',
then: Object.assign({}, makeOpBlock({
exit: agentToOptText,
sourceExit: {
':': colonTextToEnd,
'\n': hiddenEnd,
},
}), {
'...': {type: 'operator', then: {
'': {
type: 'variable',
suggest: [{known: 'DelayedAgent'}],
then: {
'': 0,
':': CM_ERROR,
'\n': end,
},
},
}},
}),
};
const connectors = {};
arrows.forEach((arrow) => (connectors[arrow] = connect));
const labelIndicator = {
type: 'operator',
override: 'Label',
then: {},
};
const hiddenLabelIndicator = {
type: 'operator',
suggest: [],
override: 'Label',
then: {},
};
const firstAgent = {
type: 'variable',
suggest: [{known: 'Agent'}],
then: Object.assign({
'': 0,
}, connectors, {
':': labelIndicator,
}),
};
const firstAgentDelayed = {
type: 'variable',
suggest: [{known: 'DelayedAgent'}],
then: Object.assign({
'': 0,
':': hiddenLabelIndicator,
}, connectors),
};
const firstAgentNoFlags = Object.assign({}, firstAgent, {
then: Object.assign({}, firstAgent.then, {
'is': {type: 'keyword', then: agentInfoList},
}),
});
return Object.assign({
'...': {type: 'operator', then: {
'': firstAgentDelayed,
}},
}, makeOpBlock({
exit: firstAgent,
sourceExit: Object.assign({
'': firstAgent,
':': hiddenLabelIndicator,
}, connectors),
blankExit: firstAgentNoFlags,
}));
}
const commonGroup = {type: 'keyword', then: {
'': textToEnd,
':': {type: 'operator', then: {
'': textToEnd,
}},
'\n': end,
}};
const BASE_THEN = {
'title': {type: 'keyword', then: {
'': textToEnd,
}},
'theme': {type: 'keyword', then: {
'': {
type: 'string',
suggest: [{global: 'themes', suffix: '\n'}],
then: {
'': 0,
'\n': end,
},
},
}},
'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', then: {
'none': {type: 'keyword', then: {}},
'cross': {type: 'keyword', then: {}},
'box': {type: 'keyword', then: {}},
'fade': {type: 'keyword', then: {}},
'bar': {type: 'keyword', then: {}},
}},
'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', then: {
'': aliasListToEnd,
'as': CM_ERROR,
}},
'begin': {type: 'keyword', then: {
'': aliasListToEnd,
'reference': refDef,
'as': CM_ERROR,
}},
'end': {type: 'keyword', then: {
'': aliasListToEnd,
'as': CM_ERROR,
'\n': end,
}},
'if': commonGroup,
'else': {type: 'keyword', suggest: ['else\n', 'else if: '], then: {
'if': {type: 'keyword', suggest: ['if: '], then: {
'': textToEnd,
':': {type: 'operator', then: {
'': textToEnd,
}},
}},
'\n': end,
}},
'repeat': commonGroup,
'group': commonGroup,
'note': {type: 'keyword', then: {
'over': {type: 'keyword', then: {
'': agentListToText,
}},
'left': makeSideNote('left'),
'right': makeSideNote('right'),
'between': {type: 'keyword', then: {
'': agentListTo({':': CM_ERROR}, agentListToText),
}},
}},
'state': {type: 'keyword', suggest: ['state over '], then: {
'over': {type: 'keyword', then: {
'': {
type: 'variable',
suggest: [{known: 'Agent'}],
then: {
'': 0,
',': CM_ERROR,
':': colonTextToEnd,
},
},
}},
}},
'text': {type: 'keyword', then: {
'left': makeSideNote('left'),
'right': makeSideNote('right'),
}},
'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', then: {
':': {type: 'operator', then: {}},
'with': {type: 'keyword', then: {
'': {type: 'variable', suggest: [{known: 'Label'}], then: {
'': 0,
':': {type: 'operator', then: {}},
}},
}},
}},
};
return (arrows) => {
const arrowConnect = makeCMConnect(arrows);
const parallel = {};
for(const task of PARALLEL_TASKS) {
parallel[task] = BASE_THEN[task];
}
Object.assign(parallel, arrowConnect);
return {
type: 'error line-error',
then: Object.assign(
{},
BASE_THEN,
{'&': {type: 'keyword', then: parallel}},
arrowConnect
),
};
};
})());
/* eslint-enable sort-keys */
function cmCappedToken(token, current) {
if(Object.keys(current.then).length > 0) {
return {q: false, suffix: ' ', v: token};
} else {
return {q: false, suffix: '\n', v: token};
}
}
function cmGetSuggestions(state, token, current) {
const suggestions = current.suggest || [''];
return flatMap(suggestions, (suggest) => {
if(typeof suggest === 'object') {
if(suggest.known) {
return state['known' + suggest.known] || [];
} else {
return [suggest];
}
} else if(suggest === '') {
return [cmCappedToken(token, current)];
} else if(typeof suggest === 'string') {
return [{q: (token === ''), v: suggest}];
} else {
throw new Error('Invalid suggestion type ' + suggest);
}
});
}
function cmMakeCompletions(state, path) {
const comp = [];
const current = last(path);
Object.keys(current.then).forEach((token) => {
let next = current.then[token];
if(typeof next === 'number') {
next = path[path.length - next - 1];
}
mergeSets(
comp,
cmGetSuggestions(state, token, next),
suggestionsEqual
);
});
return comp;
}
function getSuggestionCategory(suggestions) {
for(const suggestion of suggestions) {
if(typeof suggestion === 'object' && suggestion.known) {
return suggestion.known;
}
}
return null;
}
function appendToken(base, token) {
return base + (base ? token.s : '') + token.v;
}
function storeKnownEntity(state, type, value) {
mergeSets(
state['known' + type],
[{q: true, suffix: ' ', v: value}],
suggestionsEqual
);
}
function updateKnownEntities(state, locals, token, current) {
const known = getSuggestionCategory(current.suggest || ['']);
if(locals.type && known !== locals.type) {
storeKnownEntity(state, current.override || locals.type, locals.value);
locals.value = '';
}
if(known) {
locals.value = appendToken(locals.value, token);
}
locals.type = known;
}
function cmCheckToken(state, eol, commands) {
const suggestions = {
type: '',
value: '',
};
let current = commands;
const path = [current];
state.line.forEach((token, i) => {
if(i === state.line.length - 1) {
state.completions = cmMakeCompletions(state, path);
}
const keywordToken = token.q ? '' : token.v;
let found = current.then[keywordToken];
if(typeof found === 'undefined') {
found = current.then[''];
state.isVar = true;
} else {
state.isVar = token.q;
}
if(typeof found === 'number') {
path.length -= found;
} else {
path.push(found || CM_ERROR);
}
current = last(path);
updateKnownEntities(state, suggestions, token, current);
});
if(eol) {
updateKnownEntities(state, suggestions, null, {});
}
state.nextCompletions = cmMakeCompletions(state, path);
state.valid = (
Boolean(current.then['\n']) ||
Object.keys(current.then).length === 0
);
return current.type;
}
function getInitialToken(block) {
const baseToken = (block.baseToken || {});
return {
quoted: baseToken.q || false,
value: baseToken.v || '',
};
}
const NO_TOKEN = -1;
class Mode {
constructor(tokenDefinitions, arrows) {
this.tokenDefinitions = tokenDefinitions;
this.commands = makeCommands(arrows);
this.lineComment = '#';
}
startState() {
return {
beginCompletions: cmMakeCompletions({}, [this.commands]),
completions: [],
current: '',
currentQuoted: false,
currentSpace: '',
currentType: NO_TOKEN,
indent: 0,
isVar: true,
knownAgent: [],
knownDelayedAgent: [],
knownLabel: [],
line: [],
nextCompletions: [],
valid: true,
};
}
_matchPattern(stream, pattern, consume) {
if(!pattern) {
return null;
}
pattern.lastIndex = 0;
return stream.match(pattern, consume);
}
_tokenBegin(stream, state) {
state.currentSpace = '';
for(let lastChar = ''; !stream.eol(); lastChar = stream.next()) {
state.currentSpace += lastChar;
for(let i = 0; i < this.tokenDefinitions.length; ++ i) {
const block = this.tokenDefinitions[i];
if(this._matchPattern(stream, block.start, true)) {
state.currentType = i;
const {value, quoted} = getInitialToken(block);
state.current = value;
state.currentQuoted = quoted;
return true;
}
}
}
return false;
}
_tokenCheckEscape(stream, state, block) {
const match = this._matchPattern(stream, block.escape, true);
if(match) {
state.current += block.escapeWith(match);
}
}
_addToken(state) {
state.line.push({
q: state.currentQuoted,
s: state.currentSpace,
v: state.current,
});
}
_tokenEndFound(stream, state, block, match) {
state.currentType = NO_TOKEN;
if(block.includeEnd) {
state.current += match[0];
}
if(block.omit) {
return 'comment';
}
this._addToken(state);
return cmCheckToken(state, stream.eol(), this.commands);
}
_tokenEOLFound(stream, state, block) {
state.current += '\n';
if(block.omit) {
return 'comment';
}
this._addToken(state);
const type = cmCheckToken(state, false, this.commands);
state.line.pop();
return type;
}
_tokenEnd(stream, state) {
for(;;) {
const block = this.tokenDefinitions[state.currentType];
this._tokenCheckEscape(stream, state, block);
if(block.end) {
const match = this._matchPattern(stream, block.end, true);
if(match) {
return this._tokenEndFound(stream, state, block, match);
}
} else {
return this._tokenEndFound(stream, state, block, null);
}
if(stream.eol()) {
return this._tokenEOLFound(stream, state, block);
}
state.current += stream.next();
}
}
_tokenContinueOrBegin(stream, state) {
if(state.currentType === NO_TOKEN) {
if(stream.sol()) {
state.line.length = 0;
state.valid = true;
}
if(!this._tokenBegin(stream, state)) {
return '';
}
}
return this._tokenEnd(stream, state);
}
_isLineTerminated(state) {
return state.currentType !== NO_TOKEN || state.valid;
}
token(stream, state) {
state.completions = state.nextCompletions;
state.isVar = true;
const type = this._tokenContinueOrBegin(stream, state);
if(stream.eol() && !this._isLineTerminated(state)) {
return 'line-error ' + type;
}
return type;
}
indent(state) {
return state.indent;
}
}
function execAt(str, reg, i) {
reg.lastIndex = i;
return reg.exec(str);
}
function unescape(match) {
if(match[1] === 'n') {
return '\n';
}
return match[1];
}
const TOKENS = [
{end: /(?=\n)|$/y, omit: true, start: /#/y},
{
baseToken: {q: true},
end: /"/y,
escape: /\\(.)/y,
escapeWith: unescape,
start: /"/y,
},
{baseToken: {v: '...'}, start: /\.\.\./y},
{end: /(?=[ \t\r\n:+~\-*!<,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<,])/y},
{
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
includeEnd: true,
start: /(?=[~\-<])/y,
},
{baseToken: {v: ','}, start: /,/y},
{baseToken: {v: ':'}, start: /:/y},
{baseToken: {v: '!'}, start: /!/y},
{baseToken: {v: '+'}, start: /\+/y},
{baseToken: {v: '*'}, start: /\*/y},
{baseToken: {v: '\n'}, start: /\n/y},
];
function tokFindBegin(src, i) {
for(let j = 0; j < TOKENS.length; ++ j) {
const block = TOKENS[j];
const match = execAt(src, block.start, i);
if(match) {
return {
appendSpace: '',
appendValue: '',
end: !block.end,
newBlock: block,
skip: match[0].length,
};
}
}
return {
appendSpace: src[i],
appendValue: '',
end: false,
newBlock: null,
skip: 1,
};
}
function tokContinuePart(src, i, block) {
if(block.escape) {
const match = execAt(src, block.escape, i);
if(match) {
return {
appendSpace: '',
appendValue: block.escapeWith(match),
end: false,
newBlock: null,
skip: match[0].length,
};
}
}
const match = execAt(src, block.end, i);
if(match) {
return {
appendSpace: '',
appendValue: block.includeEnd ? match[0] : '',
end: true,
newBlock: null,
skip: match[0].length,
};
}
return {
appendSpace: '',
appendValue: src[i],
end: false,
newBlock: null,
skip: 1,
};
}
function tokAdvance(src, i, block) {
if(block) {
return tokContinuePart(src, i, block);
} else {
return tokFindBegin(src, i);
}
}
function copyPos(pos) {
return {ch: pos.ch, i: pos.i, ln: pos.ln};
}
function advancePos(pos, src, steps) {
for(let i = 0; i < steps; ++ i) {
++ pos.ch;
if(src[pos.i + i] === '\n') {
++ pos.ln;
pos.ch = 0;
}
}
pos.i += steps;
}
class TokenState {
constructor(src) {
this.src = src;
this.block = null;
this.token = null;
this.pos = {ch: 0, i: 0, ln: 0};
this.reset();
}
isOver() {
return this.pos.i > this.src.length;
}
reset() {
this.token = {b: null, e: null, q: false, s: '', v: ''};
this.block = null;
}
beginToken(advance) {
this.block = advance.newBlock;
Object.assign(this.token, this.block.baseToken);
this.token.b = copyPos(this.pos);
}
endToken() {
let tok = null;
if(!this.block.omit) {
this.token.e = copyPos(this.pos);
tok = this.token;
}
this.reset();
return tok;
}
advance() {
const advance = tokAdvance(this.src, this.pos.i, this.block);
if(advance.newBlock) {
this.beginToken(advance);
}
this.token.s += advance.appendSpace;
this.token.v += advance.appendValue;
advancePos(this.pos, this.src, advance.skip);
if(advance.end) {
return this.endToken();
} else {
return null;
}
}
}
function posStr(pos) {
return 'line ' + (pos.ln + 1) + ', character ' + pos.ch;
}
class Tokeniser {
tokenise(src) {
const tokens = [];
const state = new TokenState(src);
while(!state.isOver()) {
const token = state.advance();
if(token) {
tokens.push(token);
}
}
if(state.block) {
throw new Error(
'Unterminated literal (began at ' +
posStr(state.token.b) + ')'
);
}
return tokens;
}
getCodeMirrorMode(arrows) {
return new Mode(TOKENS, arrows);
}
splitLines(tokens) {
const lines = [];
let line = [];
tokens.forEach((token) => {
if(!token.q && token.v === '\n') {
if(line.length > 0) {
lines.push(line);
line = [];
}
} else {
line.push(token);
}
});
if(line.length > 0) {
lines.push(line);
}
return lines;
}
}
const LABEL_PATTERN = /(.*?)<([^<>]*)>/g;
const DP_PATTERN = /\.([0-9]*)/;
function countDP(value) {
const match = DP_PATTERN.exec(value);
if(!match || !match[1]) {
return 0;
}
return match[1].length;
}
function parseCounter(args) {
let start = 1;
let inc = 1;
let dp = 0;
if(args[0]) {
start = Number(args[0]);
dp = Math.max(dp, countDP(args[0]));
}
if(args[1]) {
inc = Number(args[1]);
dp = Math.max(dp, countDP(args[1]));
}
if(Number.isNaN(start) || Number.isNaN(inc)) {
return null;
}
return {dp, inc, start};
}
function parseToken(token) {
if(token === 'label') {
return {token: 'label'};
}
const p = token.indexOf(' ');
let type = null;
let args = null;
if(p === -1) {
type = token;
args = [];
} else {
type = token.substr(0, p);
args = token.substr(p + 1).split(',');
}
let result = null;
if(type === 'inc') {
result = parseCounter(args);
}
return result || ('<' + token + '>');
}
function parsePattern(raw) {
const pattern = [];
let match = null;
let end = 0;
LABEL_PATTERN.lastIndex = 0;
while((match = LABEL_PATTERN.exec(raw))) {
if(match[1]) {
pattern.push(match[1]);
}
if(match[2]) {
pattern.push(parseToken(match[2]));
}
end = LABEL_PATTERN.lastIndex;
}
const remainder = raw.substr(end);
if(remainder) {
pattern.push(remainder);
}
return pattern;
}
const STYLES = [
{
attrs: {'font-style': 'italic'},
begin: {matcher: /<i>/g, skip: 0},
end: {matcher: /<\/i>/g, skip: 0},
}, {
attrs: {'font-style': 'italic'},
begin: {matcher: /[\s_~`>]\*(?=\S)/g, skip: 1},
end: {matcher: /\S\*(?=[\s_~`<])/g, skip: 1},
}, {
attrs: {'font-style': 'italic'},
begin: {matcher: /[\s*~`>]_(?=\S)/g, skip: 1},
end: {matcher: /\S_(?=[\s*~`<])/g, skip: 1},
}, {
attrs: {'font-weight': 'bolder'},
begin: {matcher: /<b>/g, skip: 0},
end: {matcher: /<\/b>/g, skip: 0},
}, {
attrs: {'font-weight': 'bolder'},
begin: {matcher: /[\s_~`>]\*\*(?=\S)/g, skip: 1},
end: {matcher: /\S\*\*(?=[\s_~`<])/g, skip: 1},
}, {
attrs: {'font-weight': 'bolder'},
begin: {matcher: /[\s*~`>]__(?=\S)/g, skip: 1},
end: {matcher: /\S__(?=[\s*~`<])/g, skip: 1},
}, {
attrs: {'text-decoration': 'line-through'},
begin: {matcher: /<s>/g, skip: 0},
end: {matcher: /<\/s>/g, skip: 0},
}, {
attrs: {'text-decoration': 'line-through'},
begin: {matcher: /[\s_*`>]~(?=\S)/g, skip: 1},
end: {matcher: /\S~(?=[\s_*`<])/g, skip: 1},
}, {
attrs: {'text-decoration': 'overline'},
begin: {matcher: /<o>/g, skip: 0},
end: {matcher: /<\/o>/g, skip: 0},
}, {
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
begin: {matcher: /[\s_*~.>]`(?=\S)/g, skip: 1},
end: {matcher: /\S`(?=[\s_*~.<])/g, skip: 1},
}, {
attrs: {'text-decoration': 'underline'},
begin: {matcher: /<u>/g, skip: 0},
end: {matcher: /<\/u>/g, skip: 0},
}, {
attrs: {'baseline-shift': '70%', 'font-size': '0.6em'},
begin: {matcher: /<sup>/g, skip: 0},
end: {matcher: /<\/sup>/g, skip: 0},
}, {
attrs: {'baseline-shift': '-20%', 'font-size': '0.6em'},
begin: {matcher: /<sub>/g, skip: 0},
end: {matcher: /<\/sub>/g, skip: 0},
}, {
attrs: {'fill': '#DD0000'},
begin: {matcher: /<red>/g, skip: 0},
end: {matcher: /<\/red>/g, skip: 0},
}, {
attrs: {'filter': 'highlight'},
begin: {matcher: /<highlight>/g, skip: 0},
end: {matcher: /<\/highlight>/g, skip: 0},
},
];
function findNext(line, p, active) {
const virtLine = ' ' + line + ' ';
let styleIndex = -1;
let bestStart = virtLine.length;
let bestEnd = 0;
STYLES.forEach(({begin, end}, ind) => {
const search = active[ind] ? end : begin;
search.matcher.lastIndex = p + 1 - search.skip;
const m = search.matcher.exec(virtLine);
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
if(
beginInd < bestStart ||
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
) {
styleIndex = ind;
bestStart = beginInd;
bestEnd = search.matcher.lastIndex;
}
});
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
}
function combineAttrs(activeCount, active) {
if(!activeCount) {
return null;
}
const attrs = {};
const decorations = [];
active.forEach((on, ind) => {
if(!on) {
return;
}
const activeAttrs = STYLES[ind].attrs;
const decoration = activeAttrs['text-decoration'];
if(decoration && !decorations.includes(decoration)) {
decorations.push(decoration);
}
Object.assign(attrs, activeAttrs);
});
if(decorations.length > 1) {
attrs['text-decoration'] = decorations.join(' ');
}
return attrs;
}
function shrinkWhitespace(text) {
return text.replace(/[\f\n\r\t\v ]+/g, ' ');
}
function trimCollapsible(text) {
return text.replace(/^[\f\n\r\t\v ]+|[\f\n\r\t\v ]+$/g, '');
}
function parseMarkdown(text) {
if(!text) {
return [];
}
const active = STYLES.map(() => false);
let activeCount = 0;
let attrs = null;
const lines = trimCollapsible(text).split('\n');
const result = [];
lines.forEach((line) => {
const ln = shrinkWhitespace(trimCollapsible(line));
const parts = [];
let p = 0;
for(;;) {
const {styleIndex, start, end} = findNext(ln, p, active);
if(styleIndex === -1) {
break;
}
if(active[styleIndex]) {
active[styleIndex] = false;
-- activeCount;
} else {
active[styleIndex] = true;
++ activeCount;
}
if(start > p) {
parts.push({attrs, text: ln.substring(p, start)});
}
attrs = combineAttrs(activeCount, active);
p = end;
}
if(p < ln.length) {
parts.push({attrs, text: ln.substr(p)});
}
result.push(parts);
});
return result;
}
const BLOCK_TYPES = new Map();
BLOCK_TYPES.set('if', {
blockType: 'if',
skip: [],
tag: 'if',
type: 'block begin',
});
BLOCK_TYPES.set('else', {
blockType: 'else',
skip: ['if'],
tag: 'else',
type: 'block split',
});
BLOCK_TYPES.set('repeat', {
blockType: 'repeat',
skip: [],
tag: 'repeat',
type: 'block begin',
});
BLOCK_TYPES.set('group', {
blockType: 'group',
skip: [],
tag: '',
type: 'block begin',
});
const CONNECT = {
agentFlags: {
'!': {flag: 'end'},
'*': {allowBlankName: true, blankNameFlag: 'source', flag: 'begin'},
'+': {flag: 'start'},
'-': {flag: 'stop'},
},
types: ((() => {
const lTypes = [
{tok: '', type: 0},
{tok: '<', type: 1},
{tok: '<<', type: 2},
];
const mTypes = [
{tok: '-', type: 'solid'},
{tok: '--', type: 'dash'},
{tok: '~', type: 'wave'},
];
const rTypes = [
{tok: '', type: 0},
{tok: '>', type: 1},
{tok: '>>', type: 2},
{tok: 'x', type: 3},
];
const arrows = (combine([lTypes, mTypes, rTypes])
.filter((arrow) => (arrow[0].type !== 0 || arrow[2].type !== 0))
);
const types = new Map();
arrows.forEach((arrow) => {
types.set(arrow.map((part) => part.tok).join(''), {
left: arrow[0].type,
line: arrow[1].type,
right: arrow[2].type,
});
});
return types;
})()),
};
const TERMINATOR_TYPES = [
'none',
'box',
'cross',
'fade',
'bar',
];
const NOTE_TYPES = new Map();
NOTE_TYPES.set('text', {
mode: 'text',
types: {
'left': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
},
'right': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note right',
},
},
});
NOTE_TYPES.set('note', {
mode: 'note',
types: {
'between': {
max: Number.POSITIVE_INFINITY,
min: 2,
skip: [],
type: 'note between',
},
'left': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note left',
},
'over': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: [],
type: 'note over',
},
'right': {
max: Number.POSITIVE_INFINITY,
min: 0,
skip: ['of'],
type: 'note right',
},
},
});
NOTE_TYPES.set('state', {
mode: 'state',
types: {
'over': {max: 1, min: 1, skip: [], type: 'note over'},
},
});
const DIVIDER_TYPES = new Map();
DIVIDER_TYPES.set('line', {defaultHeight: 6});
DIVIDER_TYPES.set('space', {defaultHeight: 6});
DIVIDER_TYPES.set('delay', {defaultHeight: 30});
DIVIDER_TYPES.set('tear', {defaultHeight: 6});
const AGENT_MANIPULATION_TYPES = new Map();
AGENT_MANIPULATION_TYPES.set('define', {type: 'agent define'});
AGENT_MANIPULATION_TYPES.set('begin', {mode: 'box', type: 'agent begin'});
AGENT_MANIPULATION_TYPES.set('end', {mode: 'cross', type: 'agent end'});
function makeError(message, token = null) {
let suffix = '';
if(token) {
suffix = (
' at line ' + (token.b.ln + 1) +
', character ' + token.b.ch
);
}
return new Error(message + suffix);
}
function endIndexInLine(line, end = null) {
if(end === null) {
return line.length;
}
return end;
}
function joinLabel(line, begin = 0, end = null) {
const e = endIndexInLine(line, end);
if(e <= begin) {
return '';
}
let result = line[begin].v;
for(let i = begin + 1; i < e; ++ i) {
result += line[i].s + line[i].v;
}
return result;
}
function readNumber(line, begin = 0, end = null, def = Number.NAN) {
const text = joinLabel(line, begin, end);
return Number(text || def);
}
function tokenKeyword(token) {
if(!token || token.q) {
return null;
}
return token.v;
}
function skipOver(line, start, skip, error = null) {
for(let i = 0; i < skip.length; ++ i) {
const expected = skip[i];
const token = line[start + i];
if(tokenKeyword(token) !== expected) {
if(error) {
throw makeError(
error + '; expected "' + expected + '"',
token
);
} else {
return start;
}
}
}
return start + skip.length;
}
function findTokens(line, tokens, {
start = 0,
limit = null,
orEnd = false,
} = {}) {
const e = endIndexInLine(line, limit);
for(let i = start; i <= e - tokens.length; ++ i) {
if(skipOver(line, i, tokens) !== i) {
return i;
}
}
return orEnd ? e : -1;
}
function findFirstToken(line, tokenMap, {start = 0, limit = null} = {}) {
const e = endIndexInLine(line, limit);
for(let pos = start; pos < e; ++ pos) {
const value = tokenMap.get(tokenKeyword(line[pos]));
if(value) {
return {pos, value};
}
}
return null;
}
function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) {
let aliasSep = end;
if(enableAlias) {
aliasSep = findTokens(line, ['as'], {limit: end, orEnd: true, start});
}
if(start >= aliasSep && !allowBlankName) {
let errPosToken = line[start];
if(!errPosToken) {
errPosToken = {b: last(line).e};
}
throw makeError('Missing agent name', errPosToken);
}
return {
alias: joinLabel(line, aliasSep + 1, end),
name: joinLabel(line, start, aliasSep),
};
}
function readAgent(line, start, end, {
flagTypes = {},
aliases = false,
} = {}) {
const flags = [];
const blankNameFlags = [];
let p = start;
let allowBlankName = false;
for(; p < end; ++ p) {
const token = line[p];
const rawFlag = tokenKeyword(token);
const flag = flagTypes[rawFlag];
if(!flag) {
break;
}
if(flags.includes(flag.flag)) {
throw makeError('Duplicate agent flag: ' + rawFlag, token);
}
allowBlankName = allowBlankName || Boolean(flag.allowBlankName);
flags.push(flag.flag);
blankNameFlags.push(flag.blankNameFlag);
}
const {name, alias} = readAgentAlias(line, p, end, {
allowBlankName,
enableAlias: aliases,
});
return {
alias,
flags: name ? flags : blankNameFlags,
name,
};
}
function readAgentList(line, start, end, readAgentOpts) {
const list = [];
let currentStart = -1;
for(let i = start; i < end; ++ i) {
const token = line[i];
if(tokenKeyword(token) === ',') {
if(currentStart !== -1) {
list.push(readAgent(line, currentStart, i, readAgentOpts));
currentStart = -1;
}
} else if(currentStart === -1) {
currentStart = i;
}
}
if(currentStart !== -1) {
list.push(readAgent(line, currentStart, end, readAgentOpts));
}
return list;
}
const PARSERS = [
{begin: ['title'], fn: (line, meta) => { // Title
meta.title = joinLabel(line, 1);
return true;
}},
{begin: ['theme'], fn: (line, meta) => { // Theme
meta.theme = joinLabel(line, 1);
return true;
}},
{begin: ['terminators'], fn: (line, meta) => { // Terminators
const type = tokenKeyword(line[1]);
if(!type) {
throw makeError('Unspecified termination', line[0]);
}
if(TERMINATOR_TYPES.indexOf(type) === -1) {
throw makeError('Unknown termination "' + type + '"', line[1]);
}
meta.terminators = type;
return true;
}},
{begin: ['headers'], fn: (line, meta) => { // Headers
const type = tokenKeyword(line[1]);
if(!type) {
throw makeError('Unspecified header', line[0]);
}
if(TERMINATOR_TYPES.indexOf(type) === -1) {
throw makeError('Unknown header "' + type + '"', line[1]);
}
meta.headers = type;
return true;
}},
{begin: ['divider'], fn: (line) => { // Divider
const labelSep = findTokens(line, [':'], {orEnd: true});
const heightSep = findTokens(line, ['with', 'height'], {
limit: labelSep,
orEnd: true,
});
const mode = joinLabel(line, 1, heightSep) || 'line';
if(!DIVIDER_TYPES.has(mode)) {
throw makeError('Unknown divider type', line[1]);
}
const height = readNumber(
line,
heightSep + 2,
labelSep,
DIVIDER_TYPES.get(mode).defaultHeight
);
if(Number.isNaN(height) || height < 0) {
throw makeError('Invalid divider height', line[heightSep + 2]);
}
return {
height,
label: joinLabel(line, labelSep + 1),
mode,
type: 'divider',
};
}},
{begin: ['autolabel'], fn: (line) => { // Autolabel
let raw = null;
if(tokenKeyword(line[1]) === 'off') {
raw = '<label>';
} else {
raw = joinLabel(line, 1);
}
return {
pattern: parsePattern(raw),
type: 'label pattern',
};
}},
{begin: ['end'], fn: (line) => { // Block End
if(line.length !== 1) {
return null;
}
return {type: 'block end'};
}},
{begin: [], fn: (line) => { // Block
const type = BLOCK_TYPES.get(tokenKeyword(line[0]));
if(!type) {
return null;
}
let skip = 1;
if(line.length > skip) {
skip = skipOver(line, skip, type.skip, 'Invalid block command');
}
skip = skipOver(line, skip, [':']);
return {
blockType: type.blockType,
label: joinLabel(line, skip),
tag: type.tag,
type: type.type,
};
}},
{begin: ['begin', 'reference'], fn: (line) => { // Begin reference
let agents = [];
const labelSep = findTokens(line, [':']);
if(tokenKeyword(line[2]) === 'over' && labelSep > 3) {
agents = readAgentList(line, 3, labelSep);
} else if(labelSep !== 2) {
throw makeError('Expected ":" or "over"', line[2]);
}
const def = readAgent(
line,
labelSep + 1,
line.length,
{aliases: true}
);
if(!def.alias) {
throw makeError('Reference must have an alias', line[labelSep]);
}
return {
agents,
alias: def.alias,
blockType: 'ref',
label: def.name,
tag: 'ref',
type: 'group begin',
};
}},
{begin: [], fn: (line) => { // Agent
const type = AGENT_MANIPULATION_TYPES.get(tokenKeyword(line[0]));
if(!type || line.length <= 1) {
return null;
}
return Object.assign({
agents: readAgentList(line, 1, line.length, {aliases: true}),
}, type);
}},
{begin: ['simultaneously'], fn: (line) => { // Async
if(tokenKeyword(last(line)) !== ':') {
return null;
}
let target = '';
if(line.length > 2) {
if(tokenKeyword(line[1]) !== 'with') {
return null;
}
target = joinLabel(line, 2, line.length - 1);
}
return {
target,
type: 'async',
};
}},
{begin: [], fn: (line) => { // Note
const mode = NOTE_TYPES.get(tokenKeyword(line[0]));
const labelSep = findTokens(line, [':']);
if(!mode || labelSep === -1) {
return null;
}
const type = mode.types[tokenKeyword(line[1])];
if(!type) {
return null;
}
let skip = 2;
skip = skipOver(line, skip, type.skip);
const agents = readAgentList(line, skip, labelSep);
if(agents.length < type.min) {
throw makeError('Too few agents for ' + mode.mode, line[0]);
}
if(agents.length > type.max) {
throw makeError('Too many agents for ' + mode.mode, line[0]);
}
return {
agents,
label: joinLabel(line, labelSep + 1),
mode: mode.mode,
type: type.type,
};
}},
{begin: [], fn: (line) => { // Connect
const labelSep = findTokens(line, [':'], {orEnd: true});
const connectionToken = findFirstToken(
line,
CONNECT.types,
{limit: labelSep - 1, start: 0}
);
if(!connectionToken) {
return null;
}
const connectPos = connectionToken.pos;
const readOpts = {
flagTypes: CONNECT.agentFlags,
};
if(tokenKeyword(line[0]) === '...') {
return {
agent: readAgent(line, connectPos + 1, labelSep, readOpts),
label: joinLabel(line, labelSep + 1),
options: connectionToken.value,
tag: joinLabel(line, 1, connectPos),
type: 'connect-delay-end',
};
} else if(tokenKeyword(line[connectPos + 1]) === '...') {
if(labelSep !== line.length) {
throw makeError(
'Cannot label beginning of delayed connection',
line[labelSep]
);
}
return {
agent: readAgent(line, 0, connectPos, readOpts),
options: connectionToken.value,
tag: joinLabel(line, connectPos + 2, labelSep),
type: 'connect-delay-begin',
};
} else {
return {
agents: [
readAgent(line, 0, connectPos, readOpts),
readAgent(line, connectPos + 1, labelSep, readOpts),
],
label: joinLabel(line, labelSep + 1),
options: connectionToken.value,
type: 'connect',
};
}
}},
{begin: [], fn: (line) => { // Marker
if(line.length < 2 || tokenKeyword(last(line)) !== ':') {
return null;
}
return {
name: joinLabel(line, 0, line.length - 1),
type: 'mark',
};
}},
{begin: [], fn: (line) => { // Options
const sepPos = findTokens(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: 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 {
agent,
options,
type: 'agent options',
};
}},
];
function stageFromLine(line, meta) {
for(const {begin, fn} of PARSERS) {
if(skipOver(line, 0, begin) !== begin.length) {
continue;
}
const stage = fn(line, meta);
if(stage) {
return stage;
}
}
return null;
}
function parseLine(line, {meta, stages}) {
let parallel = false;
const [start] = line;
if(tokenKeyword(start) === '&') {
parallel = true;
line.splice(0, 1);
}
const stage = stageFromLine(line, meta);
if(!stage) {
throw makeError('Unrecognised command: ' + joinLabel(line), line[0]);
} else if(typeof stage === 'object') {
stage.ln = start.b.ln;
stage.parallel = parallel;
stages.push(stage);
} else if(parallel) {
throw makeError('Metadata cannot be parallel', start);
}
}
const SHARED_TOKENISER = new Tokeniser();
class Parser {
getCodeMirrorMode() {
return SHARED_TOKENISER.getCodeMirrorMode(
Array.from(CONNECT.types.keys())
);
}
parseLines(lines, src) {
const result = {
meta: {
code: src,
headers: 'box',
terminators: 'none',
textFormatter: parseMarkdown,
theme: '',
title: '',
},
stages: [],
};
lines.forEach((line) => parseLine(line, result));
return result;
}
parse(src) {
const tokens = SHARED_TOKENISER.tokenise(src);
const lines = SHARED_TOKENISER.splitLines(tokens);
return this.parseLines(lines, src);
}
}
/* eslint-disable spaced-comment */
/* eslint-disable capitalized-comments */
class BaseComponent {
makeState(/*state*/) {
// Override if required
}
resetState(state) {
// Override if required
this.makeState(state);
}
prepareMeasurements(/*stage, {
renderer,
theme,
agentInfos,
textSizer,
state,
components,
}*/) {
// Override if required
}
separationPre(/*stage, {
renderer,
theme,
agentInfos,
visibleAgentIDs,
momentaryAgentIDs,
textSizer,
addSpacing,
addSeparation,
state,
components,
}*/) {
// Override if required
}
separation(/*stage, {
renderer,
theme,
agentInfos,
visibleAgentIDs,
momentaryAgentIDs,
textSizer,
addSpacing,
addSeparation,
state,
components,
}*/) {
// Override if required
}
renderPre(/*stage, {
renderer,
theme,
agentInfos,
textSizer,
state,
components,
}*/) {
// Override if required
// Return {topShift, agentIDs, asynchronousY}
}
render(/*stage, {
renderer,
topY,
primaryY,
fillLayer,
blockLayer,
theme,
agentInfos,
svg,
textSizer,
addDef,
makeRegion,
state,
components,
}*/) {
// Override if required
// Return bottom Y coordinate
}
renderHidden(/*stage, {
(same args as render, with primaryY = topY)
}*/) {
// Override if required
}
shouldHide(/*stage, {
renderer,
theme,
agentInfos,
textSizer,
state,
components,
}*/) {
// Override if required
// Return {self, nest}
}
}
function cleanRenderPreResult({
agentIDs = [],
topShift = 0,
y = null,
asynchronousY = null,
} = {}, currentY = null) {
let ts = topShift;
if(y !== null && currentY !== null) {
ts = Math.max(ts, y - currentY);
}
return {
agentIDs,
asynchronousY: (asynchronousY === null) ? currentY : asynchronousY,
topShift: ts,
y,
};
}
const components = new Map();
function register(name, component) {
components.set(name, component);
}
function getComponents() {
return components;
}
const OUTLINE_ATTRS = {
'class': 'outline',
'fill': 'transparent',
};
class CapBox {
getConfig(options, env) {
let config = null;
if(options.includes('database')) {
config = env.theme.agentCap.database;
}
return config || env.theme.agentCap.box;
}
prepareMeasurements({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
}
separation({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const width = (
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left +
config.padding.right
);
return {
left: width / 2,
radius: width / 2,
right: width / 2,
};
}
topShift({formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const height = (
env.textSizer.measureHeight(config.labelAttrs, formattedLabel) +
config.padding.top +
config.padding.bottom
);
return Math.max(0, height - config.arrowBottom);
}
render(y, {x, formattedLabel, options}, env) {
const config = this.getConfig(options, env);
const text = env.svg.boxedText(config, formattedLabel, {x, y});
env.makeRegion().add(
text,
env.svg.box(OUTLINE_ATTRS, {
height: text.height,
width: text.width,
x: x - text.width / 2,
y,
})
);
return {
height: text.height,
lineBottom: text.height,
lineTop: 0,
};
}
}
class CapCross {
prepareMeasurements() {
// No-op
}
separation(agentInfo, env) {
const config = env.theme.agentCap.cross;
return {
left: config.size / 2,
radius: 0,
right: config.size / 2,
};
}
topShift(agentInfo, env) {
const config = env.theme.agentCap.cross;
return config.size / 2;
}
render(y, {x, options}, env) {
const config = env.theme.agentCap.cross;
const d = config.size / 2;
env.makeRegion().add(
config.render({
options,
radius: d,
x,
y: y + d,
}),
env.svg.box(OUTLINE_ATTRS, {
height: d * 2,
width: d * 2,
x: x - d,
y,
})
);
return {
height: d * 2,
lineBottom: d,
lineTop: d,
};
}
}
class CapBar {
prepareMeasurements({formattedLabel}, env) {
const config = env.theme.agentCap.box;
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
}
separation({formattedLabel}, env) {
const config = env.theme.agentCap.box;
const width = (
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left +
config.padding.right
);
return {
left: width / 2,
radius: width / 2,
right: width / 2,
};
}
topShift(agentInfo, env) {
const config = env.theme.agentCap.bar;
return config.height / 2;
}
render(y, {x, formattedLabel, options}, env) {
const boxCfg = env.theme.agentCap.box;
const barCfg = env.theme.agentCap.bar;
const width = (
env.textSizer.measure(boxCfg.labelAttrs, formattedLabel).width +
boxCfg.padding.left +
boxCfg.padding.right
);
const {height} = barCfg;
env.makeRegion().add(
barCfg.render({
height,
options,
width,
x: x - width / 2,
y,
}),
env.svg.box(OUTLINE_ATTRS, {
height,
width,
x: x - width / 2,
y,
})
);
return {
height,
lineBottom: height,
lineTop: 0,
};
}
}
class CapFade {
prepareMeasurements() {
// No-op
}
separation({currentRad}) {
return {
left: currentRad,
radius: currentRad,
right: currentRad,
};
}
topShift(agentInfo, env, isBegin) {
const config = env.theme.agentCap.fade;
return isBegin ? config.height : 0;
}
render(y, {x}, env, isBegin) {
const config = env.theme.agentCap.fade;
const ratio = config.height / (config.height + config.extend);
const gradID = env.addDef(isBegin ? 'FadeIn' : 'FadeOut', () => (
env.svg.linearGradient({
'x1': '0%',
'x2': '0%',
'y1': isBegin ? '100%' : '0%',
'y2': isBegin ? '0%' : '100%',
}, [
{
'offset': '0%',
'stop-color': '#FFFFFF',
},
{
'offset': (100 * ratio).toFixed(3) + '%',
'stop-color': '#000000',
},
])
));
env.lineMaskLayer.add(env.svg.box({
'fill': 'url(#' + gradID + ')',
}, {
height: config.height + config.extend,
width: config.width,
x: x - config.width / 2,
y: y - (isBegin ? config.extend : 0),
}));
env.makeRegion().add(env.svg.box(OUTLINE_ATTRS, {
height: config.height,
width: config.width,
x: x - config.width / 2,
y,
}));
return {
height: config.height,
lineBottom: 0,
lineTop: config.height,
};
}
}
class CapNone {
prepareMeasurements() {
// No-op
}
separation({currentRad}) {
return {
left: currentRad,
radius: currentRad,
right: currentRad,
};
}
topShift(agentInfo, env) {
const config = env.theme.agentCap.none;
return config.height;
}
render(y, {x}, env) {
const config = env.theme.agentCap.none;
const w = 10;
env.makeRegion().add(env.svg.box(OUTLINE_ATTRS, {
height: config.height,
width: w,
x: x - w / 2,
y,
}));
return {
height: config.height,
lineBottom: 0,
lineTop: config.height,
};
}
}
const AGENT_CAPS = {
'bar': new CapBar(),
'box': new CapBox(),
'cross': new CapCross(),
'fade': new CapFade(),
'none': new CapNone(),
};
class AgentCap extends BaseComponent {
constructor(begin) {
super();
this.begin = begin;
}
prepareMeasurements({mode, agentIDs}, env) {
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode];
cap.prepareMeasurements(agentInfo, env, this.begin);
});
}
separationPre({mode, agentIDs}, env) {
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode];
const sep = cap.separation(agentInfo, env, this.begin);
env.addSpacing(id, sep);
agentInfo.currentMaxRad = Math.max(
agentInfo.currentMaxRad,
sep.radius
);
});
}
separation({agentIDs}, env) {
if(this.begin) {
mergeSets(env.visibleAgentIDs, agentIDs);
} else {
removeAll(env.visibleAgentIDs, agentIDs);
}
}
renderPre({mode, agentIDs}, env) {
let maxTopShift = 0;
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode];
const topShift = cap.topShift(agentInfo, env, this.begin);
maxTopShift = Math.max(maxTopShift, topShift);
const r = cap.separation(agentInfo, env, this.begin).radius;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
return {
agentIDs,
topShift: maxTopShift,
};
}
render({mode, agentIDs}, env) {
let maxEnd = 0;
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
const cap = AGENT_CAPS[mode];
const topShift = cap.topShift(agentInfo, env, this.begin);
const y0 = env.primaryY - topShift;
const shifts = cap.render(y0, agentInfo, env, this.begin);
maxEnd = Math.max(maxEnd, y0 + shifts.height);
if(this.begin) {
env.drawAgentLine(id, y0 + shifts.lineBottom);
} else {
env.drawAgentLine(id, y0 + shifts.lineTop, true);
}
});
return maxEnd + env.theme.actionMargin;
}
renderHidden({agentIDs}, env) {
agentIDs.forEach((id) => {
env.drawAgentLine(id, env.topY, !this.begin);
});
}
}
register('agent begin', new AgentCap(true));
register('agent end', new AgentCap(false));
class AgentHighlight extends BaseComponent {
radius(highlighted, env) {
return highlighted ? env.theme.agentLineHighlightRadius : 0;
}
separationPre({agentIDs, highlighted}, env) {
const r = this.radius(highlighted, env);
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
agentInfo.currentRad = r;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
}
renderPre({agentIDs, highlighted}, env) {
const r = this.radius(highlighted, env);
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
}
render({agentIDs, highlighted}, env) {
const r = this.radius(highlighted, env);
agentIDs.forEach((id) => {
env.drawAgentLine(id, env.primaryY);
env.agentInfos.get(id).currentRad = r;
});
return env.primaryY + env.theme.actionMargin;
}
renderHidden(stage, env) {
this.render(stage, env);
}
}
register('agent highlight', new AgentHighlight());
const OUTLINE_ATTRS$1 = {
'class': 'outline',
'fill': 'transparent',
};
class BlockSplit extends BaseComponent {
prepareMeasurements({left, tag, label}, env) {
const blockInfo = env.state.blocks.get(left);
const config = env.theme.getBlock(blockInfo.type).section;
env.textSizer.expectMeasure(config.tag.labelAttrs, tag);
env.textSizer.expectMeasure(config.label.labelAttrs, label);
}
separation({left, right, tag, label}, env) {
const blockInfo = env.state.blocks.get(left);
const config = env.theme.getBlock(blockInfo.type).section;
const width = (
env.textSizer.measure(config.tag.labelAttrs, tag).width +
config.tag.padding.left +
config.tag.padding.right +
env.textSizer.measure(config.label.labelAttrs, label).width +
config.label.padding.left +
config.label.padding.right
);
env.addSeparation(left, right, width);
}
renderPre({left, right}) {
return {
agentIDs: [left, right],
};
}
render({left, right, tag, label}, env, first = false) {
const blockInfo = env.state.blocks.get(left);
const config = env.theme.getBlock(blockInfo.type);
const agentInfoL = env.agentInfos.get(left);
const agentInfoR = env.agentInfos.get(right);
let y = env.primaryY;
if(!first) {
y += config.section.padding.bottom;
}
const clickable = env.makeRegion();
const tagRender = env.svg.boxedText({
boxAttrs: config.section.tag.boxAttrs,
boxRenderer: config.section.tag.boxRenderer,
labelAttrs: config.section.tag.labelAttrs,
padding: config.section.tag.padding,
}, tag, {x: agentInfoL.x, y});
const labelRender = env.svg.boxedText({
boxAttrs: {'fill': '#000000'},
labelAttrs: config.section.label.labelAttrs,
padding: config.section.label.padding,
}, label, {x: agentInfoL.x + tagRender.width, y});
const labelHeight = Math.max(
Math.max(tagRender.height, labelRender.height),
config.section.label.minHeight
);
blockInfo.hold.add(tagRender.box);
env.lineMaskLayer.add(labelRender.box);
clickable.add(
env.svg.box(OUTLINE_ATTRS$1, {
height: labelHeight,
width: agentInfoR.x - agentInfoL.x,
x: agentInfoL.x,
y,
}),
tagRender.label,
labelRender.label
);
if(!first) {
blockInfo.hold.add(config.sepRenderer({
'x1': agentInfoL.x,
'x2': agentInfoR.x,
'y1': y,
'y2': y,
}));
} else if(blockInfo.canHide) {
clickable.addClass(blockInfo.hide ? 'collapsed' : 'expanded');
}
return y + labelHeight + config.section.padding.top;
}
}
class BlockBegin extends BlockSplit {
makeState(state) {
state.blocks = new Map();
}
resetState(state) {
state.blocks.clear();
}
storeBlockInfo(stage, env) {
const {canHide} = stage;
const blockInfo = {
canHide,
hide: canHide && env.renderer.isCollapsed(stage.ln),
hold: null,
startY: null,
type: stage.blockType,
};
env.state.blocks.set(stage.left, blockInfo);
return blockInfo;
}
prepareMeasurements(stage, env) {
this.storeBlockInfo(stage, env);
super.prepareMeasurements(stage, env);
}
separationPre(stage, env) {
this.storeBlockInfo(stage, env);
super.separationPre(stage, env);
}
separation(stage, env) {
mergeSets(env.visibleAgentIDs, [stage.left, stage.right]);
super.separation(stage, env);
}
renderPre(stage, env) {
const blockInfo = this.storeBlockInfo(stage, env);
const config = env.theme.getBlock(blockInfo.type);
return {
agentIDs: [stage.left, stage.right],
topShift: config.margin.top,
};
}
render(stage, env) {
const hold = env.svg.el('g');
env.blockLayer.add(hold);
const blockInfo = env.state.blocks.get(stage.left);
blockInfo.hold = hold;
blockInfo.startY = env.primaryY;
return super.render(stage, env, true);
}
shouldHide({left}, env) {
const blockInfo = env.state.blocks.get(left);
return {
nest: blockInfo.hide ? 1 : 0,
self: false,
};
}
}
class BlockEnd extends BaseComponent {
separation({left, right}, env) {
removeAll(env.visibleAgentIDs, [left, right]);
}
renderPre({left, right}, env) {
const blockInfo = env.state.blocks.get(left);
const config = env.theme.getBlock(blockInfo.type);
return {
agentIDs: [left, right],
topShift: config.section.padding.bottom,
};
}
render({left, right}, env) {
const blockInfo = env.state.blocks.get(left);
const config = env.theme.getBlock(blockInfo.type);
const agentInfoL = env.agentInfos.get(left);
const agentInfoR = env.agentInfos.get(right);
let renderFn = config.boxRenderer;
if(blockInfo.hide) {
renderFn = config.collapsedBoxRenderer || renderFn;
}
let shapes = renderFn({
height: env.primaryY - blockInfo.startY,
width: agentInfoR.x - agentInfoL.x,
x: agentInfoL.x,
y: blockInfo.startY,
});
if(!shapes.shape) {
shapes = {shape: shapes};
}
blockInfo.hold.add(shapes.shape);
env.fillLayer.add(shapes.fill);
env.lineMaskLayer.add(shapes.mask);
return env.primaryY + config.margin.bottom + env.theme.actionMargin;
}
shouldHide({left}, env) {
const blockInfo = env.state.blocks.get(left);
return {
nest: blockInfo.hide ? -1 : 0,
self: false,
};
}
}
register('block begin', new BlockBegin());
register('block split', new BlockSplit());
register('block end', new BlockEnd());
const OUTLINE_ATTRS$2 = {
'class': 'outline',
'fill': 'transparent',
};
class Arrowhead {
constructor(propName) {
this.propName = propName;
}
getConfig(theme) {
return theme.connect.arrow[this.propName];
}
short(theme) {
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;
if(join === 'round') {
return lineStroke + t;
} else {
const h = arrow.height / 2;
const w = arrow.width;
const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1);
return lineStroke + arrowDistance;
}
}
render(layer, theme, pt, dir) {
const config = this.getConfig(theme);
const short = this.short(theme);
layer.add(config.render(config.attrs, {
dir,
height: config.height,
width: config.width,
x: pt.x + short * dir.dx,
y: pt.y + short * dir.dy,
}));
}
width(theme) {
return this.short(theme) + this.getConfig(theme).width;
}
height(theme) {
return this.getConfig(theme).height;
}
lineGap(theme, line) {
const arrow = this.getConfig(theme);
const short = this.short(theme);
if(arrow.attrs.fill === 'none') {
const h = arrow.height / 2;
const w = arrow.width;
const safe = short + (line.attrs['stroke-width'] / 2) * (w / h);
return (short + safe) / 2;
} else {
return short + arrow.width / 2;
}
}
}
class Arrowcross {
getConfig(theme) {
return theme.connect.arrow.cross;
}
render(layer, theme, pt, dir) {
const config = this.getConfig(theme);
layer.add(config.render({
radius: config.radius,
x: pt.x + config.short * dir.dx,
y: pt.y + config.short * dir.dy,
}));
}
width(theme) {
const config = this.getConfig(theme);
return config.short + config.radius;
}
height(theme) {
return this.getConfig(theme).radius * 2;
}
lineGap(theme) {
return this.getConfig(theme).short;
}
}
const ARROWHEADS = [
{
height: () => 0,
lineGap: () => 0,
render: () => null,
width: () => 0,
},
new Arrowhead('single'),
new Arrowhead('double'),
new Arrowcross(),
];
class Connect extends BaseComponent {
prepareMeasurements({agentIDs, label}, env) {
const config = env.theme.connect;
const loopback = (agentIDs[0] === agentIDs[1]);
const labelAttrs = (loopback ?
config.label.loopbackAttrs : config.label.attrs);
env.textSizer.expectMeasure(labelAttrs, label);
}
separationPre({agentIDs}, env) {
const r = env.theme.connect.source.radius;
agentIDs.forEach((id) => {
const agentInfo = env.agentInfos.get(id);
if(!agentInfo.isVirtualSource) {
return;
}
agentInfo.currentRad = r;
agentInfo.currentMaxRad = Math.max(agentInfo.currentMaxRad, r);
});
}
separation({label, agentIDs, options}, env) {
const config = env.theme.connect;
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const loopback = (agentIDs[0] === agentIDs[1]);
const labelAttrs = (loopback ?
config.label.loopbackAttrs : config.label.attrs);
let labelWidth = env.textSizer.measure(labelAttrs, label).width;
if(labelWidth > 0) {
labelWidth += config.label.padding * 2;
}
const info1 = env.agentInfos.get(agentIDs[0]);
if(loopback) {
env.addSpacing(agentIDs[0], {
left: 0,
right: (
info1.currentMaxRad +
Math.max(
labelWidth + lArrow.width(env.theme),
rArrow.width(env.theme)
) +
config.loopbackRadius
),
});
} else {
const info2 = env.agentInfos.get(agentIDs[1]);
env.addSeparation(
agentIDs[0],
agentIDs[1],
info1.currentMaxRad +
info2.currentMaxRad +
labelWidth +
Math.max(
lArrow.width(env.theme),
rArrow.width(env.theme)
) * 2
);
}
mergeSets(env.momentaryAgentIDs, agentIDs);
}
renderRevArrowLine({x1, y1, x2, y2, xR}, options, env, clickable) {
const line = env.theme.getConnectLine(options.line);
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const dx1 = lArrow.lineGap(env.theme, line);
const dx2 = rArrow.lineGap(env.theme, line);
const rendered = line.renderRev({
rad: env.theme.connect.loopbackRadius,
x1: x1 + dx1,
x2: x2 + dx2,
xR,
y1,
y2,
});
clickable.add(rendered.shape);
lArrow.render(clickable, env.theme, {
x: rendered.p1.x - dx1,
y: rendered.p1.y,
}, {dx: 1, dy: 0});
rArrow.render(clickable, env.theme, {
x: rendered.p2.x - dx2,
y: rendered.p2.y,
}, {dx: 1, dy: 0});
}
renderSelfConnect({label, agentIDs, options}, env, from, yBegin) {
const config = env.theme.connect;
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const to = env.agentInfos.get(agentIDs[1]);
const height = label ? (
env.textSizer.measureHeight(config.label.attrs, label) +
config.label.margin.top +
config.label.margin.bottom
) : 0;
const xL = (
from.x + from.currentMaxRad +
lArrow.width(env.theme) +
(label ? config.label.padding : 0)
);
const renderedText = env.svg.boxedText({
boxAttrs: {'fill': '#000000'},
labelAttrs: config.label.loopbackAttrs,
padding: config.mask.padding,
}, label, {
x: xL - config.mask.padding.left,
y: yBegin - height + config.label.margin.top,
});
const labelW = (label ? (
renderedText.width +
config.label.padding -
config.mask.padding.left -
config.mask.padding.right
) : 0);
const xR = Math.max(
to.x + to.currentMaxRad + rArrow.width(env.theme),
xL + labelW
);
const raise = Math.max(height, lArrow.height(env.theme) / 2);
const arrowDip = rArrow.height(env.theme) / 2;
env.lineMaskLayer.add(renderedText.box);
const clickable = env.makeRegion().add(
env.svg.box(OUTLINE_ATTRS$2, {
'height': raise + env.primaryY - yBegin + arrowDip,
'width': xR + config.loopbackRadius - from.x,
'x': from.x,
'y': yBegin - raise,
}),
renderedText.label
);
this.renderRevArrowLine({
x1: from.x + from.currentMaxRad,
x2: to.x + to.currentMaxRad,
xR,
y1: yBegin,
y2: env.primaryY,
}, options, env, clickable);
return (
env.primaryY +
Math.max(arrowDip, 0) +
env.theme.actionMargin
);
}
renderArrowLine({x1, y1, x2, y2}, options, env, clickable) {
const line = env.theme.getConnectLine(options.line);
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const len = Math.sqrt(
(x2 - x1) * (x2 - x1) +
(y2 - y1) * (y2 - y1)
);
const d1 = lArrow.lineGap(env.theme, line);
const d2 = rArrow.lineGap(env.theme, line);
const dx = (x2 - x1) / len;
const dy = (y2 - y1) / len;
const rendered = line.renderFlat({
x1: x1 + d1 * dx,
x2: x2 - d2 * dx,
y1: y1 + d1 * dy,
y2: y2 - d2 * dy,
});
clickable.add(rendered.shape);
const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy};
const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy};
lArrow.render(clickable, env.theme, p1, {dx, dy});
rArrow.render(clickable, env.theme, p2, {dx: -dx, dy: -dy});
return {
lArrow,
p1,
p2,
rArrow,
};
}
renderVirtualSources({from, to, rendered}, env, clickable) {
const config = env.theme.connect.source;
if(from.isVirtualSource) {
clickable.add(config.render({
radius: config.radius,
x: rendered.p1.x - config.radius,
y: rendered.p1.y,
}));
}
if(to.isVirtualSource) {
clickable.add(config.render({
radius: config.radius,
x: rendered.p2.x + config.radius,
y: rendered.p2.y,
}));
}
}
renderSimpleLabel(label, {layer, x1, x2, y1, y2, height}, env) {
const config = env.theme.connect;
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
let labelLayer = layer;
const boxAttrs = {'fill': '#000000'};
if(y1 !== y2) {
const angle = Math.atan((y2 - y1) / (x2 - x1));
const transform = (
'rotate(' +
(angle * 180 / Math.PI) +
' ' + midX + ',' + midY +
')'
);
boxAttrs.transform = transform;
labelLayer = env.svg.el('g').attr('transform', transform);
layer.add(labelLayer);
}
const text = env.svg.boxedText({
boxAttrs,
labelAttrs: config.label.attrs,
padding: config.mask.padding,
}, label, {
x: midX,
y: midY + config.label.margin.top - height,
});
env.lineMaskLayer.add(text.box);
labelLayer.add(text.label);
}
renderSimpleConnect({label, agentIDs, options}, env, from, yBegin) {
const config = env.theme.connect;
const to = env.agentInfos.get(agentIDs[1]);
const dir = (from.x < to.x) ? 1 : -1;
const height = (
env.textSizer.measureHeight(config.label.attrs, label) +
config.label.margin.top +
config.label.margin.bottom
);
const x1 = from.x + from.currentMaxRad * dir;
const x2 = to.x - to.currentMaxRad * dir;
const clickable = env.makeRegion();
const rendered = this.renderArrowLine({
x1,
x2,
y1: yBegin,
y2: env.primaryY,
}, options, env, clickable);
const arrowSpread = Math.max(
rendered.lArrow.height(env.theme),
rendered.rArrow.height(env.theme)
) / 2;
const lift = Math.max(height, arrowSpread);
this.renderVirtualSources({from, rendered, to}, env, clickable);
clickable.add(env.svg.el('path')
.attrs(OUTLINE_ATTRS$2)
.attr('d', (
'M' + x1 + ',' + (yBegin - lift) +
'L' + x2 + ',' + (env.primaryY - lift) +
'L' + x2 + ',' + (env.primaryY + arrowSpread) +
'L' + x1 + ',' + (yBegin + arrowSpread) +
'Z'
)));
this.renderSimpleLabel(label, {
height,
layer: clickable,
x1,
x2,
y1: yBegin,
y2: env.primaryY,
}, env);
return env.primaryY + Math.max(
arrowSpread + env.theme.minActionMargin,
env.theme.actionMargin
);
}
renderPre({label, agentIDs, options}, env) {
const config = env.theme.connect;
const lArrow = ARROWHEADS[options.left];
const rArrow = ARROWHEADS[options.right];
const height = (
env.textSizer.measureHeight(config.label.attrs, label) +
config.label.margin.top +
config.label.margin.bottom
);
let arrowH = lArrow.height(env.theme);
if(agentIDs[0] !== agentIDs[1]) {
arrowH = Math.max(arrowH, rArrow.height(env.theme));
}
return {
agentIDs,
topShift: Math.max(arrowH / 2, height),
};
}
render(stage, env, from = null, yBegin = null) {
let yb = yBegin;
let f = from;
if(from === null) {
f = env.agentInfos.get(stage.agentIDs[0]);
yb = env.primaryY;
}
if(stage.agentIDs[0] === stage.agentIDs[1]) {
return this.renderSelfConnect(stage, env, f, yb);
} else {
return this.renderSimpleConnect(stage, env, f, yb);
}
}
}
class ConnectDelayBegin extends Connect {
makeState(state) {
state.delayedConnections = new Map();
}
resetState(state) {
state.delayedConnections.clear();
}
separation(stage, env) {
super.separation(stage, env);
mergeSets(env.momentaryAgentIDs, [stage.agentIDs[0]]);
}
renderPre(stage, env) {
return Object.assign(super.renderPre(stage, env), {
agentIDs: [stage.agentIDs[0]],
});
}
render(stage, env) {
const dc = env.state.delayedConnections;
dc.set(stage.tag, {
from: Object.assign({}, env.agentInfos.get(stage.agentIDs[0])),
stage,
y: env.primaryY,
});
return env.primaryY + env.theme.actionMargin;
}
renderHidden(stage, env) {
this.render(stage, env);
}
}
class ConnectDelayEnd extends Connect {
prepareMeasurements() {
// No-op
}
separationPre() {
// No-op
}
separation() {
// No-op
}
renderPre({tag}, env) {
const config = env.theme.connect;
const dc = env.state.delayedConnections;
const begin = dc.get(tag);
const beginStage = begin.stage;
const agentIDs = [beginStage.agentIDs[1]];
if(beginStage.agentIDs[0] === beginStage.agentIDs[1]) {
return {
agentIDs,
y: begin.y + config.loopbackRadius * 2,
};
}
return Object.assign(super.renderPre(beginStage, env), {agentIDs});
}
render({tag}, env) {
const dc = env.state.delayedConnections;
const begin = dc.get(tag);
return super.render(begin.stage, env, begin.from, begin.y);
}
}
register('connect', new Connect());
register('connect-delay-begin', new ConnectDelayBegin());
register('connect-delay-end', new ConnectDelayEnd());
const OUTLINE_ATTRS$3 = {
'class': 'outline',
'fill': 'transparent',
};
class Divider extends BaseComponent {
prepareMeasurements({mode, formattedLabel}, env) {
const config = env.theme.getDivider(mode);
env.textSizer.expectMeasure(config.labelAttrs, formattedLabel);
}
separation({mode, formattedLabel}, env) {
const config = env.theme.getDivider(mode);
const width = (
env.textSizer.measure(config.labelAttrs, formattedLabel).width +
config.padding.left +
config.padding.right
);
env.addSeparation('[', ']', width);
env.addSpacing('[', {left: config.extend, right: 0});
env.addSpacing(']', {left: 0, right: config.extend});
}
renderPre() {
return {
agentIDs: ['[', ']'],
};
}
render({mode, height, formattedLabel}, env) {
const config = env.theme.getDivider(mode);
const left = env.agentInfos.get('[');
const right = env.agentInfos.get(']');
let labelWidth = 0;
let labelHeight = 0;
if(formattedLabel) {
labelHeight = env.textSizer.measureHeight(
config.labelAttrs,
formattedLabel
);
}
const fullHeight = Math.max(height, labelHeight) + config.margin;
let labelText = null;
if(formattedLabel) {
const boxed = env.svg.boxedText({
boxAttrs: {'fill': '#000000'},
labelAttrs: config.labelAttrs,
padding: config.padding,
}, formattedLabel, {
x: (left.x + right.x) / 2,
y: (
env.primaryY +
(fullHeight - labelHeight) / 2 -
config.padding.top
),
});
env.fullMaskLayer.add(boxed.box);
labelText = boxed.label;
labelWidth = boxed.width;
}
const {shape, mask} = config.render({
env,
height,
labelHeight,
labelWidth,
width: right.x - left.x + config.extend * 2,
x: left.x - config.extend,
y: env.primaryY + (fullHeight - height) / 2,
});
env.fullMaskLayer.add(mask);
env.makeRegion({unmasked: true}).add(
env.svg.box(OUTLINE_ATTRS$3, {
'height': fullHeight,
'width': right.x - left.x + config.extend * 2,
'x': left.x - config.extend,
'y': env.primaryY,
}),
shape,
labelText
);
return env.primaryY + fullHeight + env.theme.actionMargin;
}
}
register('divider', new Divider());
class Mark extends BaseComponent {
makeState(state) {
state.marks = new Map();
}
resetState(state) {
state.marks.clear();
}
render({name}, {topY, state}) {
state.marks.set(name, topY);
}
renderHidden(stage, env) {
this.render(stage, env);
}
}
class Async extends BaseComponent {
renderPre({target}, {state}) {
let y = 0;
if(target && state.marks) {
y = state.marks.get(target) || 0;
}
return {
asynchronousY: y,
};
}
}
register('mark', new Mark());
register('async', new Async());
const OUTLINE_ATTRS$4 = {
'class': 'outline',
'fill': 'transparent',
};
function findExtremes(agentInfos, agentIDs) {
let min = null;
let max = null;
agentIDs.forEach((id) => {
const info = agentInfos.get(id);
if(min === null || info.index < min.index) {
min = info;
}
if(max === null || info.index > max.index) {
max = info;
}
});
return {
left: min.id,
right: max.id,
};
}
function findEdges(fullW, {
x0 = null,
x1 = null,
xMid = null,
}) {
let xL = x0;
let xR = x1;
if(xL === null && xMid !== null) {
xL = xMid - fullW / 2;
}
if(xR === null && xL !== null) {
xR = xL + fullW;
} else if(xL === null) {
xL = xR - fullW;
}
return {xL, xR};
}
function textAnchorX(anchor, {xL, xR}, padding) {
switch(anchor) {
case 'middle':
return (xL + padding.left + xR - padding.right) / 2;
case 'end':
return xR - padding.right;
default:
return xL + padding.left;
}
}
class NoteComponent extends BaseComponent {
prepareMeasurements({mode, label}, env) {
const config = env.theme.getNote(mode);
env.textSizer.expectMeasure(config.labelAttrs, label);
}
renderPre({agentIDs}) {
return {agentIDs};
}
renderNote({
label,
mode,
position,
}, env) {
const config = env.theme.getNote(mode);
const {padding} = config;
const y = env.topY + config.margin.top + padding.top;
const labelNode = env.svg.formattedText(config.labelAttrs, label);
const size = env.textSizer.measure(labelNode);
const fullW = (size.width + padding.left + padding.right);
const fullH = (size.height + padding.top + padding.bottom);
const edges = findEdges(fullW, position);
labelNode.set({
x: textAnchorX(config.labelAttrs['text-anchor'], edges, padding),
y,
});
const boundingBox = {
height: fullH,
width: edges.xR - edges.xL,
x: edges.xL,
y: env.topY + config.margin.top,
};
env.makeRegion().add(
config.boxRenderer(boundingBox),
env.svg.box(OUTLINE_ATTRS$4, boundingBox),
labelNode
);
return (
env.topY +
config.margin.top +
fullH +
config.margin.bottom +
env.theme.actionMargin
);
}
}
class NoteOver extends NoteComponent {
separation({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode);
const width = (
env.textSizer.measure(config.labelAttrs, label).width +
config.padding.left +
config.padding.right
);
const {left, right} = findExtremes(env.agentInfos, agentIDs);
const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right);
if(infoL === infoR) {
env.addSpacing(left, {
left: width / 2,
right: width / 2,
});
} else {
const hangL = infoL.currentMaxRad + config.overlap.left;
const hangR = infoR.currentMaxRad + config.overlap.right;
env.addSeparation(left, right, width - hangL - hangR);
env.addSpacing(left, {left: hangL, right: 0});
env.addSpacing(right, {left: 0, right: hangR});
}
}
render({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode);
const {left, right} = findExtremes(env.agentInfos, agentIDs);
const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right);
if(infoL === infoR) {
const xMid = infoL.x;
return this.renderNote({
label,
mode,
position: {xMid},
}, env);
} else {
return this.renderNote({
label,
mode,
position: {
x0: infoL.x - infoL.currentMaxRad - config.overlap.left,
x1: infoR.x + infoR.currentMaxRad + config.overlap.right,
},
}, env);
}
}
}
class NoteSide extends NoteComponent {
constructor(isRight) {
super();
this.isRight = isRight;
}
separation({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode);
const {left, right} = findExtremes(env.agentInfos, agentIDs);
const width = (
env.textSizer.measure(config.labelAttrs, label).width +
config.padding.left +
config.padding.right +
config.margin.left +
config.margin.right
);
if(this.isRight) {
const info = env.agentInfos.get(right);
env.addSpacing(right, {
left: 0,
right: width + info.currentMaxRad,
});
} else {
const info = env.agentInfos.get(left);
env.addSpacing(left, {
left: width + info.currentMaxRad,
right: 0,
});
}
}
render({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode);
const {left, right} = findExtremes(env.agentInfos, agentIDs);
if(this.isRight) {
const info = env.agentInfos.get(right);
const x0 = info.x + info.currentMaxRad + config.margin.left;
return this.renderNote({
label,
mode,
position: {x0},
}, env);
} else {
const info = env.agentInfos.get(left);
const x1 = info.x - info.currentMaxRad - config.margin.right;
return this.renderNote({
label,
mode,
position: {x1},
}, env);
}
}
}
class NoteBetween extends NoteComponent {
separation({agentIDs, mode, label}, env) {
const config = env.theme.getNote(mode);
const {left, right} = findExtremes(env.agentInfos, agentIDs);
const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right);
env.addSeparation(
left,
right,
env.textSizer.measure(config.labelAttrs, label).width +
config.padding.left +
config.padding.right +
config.margin.left +
config.margin.right +
infoL.currentMaxRad +
infoR.currentMaxRad
);
}
render({agentIDs, mode, label}, env) {
const {left, right} = findExtremes(env.agentInfos, agentIDs);
const infoL = env.agentInfos.get(left);
const infoR = env.agentInfos.get(right);
const xMid = (
infoL.x + infoL.currentMaxRad +
infoR.x - infoR.currentMaxRad
) / 2;
return this.renderNote({
label,
mode,
position: {xMid},
}, env);
}
}
register('note over', new NoteOver());
register('note left', new NoteSide(false));
register('note right', new NoteSide(true));
register('note between', new NoteBetween());
function nullableMax(a = null, b = null) {
if(a === null) {
return b;
}
if(b === null) {
return a;
}
return Math.max(a, b);
}
function mergeResults(a, b) {
mergeSets(a.agentIDs, b.agentIDs);
return {
agentIDs: a.agentIDs,
asynchronousY: nullableMax(a.asynchronousY, b.asynchronousY),
topShift: Math.max(a.topShift, b.topShift),
y: nullableMax(a.y, b.y),
};
}
class Parallel extends BaseComponent {
invokeChildren(stage, env, methodName) {
return stage.stages.map((subStage) => {
const component = env.components.get(subStage.type);
return component[methodName](subStage, env);
});
}
prepareMeasurements(stage, env) {
this.invokeChildren(stage, env, 'prepareMeasurements');
}
separationPre(stage, env) {
this.invokeChildren(stage, env, 'separationPre');
}
separation(stage, env) {
this.invokeChildren(stage, env, 'separation');
}
renderPre(stage, env) {
const baseResults = {
agentIDs: [],
asynchronousY: null,
topShift: 0,
};
return this.invokeChildren(stage, env, 'renderPre')
.map((r) => cleanRenderPreResult(r))
.reduce(mergeResults, baseResults);
}
render(stage, env) {
const originalMakeRegion = env.makeRegion;
let bottomY = 0;
stage.stages.forEach((subStage) => {
env.makeRegion = (options = {}) => (
originalMakeRegion(Object.assign({
stageOverride: subStage,
}, options))
);
const component = env.components.get(subStage.type);
const baseY = component.render(subStage, env) || 0;
bottomY = Math.max(bottomY, baseY);
});
env.makeRegion = originalMakeRegion;
return bottomY;
}
renderHidden(stage, env) {
this.invokeChildren(stage, env, 'renderHidden');
}
shouldHide(stage, env) {
const baseResults = {
nest: 0,
self: false,
};
return this.invokeChildren(stage, env, 'shouldHide')
.reduce((result, {self = false, nest = 0} = {}) => ({
nest: result.nest + nest,
self: result.self || Boolean(self),
}), baseResults);
}
}
register('parallel', new Parallel());
function make(value, document) {
if(typeof value === 'string') {
return document.createTextNode(value);
} else if(typeof value === 'number') {
return document.createTextNode(value.toString(10));
} else if(typeof value === 'object' && value.element) {
return value.element;
} else {
return value;
}
}
function unwrap(node) {
if(node === null) {
return null;
} else if(node.element) {
return node.element;
} else {
return node;
}
}
class WrappedElement {
constructor(element) {
this.element = element;
}
addBefore(child = null, before = null) {
if(child === null) {
return this;
} else if(Array.isArray(child)) {
for(const c of child) {
this.addBefore(c, before);
}
} else {
const childElement = make(child, this.element.ownerDocument);
this.element.insertBefore(childElement, unwrap(before));
}
return this;
}
add(...child) {
return this.addBefore(child, null);
}
del(child = null) {
if(child !== null) {
this.element.removeChild(unwrap(child));
}
return this;
}
attr(key, value) {
this.element.setAttribute(key, value);
return this;
}
attrs(attrs) {
for(const k in attrs) {
if(Object.prototype.hasOwnProperty.call(attrs, k)) {
this.element.setAttribute(k, attrs[k]);
}
}
return this;
}
styles(styles) {
for(const k in styles) {
if(Object.prototype.hasOwnProperty.call(styles, k)) {
this.element.style[k] = styles[k];
}
}
return this;
}
setClass(cls) {
return this.attr('class', cls);
}
addClass(cls) {
const classes = this.element.getAttribute('class');
if(!classes) {
return this.setClass(cls);
}
const list = classes.split(' ');
if(list.includes(cls)) {
return this;
}
list.push(cls);
return this.attr('class', list.join(' '));
}
delClass(cls) {
const classes = this.element.getAttribute('class');
if(!classes) {
return this;
}
const list = classes.split(' ');
const p = list.indexOf(cls);
if(p !== -1) {
list.splice(p, 1);
this.attr('class', list.join(' '));
}
return this;
}
text(text) {
this.element.textContent = text;
return this;
}
on(event, callback, options = {}) {
if(Array.isArray(event)) {
for(const e of event) {
this.on(e, callback, options);
}
} else {
this.element.addEventListener(event, callback, options);
}
return this;
}
off(event, callback, options = {}) {
if(Array.isArray(event)) {
for(const e of event) {
this.off(e, callback, options);
}
} else {
this.element.removeEventListener(event, callback, options);
}
return this;
}
val(value) {
this.element.value = value;
return this;
}
select(start, end = null) {
this.element.selectionStart = start;
this.element.selectionEnd = (end === null) ? start : end;
return this;
}
focus() {
this.element.focus();
return this;
}
focussed() {
return this.element === this.element.ownerDocument.activeElement;
}
empty() {
while(this.element.childNodes.length > 0) {
this.element.removeChild(this.element.lastChild);
}
return this;
}
attach(parent) {
unwrap(parent).appendChild(this.element);
return this;
}
detach() {
if(this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
return this;
}
}
class DOMWrapper {
constructor(document) {
if(!document) {
throw new Error('Missing document!');
}
this.document = document;
this.wrap = this.wrap.bind(this);
this.el = this.el.bind(this);
this.txt = this.txt.bind(this);
}
wrap(element) {
if(element.element) {
return element;
} else {
return new WrappedElement(element);
}
}
el(tag, namespace = null) {
let element = null;
if(namespace === null) {
element = this.document.createElement(tag);
} else {
element = this.document.createElementNS(namespace, tag);
}
return new WrappedElement(element);
}
txt(content = '') {
return this.document.createTextNode(content);
}
}
function merge(state, newState) {
for(const k in state) {
if(Object.prototype.hasOwnProperty.call(state, k)) {
if(newState[k] !== null && typeof newState[k] !== 'undefined') {
state[k] = newState[k];
}
}
}
}
function populateSvgTextLine(svg, node, formattedLine) {
if(!Array.isArray(formattedLine)) {
throw new Error('Invalid formatted text line: ' + formattedLine);
}
formattedLine.forEach(({text, attrs}) => {
let element = text;
if(attrs) {
element = svg.el('tspan').attrs(attrs).add(text);
if(attrs.filter) {
element.attr('filter', svg.getTextFilter(attrs.filter));
}
}
node.add(element);
});
}
const EMPTY = [];
class SVGTextBlock {
constructor(container, svg, initialState = {}) {
this.container = container;
this.svg = svg;
this.state = {
attrs: {},
formatted: EMPTY,
x: 0,
y: 0,
};
this.lines = [];
this.set(initialState);
}
_rebuildLines(count) {
if(count > this.lines.length) {
while(this.lines.length < count) {
this.lines.push({
latest: '',
node: this.svg.el('text')
.attr('x', this.state.x)
.attrs(this.state.attrs)
.attach(this.container),
});
}
} else {
while(this.lines.length > count) {
this.lines.pop().node.detach();
}
}
}
_reset() {
this._rebuildLines(0);
}
_renderText() {
const {formatted} = this.state;
if(!formatted || !formatted.length) {
this._reset();
return;
}
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
this._rebuildLines(formatted.length);
this.lines.forEach((ln, i) => {
const id = JSON.stringify(formatted[i]);
if(id !== ln.latest) {
ln.node.empty();
populateSvgTextLine(this.svg, ln.node, formatted[i]);
ln.latest = id;
}
});
}
_updateX() {
this.lines.forEach(({node}) => {
node.attr('x', this.state.x);
});
}
_updateY() {
const sizer = this.svg.textSizer;
let curY = this.state.y;
for(let i = 0; i < this.lines.length; ++ i) {
const line = [this.state.formatted[i]];
const baseline = sizer.baseline(this.state.attrs, line);
this.lines[i].node.attr('y', curY + baseline);
curY += sizer.measureHeight(this.state.attrs, line);
}
}
set(newState) {
const oldState = Object.assign({}, this.state);
merge(this.state, newState);
if(this.state.attrs !== oldState.attrs) {
this._reset();
oldState.formatted = EMPTY;
}
const oldLines = this.lines.length;
if(this.state.formatted !== oldState.formatted) {
this._renderText();
}
if(this.state.x !== oldState.x) {
this._updateX();
}
if(this.state.y !== oldState.y || this.lines.length !== oldLines) {
this._updateY();
}
}
}
class TextSizer {
constructor(svg) {
this.svg = svg;
this.testers = this.svg.el('g').attrs({
// Firefox fails to measure non-displayed text
'display': firefox ? 'block' : 'none',
'visibility': 'hidden',
});
this.container = svg.body;
}
baseline({attrs}) {
return Number(attrs['font-size']);
}
measureHeight({attrs, formatted}) {
const size = this.baseline({attrs, formatted});
const lineHeight = size * (Number(attrs['line-height']) || 1);
return formatted.length * lineHeight;
}
prepMeasurement(attrs, formatted) {
const node = this.svg.el('text')
.attrs(attrs)
.attach(this.testers);
populateSvgTextLine(this.svg, node, formatted);
return node;
}
prepComplete() {
this.container.add(this.testers);
}
performMeasurement(node) {
return node.element.getComputedTextLength();
}
teardown() {
this.container.del(this.testers.empty());
}
}
class PatternedLine {
constructor(pattern = null, phase = 0) {
this.pattern = pattern;
this.dw = pattern && pattern.partWidth;
this.points = [];
this.phase = phase;
this.x = null;
this.y = null;
this.disconnect = 0;
}
_nextDelta() {
return this.pattern.getDelta(this.phase ++);
}
_link() {
if(this.disconnect === 2) {
this.points.push(this.x + ' ' + this.y);
this.disconnect = 0;
}
}
cap() {
if(this.disconnect > 0) {
this.points.push(this.x + ' ' + this.y);
this.disconnect = 0;
}
return this;
}
move(x, y) {
this.cap();
this.x = x;
this.y = y;
this.disconnect = 2;
return this;
}
line(x, y, {patterned = true} = {}) {
if(this.pattern && patterned) {
const len = Math.sqrt(
(x - this.x) * (x - this.x) +
(y - this.y) * (y - this.y)
);
const dx1 = (x - this.x) / len;
const dy1 = (y - this.y) / len;
const dx2 = -dy1;
const dy2 = dx1;
for(let pos = 0; pos + this.dw <= len; pos += this.dw) {
const delta = this._nextDelta();
this.points.push(
(this.x + pos * dx1 + delta * dx2) + ' ' +
(this.y + pos * dy1 + delta * dy2)
);
}
this.disconnect = 1;
} else {
this._link();
this.disconnect = 2;
}
this.x = x;
this.y = y;
return this;
}
arc(cx, cy, theta) {
const radius = Math.sqrt(
(cx - this.x) * (cx - this.x) +
(cy - this.y) * (cy - this.y)
);
const theta1 = Math.atan2(this.x - cx, cy - this.y);
const nextX = cx + Math.sin(theta1 + theta) * radius;
const nextY = cy - Math.cos(theta1 + theta) * radius;
if(this.pattern) {
const dir = (theta < 0 ? 1 : -1);
const dt = this.dw / radius;
for(let t = theta1; t + dt <= theta1 + theta; t += dt) {
const delta = this._nextDelta() * dir;
this.points.push(
(cx + Math.sin(t) * (radius + delta)) + ' ' +
(cy - Math.cos(t) * (radius + delta))
);
}
this.disconnect = 1;
} else {
this.points.push(
this.x + ' ' + this.y +
'A' + radius + ' ' + radius + ' 0 ' +
((Math.abs(theta) >= Math.PI) ? '1 ' : '0 ') +
((theta < 0) ? '0 ' : '1 ') +
nextX + ' ' + nextY
);
this.disconnect = 0;
}
this.x = nextX;
this.y = nextY;
return this;
}
asPath() {
this._link();
return 'M' + this.points.join('L');
}
}
const NS = 'http://www.w3.org/2000/svg';
function calculateAnchor(x, attrs, padding) {
let shift = 0;
let anchorX = x;
switch(attrs['text-anchor']) {
case 'middle':
shift = 0.5;
anchorX += (padding.left - padding.right) / 2;
break;
case 'end':
shift = 1;
anchorX -= padding.right;
break;
default:
shift = 0;
anchorX += padding.left;
break;
}
return {anchorX, shift};
}
const defaultTextSizerFactory = (svg) => new TextSizer(svg);
class TextSizerWrapper {
constructor(sizer) {
this.sizer = sizer;
this.cache = new Map();
this.active = null;
}
_expectMeasure({attrs, formatted}) {
if(!formatted.length) {
return null;
}
const attrKey = JSON.stringify(attrs);
let attrCache = this.cache.get(attrKey);
if(!attrCache) {
attrCache = {
attrs,
lines: new Map(),
};
this.cache.set(attrKey, attrCache);
}
formatted.forEach((line) => {
if(!line.length) {
return;
}
const labelKey = JSON.stringify(line);
if(!attrCache.lines.has(labelKey)) {
attrCache.lines.set(labelKey, {
formatted: line,
width: null,
});
}
});
return attrCache;
}
_measureLine(attrCache, line) {
if(!line.length) {
return 0;
}
const labelKey = JSON.stringify(line);
const cache = attrCache.lines.get(labelKey);
if(cache.width === null) {
throw new Error('Unexpected measurement of ' + line);
}
return cache.width;
}
_measureWidth(opts) {
if(!opts.formatted.length) {
return 0;
}
const attrCache = this._expectMeasure(opts);
return (opts.formatted
.map((line) => this._measureLine(attrCache, line))
.reduce((a, b) => Math.max(a, b), 0)
);
}
_getMeasurementOpts(attrs, formatted) {
const result = {attrs, formatted};
if(!formatted) {
if(attrs.textBlock) {
result.attrs = attrs.textBlock.state.attrs;
result.formatted = attrs.textBlock.state.formatted;
} else if(attrs.state) {
result.attrs = attrs.state.attrs;
result.formatted = attrs.state.formatted;
}
result.formatted = result.formatted || [];
}
if(!Array.isArray(result.formatted)) {
throw new Error('Invalid formatted text: ' + result.formatted);
}
return result;
}
expectMeasure(attrs, formatted) {
const opts = this._getMeasurementOpts(attrs, formatted);
this._expectMeasure(opts);
}
performMeasurementsPre() {
this.active = [];
this.cache.forEach(({attrs, lines}) => {
lines.forEach((cacheLine) => {
if(cacheLine.width === null) {
this.active.push({
cacheLine,
data: this.sizer.prepMeasurement(
attrs,
cacheLine.formatted
),
});
}
});
});
if(this.active.length) {
this.sizer.prepComplete();
}
}
performMeasurementsAct() {
this.active.forEach(({data, cacheLine}) => {
cacheLine.width = this.sizer.performMeasurement(data);
});
}
performMeasurementsPost() {
if(this.active.length) {
this.sizer.teardown();
}
this.active = null;
}
performMeasurements() {
/*
* Batch as many measurements as possible into a single DOM
* change, since getComputedTextLength forces a reflow.
*/
try {
this.performMeasurementsPre();
this.performMeasurementsAct();
} finally {
this.performMeasurementsPost();
}
}
measure(attrs, formatted) {
const opts = this._getMeasurementOpts(attrs, formatted);
return {
height: this.sizer.measureHeight(opts),
width: this._measureWidth(opts),
};
}
baseline(attrs, formatted) {
const opts = this._getMeasurementOpts(attrs, formatted);
return this.sizer.baseline(opts);
}
measureHeight(attrs, formatted) {
const opts = this._getMeasurementOpts(attrs, formatted);
return this.sizer.measureHeight(opts);
}
resetCache() {
this.cache.clear();
}
}
class SVG {
constructor(domWrapper, textSizerFactory = null) {
this.dom = domWrapper;
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
const fn = (textSizerFactory || defaultTextSizerFactory);
this.textFilters = new Map();
this.textSizer = new TextSizerWrapper(fn(this));
this.txt = this.txt.bind(this);
this.el = this.el.bind(this);
}
resetTextFilters() {
this.textFilters.clear();
}
registerTextFilter(name, id) {
this.textFilters.set(name, {id, used: false});
}
getTextFilter(name) {
const filter = this.textFilters.get(name);
if(!filter) {
return 'none';
}
filter.used = true;
return 'url(#' + filter.id + ')';
}
getUsedTextFilterNames() {
const used = [];
for(const [name, filter] of this.textFilters) {
if(filter.used) {
used.push(name);
}
}
return used;
}
linearGradient(attrs, stops) {
return this.el('linearGradient')
.attrs(attrs)
.add(stops.map((stop) => this.el('stop').attrs(stop)));
}
patternedLine(pattern = null, phase = 0) {
return new PatternedLine(pattern, phase);
}
txt(content) {
return this.dom.txt(content);
}
el(tag, namespace = NS) {
return this.dom.el(tag, namespace);
}
box(attrs, {height, width, x, y}) {
return this.el('rect').attrs(attrs).attrs({height, width, x, y});
}
boxFactory(attrs) {
return this.box.bind(this, attrs);
}
line(attrs, {x1, x2, y1, y2}) {
return this.el('line').attrs(attrs).attrs({x1, x2, y1, y2});
}
lineFactory(attrs) {
return this.line.bind(this, attrs);
}
circle(attrs, {x, y, radius}) {
return this.el('circle')
.attrs({
'cx': x,
'cy': y,
'r': radius,
})
.attrs(attrs);
}
circleFactory(attrs) {
return this.circle.bind(this, attrs);
}
cross(attrs, {x, y, radius}) {
return this.el('path')
.attr('d', (
'M' + (x - radius) + ' ' + (y - radius) +
'l' + (radius * 2) + ' ' + (radius * 2) +
'm0 ' + (-radius * 2) +
'l' + (-radius * 2) + ' ' + (radius * 2)
))
.attrs(attrs);
}
crossFactory(attrs) {
return this.cross.bind(this, attrs);
}
note(attrs, flickAttrs, {height, width, x, y}) {
const x0 = x;
const x1 = x + width;
const y0 = y;
const y1 = y + height;
const flick = 7;
return this.el('g').add(
this.el('polygon')
.attr('points', (
x0 + ' ' + y0 + ' ' +
(x1 - flick) + ' ' + y0 + ' ' +
x1 + ' ' + (y0 + flick) + ' ' +
x1 + ' ' + y1 + ' ' +
x0 + ' ' + y1
))
.attrs(attrs),
this.el('polyline')
.attr('points', (
(x1 - flick) + ' ' + y0 + ' ' +
(x1 - flick) + ' ' + (y0 + flick) + ' ' +
x1 + ' ' + (y0 + flick)
))
.attrs(flickAttrs)
);
}
noteFactory(attrs, flickAttrs) {
return this.note.bind(this, attrs, flickAttrs);
}
formattedText(attrs = {}, formatted = [], {x, y} = {}) {
const container = this.el('g');
const txt = new SVGTextBlock(container, this, {
attrs,
formatted,
x,
y,
});
return Object.assign(container, {
set: (state) => txt.set(state),
textBlock: txt,
});
}
formattedTextFactory(attrs) {
return this.formattedText.bind(this, attrs);
}
boxedText({
padding,
labelAttrs,
boxAttrs = {},
boxRenderer = null,
}, formatted, {x, y}) {
if(!formatted || !formatted.length) {
return Object.assign(this.el('g'), {
box: null,
height: 0,
label: null,
width: 0,
});
}
const {shift, anchorX} = calculateAnchor(x, labelAttrs, padding);
const label = this.formattedText(labelAttrs, formatted, {
x: anchorX,
y: y + padding.top,
});
const size = this.textSizer.measure(label);
const width = (size.width + padding.left + padding.right);
const height = (size.height + padding.top + padding.bottom);
const boxFn = boxRenderer || this.boxFactory(boxAttrs);
const box = boxFn({
height,
width,
x: anchorX - size.width * shift - padding.left,
y,
});
return Object.assign(this.el('g').add(box, label), {
box,
height,
label,
width,
});
}
boxedTextFactory(options) {
return this.boxedText.bind(this, options);
}
}
function findExtremes$1(agentInfos, agentIDs) {
let min = null;
let max = null;
agentIDs.forEach((id) => {
const info = agentInfos.get(id);
if(min === null || info.index < min.index) {
min = info;
}
if(max === null || info.index > max.index) {
max = info;
}
});
return {
left: min.id,
right: max.id,
};
}
function makeThemes(themes) {
if(themes.length === 0) {
throw new Error('Cannot render without a theme');
}
const themeMap = new Map();
themes.forEach((theme) => {
themeMap.set(theme.name, theme);
});
themeMap.set('', themes[0]);
return themeMap;
}
let globalNamespace = 0;
function parseNamespace(namespace) {
if(namespace === null) {
return 'R' + (globalNamespace ++);
}
return namespace;
}
class Renderer extends EventObject {
constructor({
themes = [],
namespace = null,
components = null,
document,
textSizerFactory = null,
} = {}) {
super();
this._bindMethods();
this.state = {};
this.width = 0;
this.height = 0;
this.themes = makeThemes(themes);
this.themeBuilder = null;
this.theme = null;
this.namespace = parseNamespace(namespace);
this.components = components || getComponents();
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
this.knownThemeDefs = new Set();
this.knownTextFilterDefs = new Map();
this.knownDefs = new Set();
this.highlights = new Map();
this.collapsed = new Set();
this.currentHighlight = -1;
this.buildStaticElements();
this.components.forEach((component) => {
component.makeState(this.state);
});
}
_bindMethods() {
this.separationStage = this.separationStage.bind(this);
this.prepareMeasurementsStage =
this.prepareMeasurementsStage.bind(this);
this.renderStage = this.renderStage.bind(this);
this.addThemeDef = this.addThemeDef.bind(this);
this.addThemeTextDef = this.addThemeTextDef.bind(this);
this.addDef = this.addDef.bind(this);
}
addTheme(theme) {
this.themes.set(theme.name, theme);
}
buildStaticElements() {
const {el} = this.svg;
this.metaCode = this.svg.txt();
this.themeDefs = el('defs');
this.defs = el('defs');
this.fullMask = el('mask').attrs({
'id': this.namespace + 'FullMask',
'maskUnits': 'userSpaceOnUse',
});
this.lineMask = el('mask').attrs({
'id': this.namespace + 'LineMask',
'maskUnits': 'userSpaceOnUse',
});
this.fullMaskReveal = el('rect').attr('fill', '#FFFFFF');
this.lineMaskReveal = el('rect').attr('fill', '#FFFFFF');
this.backgroundFills = el('g');
this.agentLines = el('g')
.attr('mask', 'url(#' + this.namespace + 'LineMask)');
this.blocks = el('g');
this.shapes = el('g');
this.unmaskedShapes = el('g');
this.title = this.svg.formattedText();
this.svg.body.add(
this.svg.el('metadata')
.add(this.metaCode),
this.themeDefs,
this.defs,
this.backgroundFills,
this.title,
this.unmaskedShapes,
el('g')
.attr('mask', 'url(#' + this.namespace + 'FullMask)')
.add(
this.agentLines,
this.blocks,
this.shapes
)
);
}
addThemeDef(name, generator) {
const namespacedName = this.namespace + name;
if(!this.knownThemeDefs.has(name)) {
this.knownThemeDefs.add(name);
this.themeDefs.add(generator().attr('id', namespacedName));
}
return namespacedName;
}
addThemeTextDef(name, generator) {
const namespacedName = this.namespace + name;
if(!this.knownTextFilterDefs.has(name)) {
const def = generator().attr('id', namespacedName);
this.knownTextFilterDefs.set(name, def);
}
this.svg.registerTextFilter(name, namespacedName);
}
addDef(name, generator) {
let nm = name;
let gen = generator;
if(typeof generator !== 'function') {
nm = 'P' + this.knownDefs.size;
gen = () => name;
}
const namespacedName = this.namespace + nm;
if(!this.knownDefs.has(nm)) {
this.knownDefs.add(nm);
this.defs.add(gen().attr('id', namespacedName));
}
return namespacedName;
}
addSeparation(agentID1, agentID2, dist) {
const info1 = this.agentInfos.get(agentID1);
const info2 = this.agentInfos.get(agentID2);
const d1 = info1.separations.get(agentID2) || 0;
info1.separations.set(agentID2, Math.max(d1, dist));
const d2 = info2.separations.get(agentID1) || 0;
info2.separations.set(agentID1, Math.max(d2, dist));
}
checkHidden(stage) {
const component = this.components.get(stage.type);
const env = {
agentInfos: this.agentInfos,
components: this.components,
renderer: this,
state: this.state,
textSizer: this.svg.textSizer,
theme: this.theme,
};
const hide = component.shouldHide(stage, env) || {};
const wasHidden = (this.hideNest > 0);
this.hideNest += hide.nest || 0;
const isHidden = (this.hideNest > 0);
if(this.hideNest < 0) {
throw new Error('Unexpected nesting in ' + stage.type);
}
if(wasHidden === isHidden) {
return isHidden;
} else {
return Boolean(hide.self);
}
}
separationStage(stage) {
const agentSpaces = new Map();
const agentIDs = this.visibleAgentIDs.slice();
const seps = [];
const addSpacing = (agentID, {left, right}) => {
const current = agentSpaces.get(agentID);
current.left = Math.max(current.left, left);
current.right = Math.max(current.right, right);
};
const addSeparation = (agentID1, agentID2, dist) => {
seps.push({agentID1, agentID2, dist});
};
this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad;
agentSpaces.set(agentInfo.id, {left: rad, right: rad});
});
const env = {
addSeparation,
addSpacing,
agentInfos: this.agentInfos,
components: this.components,
momentaryAgentIDs: agentIDs,
renderer: this,
state: this.state,
textSizer: this.svg.textSizer,
theme: this.theme,
visibleAgentIDs: this.visibleAgentIDs,
};
const component = this.components.get(stage.type);
if(!component) {
throw new Error('Unknown component: ' + stage.type);
}
component.separationPre(stage, env);
component.separation(stage, env);
if(this.checkHidden(stage)) {
return;
}
mergeSets(agentIDs, this.visibleAgentIDs);
seps.forEach(({agentID1, agentID2, dist}) => {
this.addSeparation(agentID1, agentID2, dist);
});
agentIDs.forEach((agentIDR) => {
const infoR = this.agentInfos.get(agentIDR);
const sepR = agentSpaces.get(agentIDR);
infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right);
infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left);
agentIDs.forEach((agentIDL) => {
const infoL = this.agentInfos.get(agentIDL);
if(infoL.index >= infoR.index) {
return;
}
const sepL = agentSpaces.get(agentIDL);
this.addSeparation(
agentIDR,
agentIDL,
sepR.left + sepL.right + this.theme.agentMargin
);
});
});
}
prepareMeasurementsStage(stage) {
const env = {
agentInfos: this.agentInfos,
components: this.components,
renderer: this,
state: this.state,
textSizer: this.svg.textSizer,
theme: this.theme,
};
const component = this.components.get(stage.type);
if(!component) {
throw new Error('Unknown component: ' + stage.type);
}
component.prepareMeasurements(stage, env);
}
checkAgentRange(agentIDs, topY = 0) {
if(agentIDs.length === 0) {
return topY;
}
const {left, right} = findExtremes$1(this.agentInfos, agentIDs);
const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x;
let baseY = topY;
this.agentInfos.forEach((agentInfo) => {
if(agentInfo.x >= leftX && agentInfo.x <= rightX) {
baseY = Math.max(baseY, agentInfo.latestY);
}
});
return baseY;
}
markAgentRange(agentIDs, y) {
if(agentIDs.length === 0) {
return;
}
const {left, right} = findExtremes$1(this.agentInfos, agentIDs);
const leftX = this.agentInfos.get(left).x;
const rightX = this.agentInfos.get(right).x;
this.agentInfos.forEach((agentInfo) => {
if(agentInfo.x >= leftX && agentInfo.x <= rightX) {
agentInfo.latestY = y;
}
});
}
drawAgentLine(agentInfo, toY) {
if(
agentInfo.latestYStart !== null &&
toY > agentInfo.latestYStart
) {
this.agentLines.add(this.theme.renderAgentLine({
className: 'agent-' + agentInfo.index + '-line',
options: agentInfo.options,
width: agentInfo.currentRad * 2,
x: agentInfo.x,
y0: agentInfo.latestYStart,
y1: toY,
}));
}
}
addHighlightObject(line, o) {
let list = this.highlights.get(line);
if(!list) {
list = [];
this.highlights.set(line, list);
}
list.push(o);
}
forwardEvent(source, sourceEvent, forwardEvent, forwardArgs) {
source.on(
sourceEvent,
this.trigger.bind(this, forwardEvent, forwardArgs)
);
}
renderStage(stage) {
this.agentInfos.forEach((agentInfo) => {
const rad = agentInfo.currentRad;
agentInfo.currentMaxRad = rad;
});
const envPre = {
agentInfos: this.agentInfos,
components: this.components,
renderer: this,
state: this.state,
textSizer: this.svg.textSizer,
theme: this.theme,
};
const component = this.components.get(stage.type);
const result = component.renderPre(stage, envPre);
const {agentIDs, topShift, asynchronousY} =
cleanRenderPreResult(result, this.currentY);
const topY = this.checkAgentRange(agentIDs, asynchronousY);
const makeRegion = ({
stageOverride = null,
unmasked = false,
} = {}) => {
const o = this.svg.el('g').setClass('region');
const targetStage = (stageOverride || stage);
this.addHighlightObject(targetStage.ln, o);
this.forwardEvent(o, 'mouseenter', 'mouseover', [targetStage]);
this.forwardEvent(o, 'mouseleave', 'mouseout', [targetStage]);
this.forwardEvent(o, 'click', 'click', [targetStage]);
this.forwardEvent(o, 'dblclick', 'dblclick', [targetStage]);
return o.attach(unmasked ? this.unmaskedShapes : this.shapes);
};
const env = {
addDef: this.addDef,
agentInfos: this.agentInfos,
blockLayer: this.blocks,
components: this.components,
drawAgentLine: (agentID, toY, andStop = false) => {
const agentInfo = this.agentInfos.get(agentID);
this.drawAgentLine(agentInfo, toY);
agentInfo.latestYStart = andStop ? null : toY;
},
fillLayer: this.backgroundFills,
fullMaskLayer: this.fullMask,
lineMaskLayer: this.lineMask,
makeRegion,
primaryY: topY + topShift,
renderer: this,
state: this.state,
svg: this.svg,
textSizer: this.svg.textSizer,
theme: this.theme,
topY,
};
let bottomY = topY;
if(this.checkHidden(stage)) {
env.primaryY = topY;
component.renderHidden(stage, env);
} else {
bottomY = Math.max(bottomY, component.render(stage, env) || 0);
}
this.markAgentRange(agentIDs, bottomY);
this.currentY = bottomY;
}
positionAgents() {
// Map guarantees insertion-order iteration
const orderedInfos = [];
this.agentInfos.forEach((agentInfo) => {
let currentX = 0;
agentInfo.separations.forEach((dist, otherAgent) => {
const otherAgentInfo = this.agentInfos.get(otherAgent);
if(otherAgentInfo.index < agentInfo.index) {
currentX = Math.max(currentX, otherAgentInfo.x + dist);
}
});
agentInfo.x = currentX;
orderedInfos.push(agentInfo);
});
let previousInfo = {x: 0};
orderedInfos.reverse().forEach((agentInfo) => {
let currentX = previousInfo.x;
previousInfo = agentInfo;
if(!agentInfo.anchorRight) {
return;
}
agentInfo.separations.forEach((dist, otherAgent) => {
const otherAgentInfo = this.agentInfos.get(otherAgent);
if(otherAgentInfo.index > agentInfo.index) {
currentX = Math.min(currentX, otherAgentInfo.x - dist);
}
});
agentInfo.x = currentX;
});
this.agentInfos.forEach(({x, maxRPad, maxLPad}) => {
this.minX = Math.min(this.minX, x - maxLPad);
this.maxX = Math.max(this.maxX, x + maxRPad);
});
}
buildAgentInfos(agents) {
this.agentInfos = new Map();
agents.forEach((agent, index) => {
this.agentInfos.set(agent.id, {
anchorRight: agent.anchorRight,
currentMaxRad: 0,
currentRad: 0,
formattedLabel: agent.formattedLabel,
id: agent.id,
index,
isVirtualSource: agent.isVirtualSource,
latestY: 0,
latestYStart: null,
maxLPad: 0,
maxRPad: 0,
options: agent.options,
separations: new Map(),
x: null,
});
});
}
updateBounds(stagesHeight) {
const cx = (this.minX + this.maxX) / 2;
const titleSize = this.svg.textSizer.measure(this.title);
const titleY = ((titleSize.height > 0) ?
(-this.theme.titleMargin - titleSize.height) : 0
);
this.title.set({x: cx, y: titleY});
const halfTitleWidth = titleSize.width / 2;
const margin = this.theme.outerMargin;
const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
const y0 = titleY - margin;
const y1 = stagesHeight + margin;
this.width = x1 - x0;
this.height = y1 - y0;
const fullSize = {
'height': this.height,
'width': this.width,
'x': x0,
'y': y0,
};
this.fullMaskReveal.attrs(fullSize);
this.lineMaskReveal.attrs(fullSize);
this.svg.body.attr('viewBox', (
x0 + ' ' + y0 + ' ' +
this.width + ' ' + this.height
));
}
_resetState() {
this.components.forEach((component) => {
component.resetState(this.state);
});
this.currentY = 0;
this.hideNest = 0;
}
_reset(theme) {
if(theme) {
this.knownThemeDefs.clear();
this.knownTextFilterDefs.clear();
this.themeDefs.empty();
}
this.knownDefs.clear();
this.highlights.clear();
this.defs.empty();
this.fullMask.empty();
this.lineMask.empty();
this.backgroundFills.empty();
this.agentLines.empty();
this.blocks.empty();
this.shapes.empty();
this.unmaskedShapes.empty();
this.defs.add(
this.fullMask.add(this.fullMaskReveal),
this.lineMask.add(this.lineMaskReveal)
);
this._resetState();
}
setHighlight(line = null) {
const ln = (line === null) ? -1 : line;
if(this.currentHighlight === ln) {
return;
}
if(this.highlights.has(this.currentHighlight)) {
this.highlights.get(this.currentHighlight).forEach((o) => {
o.delClass('focus');
});
}
if(this.highlights.has(ln)) {
this.highlights.get(ln).forEach((o) => {
o.addClass('focus');
});
}
this.currentHighlight = ln;
}
isCollapsed(line) {
return this.collapsed.has(line);
}
setCollapseAll(collapsed) {
if(collapsed) {
throw new Error('Cannot collapse all');
} else {
if(this.collapsed.size === 0) {
return false;
}
this.collapsed.clear();
}
return true;
}
_setCollapsed(line, collapsed) {
if(typeof line !== 'number') {
return false;
}
if(collapsed === this.isCollapsed(line)) {
return false;
}
if(collapsed) {
this.collapsed.add(line);
} else {
this.collapsed.delete(line);
}
return true;
}
setCollapsed(line, collapsed = true) {
if(line === null) {
return this.setCollapseAll(collapsed);
}
if(Array.isArray(line)) {
return line
.map((ln) => this._setCollapsed(ln, collapsed))
.some((changed) => changed);
}
return this._setCollapsed(line, collapsed);
}
_switchTheme(name) {
const oldThemeBuilder = this.themeBuilder;
this.themeBuilder = this.getThemeNamed(name);
if(this.themeBuilder !== oldThemeBuilder) {
this.theme = this.themeBuilder.build(this.svg);
}
this.theme.reset();
return (this.themeBuilder !== oldThemeBuilder);
}
optimisedRenderPreReflow(sequence) {
const themeChanged = this._switchTheme(sequence.meta.theme);
this._reset(themeChanged);
this.metaCode.nodeValue = sequence.meta.code;
this.svg.resetTextFilters();
this.theme.addDefs(this.addThemeDef, this.addThemeTextDef);
for(const def of this.knownTextFilterDefs.values()) {
def.detach();
}
this.title.set({
attrs: Object.assign({
'class': 'title',
}, this.theme.getTitleAttrs()),
formatted: sequence.meta.title,
});
this.svg.textSizer.expectMeasure(this.title);
this.minX = 0;
this.maxX = 0;
this.buildAgentInfos(sequence.agents);
sequence.stages.forEach(this.prepareMeasurementsStage);
this._resetState();
this.svg.textSizer.performMeasurementsPre();
}
optimisedRenderReflow() {
this.svg.textSizer.performMeasurementsAct();
}
optimisedRenderPostReflow(sequence) {
this.visibleAgentIDs = ['[', ']'];
sequence.stages.forEach(this.separationStage);
this._resetState();
this.positionAgents();
sequence.stages.forEach(this.renderStage);
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
this.svg.getUsedTextFilterNames().forEach((name) => {
this.themeDefs.add(this.knownTextFilterDefs.get(name));
});
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
this.updateBounds(stagesHeight);
const prevHighlight = this.currentHighlight;
this.currentHighlight = -1;
this.setHighlight(prevHighlight);
this.svg.textSizer.performMeasurementsPost();
this.svg.textSizer.resetCache();
}
render(sequence) {
this.optimisedRenderPreReflow(sequence);
this.optimisedRenderReflow();
this.optimisedRenderPostReflow(sequence);
}
getThemeNames() {
return (Array.from(this.themes.keys())
.filter((name) => (name !== ''))
);
}
getThemes() {
return this.getThemeNames().map((name) => this.themes.get(name));
}
getThemeNamed(themeName) {
const theme = this.themes.get(themeName);
if(theme) {
return theme;
}
return this.themes.get('');
}
getAgentX(id) {
return this.agentInfos.get(id).x;
}
dom() {
return this.svg.body.element;
}
}
/*
* Handlee font, by Joe Prince
* Downloaded from Google Fonts and converted to Base64 for embedding in
* generated SVGs
* https://fonts.google.com/specimen/Handlee
* base64 -b64 \
* < *.woff2 \
* | sed -e "s/^/"$'\t'$'\t'$'\t'"'/" -e "s/$/' +/" \
* > handlee.woff2.b64
*/
/* License
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
*/
var Handlee = {
name: 'Handlee',
woff2: (
'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' +
'jjhu2qURx1x+3R5nnI2W/z0OAAA='
),
};
class Random {
// Xorshift+ 64-bit random generator https://en.wikipedia.org/wiki/Xorshift
constructor() {
this.s = new Uint32Array(4);
}
reset() {
// Arbitrary random seed with roughly balanced 1s / 0s
// (taken from running Math.random a few times)
this.s[0] = 0x177E9C74;
this.s[1] = 0xAE6FFDCE;
this.s[2] = 0x3CF4F32B;
this.s[3] = 0x46449F88;
}
nextFloat() {
/* eslint-disable no-bitwise */ // Bit-ops are part of the algorithm
/* eslint-disable prefer-destructuring */ // Clearer this way
const range = 0x100000000;
let x0 = this.s[0];
let x1 = this.s[1];
const y0 = this.s[2];
const y1 = this.s[3];
this.s[0] = y0;
this.s[1] = y1;
x0 ^= (x0 << 23) | (x1 >>> 9);
x1 ^= (x1 << 23);
this.s[2] = x0 ^ y0 ^ (x0 >>> 17) ^ (y0 >>> 26);
this.s[3] = (
x1 ^ y1 ^
((x0 << 15) | (x1 >>> 17)) ^
((y0 << 6) | (y1 >>> 26))
);
return (((this.s[3] + y1) >>> 0) % range) / range;
/* eslint-enable no-bitwise */
/* eslint-enable prefer-destructuring */
}
}
/* eslint-disable max-lines */
const FONT$3 = Handlee.name;
const FONT_FAMILY = FONT$3 + ',cursive';
const LINE_HEIGHT$3 = 1.5;
const MAX_CHAOS = 5;
const PENCIL = {
normal: {
'stroke': 'rgba(0,0,0,0.7)',
'stroke-width': 0.8,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
},
thick: {
'stroke': 'rgba(0,0,0,0.8)',
'stroke-width': 1.2,
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
},
};
const NOTE_ATTRS$3 = {
'font-family': FONT_FAMILY,
'font-size': 8,
'line-height': LINE_HEIGHT$3,
};
const DIVIDER_LABEL_ATTRS$3 = {
'font-family': FONT_FAMILY,
'font-size': 8,
'line-height': LINE_HEIGHT$3,
'text-anchor': 'middle',
};
const RIGHT = {};
const LEFT = {};
class SketchWavePattern {
constructor(width, handedness) {
this.deltas = [
+0.0,
-0.3,
-0.6,
-0.75,
-0.45,
+0.0,
+0.45,
+0.75,
+0.6,
+0.3,
];
if(handedness !== RIGHT) {
this.deltas.reverse();
}
this.partWidth = 6 / this.deltas.length;
}
getDelta(p) {
return this.deltas[p % this.deltas.length] + Math.sin(p * 0.03) * 0.5;
}
}
function clamp(v, low, high) {
return Math.max(low, Math.min(high, v));
}
class SketchTheme extends BaseTheme {
constructor(svg, handedness = RIGHT) {
super(svg, {
'font-family': FONT$3,
'font-size': 8,
'line-height': LINE_HEIGHT$3,
});
this.handedness = (handedness === RIGHT) ? 1 : -1;
this.random = new Random();
const wave = new SketchWavePattern(4, handedness);
const sharedBlockSection = {
padding: {
top: 3,
bottom: 2,
},
tag: {
padding: {
top: 2,
left: 3,
right: 5,
bottom: 0,
},
boxRenderer: this.renderTag.bind(this),
labelAttrs: {
'font-family': FONT_FAMILY,
'font-weight': 'bold',
'font-size': 9,
'line-height': LINE_HEIGHT$3,
'text-anchor': 'left',
},
},
label: {
minHeight: 6,
padding: {
top: 2,
left: 5,
right: 3,
bottom: 1,
},
labelAttrs: {
'font-family': FONT_FAMILY,
'font-size': 8,
'line-height': LINE_HEIGHT$3,
'text-anchor': 'left',
},
},
};
Object.assign(this, {
titleMargin: 10,
outerMargin: 5,
agentMargin: 10,
actionMargin: 10,
minActionMargin: 3,
agentLineHighlightRadius: 4,
agentCap: {
box: {
padding: {
top: 5,
left: 10,
right: 10,
bottom: 5,
},
arrowBottom: 5 + 12 * 1.3 / 2,
labelAttrs: {
'font-family': FONT_FAMILY,
'font-size': 12,
'line-height': LINE_HEIGHT$3,
'text-anchor': 'middle',
},
boxRenderer: this.renderBox.bind(this),
},
database: {
padding: {
top: 12,
left: 10,
right: 10,
bottom: 2,
},
arrowBottom: 5 + 12 * 1.3 / 2,
boxRenderer: this.renderDB.bind(this, Object.assign({
'fill': '#FFFFFF',
'db-z': 5,
}, PENCIL.normal)),
labelAttrs: {
'font-family': FONT$3,
'font-size': 12,
'line-height': LINE_HEIGHT$3,
'text-anchor': 'middle',
},
},
cross: {
size: 15,
render: this.renderCross.bind(this),
},
bar: {
height: 6,
render: this.renderBar.bind(this),
},
fade: {
width: Math.ceil(MAX_CHAOS * 2 + 2),
height: 6,
extend: Math.ceil(MAX_CHAOS * 0.3 + 1),
},
none: {
height: 10,
},
},
connect: {
loopbackRadius: 6,
arrow: {
'single': {
width: 5,
height: 6,
attrs: Object.assign({
'fill': 'rgba(0,0,0,0.9)',
}, PENCIL.normal),
render: this.renderArrowHead.bind(this),
},
'double': {
width: 4,
height: 8,
attrs: Object.assign({
'fill': 'none',
}, PENCIL.normal),
render: this.renderArrowHead.bind(this),
},
'cross': {
short: 5,
radius: 3,
render: this.renderCross.bind(this),
},
},
label: {
padding: 6,
margin: {top: 2, bottom: 1},
attrs: {
'font-family': FONT_FAMILY,
'font-size': 8,
'line-height': LINE_HEIGHT$3,
'text-anchor': 'middle',
},
loopbackAttrs: {
'font-family': FONT_FAMILY,
'font-size': 8,
'line-height': LINE_HEIGHT$3,
},
},
source: {
radius: 1,
render: svg.circleFactory({
'fill': '#000000',
'stroke': '#000000',
'stroke-width': 1,
}),
},
mask: {
padding: {
top: 0,
left: 3,
right: 3,
bottom: 1,
},
},
},
agentLineAttrs: {
'': Object.assign({
'fill': 'none',
}, PENCIL.normal),
'red': {
'stroke': 'rgba(200,40,0,0.8)',
},
},
blocks: {
'ref': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: this.renderRefBlock.bind(this),
section: sharedBlockSection,
},
'': {
margin: {
top: 0,
bottom: 0,
},
boxRenderer: this.renderBlock.bind(this),
collapsedBoxRenderer: this.renderMinBlock.bind(this),
section: sharedBlockSection,
sepRenderer: this.renderSeparator.bind(this),
},
},
notes: {
'text': {
margin: {top: 0, left: 6, right: 6, bottom: 0},
padding: {top: 2, left: 2, right: 2, bottom: 2},
overlap: {left: 10, right: 10},
boxRenderer: svg.boxFactory({
'fill': '#FFFFFF',
}),
labelAttrs: NOTE_ATTRS$3,
},
'note': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 5, left: 5, right: 10, bottom: 5},
overlap: {left: 10, right: 10},
boxRenderer: this.renderNote.bind(this),
labelAttrs: NOTE_ATTRS$3,
},
'state': {
margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 7, left: 7, right: 7, bottom: 7},
overlap: {left: 10, right: 10},
boxRenderer: this.renderState.bind(this),
labelAttrs: NOTE_ATTRS$3,
},
},
dividers: {
'': {
labelAttrs: DIVIDER_LABEL_ATTRS$3,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: () => ({}),
},
'line': {
labelAttrs: DIVIDER_LABEL_ATTRS$3,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 10,
margin: 0,
render: this.renderLineDivider.bind(this),
},
'delay': {
labelAttrs: DIVIDER_LABEL_ATTRS$3,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 0,
margin: 0,
render: this.renderDelayDivider.bind(this, {
dotSize: 1,
gapSize: 2,
}),
},
'tear': {
labelAttrs: DIVIDER_LABEL_ATTRS$3,
padding: {top: 2, left: 5, right: 5, bottom: 2},
extend: 10,
margin: 10,
render: this.renderTearDivider.bind(this, {
fadeBegin: 5,
fadeSize: 10,
pattern: wave,
lineAttrs: PENCIL.normal,
}),
},
},
});
this.addConnectLine('solid', {attrs: PENCIL.normal});
this.addConnectLine('dash', {attrs: {
'stroke-dasharray': '4, 2',
}});
this.addConnectLine('wave', {
attrs: {
'stroke-linejoin': 'round',
'stroke-linecap': 'round',
},
pattern: wave,
});
}
reset() {
this.random.reset();
}
addDefs(builder, textBuilder) {
builder('sketch_font', () => {
const style = this.svg.el('style', null);
// Font must be embedded for exporting as SVG / PNG
style.text(
'@font-face{' +
'font-family:' + Handlee.name + ';' +
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
'}'
);
return style;
});
super.addDefs(builder, textBuilder);
}
vary(range, centre = 0) {
if(!range) {
return centre;
}
// Cosine distribution [-pi/2 pi/2] scaled to [-range range]
const rand = this.random.nextFloat(); // [0 1)
return centre + Math.asin(rand * 2 - 1) * 2 * range / Math.PI;
}
lineNodes(p1, p2, {
var1 = 1,
var2 = 1,
varX = 1,
varY = 1,
move = true,
}) {
const length = Math.sqrt(
(p2.x - p1.x) * (p2.x - p1.x) +
(p2.y - p1.y) * (p2.y - p1.y)
);
const rough = Math.min(Math.sqrt(length) * 0.2, MAX_CHAOS);
const x1 = this.vary(var1 * varX * rough, p1.x);
const y1 = this.vary(var1 * varY * rough, p1.y);
const x2 = this.vary(var2 * varX * rough, p2.x);
const y2 = this.vary(var2 * varY * rough, p2.y);
// -1 = p1 higher, 1 = p2 higher
const upper = clamp((y1 - y2) / (Math.abs(x1 - x2) + 0.001), -1, 1);
const frac = upper / 6 + 0.5;
/*
* Line curve is to top / left (simulates right-handed drawing)
* or top / right (left-handed)
*/
const curveX = this.vary(0.5, 0.5) * rough;
const curveY = this.vary(0.5, 0.5) * rough;
const xc = x1 * (1 - frac) + x2 * frac - curveX * this.handedness;
const yc = y1 * (1 - frac) + y2 * frac - curveY;
const nodes = (
(move ? ('M' + x1 + ' ' + y1) : '') +
'C' + xc + ' ' + yc +
',' + x2 + ' ' + y2 +
',' + x2 + ' ' + y2
);
return {
nodes,
p1: {x: x1, y: y1},
p2: {x: x2, y: y2},
};
}
renderLine(p1, p2, lineOptions) {
const line = this.lineNodes(p1, p2, lineOptions);
return this.svg.el('path')
.attrs({
'd': line.nodes,
'fill': 'none',
'stroke-dasharray': lineOptions.dash ? '6, 5' : 'none',
})
.attrs(lineOptions.attrs || (
lineOptions.thick ? PENCIL.thick : PENCIL.normal
));
}
boxNodes({x, y, width, height}) {
const lT = this.lineNodes(
{x, y},
{x: x + width, y},
{}
);
const lB = this.lineNodes(
{x: x + width, y: y + height},
{x, y: y + height},
{move: false}
);
const lR = this.lineNodes(
lT.p2,
lB.p1,
{var1: 0, var2: 0, move: false}
);
const lL = this.lineNodes(
lB.p2,
lT.p1,
{var1: 0, var2: 0.3, move: false}
);
return lT.nodes + lR.nodes + lB.nodes + lL.nodes;
}
renderBox(position, {fill = null, thick = false, attrs = null} = {}) {
return this.svg.el('path')
.attrs({
'd': this.boxNodes(position),
'fill': fill || '#FFFFFF',
})
.attrs(attrs || (thick ? PENCIL.thick : PENCIL.normal));
}
renderNote({x, y, width, height}) {
const flickSize = 5;
const lT = this.lineNodes(
{x, y},
{x: x + width - flickSize, y},
{}
);
const lF = this.lineNodes(
lT.p2,
{x: x + width, y: y + flickSize},
{move: false, var1: 0}
);
const lB = this.lineNodes(
{x: x + width, y: y + height},
{x, y: y + height},
{move: false}
);
const lR = this.lineNodes(
lF.p2,
lB.p1,
{var1: 0, var2: 0, move: false}
);
const lL = this.lineNodes(
lB.p2,
lT.p1,
{var1: 0, var2: 0.3, move: false}
);
const lF1 = this.lineNodes(
lF.p1,
{x: x + width - flickSize, y: y + flickSize},
{var1: 0.3}
);
const lF2 = this.lineNodes(
lF1.p2,
lF.p2,
{var1: 0, move: false}
);
return this.svg.el('g').add(
this.svg.el('path')
.attrs({
'd': (
lT.nodes +
lF.nodes +
lR.nodes +
lB.nodes +
lL.nodes
),
'fill': '#FFFFFF',
})
.attrs(PENCIL.normal),
this.svg.el('path')
.attrs({
'd': lF1.nodes + lF2.nodes,
'fill': 'none',
})
.attrs(PENCIL.normal)
);
}
renderLineDivider({x, y, labelWidth, width, height}) {
let shape = null;
const yPos = y + height / 2;
if(labelWidth > 0) {
shape = this.svg.el('g').add(
this.renderLine(
{x, y: yPos},
{x: x + (width - labelWidth) / 2, y: yPos},
{}
),
this.renderLine(
{x: x + (width + labelWidth) / 2, y: yPos},
{x: x + width, y: yPos},
{}
)
);
} else {
shape = this.renderLine(
{x, y: yPos},
{x: x + width, y: yPos},
{}
);
}
return {shape};
}
renderFlatConnect(pattern, attrs, {x1, y1, x2, y2}) {
if(pattern) {
const x1v = x1 + this.vary(0.3);
const x2v = x2 + this.vary(0.3);
const y1v = y1 + this.vary(1);
const y2v = y2 + this.vary(1);
return {
shape: this.svg.el('path')
.attr('d', this.svg.patternedLine(pattern)
.move(x1v, y1v)
.line(x2v, y2v)
.cap()
.asPath())
.attrs(attrs),
p1: {x: x1v, y: y1v},
p2: {x: x2v, y: y2v},
};
} else {
const ln = this.lineNodes(
{x: x1, y: y1},
{x: x2, y: y2},
{varX: 0.3}
);
return {
shape: this.svg.el('path').attr('d', ln.nodes).attrs(attrs),
p1: ln.p1,
p2: ln.p2,
};
}
}
renderRevConnect(pattern, attrs, {x1, y1, x2, y2, xR}) {
if(pattern) {
const x1v = x1 + this.vary(0.3);
const x2v = x2 + this.vary(0.3);
const y1v = y1 + this.vary(1);
const y2v = y2 + this.vary(1);
return {
shape: this.svg.el('path')
.attr('d', this.svg.patternedLine(pattern)
.move(x1v, y1v)
.line(xR, y1)
.arc(xR, (y1 + y2) / 2, Math.PI)
.line(x2v, y2v)
.cap()
.asPath())
.attrs(attrs),
p1: {x: x1v, y: y1v},
p2: {x: x2v, y: y2v},
};
} else {
const variance = Math.min((xR - x1) * 0.06, 3);
const overshoot = Math.min((xR - x1) * 0.5, 6);
const p1x = x1 + this.vary(variance, -1);
const p1y = y1 + this.vary(variance, -1);
const b1x = xR - overshoot * this.vary(0.2, 1);
const b1y = y1 - this.vary(1, 2);
const p2x = xR;
const p2y = y1 + this.vary(1, 1);
const b2x = xR;
const b2y = y2 + this.vary(2);
const p3x = x2 + this.vary(variance, -1);
const p3y = y2 + this.vary(variance, -1);
return {
shape: this.svg.el('path')
.attr('d', (
'M' + p1x + ' ' + p1y +
'C' + p1x + ' ' + p1y +
',' + b1x + ' ' + b1y +
',' + p2x + ' ' + p2y +
'S' + b2x + ' ' + b2y +
',' + p3x + ' ' + p3y
))
.attrs(attrs),
p1: {x: p1x, y: p1y},
p2: {x: p3x, y: p3y},
};
}
}
renderArrowHead(attrs, {x, y, width, height, dir}) {
const w = width * this.vary(0.2, 1);
const h = height * this.vary(0.3, 1);
const wx = w * dir.dx;
const wy = w * dir.dy;
const hy = h * 0.5 * dir.dx;
const hx = -h * 0.5 * dir.dy;
const l1 = this.lineNodes(
{x: x + wx - hx, y: y + wy - hy},
{x, y},
{var1: 2.0, var2: 0.2}
);
const l2 = this.lineNodes(
l1.p2,
{x: x + wx + hx, y: y + wy + hy},
{var1: 0, var2: 2.0, move: false}
);
const l3 = (attrs.fill === 'none') ? {nodes: ''} : this.lineNodes(
l2.p2,
l1.p1,
{var1: 0, var2: 0, move: false}
);
return this.svg.el('path')
.attr('d', l1.nodes + l2.nodes + l3.nodes)
.attrs(attrs);
}
renderState({x, y, width, height}) {
const dx = Math.min(width * 0.06, 3);
const dy = Math.min(height * 0.06, 3);
const tlX = x + dx * this.vary(0.6, 1);
const tlY = y + dy * this.vary(0.6, 1);
const trX = x + width - dx * this.vary(0.6, 1);
const trY = y + dy * this.vary(0.6, 1);
const blX = x + dx * this.vary(0.6, 1);
const blY = y + height - dy * this.vary(0.6, 1);
const brX = x + width - dx * this.vary(0.6, 1);
const brY = y + height - dy * this.vary(0.6, 1);
const mX = x + width / 2;
const mY = y + height / 2;
const cx = dx * this.vary(0.2, 1.2);
const cy = dy * this.vary(0.2, 1.2);
const bentT = y - Math.min(width * 0.005, 1);
const bentB = y + height - Math.min(width * 0.01, 2);
const bentL = x - Math.min(height * 0.01, 2) * this.handedness;
const bentR = bentL + width;
return this.svg.el('path')
.attr('d', (
'M' + tlX + ' ' + tlY +
'C' + (tlX + cx) + ' ' + (tlY - cy) +
',' + (mX - width * this.vary(0.03, 0.3)) + ' ' + bentT +
',' + mX + ' ' + bentT +
'S' + (trX - cx) + ' ' + (trY - cy) +
',' + trX + ' ' + trY +
'S' + bentR + ' ' + (mY - height * this.vary(0.03, 0.3)) +
',' + bentR + ' ' + mY +
'S' + (brX + cx) + ' ' + (brY - cy) +
',' + brX + ' ' + brY +
'S' + (mX + width * this.vary(0.03, 0.3)) + ' ' + bentB +
',' + mX + ' ' + bentB +
'S' + (blX + cx) + ' ' + (blY + cy) +
',' + blX + ' ' + blY +
'S' + bentL + ' ' + (mY + height * this.vary(0.03, 0.3)) +
',' + bentL + ' ' + mY +
'S' + (tlX - cx) + ' ' + (tlY + cy) +
',' + tlX + ' ' + tlY +
'Z'
))
.attr('fill', '#FFFFFF')
.attrs(PENCIL.normal);
}
renderRefBlock(position) {
const nodes = this.boxNodes(position);
return {
shape: this.svg.el('path')
.attrs({'d': nodes, 'fill': 'none'})
.attrs(PENCIL.thick),
mask: this.svg.el('path')
.attrs({'d': nodes, 'fill': '#000000'}),
fill: this.svg.el('path')
.attrs({'d': nodes, 'fill': '#FFFFFF'}),
};
}
renderBlock(position) {
return this.renderBox(position, {fill: 'none', thick: true});
}
renderMinBlock(position) {
return this.renderRefBlock(position);
}
renderTag({x, y, width, height}) {
const x2 = x + width;
const y2 = y + height;
const l1 = this.lineNodes(
{x: x2 + 3, y},
{x: x2 - 2, y: y2},
{}
);
const l2 = this.lineNodes(
l1.p2,
{x, y: y2 + 1},
{var1: 0, move: false}
);
const line = l1.nodes + l2.nodes;
return this.svg.el('g').add(
this.svg.el('path')
.attrs({
'd': line + 'L' + x + ' ' + y,
'fill': '#FFFFFF',
}),
this.svg.el('path')
.attrs({
'd': line,
'fill': '#FFFFFF',
})
.attrs(PENCIL.normal)
);
}
renderSeparator({x1, y1, x2, y2}) {
return this.renderLine(
{x: x1, y: y1},
{x: x2, y: y2},
{thick: true, dash: true}
);
}
renderBar({x, y, width, height}) {
return this.renderBox({x, y, width, height}, {fill: '#000000'});
}
renderCross({x, y, radius}) {
const r1 = this.vary(0.2, 1) * radius;
const l1 = this.lineNodes(
{x: x - r1, y: y - r1},
{x: x + r1, y: y + r1},
{}
);
const r2 = this.vary(0.2, 1) * radius;
const l2 = this.lineNodes(
{x: x + r2, y: y - r2},
{x: x - r2, y: y + r2},
{}
);
return this.svg.el('path')
.attrs({
'd': l1.nodes + l2.nodes,
'fill': 'none',
})
.attrs(PENCIL.normal);
}
renderAgentLine({x, y0, y1, width, className, options}) {
const attrs = this.optionsAttributes(this.agentLineAttrs, options);
if(width > 0) {
return this.renderBox({
x: x - width / 2,
y: y0,
width,
height: y1 - y0,
}, {fill: 'none', attrs}).setClass(className);
} else {
return this.renderLine(
{x, y: y0},
{x, y: y1},
{varY: 0.3, attrs}
).setClass(className);
}
}
}
class Factory$3 {
constructor(handedness = RIGHT) {
const right = (handedness === RIGHT);
this.name = right ? 'sketch' : 'sketch left handed';
this.handedness = handedness;
}
build(svg) {
return new SketchTheme(svg, this.handedness);
}
}
Object.assign(Factory$3, {RIGHT, LEFT});
const TRIMMER = /^([ \t]*)(.*)$/;
const SQUASH = {
after: '.!+', // Cannot squash after * or - in all cases
end: /[ \t\r\n]$/,
start: /^[ \t\r\n:,]/,
};
const ONGOING_QUOTE = /^"(\\.|[^"])*$/;
const REQUIRED_QUOTED = /[\r\n:,"<>\-~]/;
const QUOTE_ESCAPE = /["\\]/g;
function completionsEqual(a, b) {
return (
(a.v === b.v) &&
(a.prefix === b.prefix) &&
(a.suffix === b.suffix) &&
(a.q === b.q)
);
}
function makeRangeFrom(cm, line, chFrom) {
const ln = cm.getLine(line);
const ranges = {
squash: {ch: chFrom, line},
word: {ch: chFrom, line},
};
if(chFrom > 0 && ln[chFrom - 1] === ' ') {
if(SQUASH.after.includes(ln[chFrom - 2])) {
ranges.word.ch --;
}
ranges.squash.ch --;
}
return ranges;
}
function makeRangeTo(cm, line, chTo) {
const ln = cm.getLine(line);
const ranges = {
squash: {ch: chTo, line},
word: {ch: chTo, line},
};
if(ln[chTo] === ' ') {
ranges.squash.ch ++;
}
return ranges;
}
function wrapQuote({v, q, prefix = '', suffix = ''}, quote) {
const quo = (quote || !REQUIRED_QUOTED.test(v)) ? quote : '"';
return (
prefix +
((quo && q) ? (quo + v.replace(QUOTE_ESCAPE, '\\$&') + quo) : v) +
suffix
);
}
function makeHintItem(entry, ranges, quote) {
const quoted = wrapQuote(entry, quote);
const from = entry.q ? ranges.fromVar : ranges.fromKey;
if(quoted === '\n') {
return {
className: 'pick-virtual',
displayFrom: null,
displayText: '<END>',
from: from.squash,
text: '\n',
to: ranges.to.squash,
};
} else {
return {
className: null,
displayFrom: from.word,
displayText: quoted.trim(),
from: SQUASH.start.test(quoted) ? from.squash : from.word,
text: quoted,
to: SQUASH.end.test(quoted) ? ranges.to.squash : ranges.to.word,
};
}
}
function getGlobals({global, prefix = '', suffix = ''}, globals) {
const identified = globals[global];
if(!identified) {
return [];
}
return identified.map((item) => ({prefix, q: true, suffix, v: item}));
}
function populateGlobals(completions, globals = {}) {
for(let i = 0; i < completions.length;) {
if(completions[i].global) {
const identified = getGlobals(completions[i], globals);
mergeSets(completions, identified, completionsEqual);
completions.splice(i, 1);
} else {
++ i;
}
}
}
function getTokensUpTo(cm, pos) {
const tokens = cm.getLineTokens(pos.line);
for(let p = 0; p < tokens.length; ++ p) {
if(tokens[p].end >= pos.ch) {
tokens.length = p + 1;
break;
}
}
return tokens;
}
function getVariablePartial(tokens, pos) {
let partial = '';
let start = 0;
let fin = 0;
tokens.forEach((token) => {
if(token.state.isVar) {
partial += token.string;
fin = token.end;
} else {
partial = '';
start = token.end;
}
});
if(fin > pos.ch) {
partial = partial.substr(0, pos.ch - start);
}
const parts = TRIMMER.exec(partial);
partial = parts[2];
let quote = '';
if(ONGOING_QUOTE.test(partial)) {
quote = partial.charAt(0);
partial = partial.substr(1);
}
return {
from: start + parts[1].length,
partial,
quote,
valid: fin >= start,
};
}
function getKeywordPartial(token, pos) {
let partial = token.string;
if(token.end > pos.ch) {
partial = partial.substr(0, pos.ch - token.start);
}
const parts = TRIMMER.exec(partial);
return {
from: token.start + parts[1].length,
partial: parts[2],
valid: true,
};
}
function suggestDropdownLocation(list, fromKey) {
let p = null;
list.forEach(({displayFrom}) => {
if(displayFrom) {
if(
!p ||
displayFrom.line > p.line ||
(displayFrom.line === p.line && displayFrom.ch > p.ch)
) {
p = displayFrom;
}
}
});
return p || fromKey.word;
}
function partialMatch(v, p) {
return p.valid && v.startsWith(p.partial);
}
function getCompletions(cur, token, globals) {
let completions = null;
if(cur.ch > 0 && token.state.line.length > 0) {
completions = token.state.completions.slice();
} else {
completions = token.state.beginCompletions
.concat(token.state.knownAgent);
}
populateGlobals(completions, globals);
return completions;
}
function getHints(cm, options) {
const cur = cm.getCursor();
const tokens = getTokensUpTo(cm, cur);
const token = last(tokens) || cm.getTokenAt(cur);
const pVar = getVariablePartial(tokens, cur);
const pKey = getKeywordPartial(token, cur);
const completions = getCompletions(cur, token, cm.options.globals);
const ranges = {
fromKey: makeRangeFrom(cm, cur.line, pKey.from),
fromVar: makeRangeFrom(cm, cur.line, pVar.from),
to: makeRangeTo(cm, cur.line, token.end),
};
let selfValid = null;
const list = (completions
.filter((o) => (
(o.q || !pVar.quote) &&
partialMatch(o.v, o.q ? pVar : pKey)
))
.map((o) => {
if(!options.completeSingle) {
if(o.v === (o.q ? pVar : pKey).partial) {
selfValid = o;
return null;
}
}
return makeHintItem(o, ranges, pVar.quote);
})
.filter((opt) => (opt !== null))
);
if(selfValid && list.length > 0) {
list.unshift(makeHintItem(selfValid, ranges, pVar.quote));
}
return {
from: suggestDropdownLocation(list, ranges.fromKey),
list,
to: ranges.to.word,
};
}
const themes = [
new Factory(),
new Factory$2(),
new Factory$1(),
new Factory$3(Factory$3.RIGHT),
new Factory$3(Factory$3.LEFT),
];
const SharedParser = new Parser();
const SharedGenerator = new Generator();
const CMMode = SharedParser.getCodeMirrorMode();
function registerCodeMirrorMode(CodeMirror, modeName = 'sequence') {
const cm = CodeMirror || window.CodeMirror;
cm.defineMode(modeName, () => CMMode);
cm.registerHelper('hint', modeName, getHints);
}
function addTheme(theme) {
themes.push(theme);
}
function extractCodeFromSVG(svg) {
const dom = new DOMParser().parseFromString(svg, 'image/svg+xml');
const meta = dom.querySelector('metadata');
if(!meta) {
return '';
}
return meta.textContent;
}
function renderAll(diagrams) {
const errors = [];
function storeError(sd, e) {
errors.push(e);
}
diagrams.forEach((diagram) => {
diagram.addEventListener('error', storeError);
diagram.optimisedRenderPreReflow();
});
diagrams.forEach((diagram) => {
diagram.optimisedRenderReflow();
});
diagrams.forEach((diagram) => {
diagram.optimisedRenderPostReflow();
diagram.removeEventListener('error', storeError);
});
if(errors.length > 0) {
throw errors;
}
}
function pickDocument(container) {
if(container) {
return container.ownerDocument || null;
} else if(typeof window === 'undefined') {
return null;
} else {
return window.document;
}
}
class SequenceDiagram extends EventObject {
/* eslint-disable complexity */ // Just some defaults
constructor(code = null, options = {}) {
/* eslint-enable complexity */
super();
let opts = null;
if(code && typeof code === 'object') {
opts = code;
this.code = opts.code;
} else {
opts = options;
this.code = code;
}
Object.assign(this, {
exporter: new Exporter(),
generator: SharedGenerator,
isInteractive: false,
latestProcessed: null,
parser: SharedParser,
registerCodeMirrorMode,
renderer: new Renderer(Object.assign({
document: pickDocument(opts.container),
themes,
}, opts)),
textSizerFactory: opts.textSizerFactory || null,
});
this.renderer.addEventForwarding(this);
if(opts.container) {
opts.container.appendChild(this.dom());
}
if(opts.interactive) {
this.addInteractivity();
}
if(typeof this.code === 'string' && opts.render !== false) {
this.render();
}
}
clone(options = {}) {
const reference = (options.container || this.renderer.dom());
return new SequenceDiagram(Object.assign({
code: this.code,
components: this.renderer.components,
container: null,
document: reference.ownerDocument,
interactive: this.isInteractive,
namespace: null,
textSizerFactory: this.textSizerFactory,
themes: this.renderer.getThemes(),
}, options));
}
set(code = '', {render = true} = {}) {
if(this.code === code) {
return;
}
this.code = code;
if(render) {
this.render();
}
}
process(code) {
const parsed = this.parser.parse(code);
return this.generator.generate(parsed);
}
addTheme(theme) {
this.renderer.addTheme(theme);
}
setHighlight(line) {
this.renderer.setHighlight(line);
}
isCollapsed(line) {
return this.renderer.isCollapsed(line);
}
setCollapsed(line, collapsed = true, {render = true} = {}) {
if(!this.renderer.setCollapsed(line, collapsed)) {
return false;
}
if(render && this.latestProcessed) {
this.render(this.latestProcessed);
}
return true;
}
collapse(line, options) {
return this.setCollapsed(line, true, options);
}
expand(line, options) {
return this.setCollapsed(line, false, options);
}
toggleCollapsed(line, options) {
return this.setCollapsed(line, !this.isCollapsed(line), options);
}
expandAll(options) {
return this.setCollapsed(null, false, options);
}
getThemeNames() {
return this.renderer.getThemeNames();
}
getThemes() {
return this.renderer.getThemes();
}
_updateSize({width = null, height = null, zoom = null}) {
if(width !== null) {
if(height === null) {
this.renderer.height *= width / this.renderer.width;
} else {
this.renderer.height = height;
}
this.renderer.width = width;
} else if(height !== null) {
this.renderer.width *= height / this.renderer.height;
this.renderer.height = height;
} else if(zoom !== null) {
this.renderer.width *= zoom;
this.renderer.height *= zoom;
}
}
getSVGCodeSynchronous({size = {}} = {}) {
this._updateSize(size);
return this.exporter.getSVGContent(this.renderer);
}
getSVGCode(options) {
return Promise.resolve(this.getSVGCodeSynchronous(options));
}
getSVGSynchronous({size = {}} = {}) {
this._updateSize(size);
return this.exporter.getSVGURL(this.renderer);
}
getSVG({size = {}} = {}) {
this._updateSize(size);
return Promise.resolve({
latest: true,
url: this.getSVGSynchronous(),
});
}
getCanvas({resolution = 1, size = {}} = {}) {
this._updateSize(size);
return new Promise((resolve) => {
this.exporter.getCanvas(this.renderer, resolution, resolve);
});
}
getPNG({resolution = 1, size = {}} = {}) {
this._updateSize(size);
return new Promise((resolve) => {
this.exporter.getPNGURL(
this.renderer,
resolution,
(url, latest) => {
resolve({latest, url});
}
);
});
}
getSize() {
return {
height: this.renderer.height,
width: this.renderer.width,
};
}
_revertParent(state) {
const dom = this.renderer.dom();
if(dom.parentNode !== state.originalParent) {
dom.parentNode.removeChild(dom);
if(state.originalParent) {
state.originalParent.appendChild(dom);
}
}
}
_sendRenderError(e) {
this._revertParent(this.renderState);
this.renderState.error = true;
this.trigger('error', [this, e]);
}
optimisedRenderPreReflow(processed = null) {
const dom = this.renderer.dom();
this.renderState = {
error: false,
originalParent: dom.parentNode,
processed,
};
const state = this.renderState;
if(!dom.isConnected) {
if(state.originalParent) {
state.originalParent.removeChild(dom);
}
dom.ownerDocument.body.appendChild(dom);
}
try {
if(!state.processed) {
state.processed = this.process(this.code);
}
this.renderer.optimisedRenderPreReflow(state.processed);
} catch(e) {
this._sendRenderError(e);
}
}
optimisedRenderReflow() {
try {
if(!this.renderState.error) {
this.renderer.optimisedRenderReflow();
}
} catch(e) {
this._sendRenderError(e);
}
}
optimisedRenderPostReflow() {
const state = this.renderState;
try {
if(!state.error) {
this.renderer.optimisedRenderPostReflow(state.processed);
}
} catch(e) {
this._sendRenderError(e);
}
this.renderState = null;
if(!state.error) {
this._revertParent(state);
this.latestProcessed = state.processed;
this.trigger('render', [this]);
}
}
render(processed = null) {
let latestError = null;
function storeError(sd, e) {
latestError = e;
}
this.addEventListener('error', storeError);
this.optimisedRenderPreReflow(processed);
this.optimisedRenderReflow();
this.optimisedRenderPostReflow();
this.removeEventListener('error', storeError);
if(latestError) {
throw latestError;
}
}
setContainer(node = null) {
const dom = this.dom();
if(dom.parentNode) {
dom.parentNode.removeChild(dom);
}
if(node) {
node.appendChild(dom);
}
}
addInteractivity() {
if(this.isInteractive) {
return;
}
this.isInteractive = true;
this.addEventListener('click', (element) => {
this.toggleCollapsed(element.ln);
});
}
extractCodeFromSVG(svg) {
return extractCodeFromSVG(svg);
}
renderAll(diagrams) {
return renderAll(diagrams);
}
dom() {
return this.renderer.dom();
}
}
function datasetBoolean(value) {
return typeof value !== 'undefined' && value !== 'false';
}
function parseTagOptions(element) {
return {
interactive: datasetBoolean(element.dataset.sdInteractive),
namespace: element.dataset.sdNamespace || null,
};
}
function convertOne(element, code = null, options = {}) {
if(element.tagName === 'svg') {
return null;
}
const tagOptions = parseTagOptions(element);
const diagram = new SequenceDiagram(
(code === null) ? element.textContent : code,
Object.assign(tagOptions, options)
);
const newElement = diagram.dom();
const attrs = element.attributes;
for(let i = 0; i < attrs.length; ++ i) {
newElement.setAttribute(
attrs[i].nodeName,
attrs[i].nodeValue
);
}
element.parentNode.replaceChild(newElement, element);
return diagram;
}
function convert(elements, code = null, options = {}) {
let c = null;
let opts = null;
if(code && typeof code === 'object') {
opts = code;
c = opts.code;
} else {
opts = options;
c = code;
}
if(Array.isArray(elements)) {
const nodrawOpts = Object.assign({}, opts, {render: false});
const diagrams = elements
.map((el) => convertOne(el, c, nodrawOpts))
.filter((diagram) => (diagram !== null));
if(opts.render !== false) {
renderAll(diagrams);
}
return diagrams;
} else {
return convertOne(elements, c, opts);
}
}
function convertAll(root = null, className = 'sequence-diagram') {
let r = null;
let cls = null;
if(typeof root === 'string') {
r = null;
cls = root;
} else {
r = root;
cls = className;
}
let elements = null;
if(r && typeof r.length !== 'undefined') {
elements = r;
} else {
elements = (r || window.document).getElementsByClassName(cls);
}
// Convert from "live" collection to static to avoid infinite loops:
const els = [];
for(let i = 0; i < elements.length; ++ i) {
els.push(elements[i]);
}
// Convert elements
convert(els);
}
function getDefaultThemeNames() {
return themes.map((theme) => theme.name);
}
Object.assign(SequenceDiagram, {
Exporter,
Generator,
Parser,
Renderer,
addTheme,
convert,
convertAll,
extractCodeFromSVG,
getDefaultThemeNames,
registerCodeMirrorMode,
renderAll,
themes,
});
function encodeChar(c) {
return '&#' + c.codePointAt(0).toString(10) + ';';
}
function escapeHTML(text) {
return text.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\r\n\t -%'-;=?-~]/g,
encodeChar
);
}
function escapeQuoted(text) {
return text.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\r\n\t !#$%'-;=?-~]/g,
encodeChar
);
}
class TextNode {
constructor(content) {
this.parentNode = null;
this.nodeValue = content;
}
contains() {
return false;
}
get textContent() {
return this.nodeValue;
}
set textContent(value) {
this.nodeValue = value;
}
get isConnected() {
if(this.parentNode !== null) {
return this.parentNode.isConnected;
}
return false;
}
get innerHTML() {
return escapeHTML(this.nodeValue);
}
get outerHTML() {
return this.innerHTML;
}
}
class ElementNode {
constructor(ownerDocument, tag, namespace) {
this.ownerDocument = ownerDocument;
this.tagName = tag;
this.namespaceURI = namespace;
this.parentNode = null;
this.childNodes = [];
this.attributes = new Map();
this.style = {};
this.listeners = new Map();
}
setAttribute(key, value) {
let v = null;
if(typeof value === 'number') {
v = value.toString(10);
} else if(typeof value === 'string') {
v = value;
} else {
throw new Error('Bad value ' + value + ' for attribute ' + key);
}
this.attributes.set(key, v);
}
getAttribute(key) {
return this.attributes.get(key);
}
addEventListener(event, fn) {
let list = this.listeners.get(event);
if(!list) {
list = [];
this.listeners.set(event, list);
}
list.push(fn);
}
removeEventListener(event, fn) {
const list = this.listeners.get(event) || [];
const index = list.indexOf(fn);
if(index !== -1) {
list.splice(index, 1);
}
}
dispatchEvent(e) {
const list = this.listeners.get(e.type) || [];
list.forEach((fn) => fn(e));
}
contains(descendant) {
let check = descendant;
while(check) {
if(check === this) {
return true;
}
check = check.parentNode;
}
return false;
}
getElementsByTagName(tag) {
const result = [];
this.traverseDescendants((o) => {
if(o.tagName === tag) {
result.push(o);
}
});
return result;
}
getElementsByClassName(className) {
const result = [];
const check = ' ' + className + ' ';
this.traverseDescendants((o) => {
const cls = ' ' + (o.getAttribute('class') || '') + ' ';
if(cls.indexOf(check) !== -1) {
result.push(o);
}
});
return result;
}
traverseDescendants(fn) {
if(fn(this) === false) {
return;
}
for(const child of this.childNodes) {
if(child.traverseDescendants) {
child.traverseDescendants(fn);
}
}
}
get firstChild() {
return this.childNodes[0] || null;
}
get lastChild() {
return this.childNodes[this.childNodes.length - 1] || null;
}
indexOf(child) {
const index = this.childNodes.indexOf(child);
if(index === -1) {
throw new Error(child + ' is not a child of ' + this);
}
return index;
}
insertBefore(child, existingChild) {
if(child.contains(this)) {
throw new Error('Cyclic node structures are not permitted');
}
if(child.parentNode !== null) {
child.parentNode.removeChild(child);
}
if(existingChild === null) {
this.childNodes.push(child);
} else {
this.childNodes.splice(this.indexOf(existingChild), 0, child);
}
child.parentNode = this;
return child;
}
appendChild(child) {
return this.insertBefore(child, null);
}
removeChild(child) {
this.childNodes.splice(this.indexOf(child), 1);
child.parentNode = null;
return child;
}
replaceChild(newChild, oldChild) {
if(newChild === oldChild) {
return oldChild;
}
this.insertBefore(newChild, oldChild);
return this.removeChild(oldChild);
}
get isConnected() {
return true;
}
get textContent() {
let text = '';
for(const child of this.childNodes) {
text += child.textContent;
}
return text;
}
set textContent(value) {
for(const child of this.childNodes) {
child.parentNode = null;
}
this.childNodes.length = 0;
this.appendChild(new TextNode(value));
}
get innerHTML() {
let html = '';
for(const child of this.childNodes) {
html += child.outerHTML;
}
return html;
}
get outerHTML() {
let attrs = '';
for(const [key, value] of this.attributes) {
attrs += ' ' + key + '="' + escapeQuoted(value) + '"';
}
return (
'<' + this.tagName + attrs + '>' +
this.innerHTML +
'</' + this.tagName + '>'
);
}
}
class VirtualDocument {
createElement(tag) {
return new ElementNode(this, tag, '');
}
createElementNS(ns, tag) {
return new ElementNode(this, tag, ns || '');
}
createTextNode(content) {
return new TextNode(content);
}
}
const fs = require('fs');
const opentype = require('opentype.js');
const path = require('path');
const FONTDIR = process.env.FONTDIR || './fonts/';
const FONTS = new Map();
function loadFont(relativePath) {
// Must be synchronous so that measurements are ready once startup completes
/* eslint-disable no-sync */
const data = fs.readFileSync(path.join(FONTDIR, relativePath));
/* eslint-enable no-sync */
return opentype.parse(data.buffer);
}
function addFont(name, variants) {
const types = new Map();
for(const v in variants) {
if(Object.prototype.hasOwnProperty.call(variants, v)) {
const font = loadFont(variants[v]);
font.id = name + '-' + v;
types.set(v, font);
}
}
FONTS.set(name.toLowerCase(), types);
}
addFont('sans-serif', {
'': 'liberation-fonts/LiberationSans-Regular.ttf',
'bold': 'liberation-fonts/LiberationSans-Bold.ttf',
'bold-italic': 'liberation-fonts/LiberationSans-BoldItalic.ttf',
'italic': 'liberation-fonts/LiberationSans-Italic.ttf',
});
addFont('monospace', {
'': 'liberation-fonts/LiberationMono-Regular.ttf',
'bold': 'liberation-fonts/LiberationMono-Bold.ttf',
'bold-italic': 'liberation-fonts/LiberationMono-BoldItalic.ttf',
'italic': 'liberation-fonts/LiberationMono-Italic.ttf',
});
addFont('handlee', {
'': 'handlee/Handlee.ttf',
});
const DEFAULT_FONT = 'sans-serif';
const OPENTYPE_OPTIONS = {hinting: true};
function getFont(attrs) {
const family = (attrs['font-family'] || DEFAULT_FONT).split(',');
for(const nm of family) {
const name = nm.trim().replace(/['"]/g, '').toLowerCase();
const font = FONTS.get(name);
if(font) {
return font;
}
}
return FONTS.get(DEFAULT_FONT);
}
function tryVariant(font, condition, name) {
if(!condition) {
return null;
}
return font.get(name) || null;
}
function getVariantRaw(font, {bold, italic}) {
return (
tryVariant(font, bold && italic, 'bold-italic') ||
tryVariant(font, bold, 'bold') ||
tryVariant(font, italic, 'italic') ||
font.get('')
);
}
function getVariant(font, attrs) {
const weight = attrs['font-weight'] || '';
const style = attrs['font-style'] || '';
const bold = (weight.includes('bold') || Number(weight) > 400);
const italic = style.includes('italic');
return getVariantRaw(font, {bold, italic});
}
function getFontSize(attrs) {
return Number(attrs['font-size']);
}
function measure(attrs, text) {
const font = getFont(attrs);
const variant = getVariant(font, attrs);
const size = getFontSize(attrs);
return variant.getAdvanceWidth(text, size, OPENTYPE_OPTIONS);
}
function combineAttrs$1(base, next) {
if(!next) {
return base;
}
const result = Object.assign({}, base, next);
const nextSize = next['font-size'];
if(typeof nextSize === 'string' && nextSize.endsWith('em')) {
const ratio = Number.parseFloat(
nextSize.substr(0, nextSize.length - 2)
);
result['font-size'] = base['font-size'] * ratio;
}
return result;
}
class VirtualTextSizer {
baseline({attrs}) {
return getFontSize(attrs);
}
measureHeight({attrs, formatted}) {
const size = this.baseline({attrs, formatted});
const lineHeight = size * (Number(attrs['line-height']) || 1);
return formatted.length * lineHeight;
}
prepMeasurement(attrs, formatted) {
return {attrs, formatted};
}
prepComplete() {
// No-op
}
performMeasurement({attrs, formatted}) {
let len = 0;
for(const part of formatted) {
if(!part.text) {
continue;
}
const combinedAttrs = combineAttrs$1(attrs, part.attrs);
len += measure(combinedAttrs, part.text);
}
return len;
}
teardown() {
// No-op
}
}
const virtualTextSizerFactory = () => new VirtualTextSizer();
class VirtualSequenceDiagram extends SequenceDiagram {
constructor(code = null, options = {}) {
const opts = {
document: new VirtualDocument(),
namespace: '',
textSizerFactory: virtualTextSizerFactory,
};
if(code && typeof code === 'object') {
Object.assign(opts, code);
} else {
Object.assign(opts, options, {code});
}
Object.assign(opts, {
container: null,
interactive: false,
});
super(opts);
}
}
function render(code, options = {}) {
return new VirtualSequenceDiagram(code, options)
.getSVGCodeSynchronous({size: options.size});
}
Object.assign(VirtualSequenceDiagram, {
Exporter: SequenceDiagram.Exporter,
Generator: SequenceDiagram.Generator,
Parser: SequenceDiagram.Parser,
Renderer: SequenceDiagram.Renderer,
addTheme: SequenceDiagram.addTheme,
getDefaultThemeNames: SequenceDiagram.getDefaultThemeNames,
render,
themes: SequenceDiagram.themes,
});
const out = {
SequenceDiagram,
VirtualDocument,
VirtualSequenceDiagram,
virtualTextSizerFactory,
};
if(typeof exports !== 'undefined') {
Object.assign(exports, out);
} else if(window.define && window.define.amd) {
Object.assign(SequenceDiagram, out);
window.define(() => SequenceDiagram);
} else {
window.document.addEventListener('DOMContentLoaded', () => {
SequenceDiagram.convertAll();
}, {once: true});
if(window.CodeMirror) {
SequenceDiagram.registerCodeMirrorMode(window.CodeMirror);
}
Object.assign(window, out);
}
}());