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.

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