(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('graphre')) : typeof define === 'function' && define.amd ? define(['exports', 'graphre'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.nomnoml = {}, global.graphre)); })(this, (function (exports, graphre) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function range([min, max], count) { var output = []; for (var i = 0; i < count; i++) output.push(min + ((max - min) * i) / (count - 1)); return output; } function sum(list, transform) { for (var i = 0, sum = 0, len = list.length; i < len; i++) sum += transform(list[i]); return sum; } function find(list, predicate) { for (var i = 0; i < list.length; i++) if (predicate(list[i])) return list[i]; return undefined; } function last(list) { return list[list.length - 1]; } function hasSubstring(haystack, needle) { if (needle === '') return true; if (!haystack) return false; return haystack.indexOf(needle) !== -1; } function indexBy(list, key) { var obj = {}; for (var i = 0; i < list.length; i++) obj[list[i][key]] = list[i]; return obj; } function uniqueBy(list, property) { var seen = {}; var out = []; for (var i = 0; i < list.length; i++) { var key = list[i][property]; if (!seen[key]) { seen[key] = true; out.push(list[i]); } } return out; } var util = /*#__PURE__*/Object.freeze({ __proto__: null, range: range, sum: sum, find: find, last: last, hasSubstring: hasSubstring, indexBy: indexBy, uniqueBy: uniqueBy }); function buildStyle(conf, title, body = {}) { return { title: { bold: title.bold || false, underline: title.underline || false, italic: title.italic || false, center: title.center || false, }, body: { bold: body.bold || false, underline: body.underline || false, italic: body.italic || false, center: body.center || false, }, dashed: conf.dashed || false, fill: conf.fill || undefined, stroke: conf.stroke || undefined, visual: conf.visual || 'class', direction: conf.direction || undefined, }; } var styles = { ABSTRACT: buildStyle({ visual: 'class' }, { center: true, italic: true }), ACTOR: buildStyle({ visual: 'actor' }, { center: true }, { center: true }), CHOICE: buildStyle({ visual: 'rhomb' }, { center: true }, { center: true }), CLASS: buildStyle({ visual: 'class' }, { center: true, bold: true }), DATABASE: buildStyle({ visual: 'database' }, { center: true, bold: true }, { center: true }), END: buildStyle({ visual: 'end' }, {}), FRAME: buildStyle({ visual: 'frame' }, {}), HIDDEN: buildStyle({ visual: 'hidden' }, {}), INPUT: buildStyle({ visual: 'input' }, { center: true }), INSTANCE: buildStyle({ visual: 'class' }, { center: true, underline: true }), LABEL: buildStyle({ visual: 'none' }, { center: true }), LOLLIPOP: buildStyle({ visual: 'lollipop' }, { center: true }), NOTE: buildStyle({ visual: 'note' }, {}), PACKAGE: buildStyle({ visual: 'package' }, {}), RECEIVER: buildStyle({ visual: 'receiver' }, {}), REFERENCE: buildStyle({ visual: 'class', dashed: true }, { center: true }), SENDER: buildStyle({ visual: 'sender' }, {}), SOCKET: buildStyle({ visual: 'socket' }, {}), START: buildStyle({ visual: 'start' }, {}), STATE: buildStyle({ visual: 'roundrect' }, { center: true }), SYNC: buildStyle({ visual: 'sync' }, { center: true }), TABLE: buildStyle({ visual: 'table' }, { center: true, bold: true }), TRANSCEIVER: buildStyle({ visual: 'transceiver' }, {}), USECASE: buildStyle({ visual: 'ellipse' }, { center: true }, { center: true }), }; function box(config, clas) { clas.width = Math.max(...clas.compartments.map((e) => e.width)); clas.height = sum(clas.compartments, (e) => e.height); clas.dividers = []; var y = 0; for (var comp of clas.compartments) { comp.x = 0; comp.y = y; comp.width = clas.width; y += comp.height; if (comp != last(clas.compartments)) clas.dividers.push([ { x: 0, y: y }, { x: clas.width, y: y }, ]); } } function icon(config, clas) { clas.dividers = []; clas.compartments = []; clas.width = config.fontSize * 2.5; clas.height = config.fontSize * 2.5; } function labelledIcon(config, clas) { clas.width = config.fontSize * 1.5; clas.height = config.fontSize * 1.5; clas.dividers = []; var y = config.direction == 'LR' ? clas.height - config.padding : -clas.height / 2; for (var comp of clas.compartments) { if (config.direction == 'LR') { comp.x = clas.width / 2 - comp.width / 2; comp.y = y; } else { comp.x = clas.width / 2 + config.padding / 2; comp.y = y; } y += comp.height; } } var layouters = { actor: function (config, clas) { clas.width = Math.max(config.padding * 2, ...clas.compartments.map((e) => e.width)); clas.height = config.padding * 3 + sum(clas.compartments, (e) => e.height); clas.dividers = []; var y = config.padding * 3; for (var comp of clas.compartments) { comp.x = 0; comp.y = y; comp.width = clas.width; y += comp.height; if (comp != last(clas.compartments)) clas.dividers.push([ { x: config.padding, y: y }, { x: clas.width - config.padding, y: y }, ]); } }, class: box, database: function (config, clas) { clas.width = Math.max(...clas.compartments.map((e) => e.width)); clas.height = sum(clas.compartments, (e) => e.height) + config.padding * 2; clas.dividers = []; var y = config.padding * 1.5; for (var comp of clas.compartments) { comp.x = 0; comp.y = y; comp.width = clas.width; y += comp.height; if (comp != last(clas.compartments)) { var path = range([0, Math.PI], 16).map((a) => ({ x: clas.width * 0.5 * (1 - Math.cos(a)), y: y + config.padding * (0.75 * Math.sin(a) - 0.5), })); clas.dividers.push(path); } } }, ellipse: function (config, clas) { var width = Math.max(...clas.compartments.map((e) => e.width)); var height = sum(clas.compartments, (e) => e.height); clas.width = width * 1.25; clas.height = height * 1.25; clas.dividers = []; var y = height * 0.125; var sq = (x) => x * x; var rimPos = (y) => Math.sqrt(sq(0.5) - sq(y / clas.height - 0.5)) * clas.width; for (var comp of clas.compartments) { comp.x = width * 0.125; comp.y = y; comp.width = width; y += comp.height; if (comp != last(clas.compartments)) clas.dividers.push([ { x: clas.width / 2 + rimPos(y) - 1, y: y }, { x: clas.width / 2 - rimPos(y) + 1, y: y }, ]); } }, end: icon, frame: function (config, clas) { var w = clas.compartments[0].width; var h = clas.compartments[0].height; clas.compartments[0].width += h / 2; box(config, clas); if (clas.dividers.length) clas.dividers.shift(); clas.dividers.unshift([ { x: 0, y: h }, { x: w - h / 4, y: h }, { x: w + h / 4, y: h / 2 }, { x: w + h / 4, y: 0 }, ]); }, hidden: function (config, clas) { clas.dividers = []; clas.compartments = []; clas.width = 1; clas.height = 1; }, input: box, lollipop: labelledIcon, none: box, note: box, package: box, receiver: box, rhomb: function (config, clas) { var width = Math.max(...clas.compartments.map((e) => e.width)); var height = sum(clas.compartments, (e) => e.height); clas.width = width * 1.5; clas.height = height * 1.5; clas.dividers = []; var y = height * 0.25; for (var comp of clas.compartments) { comp.x = width * 0.25; comp.y = y; comp.width = width; y += comp.height; var slope = clas.width / clas.height; if (comp != last(clas.compartments)) clas.dividers.push([ { x: clas.width / 2 + (y < clas.height / 2 ? y * slope : (clas.height - y) * slope), y: y, }, { x: clas.width / 2 - (y < clas.height / 2 ? y * slope : (clas.height - y) * slope), y: y, }, ]); } }, roundrect: box, sender: box, socket: labelledIcon, start: icon, sync: function (config, clas) { clas.dividers = []; clas.compartments = []; if (config.direction == 'LR') { clas.width = config.lineWidth * 3; clas.height = config.fontSize * 5; } else { clas.width = config.fontSize * 5; clas.height = config.lineWidth * 3; } }, table: function (config, clas) { if (clas.compartments.length == 1) { box(config, clas); return; } var gridcells = clas.compartments.slice(1); var rows = [[]]; function isRowBreak(e) { return !e.lines.length && !e.nodes.length && !e.relations.length; } function isRowFull(e) { var current = last(rows); return rows[0] != current && rows[0].length == current.length; } function isEnd(e) { return comp == last(gridcells); } for (var comp of gridcells) { if (!isEnd() && isRowBreak(comp) && last(rows).length) { rows.push([]); } else if (isRowFull()) { rows.push([comp]); } else { last(rows).push(comp); } } var header = clas.compartments[0]; var cellW = Math.max(header.width / rows[0].length, ...gridcells.map((e) => e.width)); var cellH = Math.max(...gridcells.map((e) => e.height)); clas.width = cellW * rows[0].length; clas.height = header.height + cellH * rows.length; var hh = header.height; clas.dividers = [ [ { x: 0, y: header.height }, { x: 0, y: header.height }, ], ...rows.map((e, i) => [ { x: 0, y: hh + i * cellH }, { x: clas.width, y: hh + i * cellH }, ]), ...rows[0].map((e, i) => [ { x: (i + 1) * cellW, y: hh }, { x: (i + 1) * cellW, y: clas.height }, ]), ]; header.x = 0; header.y = 0; header.width = clas.width; for (var i = 0; i < rows.length; i++) { for (var j = 0; j < rows[i].length; j++) { var cell = rows[i][j]; cell.x = j * cellW; cell.y = hh + i * cellH; cell.width = cellW; } } clas.compartments = clas.compartments.filter((e) => !isRowBreak(e)); }, transceiver: box, }; var visualizers = { actor: function (node, x, y, config, g) { var a = config.padding / 2; var yp = y + a * 4; var faceCenter = { x: node.x, y: yp - a }; g.circle(faceCenter, a).fillAndStroke(); g.path([ { x: node.x, y: yp }, { x: node.x, y: yp + 2 * a }, ]).stroke(); g.path([ { x: node.x - a, y: yp + a }, { x: node.x + a, y: yp + a }, ]).stroke(); g.path([ { x: node.x - a, y: yp + a + config.padding }, { x: node.x, y: yp + config.padding }, { x: node.x + a, y: yp + a + config.padding }, ]).stroke(); }, class: function (node, x, y, config, g) { // Slightly rounded rectangle for default class visual var r = Math.min(6, node.height/4); // subtle corner radius if (g.roundRect) g.roundRect(x, y, node.width, node.height, r).fillAndStroke(); else g.rect(x, y, node.width, node.height).fillAndStroke(); }, database: function (node, x, y, config, g) { var pad = config.padding; var cy = y - pad / 2; var pi = 3.1416; g.rect(x, y + pad, node.width, node.height - pad * 1.5).fill(); g.path([ { x: x, y: cy + pad * 1.5 }, { x: x, y: cy - pad * 0.5 + node.height }, ]).stroke(); g.path([ { x: x + node.width, y: cy + pad * 1.5 }, { x: x + node.width, y: cy - pad * 0.5 + node.height }, ]).stroke(); g.ellipse({ x: node.x, y: cy + pad * 1.5 }, node.width, pad * 1.5).fillAndStroke(); g.ellipse({ x: node.x, y: cy - pad * 0.5 + node.height }, node.width, pad * 1.5, 0, pi).fillAndStroke(); }, ellipse: function (node, x, y, config, g) { g.ellipse({ x: node.x, y: node.y }, node.width, node.height).fillAndStroke(); }, end: function (node, x, y, config, g) { g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 3).fillAndStroke(); g.fillStyle(config.stroke); g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 3 - config.padding / 2).fill(); }, frame: function (node, x, y, config, g) { g.rect(x, y, node.width, node.height).fillAndStroke(); }, hidden: function (node, x, y, config, g) { }, input: function (node, x, y, config, g) { g.circuit([ { x: x + config.padding, y: y }, { x: x + node.width, y: y }, { x: x + node.width - config.padding, y: y + node.height }, { x: x, y: y + node.height }, ]).fillAndStroke(); }, lollipop: function (node, x, y, config, g) { g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 2.5).fillAndStroke(); }, none: function (node, x, y, config, g) { }, note: function (node, x, y, config, g) { g.circuit([ { x: x, y: y }, { x: x + node.width - config.padding, y: y }, { x: x + node.width, y: y + config.padding }, { x: x + node.width, y: y + node.height }, { x: x, y: y + node.height }, { x: x, y: y }, ]).fillAndStroke(); g.path([ { x: x + node.width - config.padding, y: y }, { x: x + node.width - config.padding, y: y + config.padding }, { x: x + node.width, y: y + config.padding }, ]).stroke(); }, package: function (node, x, y, config, g) { var headHeight = node.compartments[0].height; g.rect(x, y + headHeight, node.width, node.height - headHeight).fillAndStroke(); var w = g.measureText(node.name).width + 2 * config.padding; g.circuit([ { x: x, y: y + headHeight }, { x: x, y: y }, { x: x + w, y: y }, { x: x + w, y: y + headHeight }, ]).fillAndStroke(); }, receiver: function (node, x, y, config, g) { g.circuit([ { x: x - config.padding, y: y }, { x: x + node.width, y: y }, { x: x + node.width, y: y + node.height }, { x: x - config.padding, y: y + node.height }, { x: x, y: y + node.height / 2 }, ]).fillAndStroke(); }, rhomb: function (node, x, y, config, g) { g.circuit([ { x: node.x, y: y }, { x: x + node.width, y: node.y }, { x: node.x, y: y + node.height }, { x: x, y: node.y }, ]).fillAndStroke(); }, roundrect: function (node, x, y, config, g) { var r = Math.min(config.padding * 2 * config.leading, node.height / 2); g.roundRect(x, y, node.width, node.height, r).fillAndStroke(); }, sender: function (node, x, y, config, g) { g.circuit([ { x: x, y: y }, { x: x + node.width - config.padding, y: y }, { x: x + node.width, y: y + node.height / 2 }, { x: x + node.width - config.padding, y: y + node.height }, { x: x, y: y + node.height }, ]).fillAndStroke(); }, socket: function (node, x, y, config, g) { var from = config.direction === 'TB' ? Math.PI : Math.PI / 2; var to = config.direction === 'TB' ? 2 * Math.PI : -Math.PI / 2; g.ellipse({ x: node.x, y: node.y }, node.width, node.height, from, to).stroke(); }, start: function (node, x, y, config, g) { g.fillStyle(config.stroke); g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 2.5).fill(); }, sync: function (node, x, y, config, g) { g.fillStyle(config.stroke); g.rect(x, y, node.width, node.height).fillAndStroke(); }, table: function (node, x, y, config, g) { g.rect(x, y, node.width, node.height).fillAndStroke(); }, transceiver: function (node, x, y, config, g) { g.circuit([ { x: x - config.padding, y: y }, { x: x + node.width - config.padding, y: y }, { x: x + node.width, y: y + node.height / 2 }, { x: x + node.width - config.padding, y: y + node.height }, { x: x - config.padding, y: y + node.height }, { x: x, y: y + node.height / 2 }, ]).fillAndStroke(); }, }; function layout(measurer, config, ast) { function measureLines(lines, fontWeight) { if (!lines.length) return { width: 0, height: config.padding }; measurer.setFont(config.font, config.fontSize, fontWeight, 'normal'); return { width: Math.round(Math.max(...lines.map(measurer.textWidth)) + 2 * config.padding), height: Math.round(measurer.textHeight() * lines.length + 2 * config.padding), }; } function layoutCompartment(c, compartmentIndex, style) { var _a, _b; var textSize = measureLines(c.lines, compartmentIndex ? 'normal' : 'bold'); if (!c.nodes.length && !c.relations.length) { c.width = textSize.width; c.height = textSize.height; c.offset = { x: config.padding, y: config.padding }; return; } var styledConfig = Object.assign(Object.assign({}, config), { direction: (_a = style.direction) !== null && _a !== void 0 ? _a : config.direction }); // Auto group root-level nodes by classifier type (tags like ) when: // - No relations // - We are at the root compartment // - We haven't already grouped once if (c.isRoot && !c._groupedByType && c.relations.length === 0) { var groups = {}; for (var n of c.nodes) { var t = n.type || 'CLASS'; if (t === 'CLASS' || t === 'HIDDEN') continue; // skip default/hidden (groups[t] = groups[t] || []).push(n); } var newRootNodes = []; var consumed = new Set(); for (var n of c.nodes) { if (consumed.has(n)) continue; var t = n.type || 'CLASS'; if (groups[t] && groups[t].length > 1 && t !== 'CLASS' && t !== 'HIDDEN') { // Build a container classifier to hold this group's nodes var memberNodes = groups[t]; memberNodes.forEach(m => consumed.add(m)); // Title compartment with group name; second compartment holds members as child nodes var title = t.charAt(0) + t.slice(1).toLowerCase(); var groupComp = new Compartment([title], memberNodes, []); var groupClassifier = new Classifier('PACKAGE', title + '_group', [groupComp]); newRootNodes.push(groupClassifier); } else { newRootNodes.push(n); consumed.add(n); } } c.nodes = newRootNodes; c._groupedByType = true; } c.nodes.forEach((e) => layoutClassifier(e, styledConfig)); // Manual layouts for simpler, predictable stacking/wrapping when there are no relations // 1) For nested compartments (non-root), stack child nodes vertically // 2) For the root compartment, lay out parent boxes in a 2-column grid, wrapping to new rows if (c.relations.length === 0 && c.nodes.length > 0) { // Helper to compute compartment bounds and offset given inner graph size function finalizeCompartmentBounds(innerWidth, innerHeight, left = 0, top = 0) { var graphHeight = innerHeight ? innerHeight + 2 * config.gutter : 0; var graphWidth = innerWidth ? innerWidth + 2 * config.gutter : 0; c.width = Math.max(textSize.width, graphWidth) + 2 * config.padding; c.height = textSize.height + graphHeight + config.padding; c.offset = { x: config.padding - left, y: config.padding - top }; } if (c.isRoot === true) { // Root-level grid: 3 columns, wrap rows var gridCols = 3; // Use layout sizes (which include edge margins) for spacing accuracy var colW = Math.max(...c.nodes.map(n => n.layoutWidth)); var colCenters = []; for (var ci = 0; ci < gridCols; ci++) { colCenters.push(ci * (colW + config.spacing) + colW / 2); } // Group nodes into rows of gridCols var rows = []; for (var i = 0; i < c.nodes.length; i += gridCols) { rows.push(c.nodes.slice(i, i + gridCols)); } // Compute row heights (max of items in row) var rowHeights = rows.map(r => Math.max(...r.map(n => n.layoutHeight))); // Position nodes top-aligned per row (all boxes in a row share the same top edge) var yAcc = 0; // current row top rows.forEach((r, ri) => { if (ri > 0) yAcc += config.spacing; // vertical spacing between rows var rowH = rowHeights[ri]; var rowTop = yAcc; r.forEach((n, ci) => { var col = ci; // column index in row n.x = colCenters[col]; // top edge at rowTop; convert to center coordinate for nomnoml internal representation n.y = rowTop + n.layoutHeight / 2; }); yAcc += rowH; // advance by max row height }); var totalWidth = (gridCols * colW) + ((gridCols - 1) * config.spacing); var totalHeight = rowHeights.reduce((a, b) => a + b, 0) + (rowHeights.length > 1 ? (rowHeights.length - 1) * config.spacing : 0); finalizeCompartmentBounds(totalWidth, totalHeight, 0, 0); return; } else { // Nested compartment: strict vertical stack var colW_1 = Math.max(...c.nodes.map(n => n.layoutWidth)); var xCenter = colW_1 / 2; var totalH = 0; for (var i = 0; i < c.nodes.length; i++) { var n = c.nodes[i]; if (i > 0) totalH += config.spacing; n.x = xCenter; n.y = totalH + n.layoutHeight / 2; totalH += n.layoutHeight; } finalizeCompartmentBounds(colW_1, totalH, 0, 0); return; } } var g = new graphre.graphlib.Graph(); g.setGraph({ rankdir: style.direction || config.direction, nodesep: config.spacing, edgesep: config.spacing, ranksep: config.spacing, acyclicer: config.acyclicer, ranker: config.ranker, }); for (var e of c.nodes) { g.setNode(e.name, { width: e.layoutWidth, height: e.layoutHeight }); } for (var r of c.relations) { if (r.assoc.indexOf('_') > -1) { g.setEdge(r.start, r.end, { id: r.id, minlen: 0 }); } else if (((_b = config.gravity) !== null && _b !== void 0 ? _b : 1) != 1) { g.setEdge(r.start, r.end, { id: r.id, minlen: config.gravity }); } else { g.setEdge(r.start, r.end, { id: r.id }); } } graphre.layout(g); var rels = indexBy(c.relations, 'id'); var nodes = indexBy(c.nodes, 'name'); g.nodes().forEach((name) => { var node = g.node(name); nodes[name].x = node.x; nodes[name].y = node.y; }); var left = 0; var right = 0; var top = 0; var bottom = 0; g.edges().forEach((edgeObj) => { var edge = g.edge(edgeObj); var start = nodes[edgeObj.v]; var end = nodes[edgeObj.w]; var rel = rels[edge.id]; rel.path = [start, ...edge.points, end].map(toPoint); var startP = rel.path[1]; var endP = rel.path[rel.path.length - 2]; layoutLabel(rel.startLabel, startP, adjustQuadrant(quadrant(startP, start, 4), start, end)); layoutLabel(rel.endLabel, endP, adjustQuadrant(quadrant(endP, end, 2), end, start)); left = Math.min(left, rel.startLabel.x, rel.endLabel.x, ...edge.points.map((e) => e.x), ...edge.points.map((e) => e.x)); right = Math.max(right, rel.startLabel.x + rel.startLabel.width, rel.endLabel.x + rel.endLabel.width, ...edge.points.map((e) => e.x)); top = Math.min(top, rel.startLabel.y, rel.endLabel.y, ...edge.points.map((e) => e.y)); bottom = Math.max(bottom, rel.startLabel.y + rel.startLabel.height, rel.endLabel.y + rel.endLabel.height, ...edge.points.map((e) => e.y)); }); var graph = g.graph(); var width = Math.max(graph.width, right - left); var height = Math.max(graph.height, bottom - top); var graphHeight = height ? height + 2 * config.gutter : 0; var graphWidth = width ? width + 2 * config.gutter : 0; c.width = Math.max(textSize.width, graphWidth) + 2 * config.padding; c.height = textSize.height + graphHeight + config.padding; c.offset = { x: config.padding - left, y: config.padding - top }; } function toPoint(o) { return { x: o.x, y: o.y }; } function layoutLabel(label, point, quadrant) { if (!label.text) { label.width = 0; label.height = 0; label.x = point.x; label.y = point.y; } else { var fontSize = config.fontSize; var lines = label.text.split('`'); label.width = Math.max(...lines.map((l) => measurer.textWidth(l))); label.height = fontSize * lines.length; label.x = point.x + (quadrant == 1 || quadrant == 4 ? config.padding : -label.width - config.padding); label.y = point.y + (quadrant == 3 || quadrant == 4 ? config.padding : -label.height - config.padding); } } function quadrant(point, node, fallback) { if (point.x < node.x && point.y < node.y) return 1; if (point.x > node.x && point.y < node.y) return 2; if (point.x > node.x && point.y > node.y) return 3; if (point.x < node.x && point.y > node.y) return 4; return fallback; } function adjustQuadrant(quadrant, point, opposite) { if (opposite.x == point.x || opposite.y == point.y) return quadrant; var flipHorizontally = [4, 3, 2, 1]; var flipVertically = [2, 1, 4, 3]; var oppositeQuadrant = opposite.y < point.y ? (opposite.x < point.x ? 2 : 1) : opposite.x < point.x ? 3 : 4; if (oppositeQuadrant === quadrant) { if (config.direction === 'LR') return flipHorizontally[quadrant - 1]; if (config.direction === 'TB') return flipVertically[quadrant - 1]; } return quadrant; } function layoutClassifier(clas, config) { var style = config.styles[clas.type] || styles.CLASS; clas.compartments.forEach((co, i) => layoutCompartment(co, i, style)); layouters[style.visual](config, clas); // Post layout augmentation: allocate space for left image if requested if (clas.imageUrl) { // Image is square with size equal to overall height of node // After layouter runs, height is known. Widen node and shift compartments/dividers var imgW = clas.height; // square area clas.width += imgW; clas.compartments.forEach(comp => { comp.x += imgW; }); clas.dividers = clas.dividers.map(div => div.map(p => ({ x: p.x + imgW, y: p.y }))); } clas.layoutWidth = clas.width + 2 * config.edgeMargin; clas.layoutHeight = clas.height + 2 * config.edgeMargin; } // Mark the root compartment to enable special layout behavior at the top level ast.isRoot = true; layoutCompartment(ast, 0, styles.CLASS); return ast; } class Compartment { constructor(lines, nodes, relations) { this.lines = lines; this.nodes = nodes; this.relations = relations; } } class Relation { } class Classifier { constructor(type, name, compartments) { this.type = type; this.name = name; this.compartments = compartments; this.dividers = []; } } /* parser generated by jison 0.4.18 */ /* Returns a Parser object of the following structure: Parser: { yy: {} } Parser.prototype: { yy: {}, trace: function(), symbols_: {associative list: name ==> number}, terminals_: {associative list: number ==> name}, productions_: [...], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), table: [...], defaultActions: {...}, parseError: function(str, hash), parse: function(input), lexer: { EOF: 1, parseError: function(str, hash), setInput: function(input), input: function(), unput: function(str), more: function(), less: function(n), pastInput: function(), upcomingInput: function(), showPosition: function(), test_match: function(regex_match_array, rule_index), next: function(), lex: function(), begin: function(condition), popState: function(), _currentRules: function(), topState: function(), pushState: function(condition), options: { ranges: boolean (optional: true ==> token location info will include a .range[] member) flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) }, performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), rules: [...], conditions: {associative list: name ==> set}, } } token location info (@$, _$, etc.): { first_line: n, last_line: n, first_column: n, last_column: n, range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) } the parseError function receives a 'hash' object with these members for lexer and parser errors: { text: (matched text) token: (the produced terminal token, if any) line: (yylineno) } while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { loc: (yylloc) expected: (string describing the set of expected tokens) recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ var nomnomlCoreParser = (function(){ var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,5],$V1=[1,8],$V2=[5,6,12,14],$V3=[12,14],$V4=[1,22]; var parser = {trace: function trace () { }, yy: {}, symbols_: {"error":2,"root":3,"compartment":4,"EOF":5,"SEP":6,"slot":7,"IDENT":8,"class":9,"association":10,"parts":11,"|":12,"[":13,"]":14,"$accept":0,"$end":1}, terminals_: {2:"error",5:"EOF",6:"SEP",8:"IDENT",12:"|",13:"[",14:"]"}, productions_: [0,[3,2],[3,3],[3,4],[3,3],[7,1],[7,1],[7,1],[4,1],[4,3],[11,1],[11,3],[11,2],[10,3],[9,3]], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { /* this == yyval */ var $0 = $$.length - 1; switch (yystate) { case 1: case 2: return $$[$0-1] case 3: case 4: return $$[$0-2] case 5: this.$ = $$[$0].trim().replace(/\\(\[|\]|\|)/g, '$'+'1'); break; case 6: case 7: this.$ = $$[$0]; break; case 8: case 10: this.$ = [$$[$0]]; break; case 9: this.$ = $$[$0-2].concat($$[$0]); break; case 11: this.$ = $$[$0-2].concat([$$[$0]]); break; case 12: this.$ = $$[$0-1].concat([[]]); break; case 13: var t = $$[$0-1].trim().replace(/\\(\[|\]|\|)/g, '$'+'1').match('^(.*?)([)<:o+(]*[-_]/?[-_]*[):o+>(]*)(.*)$'); if (!t) { throw new Error('line '+_$[$0].first_line+': Classifiers must be separated by a relation or a line break') } this.$ = {assoc:t[2], start:$$[$0-2], end:$$[$0], startLabel:t[1].trim(), endLabel:t[3].trim()}; break; case 14: var type = 'CLASS'; var id = $$[$0-1][0][0]; var typeMatch = $$[$0-1][0][0].match('<([a-z]*)>(.*)'); if (typeMatch) { type = typeMatch[1].toUpperCase(); id = typeMatch[2].trim(); } $$[$0-1][0][0] = id; this.$ = {type:type, id:id, parts:$$[$0-1]}; break; } }, table: [{3:1,4:2,6:[1,3],7:4,8:$V0,9:6,10:7,13:$V1},{1:[3]},{5:[1,9],6:[1,10]},{4:11,7:4,8:$V0,9:6,10:7,13:$V1},o($V2,[2,8]),o($V2,[2,5]),o($V2,[2,6],{8:[1,12]}),o($V2,[2,7]),{4:14,7:4,8:$V0,9:6,10:7,11:13,13:$V1},{1:[2,1]},{5:[1,15],7:16,8:$V0,9:6,10:7,13:$V1},{5:[1,17],6:[1,18]},{9:19,13:$V1},{12:[1,21],14:[1,20]},o($V3,[2,10],{6:$V4}),{1:[2,4]},o($V2,[2,9]),{1:[2,2]},{5:[1,23],7:16,8:$V0,9:6,10:7,13:$V1},o($V2,[2,13]),o([5,6,8,12,14],[2,14]),o($V3,[2,12],{7:4,9:6,10:7,4:24,8:$V0,13:$V1}),{7:16,8:$V0,9:6,10:7,13:$V1},{1:[2,3]},o($V3,[2,11],{6:$V4})], defaultActions: {9:[2,1],15:[2,4],17:[2,2],23:[2,3]}, parseError: function parseError (str, hash) { if (hash.recoverable) { this.trace(str); } else { var error = new Error(str); error.hash = hash; throw error; } }, parse: function parse(input) { var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, TERROR = 2, EOF = 1; var args = lstack.slice.call(arguments, 1); var lexer = Object.create(this.lexer); var sharedState = { yy: {} }; for (var k in this.yy) { if (Object.prototype.hasOwnProperty.call(this.yy, k)) { sharedState.yy[k] = this.yy[k]; } } lexer.setInput(input, sharedState.yy); sharedState.yy.lexer = lexer; sharedState.yy.parser = this; if (typeof lexer.yylloc == 'undefined') { lexer.yylloc = {}; } var yyloc = lexer.yylloc; lstack.push(yyloc); var ranges = lexer.options && lexer.options.ranges; if (typeof sharedState.yy.parseError === 'function') { this.parseError = sharedState.yy.parseError; } else { this.parseError = Object.getPrototypeOf(this).parseError; } var lex = function () { var token; token = lexer.lex() || EOF; if (typeof token !== 'number') { token = self.symbols_[token] || token; } return token; }; var symbol, state, action, r, yyval = {}, p, len, newState, expected; while (true) { state = stack[stack.length - 1]; if (this.defaultActions[state]) { action = this.defaultActions[state]; } else { if (symbol === null || typeof symbol == 'undefined') { symbol = lex(); } action = table[state] && table[state][symbol]; } if (typeof action === 'undefined' || !action.length || !action[0]) { var errStr = ''; expected = []; for (p in table[state]) { if (this.terminals_[p] && p > TERROR) { expected.push('\'' + this.terminals_[p] + '\''); } } if (lexer.showPosition) { errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; } else { errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); } this.parseError(errStr, { text: lexer.match, token: this.terminals_[symbol] || symbol, line: lexer.yylineno, loc: yyloc, expected: expected }); } if (action[0] instanceof Array && action.length > 1) { throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); } switch (action[0]) { case 1: stack.push(symbol); vstack.push(lexer.yytext); lstack.push(lexer.yylloc); stack.push(action[1]); symbol = null; { yyleng = lexer.yyleng; yytext = lexer.yytext; yylineno = lexer.yylineno; yyloc = lexer.yylloc; } break; case 2: len = this.productions_[action[1]][1]; yyval.$ = vstack[vstack.length - len]; yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column }; if (ranges) { yyval._$.range = [ lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1] ]; } r = this.performAction.apply(yyval, [ yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack ].concat(args)); if (typeof r !== 'undefined') { return r; } if (len) { stack = stack.slice(0, -1 * len * 2); vstack = vstack.slice(0, -1 * len); lstack = lstack.slice(0, -1 * len); } stack.push(this.productions_[action[1]][0]); vstack.push(yyval.$); lstack.push(yyval._$); newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; stack.push(newState); break; case 3: return true; } } return true; }}; /* generated by jison-lex 0.3.4 */ var lexer = (function(){ var lexer = ({ EOF:1, parseError:function parseError(str, hash) { if (this.yy.parser) { this.yy.parser.parseError(str, hash); } else { throw new Error(str); } }, // resets the lexer, sets new input setInput:function (input, yy) { this.yy = yy || this.yy || {}; this._input = input; this._more = this._backtrack = this.done = false; this.yylineno = this.yyleng = 0; this.yytext = this.matched = this.match = ''; this.conditionStack = ['INITIAL']; this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0 }; if (this.options.ranges) { this.yylloc.range = [0,0]; } this.offset = 0; return this; }, // consumes and returns one char from the input input:function () { var ch = this._input[0]; this.yytext += ch; this.yyleng++; this.offset++; this.match += ch; this.matched += ch; var lines = ch.match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno++; this.yylloc.last_line++; } else { this.yylloc.last_column++; } if (this.options.ranges) { this.yylloc.range[1]++; } this._input = this._input.slice(1); return ch; }, // unshifts one char (or a string) into the input unput:function (ch) { var len = ch.length; var lines = ch.split(/(?:\r\n?|\n)/g); this._input = ch + this._input; this.yytext = this.yytext.substr(0, this.yytext.length - len); //this.yyleng -= len; this.offset -= len; var oldLines = this.match.split(/(?:\r\n?|\n)/g); this.match = this.match.substr(0, this.match.length - 1); this.matched = this.matched.substr(0, this.matched.length - 1); if (lines.length - 1) { this.yylineno -= lines.length - 1; } var r = this.yylloc.range; this.yylloc = { first_line: this.yylloc.first_line, last_line: this.yylineno + 1, first_column: this.yylloc.first_column, last_column: lines ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length : this.yylloc.first_column - len }; if (this.options.ranges) { this.yylloc.range = [r[0], r[0] + this.yyleng - len]; } this.yyleng = this.yytext.length; return this; }, // When called from action, caches matched text and appends it on next action more:function () { this._more = true; return this; }, // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. reject:function () { if (this.options.backtrack_lexer) { this._backtrack = true; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { text: "", token: null, line: this.yylineno }); } return this; }, // retain first n characters of the match less:function (n) { this.unput(this.match.slice(n)); }, // displays already matched input, i.e. for error messages pastInput:function () { var past = this.matched.substr(0, this.matched.length - this.match.length); return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); }, // displays upcoming input, i.e. for error messages upcomingInput:function () { var next = this.match; if (next.length < 20) { next += this._input.substr(0, 20-next.length); } return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); }, // displays the character position where the lexing error occurred, i.e. for error messages showPosition:function () { var pre = this.pastInput(); var c = new Array(pre.length + 1).join("-"); return pre + this.upcomingInput() + "\n" + c + "^"; }, // test the lexed token: return FALSE when not a match, otherwise return token test_match:function(match, indexed_rule) { var token, lines, backup; if (this.options.backtrack_lexer) { // save context backup = { yylineno: this.yylineno, yylloc: { first_line: this.yylloc.first_line, last_line: this.last_line, first_column: this.yylloc.first_column, last_column: this.yylloc.last_column }, yytext: this.yytext, match: this.match, matches: this.matches, matched: this.matched, yyleng: this.yyleng, offset: this.offset, _more: this._more, _input: this._input, yy: this.yy, conditionStack: this.conditionStack.slice(0), done: this.done }; if (this.options.ranges) { backup.yylloc.range = this.yylloc.range.slice(0); } } lines = match[0].match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno += lines.length; } this.yylloc = { first_line: this.yylloc.last_line, last_line: this.yylineno + 1, first_column: this.yylloc.last_column, last_column: lines ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length }; this.yytext += match[0]; this.match += match[0]; this.matches = match; this.yyleng = this.yytext.length; if (this.options.ranges) { this.yylloc.range = [this.offset, this.offset += this.yyleng]; } this._more = false; this._backtrack = false; this._input = this._input.slice(match[0].length); this.matched += match[0]; token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); if (this.done && this._input) { this.done = false; } if (token) { return token; } else if (this._backtrack) { // recover context for (var k in backup) { this[k] = backup[k]; } return false; // rule action called reject() implying the next rule should be tested instead. } return false; }, // return next match in input next:function () { if (this.done) { return this.EOF; } if (!this._input) { this.done = true; } var token, match, tempMatch, index; if (!this._more) { this.yytext = ''; this.match = ''; } var rules = this._currentRules(); for (var i = 0; i < rules.length; i++) { tempMatch = this._input.match(this.rules[rules[i]]); if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { match = tempMatch; index = i; if (this.options.backtrack_lexer) { token = this.test_match(tempMatch, rules[i]); if (token !== false) { return token; } else if (this._backtrack) { match = false; continue; // rule action called reject() implying a rule MISmatch. } else { // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } } else if (!this.options.flex) { break; } } } if (match) { token = this.test_match(match, rules[index]); if (token !== false) { return token; } // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } if (this._input === "") { return this.EOF; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { text: "", token: null, line: this.yylineno }); } }, // return next match that has a token lex:function lex () { var r = this.next(); if (r) { return r; } else { return this.lex(); } }, // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) begin:function begin (condition) { this.conditionStack.push(condition); }, // pop the previously active lexer condition state off the condition stack popState:function popState () { var n = this.conditionStack.length - 1; if (n > 0) { return this.conditionStack.pop(); } else { return this.conditionStack[0]; } }, // produce the lexer rule set which is active for the currently active lexer condition state _currentRules:function _currentRules () { if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; } else { return this.conditions["INITIAL"].rules; } }, // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available topState:function topState (n) { n = this.conditionStack.length - 1 - Math.abs(n || 0); if (n >= 0) { return this.conditionStack[n]; } else { return "INITIAL"; } }, // alias for begin(condition) pushState:function pushState (condition) { this.begin(condition); }, // return the number of states currently on the stack stateStackSize:function stateStackSize() { return this.conditionStack.length; }, options: {}, performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { switch($avoiding_name_collisions) { case 0:return 12 case 1:return 8 case 2:return 13 case 3:return 14 case 4:return 6 case 5:return 5 case 6:return 'INVALID' } }, rules: [/^(?:\s*\|\s*)/,/^(?:(\\(\[|\]|\|)|[^\]\[|;\n])+)/,/^(?:\[)/,/^(?:\s*\])/,/^(?:[ ]*(;|\n)+[ ]*)/,/^(?:$)/,/^(?:.)/], conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6],"inclusive":true}} }); return lexer; })(); parser.lexer = lexer; function Parser () { this.yy = {}; } Parser.prototype = parser;parser.Parser = Parser; return new Parser; })(); function parse(source) { function onlyCompilables(line) { var ok = line[0] !== '#' && line.trim().substring(0, 2) !== '//'; return ok ? line.trim() : ''; } function isDirective(line) { return line.text[0] === '#'; } var lines = source.split('\n').map((s, i) => ({ text: s, index: i })); var pureDirectives = lines.filter(isDirective); var directives = {}; pureDirectives.forEach((line) => { try { var tokens = line.text.substring(1).split(':'); directives[tokens[0].trim()] = tokens[1].trim(); } catch (e) { throw new Error('line ' + (line.index + 1) + ': Malformed directive'); } }); var pureDiagramCode = lines.map((e) => onlyCompilables(e.text)).join('\n'); if (pureDiagramCode == '') { return { root: new Compartment([], [], []), config: getConfig(directives), }; } var parseTree = intermediateParse(pureDiagramCode); return { root: transformParseIntoSyntaxTree(parseTree), config: getConfig(directives), }; function directionToDagre(word) { if (word == 'down') return 'TB'; if (word == 'right') return 'LR'; else return 'TB'; } function parseRanker(word) { if (word == 'network-simplex' || word == 'tight-tree' || word == 'longest-path') { return word; } return 'network-simplex'; } function parseCustomStyle(styleDef) { var contains = hasSubstring; var floatingKeywords = styleDef.replace(/[a-z]*=[^ ]+/g, ''); var titleDef = last(styleDef.match('title=([^ ]*)') || ['']); var bodyDef = last(styleDef.match('body=([^ ]*)') || ['']); return { title: { bold: contains(titleDef, 'bold') || contains(floatingKeywords, 'bold'), underline: contains(titleDef, 'underline') || contains(floatingKeywords, 'underline'), italic: contains(titleDef, 'italic') || contains(floatingKeywords, 'italic'), center: !(contains(titleDef, 'left') || contains(styleDef, 'align=left')), }, body: { bold: contains(bodyDef, 'bold'), underline: contains(bodyDef, 'underline'), italic: contains(bodyDef, 'italic'), center: contains(bodyDef, 'center'), }, dashed: contains(styleDef, 'dashed'), fill: last(styleDef.match('fill=([^ ]*)') || []), stroke: last(styleDef.match('stroke=([^ ]*)') || []), visual: (last(styleDef.match('visual=([^ ]*)') || []) || 'class'), direction: directionToDagre(last(styleDef.match('direction=([^ ]*)') || [])), }; } function getConfig(d) { var _a; var userStyles = {}; for (var key in d) { if (key[0] != '.') continue; var styleDef = d[key]; userStyles[key.substring(1).toUpperCase()] = parseCustomStyle(styleDef); } return { arrowSize: +d.arrowSize || 1, bendSize: +d.bendSize || 0.3, direction: directionToDagre(d.direction), gutter: +d.gutter || 20, edgeMargin: +d.edgeMargin || 0, gravity: +((_a = d.gravity) !== null && _a !== void 0 ? _a : 1), edges: d.edges == 'hard' ? 'hard' : 'rounded', fill: (d.fill || '#eee8d5;#fdf6e3;#eee8d5;#fdf6e3').split(';'), background: d.background || 'transparent', fillArrows: d.fillArrows === 'true', font: d.font || 'Helvetica', fontSize: +d.fontSize || 12, leading: +d.leading || 1.25, lineWidth: +d.lineWidth || 3, padding: +d.padding || 8, spacing: +d.spacing || 40, stroke: d.stroke || '#33322E', title: d.title || '', zoom: +d.zoom || 1, acyclicer: d.acyclicer === 'greedy' ? 'greedy' : undefined, ranker: parseRanker(d.ranker), styles: Object.assign(Object.assign({}, styles), userStyles), }; } } function intermediateParse(source) { return nomnomlCoreParser.parse(source); } function transformParseIntoSyntaxTree(entity) { function isAstClassifier(obj) { return obj.parts !== undefined; } function isAstRelation(obj) { return obj.assoc !== undefined; } var relationId = 0; function transformCompartment(slots) { var lines = []; var rawClassifiers = []; var relations = []; slots.forEach((p) => { if (typeof p === 'string') lines.push(p); if (isAstRelation(p)) { rawClassifiers.push(p.start); rawClassifiers.push(p.end); relations.push({ id: relationId++, assoc: p.assoc, start: p.start.parts[0][0], end: p.end.parts[0][0], startLabel: { text: p.startLabel }, endLabel: { text: p.endLabel }, }); } if (isAstClassifier(p)) { rawClassifiers.push(p); } }); var allClassifiers = rawClassifiers .map(transformClassifier) .sort((a, b) => b.compartments.length - a.compartments.length); var uniqClassifiers = uniqueBy(allClassifiers, 'name'); var uniqRelations = relations.filter((a) => { for (var b of relations) { if (a === b) return true; if (b.start == a.start && b.end == a.end) return false; } return true; }); return new Compartment(lines, uniqClassifiers, uniqRelations); } function extractEnhancements(firstLine) { // Syntax: text$imageUrl£borderColour (each optional after its marker) // Extended Syntax (added): text$£dataOrBase64Image£borderColour OR text$£dataOrBase64Image£ // Rationale: Allow embedding data URLs (which may contain semicolons) or raw base64 blobs // enclosed between two £ delimiters immediately after a $ marker. Example: // [Label$£data:image/png;base64,iVBORw0KGgo...£] // [Label$£iVBORw0KGgo...£#ff0000] // Backwards compatibility: Original single £ after $ still works for specifying borderColour. if (!firstLine) return { text: firstLine }; var text = firstLine; var imageUrl = undefined; var borderColour = undefined; var dollar = firstLine.indexOf('$'); var pound = firstLine.indexOf('£'); if (dollar >= 0) { // Text part always before $ text = firstLine.substring(0, dollar).trim(); var afterDollar = firstLine.substring(dollar + 1); if (afterDollar.startsWith('£')) { // New extended syntax: $£...£ (image between first two £) var secondPound = firstLine.indexOf('£', dollar + 2); // find closing £ if (secondPound !== -1) { imageUrl = firstLine.substring(dollar + 2, secondPound).trim(); // Anything after second £ may contain another £ introducing border colour (legacy style) // e.g. Label$£data£#ff0 OR Label$£data££border (we'll support single instance) var remainder = firstLine.substring(secondPound + 1).trim(); // If remainder starts with £ treat content after it as borderColour (to allow explicit empty section) if (remainder.startsWith('£')) { remainder = remainder.substring(1).trim(); } if (remainder) borderColour = remainder; } else { // Malformed (no closing £) - fallback to legacy parsing treating rest as image imageUrl = afterDollar.substring(1).trim(); } } else { // Legacy syntax: $imageUrl£borderColour OR $imageUrl var poundAfter = afterDollar.indexOf('£'); if (poundAfter >= 0) { imageUrl = afterDollar.substring(0, poundAfter).trim(); borderColour = afterDollar.substring(poundAfter + 1).trim(); } else { imageUrl = afterDollar.trim(); } } } else if (pound >= 0) { // only border colour (legacy) text = firstLine.substring(0, pound).trim(); borderColour = firstLine.substring(pound + 1).trim(); } if (imageUrl === '') imageUrl = undefined; if (borderColour === '') borderColour = undefined; return { text, imageUrl, borderColour }; } function transformClassifier(entity) { var compartments = entity.parts.map(transformCompartment); var classifier = new Classifier(entity.type, entity.id, compartments); // Enhancement parsing scans all lines of first compartment for first occurrence of $ or £ markers if (classifier.compartments.length && classifier.compartments[0].lines.length) { var lines = classifier.compartments[0].lines; // Pre-pass: merge lines that were split on semicolons inside an extended $£...£ image segment. // The lexer treats ';' as a separator (SEP). Data/image URLs like data:image/png;base64,... // or custom forms with a semicolon (e.g. data:image/png;AAA=) would have been split. // We detect a start marker '$£' without a closing '£' on the same line and // concatenate following lines until we encounter a line containing '£'. We reinsert // semicolons between the fragments (lossy if original had different whitespace but functional). for (var mi = 0; mi < lines.length; mi++) { var l = lines[mi]; if (!l) continue; var startPos = l.indexOf('$£'); if (startPos === -1) continue; // If there's already a terminating £ after the start marker, nothing to do if (l.indexOf('£', startPos + 2) !== -1) continue; // Collect subsequent lines until we find a '£' var collected = [l]; var endIndex = -1; for (var mj = mi + 1; mj < lines.length; mj++) { collected.push(lines[mj]); if (lines[mj] && lines[mj].indexOf('£') !== -1) { endIndex = mj; break; } } if (endIndex !== -1) { var merged = collected.join(';'); // Replace the range mi..endIndex with merged line lines.splice(mi, collected.length, merged); } } for (var li = 0; li < lines.length; li++) { var original = lines[li]; if (!original) continue; if (original.indexOf('$') === -1 && original.indexOf('£') === -1) continue; var enh = extractEnhancements(original); lines[li] = enh.text || ''; if (enh.imageUrl) classifier.imageUrl = enh.imageUrl; if (enh.borderColour) classifier.borderColour = enh.borderColour; if (lines[li] === '') { lines.splice(li,1); } break; // only first marker processed } } return classifier; } return transformCompartment(entity); } function add(a, b) { return { x: a.x + b.x, y: a.y + b.y }; } function diff(a, b) { return { x: a.x - b.x, y: a.y - b.y }; } function mult(v, factor) { return { x: factor * v.x, y: factor * v.y }; } function mag(v) { return Math.sqrt(v.x * v.x + v.y * v.y); } function normalize(v) { return mult(v, 1 / mag(v)); } function rot(a) { return { x: a.y, y: -a.x }; } const empty = false; const filled = true; function getPath(config, r) { var path = r.path.slice(1, -1); var endDir = normalize(diff(path[path.length - 2], last(path))); var startDir = normalize(diff(path[1], path[0])); var size = (config.spacing * config.arrowSize) / 30; var head = 0; var end = path.length - 1; var copy = path.map((p) => ({ x: p.x, y: p.y })); var tokens = r.assoc.split(/[-_]/); copy[head] = add(copy[head], mult(startDir, size * terminatorSize(tokens[0]))); copy[end] = add(copy[end], mult(endDir, size * terminatorSize(last(tokens)))); return copy; } function terminatorSize(id) { if (id === '>' || id === '<') return 5; if (id === ':>' || id === '<:') return 10; if (id === '+') return 14; if (id === 'o') return 14; if (id === '(' || id === ')') return 11; if (id === '(o' || id === 'o)') return 11; if (id === '>o' || id === 'o<') return 15; return 0; } function drawTerminators(g, config, r) { var start = r.path[1]; var end = r.path[r.path.length - 2]; var path = r.path.slice(1, -1); var tokens = r.assoc.split(/[-_]/); drawArrowEnd(last(tokens), path, end); drawArrowEnd(tokens[0], path.reverse(), start); function drawArrowEnd(id, path, end) { var dir = normalize(diff(path[path.length - 2], last(path))); var size = (config.spacing * config.arrowSize) / 30; if (id === '>' || id === '<') drawArrow(dir, size, filled, end); else if (id === ':>' || id === '<:') drawArrow(dir, size, empty, end); else if (id === '+') drawDiamond(dir, size, filled, end); else if (id === 'o') drawDiamond(dir, size, empty, end); else if (id === '(' || id === ')') { drawSocket(dir, size, 11, end); drawStem(dir, size, 5, end); } else if (id === '(o' || id === 'o)') { drawSocket(dir, size, 11, end); drawStem(dir, size, 5, end); drawBall(dir, size, 11, end); } else if (id === '>o' || id === 'o<') { drawArrow(dir, size * 0.75, empty, add(end, mult(dir, size * 10))); drawStem(dir, size, 8, end); drawBall(dir, size, 8, end); } } function drawBall(nv, size, stem, end) { var center = add(end, mult(nv, size * stem)); g.fillStyle(config.fill[0]); g.ellipse(center, size * 6, size * 6).fillAndStroke(); } function drawStem(nv, size, stem, end) { var center = add(end, mult(nv, size * stem)); g.path([center, end]).stroke(); } function drawSocket(nv, size, stem, end) { var base = add(end, mult(nv, size * stem)); var t = rot(nv); var socket = range([-Math.PI / 2, Math.PI / 2], 12).map((a) => add(base, add(mult(nv, -6 * size * Math.cos(a)), mult(t, 6 * size * Math.sin(a))))); g.path(socket).stroke(); } function drawArrow(nv, size, isOpen, end) { const x = (s) => add(end, mult(nv, s * size)); const y = (s) => mult(rot(nv), s * size); var arrow = [add(x(10), y(4)), x(isOpen && !config.fillArrows ? 5 : 10), add(x(10), y(-4)), end]; g.fillStyle(isOpen ? config.stroke : config.fill[0]); g.circuit(arrow).fillAndStroke(); } function drawDiamond(nv, size, isOpen, end) { const x = (s) => add(end, mult(nv, s * size)); const y = (s) => mult(rot(nv), s * size); var arrow = [add(x(7), y(4)), x(14), add(x(7), y(-4)), end]; g.save(); g.fillStyle(isOpen ? config.stroke : config.fill[0]); g.circuit(arrow).fillAndStroke(); g.restore(); } } function render(graphics, config, compartment) { var g = graphics; function renderCompartment(compartment, color, style, level) { g.save(); g.translate(compartment.offset.x, compartment.offset.y); g.fillStyle(color || config.stroke); compartment.lines.forEach((text, i) => { g.textAlign(style.center ? 'center' : 'left'); var x = style.center ? compartment.width / 2 - config.padding : 0; var y = (0.5 + (i + 0.5) * config.leading) * config.fontSize; if (text) { g.fillText(text, x, y); } if (style.underline) { var w = g.measureText(text).width; y += Math.round(config.fontSize * 0.2) + 0.5; if (style.center) { g.path([ { x: x - w / 2, y: y }, { x: x + w / 2, y: y }, ]).stroke(); } else { g.path([ { x: x, y: y }, { x: x + w, y: y }, ]).stroke(); } g.lineWidth(config.lineWidth); } }); g.save(); g.translate(config.gutter, config.gutter); compartment.relations.forEach((r) => renderRelation(r)); compartment.nodes.forEach((n) => renderNode(n, level)); g.restore(); g.restore(); } function renderNode(node, level) { var x = Math.round(node.x - node.width / 2); var y = Math.round(node.y - node.height / 2); var style = config.styles[node.type] || styles.CLASS; g.save(); g.setData('name', node.name); g.save(); g.fillStyle(style.fill || config.fill[level] || last(config.fill)); g.strokeStyle(style.stroke || config.stroke); if (style.dashed) { var dash = Math.max(4, 2 * config.lineWidth); g.setLineDash([dash, dash]); } var drawNode = visualizers[style.visual] || visualizers.class; drawNode(node, x, y, config, g); // Draw optional left image area if (node.imageUrl) { var imgSize = node.height; // square size var rImg = Math.min(6, node.height/4); if (g.image) { g.image(node.imageUrl, x, y, imgSize, imgSize, rImg); } } // Optional thicker coloured left border following rounded corners if (node.borderColour) { var r = Math.min(6, node.height/4); g.save(); g.strokeStyle(node.borderColour); g.lineWidth(config.lineWidth * 2); if (g.beginPath && g.arcTo) { g.beginPath(); g.moveTo(x + r, y); g.arcTo(x, y, x, y + r, r); g.lineTo(x, y + node.height - r); g.arcTo(x, y + node.height, x + r, y + node.height, r); g.stroke(); } else { // Fallback simple line g.path([ {x: x, y: y}, {x: x, y: y + node.height} ]).stroke(); } g.restore(); } for (var divider of node.dividers) { g.path(divider.map((e) => add(e, { x, y }))).stroke(); } g.restore(); node.compartments.forEach((part, i) => { var textStyle = i == 0 ? style.title : style.body; g.save(); g.translate(x + part.x, y + part.y); g.setFont(config.font, config.fontSize, textStyle.bold ? 'bold' : 'normal', textStyle.italic ? 'italic' : 'normal'); renderCompartment(part, style.stroke, textStyle, level + 1); g.restore(); }); g.restore(); } function strokePath(p) { if (config.edges === 'rounded') { var radius = config.spacing * config.bendSize; g.beginPath(); g.moveTo(p[0].x, p[0].y); for (var i = 1; i < p.length - 1; i++) { g.arcTo(p[i].x, p[i].y, p[i + 1].x, p[i + 1].y, radius); } g.lineTo(last(p).x, last(p).y); g.stroke(); } else g.path(p).stroke(); } function renderLabel(label) { if (!label || !label.text) return; var fontSize = config.fontSize; var lines = label.text.split('`'); lines.forEach((l, i) => g.fillText(l, label.x, label.y + fontSize * (i + 1))); } function renderRelation(r) { // Start a group for the entire connector so we can tag it for interaction g.save(); if (g.setData) g.setData('connector','1'); var path = getPath(config, r); g.fillStyle(config.stroke); g.setFont(config.font, config.fontSize, 'normal', 'normal'); renderLabel(r.startLabel); renderLabel(r.endLabel); if (r.assoc !== '-/-' && r.assoc !== '_/_') { if (hasSubstring(r.assoc, '--') || hasSubstring(r.assoc, '__')) { var dash = Math.max(4, 2 * config.lineWidth); g.save(); g.setLineDash([dash, dash]); strokePath(path); g.restore(); } else strokePath(path); } drawTerminators(g, config, r); // Add invisible wider hit path to ease clicking (approximate with straight segments) try { if (g.path) { var hit = g.path(path).stroke(); // For canvas this is a no-op customization; for SVG we can mutate attributes if (hit && hit.attr) { hit.attr['stroke'] = 'transparent'; hit.attr['fill'] = 'none'; hit.attr['stroke-width'] = Math.max(10, config.lineWidth + 10); // ~5px each side extra hit.attr['pointer-events'] = 'stroke'; hit.attr['class'] = (hit.attr['class'] ? hit.attr['class'] + ' ' : '') + 'connector-hit'; } } } catch(e) { /* ignore */ } g.restore(); } function snapToPixels() { if (config.lineWidth % 2 === 1) g.translate(0.5, 0.5); } function setBackground() { g.clear(); g.save(); g.strokeStyle('transparent'); g.fillStyle(config.background); g.rect(0, 0, compartment.width, compartment.height).fill(); g.restore(); } g.save(); g.scale(config.zoom, config.zoom); snapToPixels(); setBackground(); g.setFont(config.font, config.fontSize, 'bold', 'normal'); g.lineWidth(config.lineWidth); g.lineJoin('round'); g.lineCap('round'); g.strokeStyle(config.stroke); renderCompartment(compartment, undefined, buildStyle({}, {}).title, 0); g.restore(); } function GraphicsCanvas(canvas, callbacks) { var ctx = canvas.getContext('2d'); var mousePos = { x: 0, y: 0 }; var twopi = 2 * 3.1416; function mouseEventToPos(event) { var e = canvas; return { x: event.clientX - e.getBoundingClientRect().left - e.clientLeft + e.scrollLeft, y: event.clientY - e.getBoundingClientRect().top - e.clientTop + e.scrollTop, }; } if (callbacks) { canvas.addEventListener('mousedown', function (event) { if (callbacks.mousedown) callbacks.mousedown(mouseEventToPos(event)); }); canvas.addEventListener('mouseup', function (event) { if (callbacks.mouseup) callbacks.mouseup(mouseEventToPos(event)); }); canvas.addEventListener('mousemove', function (event) { mousePos = mouseEventToPos(event); if (callbacks.mousemove) callbacks.mousemove(mouseEventToPos(event)); }); } var chainable = { stroke: function () { ctx.stroke(); return chainable; }, fill: function () { ctx.fill(); return chainable; }, fillAndStroke: function () { ctx.fill(); ctx.stroke(); return chainable; }, }; function tracePath(path, offset, s) { s = s === undefined ? 1 : s; offset = offset || { x: 0, y: 0 }; ctx.beginPath(); ctx.moveTo(offset.x + s * path[0].x, offset.y + s * path[0].y); for (var i = 1, len = path.length; i < len; i++) ctx.lineTo(offset.x + s * path[i].x, offset.y + s * path[i].y); return chainable; } return { mousePos: function () { return mousePos; }, width: function () { return canvas.width; }, height: function () { return canvas.height; }, clear: function () { ctx.clearRect(0, 0, canvas.width, canvas.height); }, circle: function (p, r) { ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, twopi); return chainable; }, image: function (url, x, y, w, h, r) { var img = new Image(); img.onload = function(){ try { if (r && ctx.beginPath) { ctx.save(); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); ctx.clip(); ctx.drawImage(img, x, y, w, h); ctx.restore(); } else { ctx.drawImage(img, x, y, w, h); } } catch(e){} }; img.src = url; return chainable; }, ellipse: function (center, rx, ry, start, stop) { if (start === undefined) start = 0; if (stop === undefined) stop = twopi; ctx.beginPath(); ctx.save(); ctx.translate(center.x, center.y); ctx.scale(1, ry / rx); ctx.arc(0, 0, rx / 2, start, stop); ctx.restore(); return chainable; }, arc: function (x, y, r, start, stop) { ctx.beginPath(); ctx.moveTo(x, y); ctx.arc(x, y, r, start, stop); return chainable; }, roundRect: function (x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); return chainable; }, rect: function (x, y, w, h) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + w, y); ctx.lineTo(x + w, y + h); ctx.lineTo(x, y + h); ctx.closePath(); return chainable; }, path: tracePath, circuit: function (path, offset, s) { tracePath(path, offset, s); ctx.closePath(); return chainable; }, setFont: function (family, size, weight, style) { ctx.font = `${weight} ${style} ${size}pt ${family}, Helvetica, sans-serif`; }, fillStyle: function (s) { ctx.fillStyle = s; }, strokeStyle: function (s) { ctx.strokeStyle = s; }, textAlign: function (a) { ctx.textAlign = a; }, lineCap: function (cap) { ctx.lineCap = cap; }, lineJoin: function (join) { ctx.lineJoin = join; }, lineWidth: function (w) { ctx.lineWidth = w; }, arcTo: function () { return ctx.arcTo.apply(ctx, arguments); }, beginPath: function () { return ctx.beginPath.apply(ctx, arguments); }, fillText: function () { return ctx.fillText.apply(ctx, arguments); }, lineTo: function () { return ctx.lineTo.apply(ctx, arguments); }, measureText: function () { return ctx.measureText.apply(ctx, arguments); }, moveTo: function () { return ctx.moveTo.apply(ctx, arguments); }, restore: function () { return ctx.restore.apply(ctx, arguments); }, setData: function (name, value) { /* canvas mode: no-op */ }, save: function () { return ctx.save.apply(ctx, arguments); }, scale: function () { return ctx.scale.apply(ctx, arguments); }, setLineDash: function () { return ctx.setLineDash.apply(ctx, arguments); }, stroke: function () { return ctx.stroke.apply(ctx, arguments); }, translate: function () { return ctx.translate.apply(ctx, arguments); }, }; } function toAttrString(obj) { return Object.entries(obj) .filter(([_, val]) => val !== undefined) .map(([key, val]) => `${key}="${xmlEncode(val)}"`) .join(' '); } function xmlEncode(str) { if ('number' === typeof str) return str.toFixed(1); return (str !== null && str !== void 0 ? str : '') .toString() .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } var charWidths = { "0": 10, "1": 10, "2": 10, "3": 10, "4": 10, "5": 10, "6": 10, "7": 10, "8": 10, "9": 10, " ": 5, "!": 5, "\"": 6, "#": 10, "$": 10, "%": 15, "&": 11, "'": 4, "(": 6, ")": 6, "*": 7, "+": 10, ",": 5, "-": 6, ".": 5, "/": 5, ":": 5, ";": 5, "<": 10, "=": 10, ">": 10, "?": 10, "@": 17, "A": 11, "B": 11, "C": 12, "D": 12, "E": 11, "F": 10, "G": 13, "H": 12, "I": 5, "J": 9, "K": 11, "L": 10, "M": 14, "N": 12, "O": 13, "P": 11, "Q": 13, "R": 12, "S": 11, "T": 10, "U": 12, "V": 11, "W": 16, "X": 11, "Y": 11, "Z": 10, "[": 5, "\\": 5, "]": 5, "^": 8, "_": 10, "`": 6, "a": 10, "b": 10, "c": 9, "d": 10, "e": 10, "f": 5, "g": 10, "h": 10, "i": 4, "j": 4, "k": 9, "l": 4, "m": 14, "n": 10, "o": 10, "p": 10, "q": 10, "r": 6, "s": 9, "t": 5, "u": 10, "v": 9, "w": 12, "x": 9, "y": 9, "z": 9, "{": 6, "|": 5, "}": 6, "~": 10 }; function GraphicsSvg(document) { var initialState = { stroke: undefined, 'stroke-width': 1, 'stroke-dasharray': undefined, 'stroke-linecap': undefined, 'stroke-linejoin': undefined, 'text-align': 'left', font: '12pt Helvetica, Arial, sans-serif', 'font-size': '12pt', }; var measurementCanvas = document ? document.createElement('canvas') : null; var ctx = measurementCanvas ? measurementCanvas.getContext('2d') : null; class Element { constructor(name, attr, parent, text) { this.elideEmpty = false; this.name = name; this.attr = attr; this.parent = parent; this.children = []; this.text = text || undefined; } stroke() { this.attr.fill = 'none'; return this; } fill() { this.attr.stroke = 'none'; return this; } fillAndStroke() { return this; } group() { return this.parent; } serialize() { var _a; const data = (_a = getDefined(this.group(), (e) => e.data)) !== null && _a !== void 0 ? _a : {}; const attrs = toAttrString(Object.assign(Object.assign({}, this.attr), data)); const content = this.children.map((o) => o.serialize()).join('\n'); if (this.text && this.children.length === 0) return `<${this.name} ${attrs}>${xmlEncode(this.text)}`; else if (this.children.length === 0) return this.elideEmpty ? '' : `<${this.name} ${attrs}>`; else return `<${this.name} ${attrs}> ${content.replace(/\n/g, '\n\t')} `; } } function getDefined(group, getter) { var _a, _b; if (!group) return getter(syntheticRoot); return (_b = (_a = getter(group)) !== null && _a !== void 0 ? _a : getDefined(group.parent, getter)) !== null && _b !== void 0 ? _b : getter(syntheticRoot); } class GroupElement extends Element { constructor(parent) { super('g', {}, parent); this.elideEmpty = true; } group() { return this; } } const syntheticRoot = new GroupElement({}); syntheticRoot.attr = initialState; var root = new Element('svg', { version: '1.1', baseProfile: 'full', xmlns: 'http://www.w3.org/2000/svg', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xmlns:ev': 'http://www.w3.org/2001/xml-events', }, undefined); var current = new GroupElement(root); current.attr = initialState; root.children.push(current); var inPathBuilderMode = false; function tracePath(path, offset = { x: 0, y: 0 }, s = 1) { var d = path .map((e, i) => (i ? 'L' : 'M') + (offset.x + s * e.x).toFixed(1) + ' ' + (offset.y + s * e.y).toFixed(1)) .join(' '); return el('path', { d: d }); } function el(type, attr, text) { var element = new Element(type, attr, current, text); current.children.push(element); return element; } return { width: function () { return 0; }, height: function () { return 0; }, clear: function () { }, circle: function (p, r) { return el('circle', { r: r, cx: p.x, cy: p.y }); }, ellipse: function (center, w, h, start = 0, stop = 0) { if (start || stop) { var path = range([start, stop], 64).map((a) => add(center, { x: (Math.cos(a) * w) / 2, y: (Math.sin(a) * h) / 2 })); return tracePath(path); } else { return el('ellipse', { cx: center.x, cy: center.y, rx: w / 2, ry: h / 2 }); } }, image: function (url, x, y, w, h, r) { if (!r) return el('image', { href: url, x: x, y: y, width: w, height: h, preserveAspectRatio: 'xMidYMid slice' }); // Define a unique clipPath id var clipId = 'clip_'+Math.random().toString(36).substring(2,9); // Create clipPath with rounded rect path var defs = new Element('defs', {}, current.parent || current); var clip = new Element('clipPath', { id: clipId }, defs); var rectEl = new Element('rect', { x: x, y: y, width: w, height: h, rx: r, ry: r }, clip); defs.children.push(clip); clip.children.push(rectEl); // Attach defs if not already (current.parent || current).children.unshift(defs); return el('image', { href: url, x: x, y: y, width: w, height: h, preserveAspectRatio: 'xMidYMid slice', 'clip-path': 'url(#'+clipId+')' }); }, arc: function (cx, cy, r) { return el('ellipse', { cx, cy, rx: r, ry: r }); }, roundRect: function (x, y, width, height, r) { return el('rect', { x, y, rx: r, ry: r, height, width }); }, rect: function (x, y, width, height) { return el('rect', { x, y, height, width }); }, path: tracePath, circuit: function (path, offset, s) { var element = tracePath(path, offset, s); element.attr.d += ' Z'; return element; }, setFont: function (family, size, weight, style) { current.attr['font-family'] = family; current.attr['font-size'] = size + 'pt'; current.attr['font-weight'] = weight; current.attr['font-style'] = style; }, strokeStyle: function (stroke) { current.attr.stroke = stroke; }, fillStyle: function (fill) { current.attr.fill = fill; }, arcTo: function (x1, y1, x2, y2) { if (inPathBuilderMode) last(current.children).attr.d += 'L' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2 + ' '; else throw new Error('can only be called after .beginPath()'); }, beginPath: function () { inPathBuilderMode = true; return el('path', { d: '' }); }, fillText: function (text, x, y) { return el('text', { x, y, stroke: 'none', font: undefined, style: undefined, 'text-anchor': getDefined(current, (e) => e.attr['text-align']) === 'center' ? 'middle' : undefined, }, text); }, lineCap: function (cap) { current.attr['stroke-linecap'] = cap; }, lineJoin: function (join) { current.attr['stroke-linejoin'] = join; }, lineTo: function (x, y) { if (inPathBuilderMode) last(current.children).attr.d += 'L' + x.toFixed(1) + ' ' + y.toFixed(1) + ' '; else throw new Error('can only be called after .beginPath()'); return current; }, lineWidth: function (w) { current.attr['stroke-width'] = w; }, measureText: function (s) { if (ctx) { if (current) ctx.font = `${getDefined(current, (e) => e.attr['font-weight'])} ${getDefined(current, (e) => e.attr['font-style'])} ${getDefined(current, (e) => e.attr['font-size'])} ${getDefined(current, (e) => e.attr['font-family'])}`; else ctx.font = `${initialState['font-weight']} ${initialState['font-style']} ${initialState['font-size']} ${initialState['font-family']}`; return ctx.measureText(s); } else { return { width: sum(s, function (c) { var _a, _b; const size = (_a = getDefined(current, (e) => e.attr['font-size'])) !== null && _a !== void 0 ? _a : 12; var scale = parseInt(size.toString()) / 12; return ((_b = charWidths[c]) !== null && _b !== void 0 ? _b : 16) * scale; }), }; } }, moveTo: function (x, y) { if (inPathBuilderMode) last(current.children).attr.d += 'M' + x.toFixed(1) + ' ' + y.toFixed(1) + ' '; else throw new Error('can only be called after .beginPath()'); }, restore: function () { if (current.parent) current = current.parent; }, save: function () { var node = new GroupElement(current); current.children.push(node); current = node; }, setData: function (name, value) { // Write directly as attribute so it survives serialization on the current group element current.attr['data-' + name] = value; }, scale: function () { }, setLineDash: function (d) { current.attr['stroke-dasharray'] = d.length === 0 ? 'none' : d[0] + ' ' + d[1]; }, stroke: function () { inPathBuilderMode = false; last(current.children).stroke(); }, textAlign: function (a) { current.attr['text-align'] = a; }, translate: function (dx, dy) { if (Number.isNaN(dx) || Number.isNaN(dy)) { throw new Error('dx and dy must be real numbers'); } current.attr.transform = `translate(${dx}, ${dy})`; }, serialize: function (size, desc, title) { if (desc) { root.children.unshift(new Element('desc', {}, undefined, desc)); } if (title) { root.children.unshift(new Element('title', {}, undefined, title)); } root.attr = { version: '1.1', baseProfile: 'full', width: size.width, height: size.height, viewbox: '0 0 ' + size.width + ' ' + size.height, xmlns: 'http://www.w3.org/2000/svg', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xmlns:ev': 'http://www.w3.org/2001/xml-events', }; return root.serialize(); }, }; } function fitCanvasSize(canvas, rect, zoom) { canvas.width = rect.width * zoom; canvas.height = rect.height * zoom; } function createMeasurer(config, graphics) { return { setFont(family, size, weight, style) { graphics.setFont(family, size, weight, style); }, textWidth(s) { return graphics.measureText(s).width; }, textHeight() { return config.leading * config.fontSize; }, }; } function parseAndRender(code, graphics, canvas, scale) { var parsedDiagram = parse(code); var config = parsedDiagram.config; var measurer = createMeasurer(config, graphics); var graphLayout = layout(measurer, config, parsedDiagram.root); if (canvas) { fitCanvasSize(canvas, graphLayout, config.zoom * scale); } config.zoom *= scale; render(graphics, config, graphLayout); return { config: config, layout: graphLayout }; } function draw(canvas, code, scale) { return parseAndRender(code, GraphicsCanvas(canvas), canvas, scale || 1); } function renderSvg(code, document) { var skCanvas = GraphicsSvg(document); var { config, layout } = parseAndRender(code, skCanvas, null, 1); return skCanvas.serialize({ width: layout.width, height: layout.height, }, code, config.title); } class ImportDepthError extends Error { constructor() { super('max_import_depth exceeded'); } } function processAsyncImports(source, loadFile, maxImportDepth = 10) { return __awaiter(this, void 0, void 0, function* () { if (maxImportDepth == -1) { throw new ImportDepthError(); } function lenientLoadFile(key) { return __awaiter(this, void 0, void 0, function* () { try { return (yield loadFile(key)) || ''; } catch (e) { return ''; } }); } var imports = []; source.replace(/#import: *(.*)/g, (a, file) => { var promise = lenientLoadFile(file).then((contents) => processAsyncImports(contents, loadFile, maxImportDepth - 1)); imports.push({ file, promise }); return ''; }); var imported = {}; for (var imp of imports) { imported[imp.file] = yield imp.promise; } return source.replace(/#import: *(.*)/g, (a, file) => imported[file]); }); } function processImports(source, loadFile, maxImportDepth = 10) { if (maxImportDepth == -1) { throw new ImportDepthError(); } function lenientLoadFile(key) { try { return loadFile(key) || ''; } catch (e) { return ''; } } return source.replace(/#import: *(.*)/g, (a, file) => processImports(lenientLoadFile(file), loadFile, maxImportDepth - 1)); } function compileFile(filepath, maxImportDepth) { var fs = require('fs'); var path = require('path'); var directory = path.dirname(filepath); var rootFileName = filepath.substr(directory.length); function loadFile(filename) { return fs.readFileSync(path.join(directory, filename), { encoding: 'utf8' }); } return processImports(loadFile(rootFileName), loadFile, maxImportDepth); } var version = '1.5.3'; exports.Classifier = Classifier; exports.Compartment = Compartment; exports.ImportDepthError = ImportDepthError; exports.Relation = Relation; exports.compileFile = compileFile; exports.draw = draw; exports.intermediateParse = intermediateParse; exports.layout = layout; exports.parse = parse; exports.processAsyncImports = processAsyncImports; exports.processImports = processImports; exports.renderSvg = renderSvg; exports.skanaar = util; exports.styles = styles; exports.transformParseIntoSyntaxTree = transformParseIntoSyntaxTree; exports.version = version; exports.visualizers = visualizers; Object.defineProperty(exports, '__esModule', { value: true }); }));