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.

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