├── dagre.js ├── fields.js ├── index.html ├── nomnoml.js └── tealview.js /fields.js: -------------------------------------------------------------------------------- 1 | var fieldNames = { 2 | "Global": { 3 | "GroupSize": false 4 | }, 5 | "Transaction": { 6 | "TypeEnum": false, 7 | "GroupIndex": false, 8 | "Header": { 9 | "Sender": false, 10 | "Fee": false, 11 | "FirstValid": false, 12 | "LastValid": false, 13 | "Note": false, 14 | "GenesisID": false, 15 | "GenesisHash": false, 16 | "Group": false, 17 | "Lease": false 18 | }, 19 | "KeyregTxnFields": { 20 | "VotePK": false, 21 | "SelectionPK": false, 22 | "VoteFirst": false, 23 | "VoteLast": false, 24 | "VoteKeyDilution": false, 25 | "Nonparticipation": false 26 | }, 27 | "PaymentTxnFields": { 28 | "Receiver": false, 29 | "Amount": false, 30 | "CloseRemainderTo": false 31 | }, 32 | "AssetConfigTxnFields": { 33 | "ConfigAsset": false, 34 | "AssetParams": { 35 | "Total": false, 36 | "DefaultFrozen": false, 37 | "UnitName": false, 38 | "AssetName": false, 39 | "URL": false, 40 | "MetadataHash": false, 41 | "Manager": false, 42 | "Reserve": false, 43 | "Freeze": false, 44 | "Clawback": false 45 | } 46 | }, 47 | "AssetTransferTxnFields": { 48 | "XferAsset": false, 49 | "AssetAmount": false, 50 | "AssetSender": false, 51 | "AssetReceiver": false, 52 | "AssetCloseTo": false 53 | }, 54 | "AssetFreezeTxnFields": { 55 | "FreezeAccount": false, 56 | "FreezeAsset": false, 57 | "AssetFrozen": false 58 | }, 59 | "ApplicationCallTxnFields": { 60 | "ApplicationID": false, 61 | "OnCompletion": false, 62 | "ApplicationArgs": false, 63 | "Accounts": false, 64 | "ApprovalProgram": false, 65 | "ClearStateProgram": false 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /nomnoml.js: -------------------------------------------------------------------------------- 1 | ;(function (factoryFn) { 2 | if (typeof module === 'object' && module.exports) 3 | module.exports = factoryFn(require('dagre')); 4 | else this.nomnoml = factoryFn(dagre); 5 | })(function (dagre) { 6 | var nomnoml; 7 | (function (nomnoml) { 8 | function buildStyle(conf) { 9 | return { 10 | bold: conf.bold || false, 11 | underline: conf.underline || false, 12 | italic: conf.italic || false, 13 | dashed: conf.dashed || false, 14 | empty: conf.empty || false, 15 | center: conf.center || false, 16 | fill: conf.fill || undefined, 17 | stroke: conf.stroke || undefined, 18 | visual: conf.visual || 'class', 19 | direction: conf.direction || undefined, 20 | hull: conf.hull || 'auto' 21 | }; 22 | } 23 | nomnoml.buildStyle = buildStyle; 24 | var Compartment = (function () { 25 | function Compartment(lines, nodes, relations) { 26 | this.lines = lines; 27 | this.nodes = nodes; 28 | this.relations = relations; 29 | } 30 | return Compartment; 31 | }()); 32 | nomnoml.Compartment = Compartment; 33 | var Relation = (function () { 34 | function Relation() { 35 | } 36 | return Relation; 37 | }()); 38 | nomnoml.Relation = Relation; 39 | var Classifier = (function () { 40 | function Classifier(type, name, compartments) { 41 | this.type = type; 42 | this.name = name; 43 | this.compartments = compartments; 44 | } 45 | return Classifier; 46 | }()); 47 | nomnoml.Classifier = Classifier; 48 | })(nomnoml || (nomnoml = {})); 49 | var nomnoml; 50 | (function (nomnoml) { 51 | function layout(measurer, config, ast) { 52 | function measureLines(lines, fontWeight) { 53 | if (!lines.length) 54 | return { width: 0, height: config.padding }; 55 | measurer.setFont(config, fontWeight, 'normal'); 56 | return { 57 | width: Math.round(nomnoml.skanaar.max(lines.map(measurer.textWidth)) + 2 * config.padding), 58 | height: Math.round(measurer.textHeight() * lines.length + 2 * config.padding) 59 | }; 60 | } 61 | function layoutCompartment(c, compartmentIndex, style) { 62 | var textSize = measureLines(c.lines, compartmentIndex ? 'normal' : 'bold'); 63 | c.width = textSize.width; 64 | c.height = textSize.height; 65 | if (!c.nodes.length && !c.relations.length) 66 | return; 67 | c.nodes.forEach(layoutClassifier); 68 | var g = new dagre.graphlib.Graph(); 69 | g.setGraph({ 70 | rankdir: style.direction || config.direction, 71 | nodesep: config.spacing, 72 | edgesep: config.spacing, 73 | ranksep: config.spacing, 74 | acyclicer: config.acyclicer, 75 | ranker: config.ranker 76 | }); 77 | c.nodes.forEach(function (e) { 78 | g.setNode(e.name, { width: e.layoutWidth, height: e.layoutHeight }); 79 | }); 80 | c.relations.forEach(function (r) { 81 | g.setEdge(r.start, r.end, { id: r.id }); 82 | }); 83 | dagre.layout(g); 84 | var rels = nomnoml.skanaar.indexBy(c.relations, 'id'); 85 | var nodes = nomnoml.skanaar.indexBy(c.nodes, 'name'); 86 | function toPoint(o) { return { x: o.x, y: o.y }; } 87 | g.nodes().forEach(function (name) { 88 | var node = g.node(name); 89 | nodes[name].x = node.x; 90 | nodes[name].y = node.y; 91 | }); 92 | g.edges().forEach(function (edgeObj) { 93 | var edge = g.edge(edgeObj); 94 | var start = nodes[edgeObj.v]; 95 | var end = nodes[edgeObj.w]; 96 | rels[edge.id].path = nomnoml.skanaar.flatten([[start], edge.points, [end]]).map(toPoint); 97 | }); 98 | var graph = g.graph(); 99 | var graphHeight = graph.height ? graph.height + 2 * config.gutter : 0; 100 | var graphWidth = graph.width ? graph.width + 2 * config.gutter : 0; 101 | c.width = Math.max(textSize.width, graphWidth) + 2 * config.padding; 102 | c.height = textSize.height + graphHeight + config.padding; 103 | } 104 | function layoutClassifier(clas) { 105 | var layout = getLayouter(clas); 106 | layout(clas); 107 | clas.layoutWidth = clas.width + 2 * config.edgeMargin; 108 | clas.layoutHeight = clas.height + 2 * config.edgeMargin; 109 | } 110 | function getLayouter(clas) { 111 | var style = config.styles[clas.type] || nomnoml.styles.CLASS; 112 | switch (style.hull) { 113 | case 'icon': return function (clas) { 114 | clas.width = config.fontSize * 2.5; 115 | clas.height = config.fontSize * 2.5; 116 | }; 117 | case 'empty': return function (clas) { 118 | clas.width = 0; 119 | clas.height = 0; 120 | }; 121 | default: return function (clas) { 122 | clas.compartments.forEach(function (co, i) { layoutCompartment(co, i, style); }); 123 | clas.width = nomnoml.skanaar.max(clas.compartments, 'width'); 124 | clas.height = nomnoml.skanaar.sum(clas.compartments, 'height'); 125 | clas.x = clas.layoutWidth / 2; 126 | clas.y = clas.layoutHeight / 2; 127 | clas.compartments.forEach(function (co) { co.width = clas.width; }); 128 | }; 129 | } 130 | } 131 | layoutCompartment(ast, 0, nomnoml.styles.CLASS); 132 | return ast; 133 | } 134 | nomnoml.layout = layout; 135 | })(nomnoml || (nomnoml = {})); 136 | var nomnoml; 137 | (function (nomnoml) { 138 | function fitCanvasSize(canvas, rect, zoom) { 139 | canvas.width = rect.width * zoom; 140 | canvas.height = rect.height * zoom; 141 | } 142 | function setFont(config, isBold, isItalic, graphics) { 143 | var style = (isBold === 'bold' ? 'bold' : ''); 144 | if (isItalic) 145 | style = 'italic ' + style; 146 | var defaultFont = 'Helvetica, sans-serif'; 147 | var font = nomnoml.skanaar.format('# #pt #, #', style, config.fontSize, config.font, defaultFont); 148 | graphics.font(font); 149 | } 150 | function parseAndRender(code, graphics, canvas, scale) { 151 | var parsedDiagram = nomnoml.parse(code); 152 | var config = parsedDiagram.config; 153 | var measurer = { 154 | setFont: function (conf, bold, ital) { 155 | setFont(conf, bold, ital, graphics); 156 | }, 157 | textWidth: function (s) { return graphics.measureText(s).width; }, 158 | textHeight: function () { return config.leading * config.fontSize; } 159 | }; 160 | var layout = nomnoml.layout(measurer, config, parsedDiagram.root); 161 | fitCanvasSize(canvas, layout, config.zoom * scale); 162 | config.zoom *= scale; 163 | nomnoml.render(graphics, config, layout, measurer.setFont); 164 | return { config: config }; 165 | } 166 | nomnoml.version = '0.6.2'; 167 | function draw(canvas, code, scale) { 168 | return parseAndRender(code, nomnoml.skanaar.Canvas(canvas), canvas, scale || 1); 169 | } 170 | nomnoml.draw = draw; 171 | function renderSvg(code, docCanvas) { 172 | var parsedDiagram = nomnoml.parse(code); 173 | var config = parsedDiagram.config; 174 | var skCanvas = nomnoml.skanaar.Svg('', docCanvas); 175 | function setFont(config, isBold, isItalic) { 176 | var style = (isBold === 'bold' ? 'bold' : ''); 177 | if (isItalic) 178 | style = 'italic ' + style; 179 | var defFont = 'Helvetica, sans-serif'; 180 | var template = 'font-weight:#; font-size:#pt; font-family:\'#\', #'; 181 | var font = nomnoml.skanaar.format(template, style, config.fontSize, config.font, defFont); 182 | skCanvas.font(font); 183 | } 184 | var measurer = { 185 | setFont: function (conf, bold, ital) { 186 | setFont(conf, bold, ital); 187 | }, 188 | textWidth: function (s) { return skCanvas.measureText(s).width; }, 189 | textHeight: function () { return config.leading * config.fontSize; } 190 | }; 191 | var layout = nomnoml.layout(measurer, config, parsedDiagram.root); 192 | nomnoml.render(skCanvas, config, layout, measurer.setFont); 193 | return skCanvas.serialize({ 194 | width: layout.width, 195 | height: layout.height 196 | }, code, config.title); 197 | } 198 | nomnoml.renderSvg = renderSvg; 199 | })(nomnoml || (nomnoml = {})); 200 | var nomnoml; 201 | (function (nomnoml) { 202 | var Line = (function () { 203 | function Line() { 204 | } 205 | return Line; 206 | }()); 207 | function parse(source) { 208 | function onlyCompilables(line) { 209 | var ok = line[0] !== '#' && line.trim().substring(0, 2) !== '//'; 210 | return ok ? line.trim() : ''; 211 | } 212 | function isDirective(line) { return line.text[0] === '#'; } 213 | var lines = source.split('\n').map(function (s, i) { 214 | return { text: s, index: i }; 215 | }); 216 | var pureDirectives = lines.filter(isDirective); 217 | var directives = {}; 218 | pureDirectives.forEach(function (line) { 219 | try { 220 | var tokens = line.text.substring(1).split(':'); 221 | directives[tokens[0].trim()] = tokens[1].trim(); 222 | } 223 | catch (e) { 224 | throw new Error('line ' + (line.index + 1)); 225 | } 226 | }); 227 | var pureDiagramCode = lines.map(function (e) { return onlyCompilables(e.text); }).join('\n').trim(); 228 | var parseTree = nomnoml.intermediateParse(pureDiagramCode); 229 | return { 230 | root: nomnoml.transformParseIntoSyntaxTree(parseTree), 231 | config: getConfig(directives) 232 | }; 233 | function directionToDagre(word) { 234 | if (word == 'down') 235 | return 'TB'; 236 | if (word == 'right') 237 | return 'LR'; 238 | else 239 | return 'TB'; 240 | } 241 | function parseRanker(word) { 242 | if (word == 'network-simplex' || word == 'tight-tree' || word == 'longest-path') { 243 | return word; 244 | } 245 | return 'network-simplex'; 246 | } 247 | function parseCustomStyle(styleDef) { 248 | var contains = nomnoml.skanaar.hasSubstring; 249 | return { 250 | bold: contains(styleDef, 'bold'), 251 | underline: contains(styleDef, 'underline'), 252 | italic: contains(styleDef, 'italic'), 253 | dashed: contains(styleDef, 'dashed'), 254 | empty: contains(styleDef, 'empty'), 255 | center: nomnoml.skanaar.last(styleDef.match('align=([^ ]*)') || []) == 'left' ? false : true, 256 | fill: nomnoml.skanaar.last(styleDef.match('fill=([^ ]*)') || []), 257 | stroke: nomnoml.skanaar.last(styleDef.match('stroke=([^ ]*)') || []), 258 | visual: nomnoml.skanaar.last(styleDef.match('visual=([^ ]*)') || []) || 'class', 259 | direction: directionToDagre(nomnoml.skanaar.last(styleDef.match('direction=([^ ]*)') || [])), 260 | hull: 'auto' 261 | }; 262 | } 263 | function getConfig(d) { 264 | var userStyles = {}; 265 | for (var key in d) { 266 | if (key[0] != '.') 267 | continue; 268 | var styleDef = d[key]; 269 | userStyles[key.substring(1).toUpperCase()] = parseCustomStyle(styleDef); 270 | } 271 | return { 272 | arrowSize: +d.arrowSize || 1, 273 | bendSize: +d.bendSize || 0.3, 274 | direction: directionToDagre(d.direction), 275 | gutter: +d.gutter || 5, 276 | edgeMargin: (+d.edgeMargin) || 0, 277 | edges: d.edges == 'hard' ? 'hard' : 'rounded', 278 | fill: (d.fill || '#eee8d5;#fdf6e3;#eee8d5;#fdf6e3').split(';'), 279 | fillArrows: d.fillArrows === 'true', 280 | font: d.font || 'Calibri', 281 | fontSize: (+d.fontSize) || 12, 282 | leading: (+d.leading) || 1.25, 283 | lineWidth: (+d.lineWidth) || 3, 284 | padding: (+d.padding) || 8, 285 | spacing: (+d.spacing) || 40, 286 | stroke: d.stroke || '#33322E', 287 | title: d.title || 'nomnoml', 288 | zoom: +d.zoom || 1, 289 | acyclicer: d.acyclicer === 'greedy' ? 'greedy' : undefined, 290 | ranker: parseRanker(d.ranker), 291 | styles: nomnoml.skanaar.merged(nomnoml.styles, userStyles) 292 | }; 293 | } 294 | } 295 | nomnoml.parse = parse; 296 | function intermediateParse(source) { 297 | return nomnomlCoreParser.parse(source); 298 | } 299 | nomnoml.intermediateParse = intermediateParse; 300 | function transformParseIntoSyntaxTree(entity) { 301 | function isAstClassifier(obj) { 302 | return obj.parts !== undefined; 303 | } 304 | function isAstRelation(obj) { 305 | return obj.assoc !== undefined; 306 | } 307 | function isAstCompartment(obj) { 308 | return Array.isArray(obj); 309 | } 310 | var relationId = 0; 311 | function transformCompartment(slots) { 312 | var lines = []; 313 | var rawClassifiers = []; 314 | var relations = []; 315 | slots.forEach(function (p) { 316 | if (typeof p === 'string') 317 | lines.push(p); 318 | if (isAstRelation(p)) { 319 | rawClassifiers.push(p.start); 320 | rawClassifiers.push(p.end); 321 | relations.push({ 322 | id: relationId++, 323 | assoc: p.assoc, 324 | start: p.start.parts[0][0], 325 | end: p.end.parts[0][0], 326 | startLabel: p.startLabel, 327 | endLabel: p.endLabel 328 | }); 329 | } 330 | if (isAstClassifier(p)) { 331 | rawClassifiers.push(p); 332 | } 333 | }); 334 | var allClassifiers = rawClassifiers 335 | .map(transformClassifier) 336 | .sort(function (a, b) { 337 | return b.compartments.length - a.compartments.length; 338 | }); 339 | var uniqClassifiers = nomnoml.skanaar.uniqueBy(allClassifiers, 'name'); 340 | var uniqRelations = relations.filter(function (a) { 341 | for (var _i = 0, relations_1 = relations; _i < relations_1.length; _i++) { 342 | var b = relations_1[_i]; 343 | if (a === b) 344 | return true; 345 | if (b.start == a.start && b.end == a.end) 346 | return false; 347 | } 348 | return true; 349 | }); 350 | return new nomnoml.Compartment(lines, uniqClassifiers, uniqRelations); 351 | } 352 | function transformClassifier(entity) { 353 | var compartments = entity.parts.map(transformCompartment); 354 | return new nomnoml.Classifier(entity.type, entity.id, compartments); 355 | } 356 | return transformCompartment(entity); 357 | } 358 | nomnoml.transformParseIntoSyntaxTree = transformParseIntoSyntaxTree; 359 | })(nomnoml || (nomnoml = {})); 360 | var nomnoml; 361 | (function (nomnoml) { 362 | function render(graphics, config, compartment, setFont) { 363 | var padding = config.padding; 364 | var g = graphics; 365 | var vm = nomnoml.skanaar.vector; 366 | function renderCompartment(compartment, style, level) { 367 | g.save(); 368 | g.translate(padding, padding); 369 | g.fillStyle(style.stroke || config.stroke); 370 | compartment.lines.forEach(function (text, i) { 371 | g.textAlign(style.center ? 'center' : 'left'); 372 | var x = style.center ? compartment.width / 2 - padding : 0; 373 | var y = (0.5 + (i + 0.5) * config.leading) * config.fontSize; 374 | if (text) { 375 | g.fillText(text, x, y); 376 | } 377 | if (style.underline) { 378 | var w = g.measureText(text).width; 379 | y += Math.round(config.fontSize * 0.2) + 0.5; 380 | g.path([{ x: x - w / 2, y: y }, { x: x + w / 2, y: y }]).stroke(); 381 | g.lineWidth(config.lineWidth); 382 | } 383 | }); 384 | g.translate(config.gutter, config.gutter); 385 | compartment.relations.forEach(function (r) { renderRelation(r, compartment); }); 386 | compartment.nodes.forEach(function (n) { renderNode(n, level); }); 387 | g.restore(); 388 | } 389 | function renderNode(node, level) { 390 | var x = Math.round(node.x - node.width / 2); 391 | var y = Math.round(node.y - node.height / 2); 392 | var style = config.styles[node.type] || nomnoml.styles.CLASS; 393 | g.fillStyle(style.fill || config.fill[level] || nomnoml.skanaar.last(config.fill)); 394 | g.strokeStyle(style.stroke || config.stroke); 395 | if (style.dashed) { 396 | var dash = Math.max(4, 2 * config.lineWidth); 397 | g.setLineDash([dash, dash]); 398 | } 399 | var drawNode = nomnoml.visualizers[style.visual] || nomnoml.visualizers["class"]; 400 | drawNode(node, x, y, config, g); 401 | g.setLineDash([]); 402 | var yDivider = (style.visual === 'actor' ? y + padding * 3 / 4 : y); 403 | node.compartments.forEach(function (part, i) { 404 | var s = i > 0 ? nomnoml.buildStyle({ stroke: style.stroke }) : style; 405 | if (s.empty) 406 | return; 407 | g.save(); 408 | g.translate(x, yDivider); 409 | setFont(config, s.bold ? 'bold' : 'normal', s.italic ? 'italic' : undefined); 410 | renderCompartment(part, s, level + 1); 411 | g.restore(); 412 | if (i + 1 === node.compartments.length) 413 | return; 414 | yDivider += part.height; 415 | if (style.visual === 'frame' && i === 0) { 416 | var w = g.measureText(node.name).width + part.height / 2 + padding; 417 | g.path([ 418 | { x: x, y: yDivider }, 419 | { x: x + w - part.height / 2, y: yDivider }, 420 | { x: x + w, y: yDivider - part.height / 2 }, 421 | { x: x + w, y: yDivider - part.height } 422 | ]).stroke(); 423 | } 424 | else { 425 | g.path([{ x: x, y: yDivider }, { x: x + node.width, y: yDivider }]).stroke(); 426 | } 427 | }); 428 | } 429 | function strokePath(p) { 430 | if (config.edges === 'rounded') { 431 | var radius = config.spacing * config.bendSize; 432 | g.beginPath(); 433 | g.moveTo(p[0].x, p[0].y); 434 | for (var i = 1; i < p.length - 1; i++) { 435 | g.arcTo(p[i].x, p[i].y, p[i + 1].x, p[i + 1].y, radius); 436 | } 437 | g.lineTo(nomnoml.skanaar.last(p).x, nomnoml.skanaar.last(p).y); 438 | g.stroke(); 439 | } 440 | else 441 | g.path(p).stroke(); 442 | } 443 | var empty = false, filled = true, diamond = true; 444 | function renderLabel(text, pos, quadrant) { 445 | if (text) { 446 | var fontSize = config.fontSize; 447 | var lines = text.split('`'); 448 | var area = { 449 | width: nomnoml.skanaar.max(lines.map(function (l) { return g.measureText(l).width; })), 450 | height: fontSize * lines.length 451 | }; 452 | var origin = { 453 | x: pos.x + ((quadrant == 1 || quadrant == 4) ? padding : -area.width - padding), 454 | y: pos.y + ((quadrant == 3 || quadrant == 4) ? padding : -area.height - padding) 455 | }; 456 | lines.forEach(function (l, i) { g.fillText(l, origin.x, origin.y + fontSize * (i + 1)); }); 457 | } 458 | } 459 | function quadrant(point, node, fallback) { 460 | if (point.x < node.x && point.y < node.y) 461 | return 1; 462 | if (point.x > node.x && point.y < node.y) 463 | return 2; 464 | if (point.x > node.x && point.y > node.y) 465 | return 3; 466 | if (point.x < node.x && point.y > node.y) 467 | return 4; 468 | return fallback; 469 | } 470 | function adjustQuadrant(quadrant, point, opposite) { 471 | if ((opposite.x == point.x) || (opposite.y == point.y)) 472 | return quadrant; 473 | var flipHorizontally = [4, 3, 2, 1]; 474 | var flipVertically = [2, 1, 4, 3]; 475 | var oppositeQuadrant = (opposite.y < point.y) ? 476 | ((opposite.x < point.x) ? 2 : 1) : 477 | ((opposite.x < point.x) ? 3 : 4); 478 | if (oppositeQuadrant === quadrant) { 479 | if (config.direction === 'LR') 480 | return flipHorizontally[quadrant - 1]; 481 | if (config.direction === 'TB') 482 | return flipVertically[quadrant - 1]; 483 | } 484 | return quadrant; 485 | } 486 | function renderRelation(r, compartment) { 487 | var startNode = nomnoml.skanaar.find(compartment.nodes, function (e) { return e.name == r.start; }); 488 | var endNode = nomnoml.skanaar.find(compartment.nodes, function (e) { return e.name == r.end; }); 489 | var start = r.path[1]; 490 | var end = r.path[r.path.length - 2]; 491 | var path = r.path.slice(1, -1); 492 | g.fillStyle(config.stroke); 493 | setFont(config, 'normal'); 494 | renderLabel(r.startLabel, start, adjustQuadrant(quadrant(start, startNode, 4), start, end)); 495 | renderLabel(r.endLabel, end, adjustQuadrant(quadrant(end, endNode, 2), end, start)); 496 | if (r.assoc !== '-/-') { 497 | if (nomnoml.skanaar.hasSubstring(r.assoc, '--')) { 498 | var dash = Math.max(4, 2 * config.lineWidth); 499 | g.setLineDash([dash, dash]); 500 | strokePath(path); 501 | g.setLineDash([]); 502 | } 503 | else 504 | strokePath(path); 505 | } 506 | function drawArrowEnd(id, path, end) { 507 | if (id === '>' || id === '<') 508 | drawArrow(path, filled, end, false); 509 | else if (id === ':>' || id === '<:') 510 | drawArrow(path, empty, end, false); 511 | else if (id === '+') 512 | drawArrow(path, filled, end, diamond); 513 | else if (id === 'o') 514 | drawArrow(path, empty, end, diamond); 515 | } 516 | var tokens = r.assoc.split('-'); 517 | drawArrowEnd(nomnoml.skanaar.last(tokens), path, end); 518 | drawArrowEnd(tokens[0], path.reverse(), start); 519 | } 520 | function drawArrow(path, isOpen, arrowPoint, diamond) { 521 | var size = config.spacing * config.arrowSize / 30; 522 | var v = vm.diff(path[path.length - 2], nomnoml.skanaar.last(path)); 523 | var nv = vm.normalize(v); 524 | function getArrowBase(s) { return vm.add(arrowPoint, vm.mult(nv, s * size)); } 525 | var arrowBase = getArrowBase(diamond ? 7 : 10); 526 | var t = vm.rot(nv); 527 | var arrowButt = (diamond) ? getArrowBase(14) 528 | : (isOpen && !config.fillArrows) ? getArrowBase(5) : arrowBase; 529 | var arrow = [ 530 | vm.add(arrowBase, vm.mult(t, 4 * size)), 531 | arrowButt, 532 | vm.add(arrowBase, vm.mult(t, -4 * size)), 533 | arrowPoint 534 | ]; 535 | g.fillStyle(isOpen ? config.stroke : config.fill[0]); 536 | g.circuit(arrow).fillAndStroke(); 537 | } 538 | function snapToPixels() { 539 | if (config.lineWidth % 2 === 1) 540 | g.translate(0.5, 0.5); 541 | } 542 | g.clear(); 543 | setFont(config, 'bold'); 544 | g.save(); 545 | g.lineWidth(config.lineWidth); 546 | g.lineJoin('round'); 547 | g.lineCap('round'); 548 | g.strokeStyle(config.stroke); 549 | g.scale(config.zoom, config.zoom); 550 | snapToPixels(); 551 | renderCompartment(compartment, nomnoml.buildStyle({ stroke: undefined }), 0); 552 | g.restore(); 553 | } 554 | nomnoml.render = render; 555 | })(nomnoml || (nomnoml = {})); 556 | var nomnoml; 557 | (function (nomnoml) { 558 | var skanaar; 559 | (function (skanaar) { 560 | function Canvas(canvas, callbacks) { 561 | var ctx = canvas.getContext('2d'); 562 | var mousePos = { x: 0, y: 0 }; 563 | var twopi = 2 * 3.1416; 564 | function mouseEventToPos(event) { 565 | var e = canvas; 566 | return { 567 | x: event.clientX - e.getBoundingClientRect().left - e.clientLeft + e.scrollLeft, 568 | y: event.clientY - e.getBoundingClientRect().top - e.clientTop + e.scrollTop 569 | }; 570 | } 571 | if (callbacks) { 572 | canvas.addEventListener('mousedown', function (event) { 573 | if (callbacks.mousedown) 574 | callbacks.mousedown(mouseEventToPos(event)); 575 | }); 576 | canvas.addEventListener('mouseup', function (event) { 577 | if (callbacks.mouseup) 578 | callbacks.mouseup(mouseEventToPos(event)); 579 | }); 580 | canvas.addEventListener('mousemove', function (event) { 581 | mousePos = mouseEventToPos(event); 582 | if (callbacks.mousemove) 583 | callbacks.mousemove(mouseEventToPos(event)); 584 | }); 585 | } 586 | var chainable = { 587 | stroke: function () { 588 | ctx.stroke(); 589 | return chainable; 590 | }, 591 | fill: function () { 592 | ctx.fill(); 593 | return chainable; 594 | }, 595 | fillAndStroke: function () { 596 | ctx.fill(); 597 | ctx.stroke(); 598 | return chainable; 599 | } 600 | }; 601 | function color255(r, g, b, a) { 602 | var optionalAlpha = a === undefined ? 1 : a; 603 | var comps = [Math.floor(r), Math.floor(g), Math.floor(b), optionalAlpha]; 604 | return 'rgba(' + comps.join() + ')'; 605 | } 606 | function tracePath(path, offset, s) { 607 | s = s === undefined ? 1 : s; 608 | offset = offset || { x: 0, y: 0 }; 609 | ctx.beginPath(); 610 | ctx.moveTo(offset.x + s * path[0].x, offset.y + s * path[0].y); 611 | for (var i = 1, len = path.length; i < len; i++) 612 | ctx.lineTo(offset.x + s * path[i].x, offset.y + s * path[i].y); 613 | return chainable; 614 | } 615 | return { 616 | mousePos: function () { return mousePos; }, 617 | width: function () { return canvas.width; }, 618 | height: function () { return canvas.height; }, 619 | background: function (r, g, b) { 620 | ctx.fillStyle = color255(r, g, b); 621 | ctx.fillRect(0, 0, canvas.width, canvas.height); 622 | }, 623 | clear: function () { 624 | ctx.clearRect(0, 0, canvas.width, canvas.height); 625 | }, 626 | circle: function (p, r) { 627 | ctx.beginPath(); 628 | ctx.arc(p.x, p.y, r, 0, twopi); 629 | return chainable; 630 | }, 631 | ellipse: function (center, rx, ry, start, stop) { 632 | if (start === undefined) 633 | start = 0; 634 | if (stop === undefined) 635 | stop = twopi; 636 | ctx.beginPath(); 637 | ctx.save(); 638 | ctx.translate(center.x, center.y); 639 | ctx.scale(1, ry / rx); 640 | ctx.arc(0, 0, rx / 2, start, stop); 641 | ctx.restore(); 642 | return chainable; 643 | }, 644 | arc: function (x, y, r, start, stop) { 645 | ctx.beginPath(); 646 | ctx.moveTo(x, y); 647 | ctx.arc(x, y, r, start, stop); 648 | return chainable; 649 | }, 650 | roundRect: function (x, y, w, h, r) { 651 | ctx.beginPath(); 652 | ctx.moveTo(x + r, y); 653 | ctx.arcTo(x + w, y, x + w, y + r, r); 654 | ctx.lineTo(x + w, y + h - r); 655 | ctx.arcTo(x + w, y + h, x + w - r, y + h, r); 656 | ctx.lineTo(x + r, y + h); 657 | ctx.arcTo(x, y + h, x, y + h - r, r); 658 | ctx.lineTo(x, y + r); 659 | ctx.arcTo(x, y, x + r, y, r); 660 | ctx.closePath(); 661 | return chainable; 662 | }, 663 | rect: function (x, y, w, h) { 664 | ctx.beginPath(); 665 | ctx.moveTo(x, y); 666 | ctx.lineTo(x + w, y); 667 | ctx.lineTo(x + w, y + h); 668 | ctx.lineTo(x, y + h); 669 | ctx.closePath(); 670 | return chainable; 671 | }, 672 | path: tracePath, 673 | circuit: function (path, offset, s) { 674 | tracePath(path, offset, s); 675 | ctx.closePath(); 676 | return chainable; 677 | }, 678 | font: function (f) { ctx.font = f; }, 679 | fillStyle: function (s) { ctx.fillStyle = s; }, 680 | strokeStyle: function (s) { ctx.strokeStyle = s; }, 681 | textAlign: function (a) { ctx.textAlign = a; }, 682 | lineCap: function (cap) { ctx.lineCap = cap; return chainable; }, 683 | lineJoin: function (join) { ctx.lineJoin = join; return chainable; }, 684 | lineWidth: function (w) { ctx.lineWidth = w; return chainable; }, 685 | arcTo: function () { return ctx.arcTo.apply(ctx, arguments); }, 686 | beginPath: function () { return ctx.beginPath.apply(ctx, arguments); }, 687 | fillText: function () { return ctx.fillText.apply(ctx, arguments); }, 688 | lineTo: function () { return ctx.lineTo.apply(ctx, arguments); }, 689 | measureText: function () { return ctx.measureText.apply(ctx, arguments); }, 690 | moveTo: function () { return ctx.moveTo.apply(ctx, arguments); }, 691 | restore: function () { return ctx.restore.apply(ctx, arguments); }, 692 | save: function () { return ctx.save.apply(ctx, arguments); }, 693 | scale: function () { return ctx.scale.apply(ctx, arguments); }, 694 | setLineDash: function () { return ctx.setLineDash.apply(ctx, arguments); }, 695 | stroke: function () { return ctx.stroke.apply(ctx, arguments); }, 696 | translate: function () { return ctx.translate.apply(ctx, arguments); } 697 | }; 698 | } 699 | skanaar.Canvas = Canvas; 700 | })(skanaar = nomnoml.skanaar || (nomnoml.skanaar = {})); 701 | })(nomnoml || (nomnoml = {})); 702 | var nomnoml; 703 | (function (nomnoml) { 704 | var skanaar; 705 | (function (skanaar) { 706 | function xmlEncode(str) { 707 | return str 708 | .replace(/&/g, '&') 709 | .replace(//g, '>') 711 | .replace(/"/g, '"') 712 | .replace(/'/g, '''); 713 | } 714 | function Svg(globalStyle, canvas) { 715 | var initialState = { 716 | x: 0, 717 | y: 0, 718 | stroke: 'none', 719 | dashArray: 'none', 720 | fill: 'none', 721 | textAlign: 'left', 722 | font: null 723 | }; 724 | var states = [initialState]; 725 | var elements = []; 726 | var ctx = canvas ? canvas.getContext('2d') : null; 727 | var canUseCanvas = false; 728 | var waitingForFirstFont = true; 729 | var docFont = ''; 730 | function Element(name, attr, content) { 731 | return { 732 | name: name, 733 | attr: attr, 734 | content: content || undefined, 735 | stroke: function () { 736 | var base = this.attr.style || ''; 737 | this.attr.style = base + 'stroke:' + lastDefined('stroke') + 738 | ';fill:none;stroke-dasharray:' + lastDefined('dashArray') + ';'; 739 | return this; 740 | }, 741 | fill: function () { 742 | var base = this.attr.style || ''; 743 | this.attr.style = base + 'stroke:none; fill:' + lastDefined('fill') + ';'; 744 | return this; 745 | }, 746 | fillAndStroke: function () { 747 | var base = this.attr.style || ''; 748 | this.attr.style = base + 'stroke:' + lastDefined('stroke') + ';fill:' + lastDefined('fill') + 749 | ';stroke-dasharray:' + lastDefined('dashArray') + ';'; 750 | return this; 751 | } 752 | }; 753 | } 754 | function State(dx, dy) { 755 | return { x: dx, y: dy, stroke: null, fill: null, textAlign: null, dashArray: 'none', font: null }; 756 | } 757 | function trans(coord, axis) { 758 | states.forEach(function (t) { coord += t[axis]; }); 759 | return coord; 760 | } 761 | function tX(coord) { return Math.round(10 * trans(coord, 'x')) / 10; } 762 | function tY(coord) { return Math.round(10 * trans(coord, 'y')) / 10; } 763 | function lastDefined(property) { 764 | for (var i = states.length - 1; i >= 0; i--) 765 | if (states[i][property]) 766 | return states[i][property]; 767 | return undefined; 768 | } 769 | function last(list) { return list[list.length - 1]; } 770 | function tracePath(path, offset, s) { 771 | s = s === undefined ? 1 : s; 772 | offset = offset || { x: 0, y: 0 }; 773 | var d = path.map(function (e, i) { 774 | return (i ? 'L' : 'M') + tX(offset.x + s * e.x) + ' ' + tY(offset.y + s * e.y); 775 | }).join(' '); 776 | return newElement('path', { d: d }); 777 | } 778 | function newElement(type, attr, content) { 779 | var element = Element(type, attr, content); 780 | elements.push(element); 781 | return element; 782 | } 783 | return { 784 | width: function () { return 0; }, 785 | height: function () { return 0; }, 786 | background: function () { }, 787 | clear: function () { }, 788 | circle: function (p, r) { 789 | var element = Element('circle', { r: r, cx: tX(p.x), cy: tY(p.y) }); 790 | elements.push(element); 791 | return element; 792 | }, 793 | ellipse: function (center, w, h, start, stop) { 794 | if (stop) { 795 | var y = tY(center.y); 796 | return newElement('path', { d: 'M' + tX(center.x - w / 2) + ' ' + y + 797 | 'A' + w / 2 + ' ' + h / 2 + ' 0 1 0 ' + tX(center.x + w / 2) + ' ' + y 798 | }); 799 | } 800 | else { 801 | return newElement('ellipse', { cx: tX(center.x), cy: tY(center.y), rx: w / 2, ry: h / 2 }); 802 | } 803 | }, 804 | arc: function (x, y, r) { 805 | return newElement('ellipse', { cx: tX(x), cy: tY(y), rx: r, ry: r }); 806 | }, 807 | roundRect: function (x, y, w, h, r) { 808 | return newElement('rect', { x: tX(x), y: tY(y), rx: r, ry: r, height: h, width: w }); 809 | }, 810 | rect: function (x, y, w, h, id, cls) { 811 | return newElement('rect', { x: tX(x), y: tY(y), height: h, width: w, id: id, class: cls }); 812 | }, 813 | path: tracePath, 814 | circuit: function (path, offset, s) { 815 | var element = tracePath(path, offset, s); 816 | element.attr.d += ' Z'; 817 | return element; 818 | }, 819 | font: function (font) { 820 | last(states).font = font; 821 | if (waitingForFirstFont) { 822 | if (ctx) { 823 | var primaryFont = font.replace(/^.*family:/, '').replace(/[, ].*$/, ''); 824 | primaryFont = primaryFont.replace(/'/g, ''); 825 | canUseCanvas = /^(Arial|Helvetica|Times|Times New Roman)$/.test(primaryFont); 826 | if (canUseCanvas) { 827 | var fontSize = font.replace(/^.*font-size:/, '').replace(/;.*$/, '') + ' '; 828 | if (primaryFont === 'Arial') { 829 | docFont = fontSize + 'Arial, Helvetica, sans-serif'; 830 | } 831 | else if (primaryFont === 'Helvetica') { 832 | docFont = fontSize + 'Helvetica, Arial, sans-serif'; 833 | } 834 | else if (primaryFont === 'Times New Roman') { 835 | docFont = fontSize + '"Times New Roman", Times, serif'; 836 | } 837 | else if (primaryFont === 'Times') { 838 | docFont = fontSize + 'Times, "Times New Roman", serif'; 839 | } 840 | } 841 | } 842 | waitingForFirstFont = false; 843 | } 844 | }, 845 | strokeStyle: function (stroke) { 846 | last(states).stroke = stroke; 847 | }, 848 | fillStyle: function (fill) { 849 | last(states).fill = fill; 850 | }, 851 | arcTo: function (x1, y1, x2, y2) { 852 | last(elements).attr.d += ('L' + tX(x1) + ' ' + tY(y1) + ' L' + tX(x2) + ' ' + tY(y2) + ' '); 853 | }, 854 | beginPath: function () { 855 | return newElement('path', { d: '' }); 856 | }, 857 | fillText: function (text, x, y) { 858 | var attr = { x: tX(x), y: tY(y), style: 'fill: ' + last(states).fill + ';' }; 859 | var font = lastDefined('font'); 860 | if (font.indexOf('bold') === -1) { 861 | attr.style += 'font-weight:normal;'; 862 | } 863 | if (font.indexOf('italic') > -1) { 864 | attr.style += 'font-style:italic;'; 865 | } 866 | if (lastDefined('textAlign') === 'center') { 867 | attr.style += 'text-anchor: middle;'; 868 | } 869 | return newElement('text', attr, text); 870 | }, 871 | lineCap: function (cap) { globalStyle += ';stroke-linecap:' + cap; return last(elements); }, 872 | lineJoin: function (join) { globalStyle += ';stroke-linejoin:' + join; return last(elements); }, 873 | lineTo: function (x, y) { 874 | last(elements).attr.d += ('L' + tX(x) + ' ' + tY(y) + ' '); 875 | return last(elements); 876 | }, 877 | lineWidth: function (w) { globalStyle += ';stroke-width:' + w; return last(elements); }, 878 | measureText: function (s) { 879 | if (canUseCanvas) { 880 | var fontStr = lastDefined('font'); 881 | var italicSpec = (/\bitalic\b/.test(fontStr) ? 'italic' : 'normal') + ' normal '; 882 | var boldSpec = /\bbold\b/.test(fontStr) ? 'bold ' : 'normal '; 883 | ctx.font = italicSpec + boldSpec + docFont; 884 | return ctx.measureText(s); 885 | } 886 | else { 887 | return { 888 | width: skanaar.sum(s, function (c) { 889 | if (c === 'M' || c === 'W') { 890 | return 14; 891 | } 892 | return c.charCodeAt(0) < 200 ? 9.5 : 16; 893 | }) 894 | }; 895 | } 896 | }, 897 | moveTo: function (x, y) { 898 | last(elements).attr.d += ('M' + tX(x) + ' ' + tY(y) + ' '); 899 | }, 900 | restore: function () { 901 | states.pop(); 902 | }, 903 | save: function () { 904 | states.push(State(0, 0)); 905 | }, 906 | scale: function () { }, 907 | setLineDash: function (d) { 908 | last(states).dashArray = (d.length === 0) ? 'none' : d[0] + ' ' + d[1]; 909 | }, 910 | stroke: function () { 911 | last(elements).stroke(); 912 | }, 913 | textAlign: function (a) { 914 | last(states).textAlign = a; 915 | }, 916 | translate: function (dx, dy) { 917 | last(states).x += dx; 918 | last(states).y += dy; 919 | }, 920 | serialize: function (size, desc, title) { 921 | function toAttr(obj) { 922 | function toKeyValue(key) { return key + '="' + obj[key] + '"'; } 923 | return Object.keys(obj).map(toKeyValue).join(' '); 924 | } 925 | function toHtml(e) { 926 | return '<' + e.name + ' ' + toAttr(e.attr) + '>' + (e.content ? xmlEncode(e.content) : '') + ''; 927 | } 928 | var elementsToSerialize = elements; 929 | if (desc) { 930 | elementsToSerialize.unshift(Element('desc', {}, desc)); 931 | } 932 | if (title) { 933 | elementsToSerialize.unshift(Element('title', {}, title)); 934 | } 935 | var innerSvg = elementsToSerialize.map(toHtml).join('\n '); 936 | var attrs = { 937 | version: '1.1', 938 | baseProfile: 'full', 939 | width: size.width, 940 | height: size.height, 941 | viewbox: '0 0 ' + size.width + ' ' + size.height, 942 | xmlns: 'http://www.w3.org/2000/svg', 943 | 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 944 | 'xmlns:ev': 'http://www.w3.org/2001/xml-events', 945 | style: lastDefined('font') + ';' + globalStyle 946 | }; 947 | return '\n ' + innerSvg + '\n'; 948 | } 949 | }; 950 | } 951 | skanaar.Svg = Svg; 952 | })(skanaar = nomnoml.skanaar || (nomnoml.skanaar = {})); 953 | })(nomnoml || (nomnoml = {})); 954 | var nomnoml; 955 | (function (nomnoml) { 956 | var skanaar; 957 | (function (skanaar) { 958 | function plucker(pluckerDef) { 959 | switch (typeof pluckerDef) { 960 | case 'undefined': return function (e) { return e; }; 961 | case 'string': return function (obj) { return obj[pluckerDef]; }; 962 | case 'number': return function (obj) { return obj[pluckerDef]; }; 963 | case 'function': return pluckerDef; 964 | } 965 | } 966 | skanaar.plucker = plucker; 967 | function max(list, plucker) { 968 | var transform = skanaar.plucker(plucker); 969 | var maximum = transform(list[0]); 970 | for (var i = 0; i < list.length; i++) { 971 | var item = transform(list[i]); 972 | maximum = (item > maximum) ? item : maximum; 973 | } 974 | return maximum; 975 | } 976 | skanaar.max = max; 977 | function sum(list, plucker) { 978 | var transform = skanaar.plucker(plucker); 979 | for (var i = 0, summation = 0, len = list.length; i < len; i++) 980 | summation += transform(list[i]); 981 | return summation; 982 | } 983 | skanaar.sum = sum; 984 | function flatten(lists) { 985 | var out = []; 986 | for (var i = 0; i < lists.length; i++) 987 | out = out.concat(lists[i]); 988 | return out; 989 | } 990 | skanaar.flatten = flatten; 991 | function find(list, predicate) { 992 | for (var i = 0; i < list.length; i++) 993 | if (predicate(list[i])) 994 | return list[i]; 995 | return undefined; 996 | } 997 | skanaar.find = find; 998 | function last(list) { 999 | return list[list.length - 1]; 1000 | } 1001 | skanaar.last = last; 1002 | function hasSubstring(haystack, needle) { 1003 | if (needle === '') 1004 | return true; 1005 | if (!haystack) 1006 | return false; 1007 | return haystack.indexOf(needle) !== -1; 1008 | } 1009 | skanaar.hasSubstring = hasSubstring; 1010 | function format(template) { 1011 | var parts = []; 1012 | for (var _i = 1; _i < arguments.length; _i++) { 1013 | parts[_i - 1] = arguments[_i]; 1014 | } 1015 | var matrix = template.split('#'); 1016 | var output = [matrix[0]]; 1017 | for (var i = 0; i < matrix.length - 1; i++) { 1018 | output.push(parts[i] || ''); 1019 | output.push(matrix[i + 1]); 1020 | } 1021 | return output.join(''); 1022 | } 1023 | skanaar.format = format; 1024 | function merged(a, b) { 1025 | function assign(target, data) { 1026 | for (var key in data) 1027 | target[key] = data[key]; 1028 | } 1029 | var obj = {}; 1030 | assign(obj, a); 1031 | assign(obj, b); 1032 | return obj; 1033 | } 1034 | skanaar.merged = merged; 1035 | function indexBy(list, key) { 1036 | var obj = {}; 1037 | for (var i = 0; i < list.length; i++) 1038 | obj[list[i][key]] = list[i]; 1039 | return obj; 1040 | } 1041 | skanaar.indexBy = indexBy; 1042 | function uniqueBy(list, pluckerDef) { 1043 | var seen = {}; 1044 | var getKey = skanaar.plucker(pluckerDef); 1045 | var out = []; 1046 | for (var i = 0; i < list.length; i++) { 1047 | var key = getKey(list[i]); 1048 | if (!seen[key]) { 1049 | seen[key] = true; 1050 | out.push(list[i]); 1051 | } 1052 | } 1053 | return out; 1054 | } 1055 | skanaar.uniqueBy = uniqueBy; 1056 | })(skanaar = nomnoml.skanaar || (nomnoml.skanaar = {})); 1057 | })(nomnoml || (nomnoml = {})); 1058 | var nomnoml; 1059 | (function (nomnoml) { 1060 | var skanaar; 1061 | (function (skanaar) { 1062 | skanaar.vector = { 1063 | dist: function (a, b) { return skanaar.vector.mag(skanaar.vector.diff(a, b)); }, 1064 | add: function (a, b) { return { x: a.x + b.x, y: a.y + b.y }; }, 1065 | diff: function (a, b) { return { x: a.x - b.x, y: a.y - b.y }; }, 1066 | mult: function (v, factor) { return { x: factor * v.x, y: factor * v.y }; }, 1067 | mag: function (v) { return Math.sqrt(v.x * v.x + v.y * v.y); }, 1068 | normalize: function (v) { return skanaar.vector.mult(v, 1 / skanaar.vector.mag(v)); }, 1069 | rot: function (a) { return { x: a.y, y: -a.x }; } 1070 | }; 1071 | })(skanaar = nomnoml.skanaar || (nomnoml.skanaar = {})); 1072 | })(nomnoml || (nomnoml = {})); 1073 | var nomnoml; 1074 | (function (nomnoml) { 1075 | nomnoml.styles = { 1076 | ABSTRACT: nomnoml.buildStyle({ visual: 'class', center: true, italic: true }), 1077 | ACTOR: nomnoml.buildStyle({ visual: 'actor', center: true }), 1078 | CHOICE: nomnoml.buildStyle({ visual: 'rhomb', center: true }), 1079 | CLASS: nomnoml.buildStyle({ visual: 'class', center: true, bold: true }), 1080 | DATABASE: nomnoml.buildStyle({ visual: 'database', center: true, bold: true }), 1081 | END: nomnoml.buildStyle({ visual: 'end', center: true, empty: true, hull: 'icon' }), 1082 | FRAME: nomnoml.buildStyle({ visual: 'frame' }), 1083 | HIDDEN: nomnoml.buildStyle({ visual: 'hidden', center: true, empty: true, hull: 'empty' }), 1084 | INPUT: nomnoml.buildStyle({ visual: 'input', center: true }), 1085 | INSTANCE: nomnoml.buildStyle({ visual: 'class', center: true, underline: true }), 1086 | LABEL: nomnoml.buildStyle({ visual: 'none' }), 1087 | NOTE: nomnoml.buildStyle({ visual: 'note' }), 1088 | PACKAGE: nomnoml.buildStyle({ visual: 'package' }), 1089 | RECEIVER: nomnoml.buildStyle({ visual: 'receiver' }), 1090 | REFERENCE: nomnoml.buildStyle({ visual: 'class', center: true, dashed: true }), 1091 | SENDER: nomnoml.buildStyle({ visual: 'sender' }), 1092 | START: nomnoml.buildStyle({ visual: 'start', center: true, empty: true, hull: 'icon' }), 1093 | STATE: nomnoml.buildStyle({ visual: 'roundrect', center: true }), 1094 | TRANSCEIVER: nomnoml.buildStyle({ visual: 'transceiver' }), 1095 | USECASE: nomnoml.buildStyle({ visual: 'ellipse', center: true }) 1096 | }; 1097 | nomnoml.visualizers = { 1098 | actor: function (node, x, y, config, g) { 1099 | var a = config.padding / 2; 1100 | var yp = y + a / 2; 1101 | var actorCenter = { x: node.x, y: yp - a }; 1102 | g.circle(actorCenter, a).fillAndStroke(); 1103 | g.path([{ x: node.x, y: yp }, { x: node.x, y: yp + 2 * a }]).stroke(); 1104 | g.path([{ x: node.x - a, y: yp + a }, { x: node.x + a, y: yp + a }]).stroke(); 1105 | g.path([{ x: node.x - a, y: yp + a + config.padding }, 1106 | { x: node.x, y: yp + config.padding }, 1107 | { x: node.x + a, y: yp + a + config.padding }]).stroke(); 1108 | }, 1109 | "class": function (node, x, y, config, g) { 1110 | g.rect(x, y, node.width, node.height, node.name, "node").fillAndStroke(); 1111 | }, 1112 | database: function (node, x, y, config, g) { 1113 | var cy = y - config.padding / 2; 1114 | var pi = 3.1416; 1115 | g.rect(x, y, node.width, node.height).fill(); 1116 | g.path([{ x: x, y: cy }, { x: x, y: cy + node.height }]).stroke(); 1117 | g.path([ 1118 | { x: x + node.width, y: cy }, 1119 | { x: x + node.width, y: cy + node.height } 1120 | ]).stroke(); 1121 | g.ellipse({ x: node.x, y: cy }, node.width, config.padding * 1.5).fillAndStroke(); 1122 | g.ellipse({ x: node.x, y: cy + node.height }, node.width, config.padding * 1.5, 0, pi) 1123 | .fillAndStroke(); 1124 | }, 1125 | ellipse: function (node, x, y, config, g) { 1126 | g.ellipse({ x: node.x, y: node.y }, node.width, node.height).fillAndStroke(); 1127 | }, 1128 | end: function (node, x, y, config, g) { 1129 | g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 3).fillAndStroke(); 1130 | g.fillStyle(config.stroke); 1131 | g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 3 - config.padding / 2).fill(); 1132 | }, 1133 | frame: function (node, x, y, config, g) { 1134 | g.rect(x, y, node.width, node.height).fillAndStroke(); 1135 | }, 1136 | hidden: function (node, x, y, config, g) { 1137 | }, 1138 | input: function (node, x, y, config, g) { 1139 | g.circuit([ 1140 | { x: x + config.padding, y: y }, 1141 | { x: x + node.width, y: y }, 1142 | { x: x + node.width - config.padding, y: y + node.height }, 1143 | { x: x, y: y + node.height } 1144 | ]).fillAndStroke(); 1145 | }, 1146 | none: function (node, x, y, config, g) { 1147 | }, 1148 | note: function (node, x, y, config, g) { 1149 | g.circuit([ 1150 | { x: x, y: y }, 1151 | { x: x + node.width - config.padding, y: y }, 1152 | { x: x + node.width, y: y + config.padding }, 1153 | { x: x + node.width, y: y + node.height }, 1154 | { x: x, y: y + node.height }, 1155 | { x: x, y: y } 1156 | ]).fillAndStroke(); 1157 | g.path([ 1158 | { x: x + node.width - config.padding, y: y }, 1159 | { x: x + node.width - config.padding, y: y + config.padding }, 1160 | { x: x + node.width, y: y + config.padding } 1161 | ]).stroke(); 1162 | }, 1163 | package: function (node, x, y, config, g) { 1164 | var headHeight = node.compartments[0].height; 1165 | g.rect(x, y + headHeight, node.width, node.height - headHeight).fillAndStroke(); 1166 | var w = g.measureText(node.name).width + 2 * config.padding; 1167 | g.circuit([ 1168 | { x: x, y: y + headHeight }, 1169 | { x: x, y: y }, 1170 | { x: x + w, y: y }, 1171 | { x: x + w, y: y + headHeight } 1172 | ]).fillAndStroke(); 1173 | }, 1174 | receiver: function (node, x, y, config, g) { 1175 | g.circuit([ 1176 | { x: x - config.padding, y: y }, 1177 | { x: x + node.width, y: y }, 1178 | { x: x + node.width, y: y + node.height }, 1179 | { x: x - config.padding, y: y + node.height }, 1180 | { x: x, y: y + node.height / 2 }, 1181 | ]).fillAndStroke(); 1182 | }, 1183 | rhomb: function (node, x, y, config, g) { 1184 | g.circuit([ 1185 | { x: node.x, y: y - config.padding }, 1186 | { x: x + node.width + config.padding, y: node.y }, 1187 | { x: node.x, y: y + node.height + config.padding }, 1188 | { x: x - config.padding, y: node.y } 1189 | ]).fillAndStroke(); 1190 | }, 1191 | roundrect: function (node, x, y, config, g) { 1192 | var r = Math.min(config.padding * 2 * config.leading, node.height / 2); 1193 | g.roundRect(x, y, node.width, node.height, r).fillAndStroke(); 1194 | }, 1195 | sender: function (node, x, y, config, g) { 1196 | g.circuit([ 1197 | { x: x, y: y }, 1198 | { x: x + node.width - config.padding, y: y }, 1199 | { x: x + node.width, y: y + node.height / 2 }, 1200 | { x: x + node.width - config.padding, y: y + node.height }, 1201 | { x: x, y: y + node.height } 1202 | ]).fillAndStroke(); 1203 | }, 1204 | start: function (node, x, y, config, g) { 1205 | g.fillStyle(config.stroke); 1206 | g.circle({ x: node.x, y: y + node.height / 2 }, node.height / 2.5).fill(); 1207 | }, 1208 | transceiver: function (node, x, y, config, g) { 1209 | g.circuit([ 1210 | { x: x - config.padding, y: y }, 1211 | { x: x + node.width, y: y }, 1212 | { x: x + node.width + config.padding, y: y + node.height / 2 }, 1213 | { x: x + node.width, y: y + node.height }, 1214 | { x: x - config.padding, y: y + node.height }, 1215 | { x: x, y: y + node.height / 2 } 1216 | ]).fillAndStroke(); 1217 | } 1218 | }; 1219 | })(nomnoml || (nomnoml = {})); 1220 | ; 1221 | /* parser generated by jison 0.4.18 */ 1222 | /* 1223 | Returns a Parser object of the following structure: 1224 | 1225 | Parser: { 1226 | yy: {} 1227 | } 1228 | 1229 | Parser.prototype: { 1230 | yy: {}, 1231 | trace: function(), 1232 | symbols_: {associative list: name ==> number}, 1233 | terminals_: {associative list: number ==> name}, 1234 | productions_: [...], 1235 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), 1236 | table: [...], 1237 | defaultActions: {...}, 1238 | parseError: function(str, hash), 1239 | parse: function(input), 1240 | 1241 | lexer: { 1242 | EOF: 1, 1243 | parseError: function(str, hash), 1244 | setInput: function(input), 1245 | input: function(), 1246 | unput: function(str), 1247 | more: function(), 1248 | less: function(n), 1249 | pastInput: function(), 1250 | upcomingInput: function(), 1251 | showPosition: function(), 1252 | test_match: function(regex_match_array, rule_index), 1253 | next: function(), 1254 | lex: function(), 1255 | begin: function(condition), 1256 | popState: function(), 1257 | _currentRules: function(), 1258 | topState: function(), 1259 | pushState: function(condition), 1260 | 1261 | options: { 1262 | ranges: boolean (optional: true ==> token location info will include a .range[] member) 1263 | flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) 1264 | 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) 1265 | }, 1266 | 1267 | performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), 1268 | rules: [...], 1269 | conditions: {associative list: name ==> set}, 1270 | } 1271 | } 1272 | 1273 | 1274 | token location info (@$, _$, etc.): { 1275 | first_line: n, 1276 | last_line: n, 1277 | first_column: n, 1278 | last_column: n, 1279 | range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) 1280 | } 1281 | 1282 | 1283 | the parseError function receives a 'hash' object with these members for lexer and parser errors: { 1284 | text: (matched text) 1285 | token: (the produced terminal token, if any) 1286 | line: (yylineno) 1287 | } 1288 | while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { 1289 | loc: (yylloc) 1290 | expected: (string describing the set of expected tokens) 1291 | recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) 1292 | } 1293 | */ 1294 | var nomnomlCoreParser = (function(){ 1295 | var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,4],$V1=[1,7],$V2=[1,9],$V3=[5,10,12,14],$V4=[12,14]; 1296 | var parser = {trace: function trace () { }, 1297 | yy: {}, 1298 | symbols_: {"error":2,"root":3,"compartment":4,"EOF":5,"slot":6,"IDENT":7,"class":8,"association":9,"SEP":10,"parts":11,"|":12,"[":13,"]":14,"$accept":0,"$end":1}, 1299 | terminals_: {2:"error",5:"EOF",7:"IDENT",10:"SEP",12:"|",13:"[",14:"]"}, 1300 | productions_: [0,[3,2],[6,1],[6,1],[6,1],[4,1],[4,3],[11,1],[11,3],[11,2],[9,3],[8,3]], 1301 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { 1302 | /* this == yyval */ 1303 | 1304 | var $0 = $$.length - 1; 1305 | switch (yystate) { 1306 | case 1: 1307 | return $$[$0-1] 1308 | break; 1309 | case 2: 1310 | this.$ = $$[$0].trim().replace(/\\(\[|\]|\|)/g, '$'+'1'); 1311 | break; 1312 | case 3: case 4: 1313 | this.$ = $$[$0]; 1314 | break; 1315 | case 5: case 7: 1316 | this.$ = [$$[$0]]; 1317 | break; 1318 | case 6: 1319 | this.$ = $$[$0-2].concat($$[$0]); 1320 | break; 1321 | case 8: 1322 | this.$ = $$[$0-2].concat([$$[$0]]); 1323 | break; 1324 | case 9: 1325 | this.$ = $$[$0-1].concat([[]]); 1326 | break; 1327 | case 10: 1328 | 1329 | var t = $$[$0-1].trim().replace(/\\(\[|\]|\|)/g, '$'+'1').match('^(.*?)([<:o+]*-/?-*[:o+>]*)(.*)$'); 1330 | this.$ = {assoc:t[2], start:$$[$0-2], end:$$[$0], startLabel:t[1].trim(), endLabel:t[3].trim()}; 1331 | 1332 | break; 1333 | case 11: 1334 | 1335 | var type = 'CLASS'; 1336 | var id = $$[$0-1][0][0]; 1337 | var typeMatch = $$[$0-1][0][0].match('<([a-z]*)>(.*)'); 1338 | if (typeMatch) { 1339 | type = typeMatch[1].toUpperCase(); 1340 | id = typeMatch[2].trim(); 1341 | } 1342 | $$[$0-1][0][0] = id; 1343 | this.$ = {type:type, id:id, parts:$$[$0-1]}; 1344 | 1345 | break; 1346 | } 1347 | }, 1348 | table: [{3:1,4:2,6:3,7:$V0,8:5,9:6,13:$V1},{1:[3]},{5:[1,8],10:$V2},o($V3,[2,5]),o($V3,[2,2]),o($V3,[2,3],{7:[1,10]}),o($V3,[2,4]),{4:12,6:3,7:$V0,8:5,9:6,11:11,13:$V1},{1:[2,1]},{6:13,7:$V0,8:5,9:6,13:$V1},{8:14,13:$V1},{12:[1,16],14:[1,15]},o($V4,[2,7],{10:$V2}),o($V3,[2,6]),o($V3,[2,10]),o([5,7,10,12,14],[2,11]),o($V4,[2,9],{6:3,8:5,9:6,4:17,7:$V0,13:$V1}),o($V4,[2,8],{10:$V2})], 1349 | defaultActions: {8:[2,1]}, 1350 | parseError: function parseError (str, hash) { 1351 | if (hash.recoverable) { 1352 | this.trace(str); 1353 | } else { 1354 | var error = new Error(str); 1355 | error.hash = hash; 1356 | throw error; 1357 | } 1358 | }, 1359 | parse: function parse(input) { 1360 | var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; 1361 | var args = lstack.slice.call(arguments, 1); 1362 | var lexer = Object.create(this.lexer); 1363 | var sharedState = { yy: {} }; 1364 | for (var k in this.yy) { 1365 | if (Object.prototype.hasOwnProperty.call(this.yy, k)) { 1366 | sharedState.yy[k] = this.yy[k]; 1367 | } 1368 | } 1369 | lexer.setInput(input, sharedState.yy); 1370 | sharedState.yy.lexer = lexer; 1371 | sharedState.yy.parser = this; 1372 | if (typeof lexer.yylloc == 'undefined') { 1373 | lexer.yylloc = {}; 1374 | } 1375 | var yyloc = lexer.yylloc; 1376 | lstack.push(yyloc); 1377 | var ranges = lexer.options && lexer.options.ranges; 1378 | if (typeof sharedState.yy.parseError === 'function') { 1379 | this.parseError = sharedState.yy.parseError; 1380 | } else { 1381 | this.parseError = Object.getPrototypeOf(this).parseError; 1382 | } 1383 | function popStack(n) { 1384 | stack.length = stack.length - 2 * n; 1385 | vstack.length = vstack.length - n; 1386 | lstack.length = lstack.length - n; 1387 | } 1388 | _token_stack: 1389 | var lex = function () { 1390 | var token; 1391 | token = lexer.lex() || EOF; 1392 | if (typeof token !== 'number') { 1393 | token = self.symbols_[token] || token; 1394 | } 1395 | return token; 1396 | }; 1397 | var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; 1398 | while (true) { 1399 | state = stack[stack.length - 1]; 1400 | if (this.defaultActions[state]) { 1401 | action = this.defaultActions[state]; 1402 | } else { 1403 | if (symbol === null || typeof symbol == 'undefined') { 1404 | symbol = lex(); 1405 | } 1406 | action = table[state] && table[state][symbol]; 1407 | } 1408 | if (typeof action === 'undefined' || !action.length || !action[0]) { 1409 | var errStr = ''; 1410 | expected = []; 1411 | for (p in table[state]) { 1412 | if (this.terminals_[p] && p > TERROR) { 1413 | expected.push('\'' + this.terminals_[p] + '\''); 1414 | } 1415 | } 1416 | if (lexer.showPosition) { 1417 | errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; 1418 | } else { 1419 | errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); 1420 | } 1421 | this.parseError(errStr, { 1422 | text: lexer.match, 1423 | token: this.terminals_[symbol] || symbol, 1424 | line: lexer.yylineno, 1425 | loc: yyloc, 1426 | expected: expected 1427 | }); 1428 | } 1429 | if (action[0] instanceof Array && action.length > 1) { 1430 | throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); 1431 | } 1432 | switch (action[0]) { 1433 | case 1: 1434 | stack.push(symbol); 1435 | vstack.push(lexer.yytext); 1436 | lstack.push(lexer.yylloc); 1437 | stack.push(action[1]); 1438 | symbol = null; 1439 | if (!preErrorSymbol) { 1440 | yyleng = lexer.yyleng; 1441 | yytext = lexer.yytext; 1442 | yylineno = lexer.yylineno; 1443 | yyloc = lexer.yylloc; 1444 | if (recovering > 0) { 1445 | recovering--; 1446 | } 1447 | } else { 1448 | symbol = preErrorSymbol; 1449 | preErrorSymbol = null; 1450 | } 1451 | break; 1452 | case 2: 1453 | len = this.productions_[action[1]][1]; 1454 | yyval.$ = vstack[vstack.length - len]; 1455 | yyval._$ = { 1456 | first_line: lstack[lstack.length - (len || 1)].first_line, 1457 | last_line: lstack[lstack.length - 1].last_line, 1458 | first_column: lstack[lstack.length - (len || 1)].first_column, 1459 | last_column: lstack[lstack.length - 1].last_column 1460 | }; 1461 | if (ranges) { 1462 | yyval._$.range = [ 1463 | lstack[lstack.length - (len || 1)].range[0], 1464 | lstack[lstack.length - 1].range[1] 1465 | ]; 1466 | } 1467 | r = this.performAction.apply(yyval, [ 1468 | yytext, 1469 | yyleng, 1470 | yylineno, 1471 | sharedState.yy, 1472 | action[1], 1473 | vstack, 1474 | lstack 1475 | ].concat(args)); 1476 | if (typeof r !== 'undefined') { 1477 | return r; 1478 | } 1479 | if (len) { 1480 | stack = stack.slice(0, -1 * len * 2); 1481 | vstack = vstack.slice(0, -1 * len); 1482 | lstack = lstack.slice(0, -1 * len); 1483 | } 1484 | stack.push(this.productions_[action[1]][0]); 1485 | vstack.push(yyval.$); 1486 | lstack.push(yyval._$); 1487 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 1488 | stack.push(newState); 1489 | break; 1490 | case 3: 1491 | return true; 1492 | } 1493 | } 1494 | return true; 1495 | }}; 1496 | /* generated by jison-lex 0.3.4 */ 1497 | var lexer = (function(){ 1498 | var lexer = ({ 1499 | 1500 | EOF:1, 1501 | 1502 | parseError:function parseError(str, hash) { 1503 | if (this.yy.parser) { 1504 | this.yy.parser.parseError(str, hash); 1505 | } else { 1506 | throw new Error(str); 1507 | } 1508 | }, 1509 | 1510 | // resets the lexer, sets new input 1511 | setInput:function (input, yy) { 1512 | this.yy = yy || this.yy || {}; 1513 | this._input = input; 1514 | this._more = this._backtrack = this.done = false; 1515 | this.yylineno = this.yyleng = 0; 1516 | this.yytext = this.matched = this.match = ''; 1517 | this.conditionStack = ['INITIAL']; 1518 | this.yylloc = { 1519 | first_line: 1, 1520 | first_column: 0, 1521 | last_line: 1, 1522 | last_column: 0 1523 | }; 1524 | if (this.options.ranges) { 1525 | this.yylloc.range = [0,0]; 1526 | } 1527 | this.offset = 0; 1528 | return this; 1529 | }, 1530 | 1531 | // consumes and returns one char from the input 1532 | input:function () { 1533 | var ch = this._input[0]; 1534 | this.yytext += ch; 1535 | this.yyleng++; 1536 | this.offset++; 1537 | this.match += ch; 1538 | this.matched += ch; 1539 | var lines = ch.match(/(?:\r\n?|\n).*/g); 1540 | if (lines) { 1541 | this.yylineno++; 1542 | this.yylloc.last_line++; 1543 | } else { 1544 | this.yylloc.last_column++; 1545 | } 1546 | if (this.options.ranges) { 1547 | this.yylloc.range[1]++; 1548 | } 1549 | 1550 | this._input = this._input.slice(1); 1551 | return ch; 1552 | }, 1553 | 1554 | // unshifts one char (or a string) into the input 1555 | unput:function (ch) { 1556 | var len = ch.length; 1557 | var lines = ch.split(/(?:\r\n?|\n)/g); 1558 | 1559 | this._input = ch + this._input; 1560 | this.yytext = this.yytext.substr(0, this.yytext.length - len); 1561 | //this.yyleng -= len; 1562 | this.offset -= len; 1563 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 1564 | this.match = this.match.substr(0, this.match.length - 1); 1565 | this.matched = this.matched.substr(0, this.matched.length - 1); 1566 | 1567 | if (lines.length - 1) { 1568 | this.yylineno -= lines.length - 1; 1569 | } 1570 | var r = this.yylloc.range; 1571 | 1572 | this.yylloc = { 1573 | first_line: this.yylloc.first_line, 1574 | last_line: this.yylineno + 1, 1575 | first_column: this.yylloc.first_column, 1576 | last_column: lines ? 1577 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) 1578 | + oldLines[oldLines.length - lines.length].length - lines[0].length : 1579 | this.yylloc.first_column - len 1580 | }; 1581 | 1582 | if (this.options.ranges) { 1583 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 1584 | } 1585 | this.yyleng = this.yytext.length; 1586 | return this; 1587 | }, 1588 | 1589 | // When called from action, caches matched text and appends it on next action 1590 | more:function () { 1591 | this._more = true; 1592 | return this; 1593 | }, 1594 | 1595 | // 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. 1596 | reject:function () { 1597 | if (this.options.backtrack_lexer) { 1598 | this._backtrack = true; 1599 | } else { 1600 | 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(), { 1601 | text: "", 1602 | token: null, 1603 | line: this.yylineno 1604 | }); 1605 | 1606 | } 1607 | return this; 1608 | }, 1609 | 1610 | // retain first n characters of the match 1611 | less:function (n) { 1612 | this.unput(this.match.slice(n)); 1613 | }, 1614 | 1615 | // displays already matched input, i.e. for error messages 1616 | pastInput:function () { 1617 | var past = this.matched.substr(0, this.matched.length - this.match.length); 1618 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 1619 | }, 1620 | 1621 | // displays upcoming input, i.e. for error messages 1622 | upcomingInput:function () { 1623 | var next = this.match; 1624 | if (next.length < 20) { 1625 | next += this._input.substr(0, 20-next.length); 1626 | } 1627 | return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); 1628 | }, 1629 | 1630 | // displays the character position where the lexing error occurred, i.e. for error messages 1631 | showPosition:function () { 1632 | var pre = this.pastInput(); 1633 | var c = new Array(pre.length + 1).join("-"); 1634 | return pre + this.upcomingInput() + "\n" + c + "^"; 1635 | }, 1636 | 1637 | // test the lexed token: return FALSE when not a match, otherwise return token 1638 | test_match:function(match, indexed_rule) { 1639 | var token, 1640 | lines, 1641 | backup; 1642 | 1643 | if (this.options.backtrack_lexer) { 1644 | // save context 1645 | backup = { 1646 | yylineno: this.yylineno, 1647 | yylloc: { 1648 | first_line: this.yylloc.first_line, 1649 | last_line: this.last_line, 1650 | first_column: this.yylloc.first_column, 1651 | last_column: this.yylloc.last_column 1652 | }, 1653 | yytext: this.yytext, 1654 | match: this.match, 1655 | matches: this.matches, 1656 | matched: this.matched, 1657 | yyleng: this.yyleng, 1658 | offset: this.offset, 1659 | _more: this._more, 1660 | _input: this._input, 1661 | yy: this.yy, 1662 | conditionStack: this.conditionStack.slice(0), 1663 | done: this.done 1664 | }; 1665 | if (this.options.ranges) { 1666 | backup.yylloc.range = this.yylloc.range.slice(0); 1667 | } 1668 | } 1669 | 1670 | lines = match[0].match(/(?:\r\n?|\n).*/g); 1671 | if (lines) { 1672 | this.yylineno += lines.length; 1673 | } 1674 | this.yylloc = { 1675 | first_line: this.yylloc.last_line, 1676 | last_line: this.yylineno + 1, 1677 | first_column: this.yylloc.last_column, 1678 | last_column: lines ? 1679 | lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : 1680 | this.yylloc.last_column + match[0].length 1681 | }; 1682 | this.yytext += match[0]; 1683 | this.match += match[0]; 1684 | this.matches = match; 1685 | this.yyleng = this.yytext.length; 1686 | if (this.options.ranges) { 1687 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 1688 | } 1689 | this._more = false; 1690 | this._backtrack = false; 1691 | this._input = this._input.slice(match[0].length); 1692 | this.matched += match[0]; 1693 | token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); 1694 | if (this.done && this._input) { 1695 | this.done = false; 1696 | } 1697 | if (token) { 1698 | return token; 1699 | } else if (this._backtrack) { 1700 | // recover context 1701 | for (var k in backup) { 1702 | this[k] = backup[k]; 1703 | } 1704 | return false; // rule action called reject() implying the next rule should be tested instead. 1705 | } 1706 | return false; 1707 | }, 1708 | 1709 | // return next match in input 1710 | next:function () { 1711 | if (this.done) { 1712 | return this.EOF; 1713 | } 1714 | if (!this._input) { 1715 | this.done = true; 1716 | } 1717 | 1718 | var token, 1719 | match, 1720 | tempMatch, 1721 | index; 1722 | if (!this._more) { 1723 | this.yytext = ''; 1724 | this.match = ''; 1725 | } 1726 | var rules = this._currentRules(); 1727 | for (var i = 0; i < rules.length; i++) { 1728 | tempMatch = this._input.match(this.rules[rules[i]]); 1729 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 1730 | match = tempMatch; 1731 | index = i; 1732 | if (this.options.backtrack_lexer) { 1733 | token = this.test_match(tempMatch, rules[i]); 1734 | if (token !== false) { 1735 | return token; 1736 | } else if (this._backtrack) { 1737 | match = false; 1738 | continue; // rule action called reject() implying a rule MISmatch. 1739 | } else { 1740 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 1741 | return false; 1742 | } 1743 | } else if (!this.options.flex) { 1744 | break; 1745 | } 1746 | } 1747 | } 1748 | if (match) { 1749 | token = this.test_match(match, rules[index]); 1750 | if (token !== false) { 1751 | return token; 1752 | } 1753 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 1754 | return false; 1755 | } 1756 | if (this._input === "") { 1757 | return this.EOF; 1758 | } else { 1759 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { 1760 | text: "", 1761 | token: null, 1762 | line: this.yylineno 1763 | }); 1764 | } 1765 | }, 1766 | 1767 | // return next match that has a token 1768 | lex:function lex () { 1769 | var r = this.next(); 1770 | if (r) { 1771 | return r; 1772 | } else { 1773 | return this.lex(); 1774 | } 1775 | }, 1776 | 1777 | // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) 1778 | begin:function begin (condition) { 1779 | this.conditionStack.push(condition); 1780 | }, 1781 | 1782 | // pop the previously active lexer condition state off the condition stack 1783 | popState:function popState () { 1784 | var n = this.conditionStack.length - 1; 1785 | if (n > 0) { 1786 | return this.conditionStack.pop(); 1787 | } else { 1788 | return this.conditionStack[0]; 1789 | } 1790 | }, 1791 | 1792 | // produce the lexer rule set which is active for the currently active lexer condition state 1793 | _currentRules:function _currentRules () { 1794 | if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { 1795 | return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; 1796 | } else { 1797 | return this.conditions["INITIAL"].rules; 1798 | } 1799 | }, 1800 | 1801 | // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available 1802 | topState:function topState (n) { 1803 | n = this.conditionStack.length - 1 - Math.abs(n || 0); 1804 | if (n >= 0) { 1805 | return this.conditionStack[n]; 1806 | } else { 1807 | return "INITIAL"; 1808 | } 1809 | }, 1810 | 1811 | // alias for begin(condition) 1812 | pushState:function pushState (condition) { 1813 | this.begin(condition); 1814 | }, 1815 | 1816 | // return the number of states currently on the stack 1817 | stateStackSize:function stateStackSize() { 1818 | return this.conditionStack.length; 1819 | }, 1820 | options: {}, 1821 | performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 1822 | var YYSTATE=YY_START; 1823 | switch($avoiding_name_collisions) { 1824 | case 0:return 12 1825 | break; 1826 | case 1:return 7 1827 | break; 1828 | case 2:return 13 1829 | break; 1830 | case 3:return 14 1831 | break; 1832 | case 4:return 10 1833 | break; 1834 | case 5:return 5 1835 | break; 1836 | case 6:return 'INVALID' 1837 | break; 1838 | } 1839 | }, 1840 | rules: [/^(?:\s*\|\s*)/,/^(?:(\\(\[|\]|\|)|[^\]\[|;\n])+)/,/^(?:\[)/,/^(?:\s*\])/,/^(?:[ ]*(;|\n)+[ ]*)/,/^(?:$)/,/^(?:.)/], 1841 | conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6],"inclusive":true}} 1842 | }); 1843 | return lexer; 1844 | })(); 1845 | parser.lexer = lexer; 1846 | function Parser () { 1847 | this.yy = {}; 1848 | } 1849 | Parser.prototype = parser;parser.Parser = Parser; 1850 | return new Parser; 1851 | })();; 1852 | return nomnoml; 1853 | }); 1854 | -------------------------------------------------------------------------------- /tealview.js: -------------------------------------------------------------------------------- 1 | // Build field names table 2 | function buildTable(o, par) { 3 | if (o !== null && typeof o == "object") { 4 | Object.keys(o).forEach(function(key) { 5 | // Add an entry 6 | var r = document.createElement("tr"); 7 | var e = document.createElement("td"); 8 | r.appendChild(e); 9 | if (typeof o[key] == "boolean") { 10 | e.innerText = key; 11 | e.id = key; 12 | e.classList.add("fcell"); 13 | } else { 14 | // Add a subtable 15 | var t = document.createElement("table"); 16 | e.appendChild(t); 17 | 18 | // Add subtable header 19 | var ir = document.createElement("tr"); 20 | var ie = document.createElement("th"); 21 | ie.innerText = key; 22 | ir.appendChild(ie); 23 | t.appendChild(ir); 24 | 25 | buildTable(o[key], t); 26 | } 27 | par.appendChild(r); 28 | }) 29 | } 30 | } 31 | 32 | var t = document.createElement("table"); 33 | t.id = "tfields"; 34 | buildTable(fieldNames, t); 35 | document.getElementById("centercol").appendChild(t); 36 | 37 | // Restore any previous script 38 | var txt = document.getElementById("tealscript"); 39 | txt.value = localStorage.getItem("teal"); 40 | 41 | var graphWrapper = document.getElementById("graphwrap"); 42 | 43 | /* 44 | * 45 | */ 46 | 47 | function Node(name, code, offset) { 48 | this.name = name; 49 | this.code = code; 50 | this.offset = offset; 51 | this.children = []; 52 | } 53 | 54 | Node.prototype.addChild = function(node) { 55 | this.children.push(node); 56 | } 57 | 58 | /* 59 | * 60 | */ 61 | 62 | /* 63 | * 64 | */ 65 | 66 | function Graph(code) { 67 | // Map node name -> node 68 | this.nodes = {}; 69 | 70 | // Map node name -> [node names] 71 | this.edges = {}; 72 | 73 | // Process branches and labels 74 | this.splitBranchesAndLabels(escapeCode(code)); 75 | } 76 | 77 | function escapeCode(code) { 78 | return code.replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\|/g, "\\|"); 79 | } 80 | 81 | Graph.prototype.splitBranchesAndLabels = function(code) { 82 | var block = []; 83 | var lastLabel = "entry"; 84 | var lines = code.split("\n"); 85 | var wasReturn = false; 86 | var wasGoto = null; 87 | var deadCodeCounter = 0 88 | 89 | var i = 0; 90 | for (i = 0; i < lines.length; i++) { 91 | // Strip comments 92 | var cs = lines[i].indexOf("//"); 93 | var line = lines[i].substring(0, cs == -1 ? undefined : cs); 94 | 95 | // Strip pragma 96 | var pr = line.indexOf("#"); 97 | if (pr != -1) { 98 | line = line.substring(0, pr); 99 | } 100 | 101 | // Strip trailing whitespace 102 | line = line.trim(); 103 | if (line === "") { 104 | continue; 105 | } 106 | block.push(line); 107 | 108 | // Match branches and labels 109 | var branchMatch = line.match(/^b(?:n?z)\s+(.*)/); 110 | var gotoMatch = line.match(/^(?:b)\s+(.*)/); 111 | var labelMatch = line.match(/(.*):/); 112 | var returnMatch = line.match(/^return.*/); 113 | var callsubMatch = line.match(/^callsub\s+(.*)/); 114 | var retsubMatch = line.match(/^retsub.*/); 115 | if ((returnMatch || retsubMatch) && !wasReturn) { 116 | wasReturn = true; 117 | continue; 118 | } 119 | if (branchMatch || labelMatch || callsubMatch || gotoMatch) { 120 | // remove last line if we seen return and this line is label - dead end 121 | if (labelMatch && wasReturn) { 122 | block.pop(); 123 | } 124 | 125 | if (labelMatch && wasGoto) { 126 | // if there was an unconditional branch and current line is label 127 | // then reset the label 128 | var newLabel = labelMatch[1]; 129 | lastLabel = labelMatch[1]; 130 | // check of there is some dead code after the unconditional branch 131 | // if so then create a appropriate block 132 | var deadCodeLines = []; 133 | while (!block[0].match(/(.*):/)) { 134 | deadCodeLines.push(block[0]) 135 | block.shift() // consume lines between unconditional brach and the label 136 | } 137 | if (deadCodeLines && deadCodeLines.length > 0) { 138 | var label = "dead_code_" + deadCodeCounter; 139 | if (!this.edges[wasGoto]) { 140 | this.edges[wasGoto] = []; 141 | } 142 | this.edges[wasGoto].push(label); 143 | var node = new Node(label, deadCodeLines.join("\n"), i); 144 | this.nodes[label] = node; 145 | deadCodeCounter++; 146 | } 147 | block.shift() // consume label line 148 | wasGoto = null; 149 | continue; 150 | } 151 | // Create a node named after the most recent label 152 | var node = new Node(lastLabel, block.join("\n"), i); 153 | this.nodes[lastLabel] = node; 154 | block = []; 155 | 156 | // If this was a label, then name the next node 157 | // after the label. Otherwise, name it after the 158 | // branch 159 | if (labelMatch) { 160 | var newLabel = labelMatch[1]; 161 | 162 | // Add an edge from the old label to the new label 163 | if (!this.edges[lastLabel]) { 164 | this.edges[lastLabel] = []; 165 | } 166 | // there was return on previous line then do not create a new edge 167 | if (!wasReturn) { 168 | this.edges[lastLabel].push(newLabel); 169 | } 170 | 171 | // Update lastLabel, which names the next block of code 172 | lastLabel = newLabel; 173 | } else if (gotoMatch) { 174 | var newLabel = gotoMatch[1] 175 | if (!this.edges[lastLabel]) { 176 | this.edges[lastLabel] = []; 177 | } 178 | // Add an edge from the old to the new destination 179 | this.edges[lastLabel].push(newLabel); 180 | wasGoto = lastLabel; 181 | lastLabel = newLabel; 182 | } else if (branchMatch || callsubMatch) { 183 | var newLabel = "fallthru_" + i + "_" + line.replace(/\s/g, "_"); 184 | 185 | var edge = branchMatch ? branchMatch[1] : callsubMatch[1] 186 | // As a branch, we will point to two nodes: 187 | // the first is the target label of the branch (bnz taken) 188 | // the second is the subsequent code (bnz not taken) 189 | if (!this.edges[lastLabel]) { 190 | this.edges[lastLabel] = []; 191 | } 192 | this.edges[lastLabel].push(edge); 193 | this.edges[lastLabel].push(newLabel); 194 | 195 | // Update lastLabel, which names the next block of code 196 | lastLabel = newLabel; 197 | } 198 | } 199 | wasReturn = false; 200 | } 201 | 202 | var node = new Node(lastLabel, block.join("\n"), i); 203 | this.nodes[lastLabel] = node; 204 | } 205 | 206 | Graph.prototype.generateNoml = function() { 207 | var lines = ["#font: courier"]; 208 | var self = this; 209 | 210 | // Add code sections 211 | Object.keys(self.nodes).forEach(function(key) { 212 | lines.push("[" + key + "|"); 213 | lines.push(self.nodes[key].code); 214 | lines.push("]"); 215 | }); 216 | 217 | Object.keys(self.edges).forEach(function(key) { 218 | var edges = self.edges[key]; 219 | for (var i = 0; i < edges.length; i++) { 220 | var edge = "" 221 | edge += "[" + key + "]->[" + edges[i] + "]"; 222 | lines.push(edge); 223 | } 224 | }); 225 | 226 | return lines.join("\n") 227 | } 228 | 229 | function initOrAppend(map, key, value) { 230 | if (!map[key]) { 231 | map[key] = {}; 232 | } 233 | map[key][value] = true; 234 | } 235 | 236 | function getCheckedFields(code) { 237 | var lines = code.split("\n"); 238 | var checked = {}; 239 | for (var i = 0; i < lines.length; i++) { 240 | var line = lines[i]; 241 | var tmatch = line.match(/^txn\s+(.*)/); 242 | if (tmatch) { 243 | initOrAppend(checked, tmatch[1], 0); 244 | continue; 245 | } 246 | 247 | var gtmatch = line.match(/^gtxn\s+(\d+)\s+(.*)/); 248 | if (gtmatch) { 249 | initOrAppend(checked, gtmatch[2], parseInt(gtmatch[1]) + 1); 250 | continue; 251 | } 252 | 253 | var globmatch = line.match(/^global\s+(.*)/); 254 | if (globmatch) { 255 | // -1 indicates that this is not a check associated with a particular txn 256 | initOrAppend(checked, globmatch[1], -1); 257 | continue; 258 | } 259 | 260 | } 261 | 262 | return checked; 263 | } 264 | 265 | var bgColors = [ 266 | "#000000", 267 | "#575757", 268 | "#AD2323", 269 | "#2A4BD7", 270 | "#1D6914", 271 | "#814A19", 272 | "#8126C0", 273 | "#A0A0A0", 274 | "#81C57A", 275 | "#9DAFFF", 276 | "#29D0D0", 277 | "#FF9233", 278 | "#FFEE33", 279 | "#E9DEBB", 280 | "#FFCDF3", 281 | "#FFFFFF", 282 | "#9DAFFF" 283 | ]; 284 | 285 | var txtColors = [ 286 | "#FFFFFF", 287 | "#FFFFFF", 288 | "#FFFFFF", 289 | "#FFFFFF", 290 | "#FFFFFF", 291 | "#FFFFFF", 292 | "#FFFFFF", 293 | "#FFFFFF", 294 | "#FFFFFF", 295 | "#FFFFFF", 296 | "#FFFFFF", 297 | "#FFFFFF", 298 | "#000000", 299 | "#000000", 300 | "#000000", 301 | "#000000", 302 | "#FFFFFF" 303 | ]; 304 | 305 | Graph.prototype.updateCheckedFields = function() { 306 | var boxes = document.getElementsByClassName("highlighted"); 307 | 308 | // Mark checked fields for each section 309 | var checked = {}; 310 | for (var i = 0; i < boxes.length; i++) { 311 | var node = this.nodes[boxes[i].id]; 312 | var c = getCheckedFields(node.code); 313 | Object.keys(c).forEach(function(key) { 314 | // Merge in checked fields 315 | checked[key] = {...checked[key], ...c[key]}; 316 | }); 317 | } 318 | 319 | // Reset checked highlight 320 | var cells = document.getElementsByClassName("fcell"); 321 | for (var i = 0; i < cells.length; i++) { 322 | cells[i].classList.remove("checked"); 323 | } 324 | 325 | // Reset selected indicators 326 | var inds = document.getElementsByClassName("check-indicator"); 327 | for (var i = inds.length - 1; i >= 0; i--) { 328 | inds[i].remove(); 329 | } 330 | 331 | // Set checked highlight 332 | Object.keys(checked).forEach(function(key) { 333 | var cell = document.getElementById(key); 334 | if (cell) { 335 | cell.classList.add("checked"); 336 | 337 | // Mark which txns we checked the field for 338 | Object.keys(checked[key]).sort((x, y)=>(parseInt(x) - parseInt(y))).forEach(function(txidx) { 339 | var txt; 340 | if (txidx == -1) { 341 | return; 342 | } else if (txidx == 0) { 343 | txt = "★"; 344 | } else { 345 | txt = String(txidx - 1); 346 | } 347 | var elt = document.createElement("span"); 348 | elt.innerText = "[" + txt + "]"; 349 | elt.classList.add("check-indicator"); 350 | var colorIdx = txidx == "this" ? 0 : txidx; 351 | elt.style.backgroundColor = bgColors[colorIdx % bgColors.length]; 352 | elt.style.color = txtColors[colorIdx % txtColors.length]; 353 | cell.appendChild(elt); 354 | }); 355 | } 356 | }) 357 | } 358 | 359 | Graph.prototype.handleClick = function(e) { 360 | e.target.classList.toggle("highlighted"); 361 | this.updateCheckedFields(); 362 | } 363 | 364 | Graph.prototype.handleClicks = function() { 365 | var boxes = document.getElementsByClassName("node"); 366 | for (var i = 0; i < boxes.length; i++) { 367 | boxes[i].addEventListener("click", this.handleClick.bind(this)); 368 | } 369 | } 370 | 371 | /* 372 | * 373 | */ 374 | 375 | function draw() { 376 | var g = new Graph(txt.value); 377 | var svg = nomnoml.renderSvg(g.generateNoml()); 378 | graphWrapper.innerHTML = svg; 379 | 380 | // Entry always starts off highlighted 381 | document.getElementById("entry").classList.toggle("highlighted"); 382 | g.updateCheckedFields(); 383 | 384 | g.handleClicks(); 385 | } 386 | 387 | txt.addEventListener('input', function() { 388 | localStorage.setItem('teal', txt.value); 389 | draw(); 390 | }); 391 | 392 | draw(); 393 | --------------------------------------------------------------------------------