mgraph.fromdot
Version:
Load DOT files into mgraph.graph
116 lines (106 loc) • 3.61 kB
JavaScript
// index.js
import parseDot from 'dotparser';
import createGraph from 'mgraph.graph';
/**
* Loads a graph from a DOT string.
*
* @param {string} dotGraph - A graph in DOT format.
* @param {object} [appendTo] - Optional: an existing graph to append to.
* @returns {object} The graph (an instance of mgraph.graph) loaded with DOT data.
*/
export default function fromDot(dotGraph, appendTo) {
const dotAST = parseDot(dotGraph);
if (dotAST.length > 1 && appendTo !== undefined) {
throw new Error(
'Dot file contains multiple graphs. Cannot use `appendTo` in this case'
);
}
if (!appendTo) {
appendTo = createGraph();
}
return loadOne(appendTo, dotAST[0]);
}
function loadOne(graph, ast) {
loadSubgraph(graph, ast);
return graph;
}
function loadSubgraph(graph, ast) {
const { children } = ast;
if (!children) return graph;
let addedNodes = [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.type === 'edge_stmt') {
addedNodes = addedNodes.concat(processEdgeStatement(graph, child));
} else if (child.type === 'node_stmt') {
addedNodes = addedNodes.concat(processNodeStatement(graph, child));
} else if (child.type === 'subgraph') {
addedNodes = addedNodes.concat(loadSubgraph(graph, child));
}
}
return addedNodes;
}
function processEdgeStatement(graph, edgeAST) {
const { edge_list, attr_list } = edgeAST;
if (edge_list.length === 0) return [];
const first = addNode(graph, edge_list[0]);
let addedNodes = [].concat(first);
const attributes = parseAttributesAsData(attr_list);
let prevNodes = first;
for (let i = 1; i < edge_list.length; i++) {
const nextNodes = addNode(graph, edge_list[i]);
addedNodes = addedNodes.concat(nextNodes);
addLink(graph, prevNodes, nextNodes, attributes);
prevNodes = nextNodes;
}
return addedNodes;
}
function processNodeStatement(graph, nodeStatement) {
return addNode(graph, nodeStatement.node_id, nodeStatement.attr_list);
}
function addNode(graph, nodeAST, attributesList) {
if (nodeAST.type === 'node_id') {
const data = mergeNodeDataIfNeeded(
parseAttributesAsData(attributesList),
graph.getNode(nodeAST.id)
);
// If no extra data, simply add the node:
graph.addNode(nodeAST.id, data || undefined);
return [nodeAST.id];
} else if (nodeAST.type === 'subgraph') {
return loadSubgraph(graph, nodeAST);
}
}
function addLink(graph, from, to, data) {
for (let i = 0; i < from.length; i++) {
for (let j = 0; j < to.length; j++) {
graph.addLink(from[i], to[j], data);
}
}
}
function parseAttributesAsData(attributesList) {
if (!attributesList || attributesList.length === 0) return undefined;
const data = Object.create(null);
for (let i = 0; i < attributesList.length; i++) {
const attr = attributesList[i];
if (attr.type !== 'attr' || attr.id === undefined) continue;
// If the attribute value appears to be an array, try to parse it as JSON.
const maybeArray =
typeof attr.eq === 'string' &&
attr.eq.startsWith('[') &&
attr.eq.endsWith(']');
if (maybeArray) {
try {
attr.eq = JSON.parse(attr.eq);
} catch (e) {
// ignore parsing errors; keep the string
}
}
data[attr.id] = attr.eq;
}
return data;
}
function mergeNodeDataIfNeeded(newData, oldNode) {
if (!oldNode || !oldNode.data) return newData;
return Object.assign(oldNode.data, newData);
}