vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

901 lines
20 KiB

  1. /**
  2. * Parse a text source containing data in DOT language into a JSON object.
  3. * The object contains two lists: one with nodes and one with edges.
  4. *
  5. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6. *
  7. * DOT language attributes: http://graphviz.org/content/attrs
  8. *
  9. * @param {String} data Text containing a graph in DOT-notation
  10. * @return {Object} graph An object containing two parameters:
  11. * {Object[]} nodes
  12. * {Object[]} edges
  13. */
  14. function parseDOT (data) {
  15. dot = data;
  16. return parseGraph();
  17. }
  18. // mapping of attributes from DOT (the keys) to vis.js (the values)
  19. var ATTR_MAPPING = {
  20. 'fontsize': 'font.size',
  21. 'fontcolor': 'font.color',
  22. 'labelfontcolor': 'font.color',
  23. 'fontname': 'font.face',
  24. 'color': ['color.border', 'color.background'],
  25. 'fillcolor': 'color.background',
  26. 'tooltip': 'title',
  27. 'labeltooltip': 'title'
  28. };
  29. // token types enumeration
  30. var TOKENTYPE = {
  31. NULL : 0,
  32. DELIMITER : 1,
  33. IDENTIFIER: 2,
  34. UNKNOWN : 3
  35. };
  36. // map with all delimiters
  37. var DELIMITERS = {
  38. '{': true,
  39. '}': true,
  40. '[': true,
  41. ']': true,
  42. ';': true,
  43. '=': true,
  44. ',': true,
  45. '->': true,
  46. '--': true
  47. };
  48. var dot = ''; // current dot file
  49. var index = 0; // current index in dot file
  50. var c = ''; // current token character in expr
  51. var token = ''; // current token
  52. var tokenType = TOKENTYPE.NULL; // type of the token
  53. /**
  54. * Get the first character from the dot file.
  55. * The character is stored into the char c. If the end of the dot file is
  56. * reached, the function puts an empty string in c.
  57. */
  58. function first() {
  59. index = 0;
  60. c = dot.charAt(0);
  61. }
  62. /**
  63. * Get the next character from the dot file.
  64. * The character is stored into the char c. If the end of the dot file is
  65. * reached, the function puts an empty string in c.
  66. */
  67. function next() {
  68. index++;
  69. c = dot.charAt(index);
  70. }
  71. /**
  72. * Preview the next character from the dot file.
  73. * @return {String} cNext
  74. */
  75. function nextPreview() {
  76. return dot.charAt(index + 1);
  77. }
  78. /**
  79. * Test whether given character is alphabetic or numeric
  80. * @param {String} c
  81. * @return {Boolean} isAlphaNumeric
  82. */
  83. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  84. function isAlphaNumeric(c) {
  85. return regexAlphaNumeric.test(c);
  86. }
  87. /**
  88. * Merge all options of object b into object b
  89. * @param {Object} a
  90. * @param {Object} b
  91. * @return {Object} a
  92. */
  93. function merge (a, b) {
  94. if (!a) {
  95. a = {};
  96. }
  97. if (b) {
  98. for (var name in b) {
  99. if (b.hasOwnProperty(name)) {
  100. a[name] = b[name];
  101. }
  102. }
  103. }
  104. return a;
  105. }
  106. /**
  107. * Set a value in an object, where the provided parameter name can be a
  108. * path with nested parameters. For example:
  109. *
  110. * var obj = {a: 2};
  111. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  112. *
  113. * @param {Object} obj
  114. * @param {String} path A parameter name or dot-separated parameter path,
  115. * like "color.highlight.border".
  116. * @param {*} value
  117. */
  118. function setValue(obj, path, value) {
  119. var keys = path.split('.');
  120. var o = obj;
  121. while (keys.length) {
  122. var key = keys.shift();
  123. if (keys.length) {
  124. // this isn't the end point
  125. if (!o[key]) {
  126. o[key] = {};
  127. }
  128. o = o[key];
  129. }
  130. else {
  131. // this is the end point
  132. o[key] = value;
  133. }
  134. }
  135. }
  136. /**
  137. * Add a node to a graph object. If there is already a node with
  138. * the same id, their attributes will be merged.
  139. * @param {Object} graph
  140. * @param {Object} node
  141. */
  142. function addNode(graph, node) {
  143. var i, len;
  144. var current = null;
  145. // find root graph (in case of subgraph)
  146. var graphs = [graph]; // list with all graphs from current graph to root graph
  147. var root = graph;
  148. while (root.parent) {
  149. graphs.push(root.parent);
  150. root = root.parent;
  151. }
  152. // find existing node (at root level) by its id
  153. if (root.nodes) {
  154. for (i = 0, len = root.nodes.length; i < len; i++) {
  155. if (node.id === root.nodes[i].id) {
  156. current = root.nodes[i];
  157. break;
  158. }
  159. }
  160. }
  161. if (!current) {
  162. // this is a new node
  163. current = {
  164. id: node.id
  165. };
  166. if (graph.node) {
  167. // clone default attributes
  168. current.attr = merge(current.attr, graph.node);
  169. }
  170. }
  171. // add node to this (sub)graph and all its parent graphs
  172. for (i = graphs.length - 1; i >= 0; i--) {
  173. var g = graphs[i];
  174. if (!g.nodes) {
  175. g.nodes = [];
  176. }
  177. if (g.nodes.indexOf(current) === -1) {
  178. g.nodes.push(current);
  179. }
  180. }
  181. // merge attributes
  182. if (node.attr) {
  183. current.attr = merge(current.attr, node.attr);
  184. }
  185. }
  186. /**
  187. * Add an edge to a graph object
  188. * @param {Object} graph
  189. * @param {Object} edge
  190. */
  191. function addEdge(graph, edge) {
  192. if (!graph.edges) {
  193. graph.edges = [];
  194. }
  195. graph.edges.push(edge);
  196. if (graph.edge) {
  197. var attr = merge({}, graph.edge); // clone default attributes
  198. edge.attr = merge(attr, edge.attr); // merge attributes
  199. }
  200. }
  201. /**
  202. * Create an edge to a graph object
  203. * @param {Object} graph
  204. * @param {String | Number | Object} from
  205. * @param {String | Number | Object} to
  206. * @param {String} type
  207. * @param {Object | null} attr
  208. * @return {Object} edge
  209. */
  210. function createEdge(graph, from, to, type, attr) {
  211. var edge = {
  212. from: from,
  213. to: to,
  214. type: type
  215. };
  216. if (graph.edge) {
  217. edge.attr = merge({}, graph.edge); // clone default attributes
  218. }
  219. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  220. return edge;
  221. }
  222. /**
  223. * Get next token in the current dot file.
  224. * The token and token type are available as token and tokenType
  225. */
  226. function getToken() {
  227. tokenType = TOKENTYPE.NULL;
  228. token = '';
  229. // skip over whitespaces
  230. while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter
  231. next();
  232. }
  233. do {
  234. var isComment = false;
  235. // skip comment
  236. if (c === '#') {
  237. // find the previous non-space character
  238. var i = index - 1;
  239. while (dot.charAt(i) === ' ' || dot.charAt(i) === '\t') {
  240. i--;
  241. }
  242. if (dot.charAt(i) === '\n' || dot.charAt(i) === '') {
  243. // the # is at the start of a line, this is indeed a line comment
  244. while (c != '' && c != '\n') {
  245. next();
  246. }
  247. isComment = true;
  248. }
  249. }
  250. if (c === '/' && nextPreview() === '/') {
  251. // skip line comment
  252. while (c != '' && c != '\n') {
  253. next();
  254. }
  255. isComment = true;
  256. }
  257. if (c === '/' && nextPreview() === '*') {
  258. // skip block comment
  259. while (c != '') {
  260. if (c === '*' && nextPreview() === '/') {
  261. // end of block comment found. skip these last two characters
  262. next();
  263. next();
  264. break;
  265. }
  266. else {
  267. next();
  268. }
  269. }
  270. isComment = true;
  271. }
  272. // skip over whitespaces
  273. while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter
  274. next();
  275. }
  276. }
  277. while (isComment);
  278. // check for end of dot file
  279. if (c === '') {
  280. // token is still empty
  281. tokenType = TOKENTYPE.DELIMITER;
  282. return;
  283. }
  284. // check for delimiters consisting of 2 characters
  285. var c2 = c + nextPreview();
  286. if (DELIMITERS[c2]) {
  287. tokenType = TOKENTYPE.DELIMITER;
  288. token = c2;
  289. next();
  290. next();
  291. return;
  292. }
  293. // check for delimiters consisting of 1 character
  294. if (DELIMITERS[c]) {
  295. tokenType = TOKENTYPE.DELIMITER;
  296. token = c;
  297. next();
  298. return;
  299. }
  300. // check for an identifier (number or string)
  301. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  302. if (isAlphaNumeric(c) || c === '-') {
  303. token += c;
  304. next();
  305. while (isAlphaNumeric(c)) {
  306. token += c;
  307. next();
  308. }
  309. if (token === 'false') {
  310. token = false; // convert to boolean
  311. }
  312. else if (token === 'true') {
  313. token = true; // convert to boolean
  314. }
  315. else if (!isNaN(Number(token))) {
  316. token = Number(token); // convert to number
  317. }
  318. tokenType = TOKENTYPE.IDENTIFIER;
  319. return;
  320. }
  321. // check for a string enclosed by double quotes
  322. if (c === '"') {
  323. next();
  324. while (c != '' && (c != '"' || (c === '"' && nextPreview() === '"'))) {
  325. token += c;
  326. if (c === '"') { // skip the escape character
  327. next();
  328. }
  329. next();
  330. }
  331. if (c != '"') {
  332. throw newSyntaxError('End of string " expected');
  333. }
  334. next();
  335. tokenType = TOKENTYPE.IDENTIFIER;
  336. return;
  337. }
  338. // something unknown is found, wrong characters, a syntax error
  339. tokenType = TOKENTYPE.UNKNOWN;
  340. while (c != '') {
  341. token += c;
  342. next();
  343. }
  344. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  345. }
  346. /**
  347. * Parse a graph.
  348. * @returns {Object} graph
  349. */
  350. function parseGraph() {
  351. var graph = {};
  352. first();
  353. getToken();
  354. // optional strict keyword
  355. if (token === 'strict') {
  356. graph.strict = true;
  357. getToken();
  358. }
  359. // graph or digraph keyword
  360. if (token === 'graph' || token === 'digraph') {
  361. graph.type = token;
  362. getToken();
  363. }
  364. // optional graph id
  365. if (tokenType === TOKENTYPE.IDENTIFIER) {
  366. graph.id = token;
  367. getToken();
  368. }
  369. // open angle bracket
  370. if (token != '{') {
  371. throw newSyntaxError('Angle bracket { expected');
  372. }
  373. getToken();
  374. // statements
  375. parseStatements(graph);
  376. // close angle bracket
  377. if (token != '}') {
  378. throw newSyntaxError('Angle bracket } expected');
  379. }
  380. getToken();
  381. // end of file
  382. if (token !== '') {
  383. throw newSyntaxError('End of file expected');
  384. }
  385. getToken();
  386. // remove temporary default options
  387. delete graph.node;
  388. delete graph.edge;
  389. delete graph.graph;
  390. return graph;
  391. }
  392. /**
  393. * Parse a list with statements.
  394. * @param {Object} graph
  395. */
  396. function parseStatements (graph) {
  397. while (token !== '' && token != '}') {
  398. parseStatement(graph);
  399. if (token === ';') {
  400. getToken();
  401. }
  402. }
  403. }
  404. /**
  405. * Parse a single statement. Can be a an attribute statement, node
  406. * statement, a series of node statements and edge statements, or a
  407. * parameter.
  408. * @param {Object} graph
  409. */
  410. function parseStatement(graph) {
  411. // parse subgraph
  412. var subgraph = parseSubgraph(graph);
  413. if (subgraph) {
  414. // edge statements
  415. parseEdge(graph, subgraph);
  416. return;
  417. }
  418. // parse an attribute statement
  419. var attr = parseAttributeStatement(graph);
  420. if (attr) {
  421. return;
  422. }
  423. // parse node
  424. if (tokenType != TOKENTYPE.IDENTIFIER) {
  425. throw newSyntaxError('Identifier expected');
  426. }
  427. var id = token; // id can be a string or a number
  428. getToken();
  429. if (token === '=') {
  430. // id statement
  431. getToken();
  432. if (tokenType != TOKENTYPE.IDENTIFIER) {
  433. throw newSyntaxError('Identifier expected');
  434. }
  435. graph[id] = token;
  436. getToken();
  437. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  438. }
  439. else {
  440. parseNodeStatement(graph, id);
  441. }
  442. }
  443. /**
  444. * Parse a subgraph
  445. * @param {Object} graph parent graph object
  446. * @return {Object | null} subgraph
  447. */
  448. function parseSubgraph (graph) {
  449. var subgraph = null;
  450. // optional subgraph keyword
  451. if (token === 'subgraph') {
  452. subgraph = {};
  453. subgraph.type = 'subgraph';
  454. getToken();
  455. // optional graph id
  456. if (tokenType === TOKENTYPE.IDENTIFIER) {
  457. subgraph.id = token;
  458. getToken();
  459. }
  460. }
  461. // open angle bracket
  462. if (token === '{') {
  463. getToken();
  464. if (!subgraph) {
  465. subgraph = {};
  466. }
  467. subgraph.parent = graph;
  468. subgraph.node = graph.node;
  469. subgraph.edge = graph.edge;
  470. subgraph.graph = graph.graph;
  471. // statements
  472. parseStatements(subgraph);
  473. // close angle bracket
  474. if (token != '}') {
  475. throw newSyntaxError('Angle bracket } expected');
  476. }
  477. getToken();
  478. // remove temporary default options
  479. delete subgraph.node;
  480. delete subgraph.edge;
  481. delete subgraph.graph;
  482. delete subgraph.parent;
  483. // register at the parent graph
  484. if (!graph.subgraphs) {
  485. graph.subgraphs = [];
  486. }
  487. graph.subgraphs.push(subgraph);
  488. }
  489. return subgraph;
  490. }
  491. /**
  492. * parse an attribute statement like "node [shape=circle fontSize=16]".
  493. * Available keywords are 'node', 'edge', 'graph'.
  494. * The previous list with default attributes will be replaced
  495. * @param {Object} graph
  496. * @returns {String | null} keyword Returns the name of the parsed attribute
  497. * (node, edge, graph), or null if nothing
  498. * is parsed.
  499. */
  500. function parseAttributeStatement (graph) {
  501. // attribute statements
  502. if (token === 'node') {
  503. getToken();
  504. // node attributes
  505. graph.node = parseAttributeList();
  506. return 'node';
  507. }
  508. else if (token === 'edge') {
  509. getToken();
  510. // edge attributes
  511. graph.edge = parseAttributeList();
  512. return 'edge';
  513. }
  514. else if (token === 'graph') {
  515. getToken();
  516. // graph attributes
  517. graph.graph = parseAttributeList();
  518. return 'graph';
  519. }
  520. return null;
  521. }
  522. /**
  523. * parse a node statement
  524. * @param {Object} graph
  525. * @param {String | Number} id
  526. */
  527. function parseNodeStatement(graph, id) {
  528. // node statement
  529. var node = {
  530. id: id
  531. };
  532. var attr = parseAttributeList();
  533. if (attr) {
  534. node.attr = attr;
  535. }
  536. addNode(graph, node);
  537. // edge statements
  538. parseEdge(graph, id);
  539. }
  540. /**
  541. * Parse an edge or a series of edges
  542. * @param {Object} graph
  543. * @param {String | Number} from Id of the from node
  544. */
  545. function parseEdge(graph, from) {
  546. while (token === '->' || token === '--') {
  547. var to;
  548. var type = token;
  549. getToken();
  550. var subgraph = parseSubgraph(graph);
  551. if (subgraph) {
  552. to = subgraph;
  553. }
  554. else {
  555. if (tokenType != TOKENTYPE.IDENTIFIER) {
  556. throw newSyntaxError('Identifier or subgraph expected');
  557. }
  558. to = token;
  559. addNode(graph, {
  560. id: to
  561. });
  562. getToken();
  563. }
  564. // parse edge attributes
  565. var attr = parseAttributeList();
  566. // create edge
  567. var edge = createEdge(graph, from, to, type, attr);
  568. addEdge(graph, edge);
  569. from = to;
  570. }
  571. }
  572. /**
  573. * Parse a set with attributes,
  574. * for example [label="1.000", shape=solid]
  575. * @return {Object | null} attr
  576. */
  577. function parseAttributeList() {
  578. var attr = null;
  579. while (token === '[') {
  580. getToken();
  581. attr = {};
  582. while (token !== '' && token != ']') {
  583. if (tokenType != TOKENTYPE.IDENTIFIER) {
  584. throw newSyntaxError('Attribute name expected');
  585. }
  586. var name = token;
  587. getToken();
  588. if (token != '=') {
  589. throw newSyntaxError('Equal sign = expected');
  590. }
  591. getToken();
  592. if (tokenType != TOKENTYPE.IDENTIFIER) {
  593. throw newSyntaxError('Attribute value expected');
  594. }
  595. var value = token;
  596. setValue(attr, name, value); // name can be a path
  597. getToken();
  598. if (token ==',') {
  599. getToken();
  600. }
  601. }
  602. if (token != ']') {
  603. throw newSyntaxError('Bracket ] expected');
  604. }
  605. getToken();
  606. }
  607. return attr;
  608. }
  609. /**
  610. * Create a syntax error with extra information on current token and index.
  611. * @param {String} message
  612. * @returns {SyntaxError} err
  613. */
  614. function newSyntaxError(message) {
  615. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  616. }
  617. /**
  618. * Chop off text after a maximum length
  619. * @param {String} text
  620. * @param {Number} maxLength
  621. * @returns {String}
  622. */
  623. function chop (text, maxLength) {
  624. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  625. }
  626. /**
  627. * Execute a function fn for each pair of elements in two arrays
  628. * @param {Array | *} array1
  629. * @param {Array | *} array2
  630. * @param {function} fn
  631. */
  632. function forEach2(array1, array2, fn) {
  633. if (Array.isArray(array1)) {
  634. array1.forEach(function (elem1) {
  635. if (Array.isArray(array2)) {
  636. array2.forEach(function (elem2) {
  637. fn(elem1, elem2);
  638. });
  639. }
  640. else {
  641. fn(elem1, array2);
  642. }
  643. });
  644. }
  645. else {
  646. if (Array.isArray(array2)) {
  647. array2.forEach(function (elem2) {
  648. fn(array1, elem2);
  649. });
  650. }
  651. else {
  652. fn(array1, array2);
  653. }
  654. }
  655. }
  656. /**
  657. * Set a nested property on an object
  658. * When nested objects are missing, they will be created.
  659. * For example setProp({}, 'font.color', 'red') will return {font: {color: 'red'}}
  660. * @param {Object} object
  661. * @param {string} path A dot separated string like 'font.color'
  662. * @param {*} value Value for the property
  663. * @return {Object} Returns the original object, allows for chaining.
  664. */
  665. function setProp(object, path, value) {
  666. var names = path.split('.');
  667. var prop = names.pop();
  668. // traverse over the nested objects
  669. var obj = object;
  670. for (var i = 0; i < names.length; i++) {
  671. var name = names[i];
  672. if (!(name in obj)) {
  673. obj[name] = {};
  674. }
  675. obj = obj[name];
  676. }
  677. // set the property value
  678. obj[prop] = value;
  679. return object;
  680. }
  681. /**
  682. * Convert an object with DOT attributes to their vis.js equivalents.
  683. * @param {Object} attr Object with DOT attributes
  684. * @return {Object} Returns an object with vis.js attributes
  685. */
  686. function convertAttr (attr) {
  687. var converted = {};
  688. for (var prop in attr) {
  689. if (attr.hasOwnProperty(prop)) {
  690. var mapping = ATTR_MAPPING[prop];
  691. if (Array.isArray(mapping)) {
  692. mapping.forEach(function (mapping_i) {
  693. setProp(converted, mapping_i, attr[prop]);
  694. })
  695. }
  696. else if (typeof mapping === 'string') {
  697. setProp(converted, mapping, attr[prop]);
  698. }
  699. else {
  700. setProp(converted, prop, attr[prop]);
  701. }
  702. }
  703. }
  704. return converted;
  705. }
  706. /**
  707. * Convert a string containing a graph in DOT language into a map containing
  708. * with nodes and edges in the format of graph.
  709. * @param {String} data Text containing a graph in DOT-notation
  710. * @return {Object} graphData
  711. */
  712. function DOTToGraph (data) {
  713. // parse the DOT file
  714. var dotData = parseDOT(data);
  715. var graphData = {
  716. nodes: [],
  717. edges: [],
  718. options: {}
  719. };
  720. // copy the nodes
  721. if (dotData.nodes) {
  722. dotData.nodes.forEach(function (dotNode) {
  723. var graphNode = {
  724. id: dotNode.id,
  725. label: String(dotNode.label || dotNode.id)
  726. };
  727. merge(graphNode, convertAttr(dotNode.attr));
  728. if (graphNode.image) {
  729. graphNode.shape = 'image';
  730. }
  731. graphData.nodes.push(graphNode);
  732. });
  733. }
  734. // copy the edges
  735. if (dotData.edges) {
  736. /**
  737. * Convert an edge in DOT format to an edge with VisGraph format
  738. * @param {Object} dotEdge
  739. * @returns {Object} graphEdge
  740. */
  741. var convertEdge = function (dotEdge) {
  742. var graphEdge = {
  743. from: dotEdge.from,
  744. to: dotEdge.to
  745. };
  746. merge(graphEdge, convertAttr(dotEdge.attr));
  747. graphEdge.arrows = (dotEdge.type === '->') ? 'to' : undefined;
  748. return graphEdge;
  749. };
  750. dotData.edges.forEach(function (dotEdge) {
  751. var from, to;
  752. if (dotEdge.from instanceof Object) {
  753. from = dotEdge.from.nodes;
  754. }
  755. else {
  756. from = {
  757. id: dotEdge.from
  758. }
  759. }
  760. // TODO: support of solid/dotted/dashed edges (attr = 'style')
  761. // TODO: support for attributes 'dir' and 'arrowhead' (edge arrows)
  762. if (dotEdge.to instanceof Object) {
  763. to = dotEdge.to.nodes;
  764. }
  765. else {
  766. to = {
  767. id: dotEdge.to
  768. }
  769. }
  770. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  771. dotEdge.from.edges.forEach(function (subEdge) {
  772. var graphEdge = convertEdge(subEdge);
  773. graphData.edges.push(graphEdge);
  774. });
  775. }
  776. forEach2(from, to, function (from, to) {
  777. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  778. var graphEdge = convertEdge(subEdge);
  779. graphData.edges.push(graphEdge);
  780. });
  781. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  782. dotEdge.to.edges.forEach(function (subEdge) {
  783. var graphEdge = convertEdge(subEdge);
  784. graphData.edges.push(graphEdge);
  785. });
  786. }
  787. });
  788. }
  789. // copy the options
  790. if (dotData.attr) {
  791. graphData.options = dotData.attr;
  792. }
  793. return graphData;
  794. }
  795. // exports
  796. exports.parseDOT = parseDOT;
  797. exports.DOTToGraph = DOTToGraph;