inno-diagram
Version:
It was created to develop Inno Linc.
281 lines (232 loc) • 8.29 kB
JavaScript
const fs = require('fs');
const { createInflate } = require('zlib');
module.exports = function()
{
var colorTable = null;
const ESCAPED_CHARS =
{
"\n": "<BR/>",
"&": "&",
"<": "<",
">": ">",
};
this.escape_label = function(label)
{
var newlabel = "";
for (var i=0; i<label.length; i++)
newlabel += replaceChar(label.charAt(i));
function replaceChar(c)
{
c = c.replace('{', '\\{').replace('}', '\\}');
c = c.replace(';', '\n');
c = c.replace('<', '\\<').replace('>', '\\>');
// c = c.replace('\\n\\n', '\\n');
return c;
}
return newlabel;
}
this.unescape_label = function(label)
{
var newlabel = label.replace(/\\\{/g, '{').replace(/\\\}/g, '}');
newlabel = newlabel.replace(/\\</g, '<').replace(/\\>/g, '>');
return newlabel;
}
this.splitYumlExpr = function(line, separators, escape = "\\")
{
var word = "";
var lastChar = null;
var parts = [];
for (var i=0; i<line.length; i++)
{
c = line[i];
if (c === escape && i + 1 < line.length)
{
word += c;
word += line[++i];
}
else if (separators.indexOf(c) >= 0 && lastChar === null)
{
if (word.length > 0) {
parts.push(word.trim());
word = "";
}
switch (c)
{
case '[':
lastChar = ']'; break;
case '(':
lastChar = ')'; break;
case '<':
lastChar = '>'; break;
case '|':
lastChar = '|'; break;
default:
lastChar = null; break;
}
word = c;
}
else if (c === lastChar)
{
lastChar = null;
parts.push(word.trim() + c);
word = "";
}
else
{
word += c;
}
}
if (word.length > 0)
parts.push(word.trim());
return parts;
}
this.extractBgAndNote = function(part, allowNote)
{
var ret = { bg: "", isNote: false, luma: 128 };
var bgParts = /^(.*)\{ *bg *: *([a-zA-Z]+\d*|#[0-9a-fA-F]{6}) *\}$/.exec(part);
if (bgParts != null && bgParts.length == 3)
{
ret.part = bgParts[1].trim();
ret.bg = bgParts[2].trim().toLowerCase();
var luma = getLuma(ret.bg);
if (luma < 64)
ret.fontcolor = "white";
else if (luma > 192)
ret.fontcolor = "black";
}
else
ret.part = part.trim();
if (allowNote && part.startsWith("note:"))
{
ret.part = ret.part.substring(5).trim();
ret.isNote = true;
}
return ret;
}
this.escape_token_escapes = function(spec)
{
return spec.replace('\\[', '\\u005b').replace('\\]', '\\u005d');
}
this.unescape_token_escapes = function(spec)
{
return spec.replace('\\u005b', '[').replace('\\u005d', ']');
}
this.recordName = function(label)
{
return label.split("|")[0].trim();
}
this.formatLabel = function(label, wrap, allowDivisors)
{
var lines = [label];
if (allowDivisors && label.indexOf("|") >= 0)
lines = label.split('|');
for (var j=0; j<lines.length; j++)
lines[j] = wordwrap(lines[j], wrap, "\n");
label = lines.join('|');
return escape_label(label);
}
this.wordwrap = function(str, width, newline)
{
if (!str || str.length<width)
return str;
var p;
for (p = width; p>0 && str[p]!=' '; p--) { }
if (p > 0) {
var left = str.substring(0, p);
var right = str.substring(p+1);
return left + newline + this.wordwrap(right, width, newline);
}
return str;
}
this.serializeDot = function(obj)
{
if (obj.shape === "record" && !/^<.+>(|<.+>)*$/.test(obj.label))
{
// Graphviz documentation says (https://www.graphviz.org/doc/info/shapes.html):
// The record-based shape has largely been superseded and greatly generalized by HTML-like labels.
// That is, instead of using shape=record, one might consider using shape=none, margin=0 and an HTML-like label. [...]
// Also note that there are problems using non-trivial edges (edges with ports or labels) between adjacent nodes
// on the same rank if one or both nodes has a record shape.
if (obj.label.includes("|")) {
// If label contains a pipe, we need to use an HTML-like label
return `[fontsize=10,label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="9" ${
obj.fillcolor ? `BGCOLOR="${obj.fillcolor}"` : ""
} ${obj.fontcolor ? `COLOR="${obj.fontcolor}"` : ""}>${
obj.label
.split("|")
.map(text => {
text = this.unescape_label(text);
let htmlTDNode = "<TD>";
for (const char of text) {
htmlTDNode += ESCAPED_CHARS[char] || char;
}
htmlTDNode += "</TD>";
return `<TR>${htmlTDNode}</TR>`;
})
.join("")}</TABLE>>]`;
}
// To avoid this issue, we can use a "rectangle" shape
obj.shape = "rectangle";
}
return (
"[" +
Object.keys(obj)
.map(
key =>
`${key}=` +
("string" === typeof obj[key] ? `"${obj[key]}"` : obj[key])
)
.join(" , ") +
" ]"
);
}
this.serializeDotElements = function(arr)
{
var dot = "";
for (var i=0; i<arr.length; i++)
{
var record = arr[i];
if (record.length == 2)
dot += ' ' + record[0] + ' ' + serializeDot(record[1]) + "\r\n";
else if (record.length == 3)
dot += ' ' + record[0] + " -> " + record[1] + ' ' + serializeDot(record[2]) + "\r\n";
}
return dot;
}
this.buildDotHeader = function(isDark)
{
var colors = isDark ? "color=white, fontcolor=white" : "color=black, fontcolor=black";
var header = "digraph G {\r\n"
+ " graph [ bgcolor=transparent, fontname=Helvetica ]\r\n"
+ " node [ shape=none, margin=0, " + colors + ", fontname=Helvetica ]\r\n"
+ " edge [ " + colors + ", fontname=Helvetica ]\r\n";
return header;
}
loadColors = function()
{
if (colorTable != null)
return;
else
colorTable = {};
var rgb = fs.readFileSync(__dirname + "/../data/rgb.txt", {encoding:"utf8", flag:"r"}).split('\n');
for (var i=0; i<rgb.length; i++)
{
var parts = /^(\d+) (\d+) (\d+) (.*)$/.exec(rgb[i]);
if (parts != null && parts.length == 5 && parts[4].indexOf(' ') < 0)
{
var luma = 0.2126 * parseFloat(parts[0]) + 0.7152 * parseFloat(parts[1]) + 0.0722 * parseFloat(parts[2]);
colorTable[parts[4].toLowerCase()] = luma;
}
}
}
getLuma = function(color)
{
loadColors();
var luma = 128;
if (color.startsWith('#'))
luma = 0.2126 * parseInt(color.substr(1,2), 16) + 0.7152 * parseInt(color.substr(3,2), 16) + 0.0722 * parseInt(color.substr(5,2), 16);
else if (colorTable.hasOwnProperty(color))
luma = colorTable[color];
return luma;
}
}