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.

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