123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945 |
- /**
- * Parse a text source containing data in DOT language into a JSON object.
- * The object contains two lists: one with nodes and one with edges.
- *
- * DOT language reference: http://www.graphviz.org/doc/info/lang.html
- *
- * DOT language attributes: http://graphviz.org/content/attrs
- *
- * @param {string} data Text containing a graph in DOT-notation
- * @return {Object} graph An object containing two parameters:
- * {Object[]} nodes
- * {Object[]} edges
- *
- * -------------------------------------------
- * TODO
- * ====
- *
- * For label handling, this is an incomplete implementation. From docs (quote #3015):
- *
- * > the escape sequences "\n", "\l" and "\r" divide the label into lines, centered,
- * > left-justified, and right-justified, respectively.
- *
- * Source: http://www.graphviz.org/content/attrs#kescString
- *
- * > As another aid for readability, dot allows double-quoted strings to span multiple physical
- * > lines using the standard C convention of a backslash immediately preceding a newline
- * > character
- * > In addition, double-quoted strings can be concatenated using a '+' operator.
- * > As HTML strings can contain newline characters, which are used solely for formatting,
- * > the language does not allow escaped newlines or concatenation operators to be used
- * > within them.
- *
- * - Currently, only '\\n' is handled
- * - Note that text explicitly says 'labels'; the dot parser currently handles escape
- * sequences in **all** strings.
- */
- function parseDOT (data) {
- dot = data;
- return parseGraph();
- }
- // mapping of attributes from DOT (the keys) to vis.js (the values)
- var NODE_ATTR_MAPPING = {
- 'fontsize': 'font.size',
- 'fontcolor': 'font.color',
- 'labelfontcolor': 'font.color',
- 'fontname': 'font.face',
- 'color': ['color.border', 'color.background'],
- 'fillcolor': 'color.background',
- 'tooltip': 'title',
- 'labeltooltip': 'title'
- };
- var EDGE_ATTR_MAPPING = Object.create(NODE_ATTR_MAPPING);
- EDGE_ATTR_MAPPING.color = 'color.color';
- EDGE_ATTR_MAPPING.style = 'dashes';
- // token types enumeration
- var TOKENTYPE = {
- NULL : 0,
- DELIMITER : 1,
- IDENTIFIER: 2,
- UNKNOWN : 3
- };
- // map with all delimiters
- var DELIMITERS = {
- '{': true,
- '}': true,
- '[': true,
- ']': true,
- ';': true,
- '=': true,
- ',': true,
- '->': true,
- '--': true
- };
- var dot = ''; // current dot file
- var index = 0; // current index in dot file
- var c = ''; // current token character in expr
- var token = ''; // current token
- var tokenType = TOKENTYPE.NULL; // type of the token
- /**
- * Get the first character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function first() {
- index = 0;
- c = dot.charAt(0);
- }
- /**
- * Get the next character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function next() {
- index++;
- c = dot.charAt(index);
- }
- /**
- * Preview the next character from the dot file.
- * @return {string} cNext
- */
- function nextPreview() {
- return dot.charAt(index + 1);
- }
- var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
- /**
- * Test whether given character is alphabetic or numeric
- * @param {string} c
- * @return {Boolean} isAlphaNumeric
- */
- function isAlphaNumeric(c) {
- return regexAlphaNumeric.test(c);
- }
- /**
- * Merge all options of object b into object b
- * @param {Object} a
- * @param {Object} b
- * @return {Object} a
- */
- function merge (a, b) {
- if (!a) {
- a = {};
- }
- if (b) {
- for (var name in b) {
- if (b.hasOwnProperty(name)) {
- a[name] = b[name];
- }
- }
- }
- return a;
- }
- /**
- * Set a value in an object, where the provided parameter name can be a
- * path with nested parameters. For example:
- *
- * var obj = {a: 2};
- * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
- *
- * @param {Object} obj
- * @param {string} path A parameter name or dot-separated parameter path,
- * like "color.highlight.border".
- * @param {*} value
- */
- function setValue(obj, path, value) {
- var keys = path.split('.');
- var o = obj;
- while (keys.length) {
- var key = keys.shift();
- if (keys.length) {
- // this isn't the end point
- if (!o[key]) {
- o[key] = {};
- }
- o = o[key];
- }
- else {
- // this is the end point
- o[key] = value;
- }
- }
- }
- /**
- * Add a node to a graph object. If there is already a node with
- * the same id, their attributes will be merged.
- * @param {Object} graph
- * @param {Object} node
- */
- function addNode(graph, node) {
- var i, len;
- var current = null;
- // find root graph (in case of subgraph)
- var graphs = [graph]; // list with all graphs from current graph to root graph
- var root = graph;
- while (root.parent) {
- graphs.push(root.parent);
- root = root.parent;
- }
- // find existing node (at root level) by its id
- if (root.nodes) {
- for (i = 0, len = root.nodes.length; i < len; i++) {
- if (node.id === root.nodes[i].id) {
- current = root.nodes[i];
- break;
- }
- }
- }
- if (!current) {
- // this is a new node
- current = {
- id: node.id
- };
- if (graph.node) {
- // clone default attributes
- current.attr = merge(current.attr, graph.node);
- }
- }
- // add node to this (sub)graph and all its parent graphs
- for (i = graphs.length - 1; i >= 0; i--) {
- var g = graphs[i];
- if (!g.nodes) {
- g.nodes = [];
- }
- if (g.nodes.indexOf(current) === -1) {
- g.nodes.push(current);
- }
- }
- // merge attributes
- if (node.attr) {
- current.attr = merge(current.attr, node.attr);
- }
- }
- /**
- * Add an edge to a graph object
- * @param {Object} graph
- * @param {Object} edge
- */
- function addEdge(graph, edge) {
- if (!graph.edges) {
- graph.edges = [];
- }
- graph.edges.push(edge);
- if (graph.edge) {
- var attr = merge({}, graph.edge); // clone default attributes
- edge.attr = merge(attr, edge.attr); // merge attributes
- }
- }
- /**
- * Create an edge to a graph object
- * @param {Object} graph
- * @param {string | number | Object} from
- * @param {string | number | Object} to
- * @param {string} type
- * @param {Object | null} attr
- * @return {Object} edge
- */
- function createEdge(graph, from, to, type, attr) {
- var edge = {
- from: from,
- to: to,
- type: type
- };
- if (graph.edge) {
- edge.attr = merge({}, graph.edge); // clone default attributes
- }
- edge.attr = merge(edge.attr || {}, attr); // merge attributes
- return edge;
- }
- /**
- * Get next token in the current dot file.
- * The token and token type are available as token and tokenType
- */
- function getToken() {
- tokenType = TOKENTYPE.NULL;
- token = '';
- // skip over whitespaces
- while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter
- next();
- }
- do {
- var isComment = false;
- // skip comment
- if (c === '#') {
- // find the previous non-space character
- var i = index - 1;
- while (dot.charAt(i) === ' ' || dot.charAt(i) === '\t') {
- i--;
- }
- if (dot.charAt(i) === '\n' || dot.charAt(i) === '') {
- // the # is at the start of a line, this is indeed a line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- }
- if (c === '/' && nextPreview() === '/') {
- // skip line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- if (c === '/' && nextPreview() === '*') {
- // skip block comment
- while (c != '') {
- if (c === '*' && nextPreview() === '/') {
- // end of block comment found. skip these last two characters
- next();
- next();
- break;
- }
- else {
- next();
- }
- }
- isComment = true;
- }
- // skip over whitespaces
- while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter
- next();
- }
- }
- while (isComment);
- // check for end of dot file
- if (c === '') {
- // token is still empty
- tokenType = TOKENTYPE.DELIMITER;
- return;
- }
- // check for delimiters consisting of 2 characters
- var c2 = c + nextPreview();
- if (DELIMITERS[c2]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c2;
- next();
- next();
- return;
- }
- // check for delimiters consisting of 1 character
- if (DELIMITERS[c]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c;
- next();
- return;
- }
- // check for an identifier (number or string)
- // TODO: more precise parsing of numbers/strings (and the port separator ':')
- if (isAlphaNumeric(c) || c === '-') {
- token += c;
- next();
- while (isAlphaNumeric(c)) {
- token += c;
- next();
- }
- if (token === 'false') {
- token = false; // convert to boolean
- }
- else if (token === 'true') {
- token = true; // convert to boolean
- }
- else if (!isNaN(Number(token))) {
- token = Number(token); // convert to number
- }
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
- }
- // check for a string enclosed by double quotes
- if (c === '"') {
- next();
- while (c != '' && (c != '"' || (c === '"' && nextPreview() === '"'))) {
- if (c === '"') { // skip the escape character
- token += c;
- next();
- } else if (c === '\\' && nextPreview() === 'n') { // Honor a newline escape sequence
- token += '\n';
- next();
- } else {
- token += c;
- }
- next();
- }
- if (c != '"') {
- throw newSyntaxError('End of string " expected');
- }
- next();
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
- }
- // something unknown is found, wrong characters, a syntax error
- tokenType = TOKENTYPE.UNKNOWN;
- while (c != '') {
- token += c;
- next();
- }
- throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
- }
- /**
- * Parse a graph.
- * @returns {Object} graph
- */
- function parseGraph() {
- var graph = {};
- first();
- getToken();
- // optional strict keyword
- if (token === 'strict') {
- graph.strict = true;
- getToken();
- }
- // graph or digraph keyword
- if (token === 'graph' || token === 'digraph') {
- graph.type = token;
- getToken();
- }
- // optional graph id
- if (tokenType === TOKENTYPE.IDENTIFIER) {
- graph.id = token;
- getToken();
- }
- // open angle bracket
- if (token != '{') {
- throw newSyntaxError('Angle bracket { expected');
- }
- getToken();
- // statements
- parseStatements(graph);
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
- }
- getToken();
- // end of file
- if (token !== '') {
- throw newSyntaxError('End of file expected');
- }
- getToken();
- // remove temporary default options
- delete graph.node;
- delete graph.edge;
- delete graph.graph;
- return graph;
- }
- /**
- * Parse a list with statements.
- * @param {Object} graph
- */
- function parseStatements (graph) {
- while (token !== '' && token != '}') {
- parseStatement(graph);
- if (token === ';') {
- getToken();
- }
- }
- }
- /**
- * Parse a single statement. Can be a an attribute statement, node
- * statement, a series of node statements and edge statements, or a
- * parameter.
- * @param {Object} graph
- */
- function parseStatement(graph) {
- // parse subgraph
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- // edge statements
- parseEdge(graph, subgraph);
- return;
- }
- // parse an attribute statement
- var attr = parseAttributeStatement(graph);
- if (attr) {
- return;
- }
- // parse node
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
- }
- var id = token; // id can be a string or a number
- getToken();
- if (token === '=') {
- // id statement
- getToken();
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
- }
- graph[id] = token;
- getToken();
- // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
- }
- else {
- parseNodeStatement(graph, id);
- }
- }
- /**
- * Parse a subgraph
- * @param {Object} graph parent graph object
- * @return {Object | null} subgraph
- */
- function parseSubgraph (graph) {
- var subgraph = null;
- // optional subgraph keyword
- if (token === 'subgraph') {
- subgraph = {};
- subgraph.type = 'subgraph';
- getToken();
- // optional graph id
- if (tokenType === TOKENTYPE.IDENTIFIER) {
- subgraph.id = token;
- getToken();
- }
- }
- // open angle bracket
- if (token === '{') {
- getToken();
- if (!subgraph) {
- subgraph = {};
- }
- subgraph.parent = graph;
- subgraph.node = graph.node;
- subgraph.edge = graph.edge;
- subgraph.graph = graph.graph;
- // statements
- parseStatements(subgraph);
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
- }
- getToken();
- // remove temporary default options
- delete subgraph.node;
- delete subgraph.edge;
- delete subgraph.graph;
- delete subgraph.parent;
- // register at the parent graph
- if (!graph.subgraphs) {
- graph.subgraphs = [];
- }
- graph.subgraphs.push(subgraph);
- }
- return subgraph;
- }
- /**
- * parse an attribute statement like "node [shape=circle fontSize=16]".
- * Available keywords are 'node', 'edge', 'graph'.
- * The previous list with default attributes will be replaced
- * @param {Object} graph
- * @returns {String | null} keyword Returns the name of the parsed attribute
- * (node, edge, graph), or null if nothing
- * is parsed.
- */
- function parseAttributeStatement (graph) {
- // attribute statements
- if (token === 'node') {
- getToken();
- // node attributes
- graph.node = parseAttributeList();
- return 'node';
- }
- else if (token === 'edge') {
- getToken();
- // edge attributes
- graph.edge = parseAttributeList();
- return 'edge';
- }
- else if (token === 'graph') {
- getToken();
- // graph attributes
- graph.graph = parseAttributeList();
- return 'graph';
- }
- return null;
- }
- /**
- * parse a node statement
- * @param {Object} graph
- * @param {string | number} id
- */
- function parseNodeStatement(graph, id) {
- // node statement
- var node = {
- id: id
- };
- var attr = parseAttributeList();
- if (attr) {
- node.attr = attr;
- }
- addNode(graph, node);
- // edge statements
- parseEdge(graph, id);
- }
- /**
- * Parse an edge or a series of edges
- * @param {Object} graph
- * @param {string | number} from Id of the from node
- */
- function parseEdge(graph, from) {
- while (token === '->' || token === '--') {
- var to;
- var type = token;
- getToken();
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- to = subgraph;
- }
- else {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier or subgraph expected');
- }
- to = token;
- addNode(graph, {
- id: to
- });
- getToken();
- }
- // parse edge attributes
- var attr = parseAttributeList();
- // create edge
- var edge = createEdge(graph, from, to, type, attr);
- addEdge(graph, edge);
- from = to;
- }
- }
- /**
- * Parse a set with attributes,
- * for example [label="1.000", shape=solid]
- * @return {Object | null} attr
- */
- function parseAttributeList() {
- var attr = null;
- // edge styles of dot and vis
- var edgeStyles = {
- 'dashed': true,
- 'solid': false,
- 'dotted': [1, 5]
- };
- while (token === '[') {
- getToken();
- attr = {};
- while (token !== '' && token != ']') {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute name expected');
- }
- var name = token;
- getToken();
- if (token != '=') {
- throw newSyntaxError('Equal sign = expected');
- }
- getToken();
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute value expected');
- }
- var value = token;
- // convert from dot style to vis
- if (name === 'style') {
- value = edgeStyles[value];
- }
- setValue(attr, name, value); // name can be a path
- getToken();
- if (token ==',') {
- getToken();
- }
- }
- if (token != ']') {
- throw newSyntaxError('Bracket ] expected');
- }
- getToken();
- }
- return attr;
- }
- /**
- * Create a syntax error with extra information on current token and index.
- * @param {string} message
- * @returns {SyntaxError} err
- */
- function newSyntaxError(message) {
- return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
- }
- /**
- * Chop off text after a maximum length
- * @param {string} text
- * @param {number} maxLength
- * @returns {String}
- */
- function chop (text, maxLength) {
- return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
- }
- /**
- * Execute a function fn for each pair of elements in two arrays
- * @param {Array | *} array1
- * @param {Array | *} array2
- * @param {function} fn
- */
- function forEach2(array1, array2, fn) {
- if (Array.isArray(array1)) {
- array1.forEach(function (elem1) {
- if (Array.isArray(array2)) {
- array2.forEach(function (elem2) {
- fn(elem1, elem2);
- });
- }
- else {
- fn(elem1, array2);
- }
- });
- }
- else {
- if (Array.isArray(array2)) {
- array2.forEach(function (elem2) {
- fn(array1, elem2);
- });
- }
- else {
- fn(array1, array2);
- }
- }
- }
- /**
- * Set a nested property on an object
- * When nested objects are missing, they will be created.
- * For example setProp({}, 'font.color', 'red') will return {font: {color: 'red'}}
- * @param {Object} object
- * @param {string} path A dot separated string like 'font.color'
- * @param {*} value Value for the property
- * @return {Object} Returns the original object, allows for chaining.
- */
- function setProp(object, path, value) {
- var names = path.split('.');
- var prop = names.pop();
- // traverse over the nested objects
- var obj = object;
- for (var i = 0; i < names.length; i++) {
- var name = names[i];
- if (!(name in obj)) {
- obj[name] = {};
- }
- obj = obj[name];
- }
- // set the property value
- obj[prop] = value;
- return object;
- }
- /**
- * Convert an object with DOT attributes to their vis.js equivalents.
- * @param {Object} attr Object with DOT attributes
- * @param {Object} mapping
- * @return {Object} Returns an object with vis.js attributes
- */
- function convertAttr (attr, mapping) {
- var converted = {};
- for (var prop in attr) {
- if (attr.hasOwnProperty(prop)) {
- var visProp = mapping[prop];
- if (Array.isArray(visProp)) {
- visProp.forEach(function (visPropI) {
- setProp(converted, visPropI, attr[prop]);
- })
- }
- else if (typeof visProp === 'string') {
- setProp(converted, visProp, attr[prop]);
- }
- else {
- setProp(converted, prop, attr[prop]);
- }
- }
- }
- return converted;
- }
- /**
- * Convert a string containing a graph in DOT language into a map containing
- * with nodes and edges in the format of graph.
- * @param {string} data Text containing a graph in DOT-notation
- * @return {Object} graphData
- */
- function DOTToGraph (data) {
- // parse the DOT file
- var dotData = parseDOT(data);
- var graphData = {
- nodes: [],
- edges: [],
- options: {}
- };
- // copy the nodes
- if (dotData.nodes) {
- dotData.nodes.forEach(function (dotNode) {
- var graphNode = {
- id: dotNode.id,
- label: String(dotNode.label || dotNode.id)
- };
- merge(graphNode, convertAttr(dotNode.attr, NODE_ATTR_MAPPING));
- if (graphNode.image) {
- graphNode.shape = 'image';
- }
- graphData.nodes.push(graphNode);
- });
- }
- // copy the edges
- if (dotData.edges) {
- /**
- * Convert an edge in DOT format to an edge with VisGraph format
- * @param {Object} dotEdge
- * @returns {Object} graphEdge
- */
- var convertEdge = function (dotEdge) {
- var graphEdge = {
- from: dotEdge.from,
- to: dotEdge.to
- };
- merge(graphEdge, convertAttr(dotEdge.attr, EDGE_ATTR_MAPPING));
- graphEdge.arrows = (dotEdge.type === '->') ? 'to' : undefined;
- return graphEdge;
- };
- dotData.edges.forEach(function (dotEdge) {
- var from, to;
- if (dotEdge.from instanceof Object) {
- from = dotEdge.from.nodes;
- }
- else {
- from = {
- id: dotEdge.from
- }
- }
- // TODO: support for attributes 'dir' and 'arrowhead' (edge arrows)
- if (dotEdge.to instanceof Object) {
- to = dotEdge.to.nodes;
- }
- else {
- to = {
- id: dotEdge.to
- }
- }
- if (dotEdge.from instanceof Object && dotEdge.from.edges) {
- dotEdge.from.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
- forEach2(from, to, function (from, to) {
- var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- if (dotEdge.to instanceof Object && dotEdge.to.edges) {
- dotEdge.to.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
- });
- }
- // copy the options
- if (dotData.attr) {
- graphData.options = dotData.attr;
- }
- return graphData;
- }
- // exports
- exports.parseDOT = parseDOT;
- exports.DOTToGraph = DOTToGraph;
|