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.

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