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
19 KiB

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