yuml2svg
Version:
UML diagramming tool based on the yUML syntax
207 lines (177 loc) • 5.76 kB
JavaScript
import Color from "color";
export const escape_label = function (label) {
const ESCAPED_CHARS = {
"{": "\\{",
"}": "\\}",
";": "\n",
"<": "\\<",
">": "\\>",
};
let newLabel = "";
for (const char of label) {
newLabel += ESCAPED_CHARS[char] || char;
}
return newLabel;
};
const unescape_label = label =>
label
.replace(/\\\{/g, "{")
.replace(/\\\}/g, "}")
.replace(/\\</g, "<")
.replace(/\\>/g, ">");
export const splitYumlExpr = function* (line, separators, escape = "\\") {
const SEPARATOR_END = {
"[": "]",
"<": ">",
"(": ")",
"|": "|",
};
let word = "";
let lastChar;
const lineLength = line.length;
for (let i = 0; i < lineLength; i++) {
const currentChar = line.charAt(i);
if (currentChar === escape && i + 1 < lineLength) {
word += currentChar + line.charAt(++i);
} else if (separators.includes(currentChar) && lastChar === undefined) {
if (word.length > 0) {
yield word.trim();
}
lastChar = SEPARATOR_END[currentChar];
word = currentChar;
} else if (currentChar === lastChar) {
lastChar = undefined;
yield word.trim() + currentChar;
word = "";
} else {
word += currentChar;
}
}
if (word.length > 0) {
yield word.trim();
}
};
export const extractBgAndNote = function (part, allowNote) {
const ret = { bg: "", isNote: false, luma: 128 };
const bgParts = /^(.*)\{\s*bg\s*:\s*([a-zA-Z]+\d*|#[0-9a-fA-F]{3,6})\s*\}$/.exec(
part,
);
if (bgParts !== null && bgParts.length === 3) {
const bgColor = Color(bgParts[2].trim());
ret.part = bgParts[1].trim();
ret.bg = bgColor.hex();
ret.fontcolor = bgColor.isDark() ? "white" : "black";
} else {
ret.part = part.trim();
}
if (allowNote && part.startsWith("note:")) {
ret.part = ret.part.substring(5).trim();
ret.isNote = true;
}
return ret;
};
export const escape_token_escapes = function (spec) {
return spec.replace("\\[", "\\u005b").replace("\\]", "\\u005d");
};
export const unescape_token_escapes = function (spec) {
return spec.replace("\\u005b", "[").replace("\\u005d", "]");
};
export const recordName = label =>
(label.includes("|") ? label.substr(0, label.indexOf("|")) : label).trim();
export const formatLabel = function (label, wrap, allowDivisors) {
const DIVISOR = "|";
const lines =
allowDivisors && label.includes(DIVISOR) ? label.split(DIVISOR) : [label];
return escape_label(
lines.map(line => wordwrap(line, wrap, "\n")).join(DIVISOR),
);
};
export const wordwrap = function (str, width, newline) {
if (str && str.length >= width) {
const p = str.lastIndexOf(" ");
if (p > 0) {
const left = str.substring(0, p);
const right = str.substring(p + 1);
return left + newline + wordwrap(right, width, newline);
}
}
return str;
};
export const serializeDot = function (node) {
if (
node.shape &&
node.shape === "record" &&
!/^<.+>(|<.+>)*$/.test(node.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 (node.label.includes("|")) {
// If label contains a pipe (I.E. multi-row record shapes), we need to use an HTML-like label
const rows = node.label.split("|");
const ESCAPED_CHARS = {
"\n": "<BR/>",
"&": "&",
"<": "<",
">": ">",
};
const createSingleCellRow = (attr, text, fontSize) => {
let htmlTDNode = "<TD " + attr;
if (text.startsWith("<")) {
const closingTagPosition = text.indexOf(">");
htmlTDNode += ` PORT="${text.substr(1, closingTagPosition - 1)}"`;
text = text.substr(closingTagPosition + 1);
}
htmlTDNode += ">";
if (fontSize) {
htmlTDNode += `<FONT POINT-SIZE="${fontSize}">`;
}
for (const char of unescape_label(text)) {
htmlTDNode += ESCAPED_CHARS[char] || char;
}
if (fontSize) {
htmlTDNode += `</FONT>`;
}
htmlTDNode += "</TD>";
return `<TR>${htmlTDNode}</TR>`;
};
const title = rows.shift();
return `[fontsize=${
node.fontsize
},label=<<TABLE CELLBORDER="1" CELLSPACING="0" CELLPADDING="9" ${
node.fillcolor ? `BGCOLOR="${node.fillcolor}"` : ""
} ${node.fontcolor ? `COLOR="${node.fontcolor}"` : ""} ${
node.style && node.style === "rounded" ? 'STYLE="ROUNDED"' : ""
}>${createSingleCellRow('BORDER="0"', title)}${rows
.map(text =>
createSingleCellRow('SIDES="T"', text, node.fontsize * 0.9),
)
.join("")}</TABLE>>]`;
}
// On single-row "record", we can use a simpler "rectangle" shape
node.shape = "rectangle";
}
return (
"[" +
Object.keys(node)
.map(
key =>
`${key}=` +
("string" === typeof node[key] ? `"${node[key]}"` : node[key]),
)
.join(" , ") +
" ]"
);
};
export const serializeDotElements = function (iterator) {
let dot = "";
for (const record of iterator) {
if (record.length === 2)
dot += `\t${record[0]} ${serializeDot(record[1])}\n`;
else if (record.length === 3)
dot += `\t${record[0]} -> ${record[1]} ${serializeDot(record[2])}\n`;
}
return dot;
};