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.

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