Browse Source

DOT parser rewritten

css_transitions
josdejong 11 years ago
parent
commit
4805f0e6d5
6 changed files with 1008 additions and 687 deletions
  1. +2
    -2
      docs/timeline.html
  2. +429
    -340
      src/graph/dotparser.js
  3. +21
    -0
      test/dot.txt
  4. +122
    -0
      test/dotparser.js
  5. +428
    -339
      vis.js
  6. +6
    -6
      vis.min.js

+ 2
- 2
docs/timeline.html View File

@ -433,7 +433,7 @@ var options = {
<th>Return Type</th>
<th>Description</th>
</tr>
<!-- TODO: document groups as soon as implemented
<tr>
<td>setGroups(groups)</td>
<td>none</td>
@ -442,7 +442,7 @@ var options = {
a DataSet, or a DataView.
</td>
</tr>
-->
<tr>
<td>setItems(items)</td>
<td>none</td>

+ 429
- 340
src/graph/dotparser.js View File

@ -1,447 +1,536 @@
/**
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} json An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*/
util.parseDOT = function (data) {
(function(exports) {
/**
* Test whether given character is a whitespace character
* @param {String} c
* @return {Boolean} isWhitespace
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*/
function isWhitespace(c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
function parseDOT (data) {
dot = data;
return parseGraph();
}
/**
* Test whether given character is a delimeter
* @param {String} c
* @return {Boolean} isDelimeter
*/
function isDelimeter(c) {
return '[]{}();,=->'.indexOf(c) != -1;
}
// token types enumeration
var TOKENTYPE = {
NULL : 0,
DELIMITER : 1,
NUMBER : 2,
STRING : 3,
UNKNOWN : 4
};
// map with all delimiters
var DELIMITERS = {
'{': true,
'}': true,
'[': true,
']': true,
';': true,
'=': true,
',': true,
'->': true,
'--': true
};
var i = -1; // current index in the data
var c = ''; // current character in the data
var dot = ''; // current dot file
var index = 0; // current index in dot file
var c = ''; // current token character in expr
var token = ''; // current token
var tokenType = TOKENTYPE.NULL; // type of the token
var graph = null; // object with the graph to be build
var nodeAttr = null; // global node attributes
var edgeAttr = null; // global edge attributes
/**
* Read the next character from the data
* Get the first character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function next() {
i++;
c = data[i];
function first() {
index = 0;
c = dot.charAt(0);
}
/**
* Preview the next character in the data
* @returns {String} nextChar
* Get the next character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function previewNext () {
return data[i + 1];
function next() {
index++;
c = dot.charAt(index);
}
/**
* Preview the next character in the data
* @returns {String} nextChar
* Preview the next character from the dot file.
* @return {String} cNext
*/
function previewPrevious () {
return data[i + 1];
function nextPreview() {
return dot.charAt(index + 1);
}
/**
* Get a text description of the the current index in the data
* @return {String} desc
* Test whether given character is alphabetic or numeric
* @param {String} c
* @return {Boolean} isAlphaNumeric
*/
function pos() {
return '(char ' + i + ')';
var regexAlphaNumeric = /[a-zA-Z_0-9.#]/;
function isAlphaNumeric(c) {
return regexAlphaNumeric.test(c);
}
/**
* Skip whitespace and comments
* Merge all properties of object b into object b
* @param {Object} a
* @param {Object} b
* @return {Object} a
*/
function parseWhitespace() {
// skip whitespace
while (c && isWhitespace(c)) {
next();
function merge (a, b) {
if (!a) {
a = {};
}
// test for comment
var cNext = data[i + 1];
var cPrev = data[i - 1];
var c2 = c + cNext;
if (c2 == '/*') {
// block comment. skip until the block is closed
while (c && !(c == '*' && data[i + 1] == '/')) {
next();
}
next();
next();
parseWhitespace();
}
else if (c2 == '//' || (c == '#' && cPrev == '\n')) {
// line comment. skip until the next return
while (c && c != '\n') {
next();
if (b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
next();
parseWhitespace();
}
return a;
}
/**
* Parse a string
* The string may be enclosed by double quotes
* @return {String | undefined} value
* Add a node to the current graph object. If there is already a node with
* the same id, their attributes will be merged.
* @param {Object} node
*/
function parseString() {
parseWhitespace();
var name = '';
if (c == '"') {
next();
while (c && c != '"') {
name += c;
next();
function addNode(node) {
if (!graph.nodes) {
graph.nodes = {};
}
var current = graph.nodes[node.id];
if (current) {
// merge attributes
if (node.attr) {
current.attr = merge(current.attr, node.attr);
}
next(); // skip the closing quote
}
else {
while (c && !isWhitespace(c) && !isDelimeter(c)) {
name += c;
next();
}
// cast string to number or boolean
if (name.length) {
var number = Number(name);
if (!isNaN(number)) {
name = number;
}
else if (name == 'true') {
name = true;
}
else if (name == 'false') {
name = false;
}
else if (name == 'null') {
name = null;
}
// add
graph.nodes[node.id] = node;
if (nodeAttr) {
node.attr = merge(node.attr, nodeAttr);
}
}
return name;
}
/**
* Parse a value, can be a string, number, or boolean.
* The value may be enclosed by double quotes
* @return {String | Number | Boolean | undefined} value
* Add an edge to the current graph obect
* @param {Object} edge
*/
function parseValue() {
parseWhitespace();
if (c == '"') {
return parseString();
function addEdge(edge) {
if (!graph.edges) {
graph.edges = [];
}
else {
var value = parseString();
if (value != undefined) {
// cast string to number or boolean
var number = Number(value);
if (!isNaN(number)) {
value = number;
}
else if (value == 'true') {
value = true;
}
else if (value == 'false') {
value = false;
}
else if (value == 'null') {
value = null;
}
}
return value;
graph.edges.push(edge);
if (edgeAttr) {
edge.attr = merge(edge.attr, edgeAttr);
}
}
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
* @return {Object | undefined} attr
* Get next token in the current dot file.
* The token and token type are available as token and tokenType
*/
function parseAttributes() {
parseWhitespace();
function getToken() {
tokenType = TOKENTYPE.NULL;
token = '';
if (c == '[') {
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n') { // space, tab, enter
next();
var attr = {};
while (c && c != ']') {
parseWhitespace();
}
var name = parseString();
if (!name) {
throw new SyntaxError('Attribute name expected ' + pos());
}
do {
var isComment = false;
parseWhitespace();
if (c != '=') {
throw new SyntaxError('Equal sign = expected ' + pos());
// skip comment
if (c == '#') {
// find the previous non-space character
var i = index - 1;
while (dot[i] == ' ' || dot[i] == '\t') {
i--;
}
next();
var value = parseValue();
if (!value) {
throw new SyntaxError('Attribute value expected ' + pos());
if (dot[i] == '\n' || dot[i] == '') {
// the # is at the start of a line, this is indeed a line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
attr[name] = value;
parseWhitespace();
if (c ==',') {
}
if (c == '/' && nextPreview() == '/') {
// skip line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
if (c == '/' && nextPreview() == '*') {
// skip block comment
while (c != '') {
if (c == '*' && nextPreview() == '/') {
// end of block comment found. skip these last two characters
next();
next();
break;
}
else {
next();
}
isComment = true;
}
}
next();
return attr;
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n') { // space, tab, enter
next();
}
}
else {
return undefined;
while (isComment);
// check for end of dot file
if (c == '') {
// token is still empty
tokenType = TOKENTYPE.DELIMITER;
return;
}
}
/**
* Parse a directed or undirected arrow '->' or '--'
* @return {String | undefined} arrow
*/
function parseArrow() {
parseWhitespace();
// check for delimiters consisting of 2 characters
var c2 = c + nextPreview();
if (DELIMITERS[c2]) {
tokenType = TOKENTYPE.DELIMITER;
token = c2;
next();
next();
return;
}
// check for delimiters consisting of 1 character
if (DELIMITERS[c]) {
tokenType = TOKENTYPE.DELIMITER;
token = c;
next();
return;
}
if (c == '-') {
// check for an identifier (number or string)
// TODO: more precise parsing of numbers/strings
if (isAlphaNumeric(c) || c == '-') {
token += c;
next();
if (c == '>' || c == '-') {
var arrow = '-' + c;
while (isAlphaNumeric(c)) {
token += c;
next();
return arrow;
}
if (!isNaN(Number(token))) {
token = Number(token);
tokenType = TOKENTYPE.NUMBER;
}
else {
throw new SyntaxError('Arrow "->" or "--" expected ' + pos());
tokenType = TOKENTYPE.STRING;
}
return;
}
// check for a string
if (c == '"') {
next();
while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
token += c;
if (c == '"') { // skip the escape character
next();
}
next();
}
if (c != '"') {
throw newSyntaxError('End of string " expected');
}
next();
tokenType = TOKENTYPE.STRING;
return;
}
return undefined;
// something unknown is found, wrong characters, a syntax error
tokenType = TOKENTYPE.UNKNOWN;
while (c != '') {
token += c;
next();
}
throw new SyntaxError('Syntax error in part "' + token + '"');
}
/**
* Parse a line separator ';'
* @return {String | undefined} separator
* Parse a graph.
* @returns {Object} graph
*/
function parseSeparator() {
parseWhitespace();
function parseGraph() {
graph = {};
nodeAttr = null;
edgeAttr = null;
first();
getToken();
// optional strict keyword
if (token == 'strict') {
graph.strict = true;
getToken();
}
if (c == ';') {
next();
return ';';
// graph or digraph keyword
if (token == 'graph' || token == 'digraph') {
graph.type = token;
getToken();
}
return undefined;
}
// graph id
if (tokenType == TOKENTYPE.STRING) {
graph.id = token;
getToken();
}
/**
* Merge all properties of object b into object b
* @param {Object} a
* @param {Object} b
*/
function merge (a, b) {
if (a && b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
// open angle bracket
if (token != '{') {
throw newSyntaxError('Angle bracket { expected');
}
}
getToken();
var nodeMap = {};
var edgeList = [];
// statements
parseStatements();
/**
* Register a node with attributes
* @param {String} id
* @param {Object} [attr]
*/
function addNode(id, attr) {
var node = {
id: String(id),
attr: attr || {}
};
if (!nodeMap[id]) {
nodeMap[id] = node;
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
else {
merge(nodeMap[id].attr, node.attr);
getToken();
// end of file
if (token !== '') {
throw newSyntaxError('End of file expected');
}
getToken();
return graph;
}
/**
* Register an edge
* @param {String} from
* @param {String} to
* @param {String} type A string "->" or "--"
* @param {Object} [attr]
* Parse a list with statements.
*/
function addEdge(from, to, type, attr) {
edgeList.push({
from: String(from),
to: String(to),
type: type,
attr: attr || {}
});
}
function parseStatements () {
while (token !== '' && token != '}') {
if (tokenType != TOKENTYPE.STRING && tokenType != TOKENTYPE.NUMBER) {
throw newSyntaxError('String expected');
}
// find the opening curly bracket
next();
while (c && c != '{') {
next();
}
if (c != '{') {
throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos())
parseStatement();
if (token == ';') {
getToken();
}
}
}
next();
// parse all data until a closing curly bracket is encountered
while (c && c != '}') {
// parse node id and optional node attributes
var id = parseString();
if (id == undefined || id === '') {
throw new SyntaxError('String with id expected ' + pos());
/**
* Parse a single statement. Can be a an attribute statement, node
* statement, a series of node statements and edge statements, or a
* parameter.
*/
function parseStatement() {
var attr;
var id = token; // can be as string or a number
getToken();
// attribute statements
if (id == 'node') {
// node attributes
attr = parseAttributes();
if (attr) {
nodeAttr = merge(nodeAttr, attr);
}
}
var attr = parseAttributes();
addNode(id, attr);
// TODO: parse global attributes
// TODO: parse global attributes "graph", "node", "edge"
// parse arrow
var type = parseArrow();
while (type) {
// parse node id
var prevId = id;
id = parseString();
if (id == undefined) {
throw new SyntaxError('String with id expected ' + pos());
else if (id == 'edge') {
// edge attributes
attr = parseAttributes();
if (attr) {
edgeAttr = merge(edgeAttr, attr);
}
addNode(id);
// parse edge attributes and register edge
}
else if (id == 'graph') {
// graph attributes
attr = parseAttributes();
addEdge(prevId, id, type, attr);
// parse next arrow (optional)
type = parseArrow();
if (attr) {
graph.attr = merge(graph.attr, attr);
}
}
else {
if (token == '=') {
// id statement
getToken();
if (!graph.attr) {
graph.attr = {};
}
graph.attr[id] = token;
getToken();
}
else {
// node statement
var node = {
id: String(id)
};
attr = parseAttributes();
if (attr) {
node.attr = attr;
}
addNode(node);
// edge statements
var from = id;
while (token == '->' || token == '--') {
var type = token;
getToken();
var to = token;
addNode({
id: String(to)
});
getToken();
attr = parseAttributes();
// create edge
var edge = {
from: String(from),
to: String(to),
type: type
};
if (attr) {
edge.attr = attr;
}
addEdge(edge);
from = to;
}
}
}
}
// parse separator (optional)
parseSeparator();
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
* @return {Object | undefined} attr
*/
function parseAttributes() {
if (token == '[') {
getToken();
var attr = {};
while (token !== '' && token != ']') {
if (tokenType != TOKENTYPE.STRING) {
throw newSyntaxError('Attribute name expected');
}
var name = token;
parseWhitespace();
}
if (c != '}') {
throw new SyntaxError('Invalid data. Curly bracket } expected');
}
getToken();
if (token != '=') {
throw newSyntaxError('Equal sign = expected');
}
getToken();
// crop data between the curly brackets
var start = data.indexOf('{');
var end = data.indexOf('}', start);
var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined;
if (tokenType != TOKENTYPE.STRING && tokenType != TOKENTYPE.NUMBER) {
throw newSyntaxError('Attribute value expected');
}
var value = token;
attr[name] = value;
if (!text) {
throw new Error('Invalid data. no curly brackets containing data found');
}
getToken();
if (token ==',') {
getToken();
}
}
getToken();
// return the results
var nodeList = [];
for (id in nodeMap) {
if (nodeMap.hasOwnProperty(id)) {
nodeList.push(nodeMap[id]);
return attr;
}
else {
return undefined;
}
}
return {
nodes: nodeList,
edges: edgeList
/**
* Create a syntax error with extra information on current token and index.
* @param {String} message
* @returns {SyntaxError} err
*/
function newSyntaxError(message) {
return new SyntaxError(message + ', got "' + token + '" (char ' + index + ')');
}
};
/**
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graphData
*/
util.DOTToGraph = function (data) {
// parse the DOT file
var dotData = util.parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {
nodes: {},
edges: {}
}
};
/**
* Merge the properties of object b into object a, and replace non-supported
* attributes with supported properties.
* @param {Object} a
* @param {Object} b
* @param {Array} [ignore] Optional array with property names to be ignored
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graphData
*/
function merge (a, b, ignore) {
for (var prop in b) {
if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) {
a[prop] = b[prop];
function DOTToGraph (data) {
// parse the DOT file
var dotData = parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {}
};
// copy the nodes
if (dotData.nodes) {
for (var id in dotData.nodes) {
if (dotData.nodes.hasOwnProperty(id)) {
var node = {
id: id,
label: id
};
merge(node, dotData.nodes[id].attr);
graphData.nodes.push(node);
}
}
}
// TODO: Convert non supported attributes to properties supported by Graph
}
dotData.nodes.forEach(function (node) {
if (node.id.toLowerCase() == 'graph') {
merge(graphData.options, node.attr);
// copy the edges
if (dotData.edges) {
dotData.edges.forEach(function (dotEdge) {
var graphEdge = {
from: dotEdge.from,
to: dotEdge.to
};
merge(graphEdge, dotEdge.attr);
graphEdge.style = (dotEdge.type == '->') ? 'arrow-end' : 'line';
graphData.edges.push(graphEdge);
});
}
else if (node.id.toLowerCase() == 'node') {
merge(graphData.options.nodes, node.attr);
}
else if (node.id.toLowerCase() == 'edge') {
merge(graphData.options.edges, node.attr);
}
else {
var graphNode = {};
graphNode.id = node.id;
graphNode.label = node.id;
merge(graphNode, node.attr);
graphData.nodes.push(graphNode);
// copy the options
if (dotData.attr) {
graphData.options = dotData.attr;
}
});
dotData.edges.forEach(function (edge) {
var graphEdge = {};
graphEdge.from = edge.from;
graphEdge.to = edge.to;
graphEdge.label = edge.id;
graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line';
merge(graphEdge, edge.attr);
graphData.edges.push(graphEdge);
});
return graphData;
};
return graphData;
}
// exports
exports.parseDOT = parseDOT;
exports.DOTToGraph = DOTToGraph;
})(typeof util !== 'undefined' ? util : exports);

+ 21
- 0
test/dot.txt View File

@ -0,0 +1,21 @@
digraph test_graph {
# this test file tries to test everything from the DOT language
rankdir=LR;
size="8,5"
font = "arial"
graph[attr1="another"" attr"]
node [shape = doublecircle]; node1 node2 node3;
node [shape = circle];
edge[length=170 fontSize=12]
node4[color=red shape=diamond]
node5[color="blue", shape=square, width=3]
/*
some block comment
*/
"node1" -> node1 [ label = "a" ];
"node2" -> node3 [label = "b" ];
"node1" -- "node4" [ label = "c" ];
node3-> node4 [ label=d] -> node5 -> 6
}

+ 122
- 0
test/dotparser.js View File

@ -0,0 +1,122 @@
var assert = require('assert'),
fs = require('fs'),
dot = require('../src/graph/dotparser.js');
fs.readFile('test/dot.txt', function (err, data) {
data = String(data);
var graph = dot.parseDOT(data);
assert.deepEqual(graph, {
"type": "digraph",
"id": "test_graph",
"attr": {
"rankdir": "LR",
"size": "8,5",
"font": "arial",
"attr1": "another\" attr"
},
"nodes": {
"6": {
"id": "6",
"attr": {
"shape": "circle"
}
},
"node1": {
"id": "node1",
"attr": {
"shape": "doublecircle"
}
},
"node2": {
"id": "node2",
"attr": {
"shape": "doublecircle"
}
},
"node3": {
"id": "node3",
"attr": {
"shape": "doublecircle"
}
},
"node4": {
"id": "node4",
"attr": {
"color": "red",
"shape": "circle"
}
},
"node5": {
"id": "node5",
"attr": {
"color": "blue",
"shape": "circle",
"width": 3
}
}
},
"edges": [
{
"from": "node1",
"to": "node1",
"type": "->",
"attr": {
"label": "a",
"length": 170,
"fontSize": 12
}
},
{
"from": "node2",
"to": "node3",
"type": "->",
"attr": {
"label": "b",
"length": 170,
"fontSize": 12
}
},
{
"from": "node1",
"to": "node4",
"type": "--",
"attr": {
"label": "c",
"length": 170,
"fontSize": 12
}
},
{
"from": "node3",
"to": "node4",
"type": "->",
"attr": {
"label": "d",
"length": 170,
"fontSize": 12
}
},
{
"from": "node4",
"to": "node5",
"type": "->",
"attr": {
"length": 170,
"fontSize": 12
}
},
{
"from": "node5",
"to": "6",
"type": "->",
"attr": {
"length": 170,
"fontSize": 12
}
}
]
});
});

+ 428
- 339
vis.js View File

@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 0.0.8
* @date 2013-06-04
* @date 2013-06-05
*
* @license
* Copyright (C) 2011-2013 Almende B.V, http://almende.com
@ -6806,453 +6806,542 @@ Timeline.prototype.getItemRange = function getItemRange() {
};
};
/**
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} json An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*/
util.parseDOT = function (data) {
(function(exports) {
/**
* Test whether given character is a whitespace character
* @param {String} c
* @return {Boolean} isWhitespace
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*/
function isWhitespace(c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
function parseDOT (data) {
dot = data;
return parseGraph();
}
// token types enumeration
var TOKENTYPE = {
NULL : 0,
DELIMITER : 1,
NUMBER : 2,
STRING : 3,
UNKNOWN : 4
};
/**
* Test whether given character is a delimeter
* @param {String} c
* @return {Boolean} isDelimeter
*/
function isDelimeter(c) {
return '[]{}();,=->'.indexOf(c) != -1;
}
// map with all delimiters
var DELIMITERS = {
'{': true,
'}': true,
'[': true,
']': true,
';': true,
'=': true,
',': true,
'->': true,
'--': true
};
var i = -1; // current index in the data
var c = ''; // current character in the data
var dot = ''; // current dot file
var index = 0; // current index in dot file
var c = ''; // current token character in expr
var token = ''; // current token
var tokenType = TOKENTYPE.NULL; // type of the token
var graph = null; // object with the graph to be build
var nodeAttr = null; // global node attributes
var edgeAttr = null; // global edge attributes
/**
* Read the next character from the data
* Get the first character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function next() {
i++;
c = data[i];
function first() {
index = 0;
c = dot.charAt(0);
}
/**
* Preview the next character in the data
* @returns {String} nextChar
* Get the next character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function previewNext () {
return data[i + 1];
function next() {
index++;
c = dot.charAt(index);
}
/**
* Preview the next character in the data
* @returns {String} nextChar
* Preview the next character from the dot file.
* @return {String} cNext
*/
function previewPrevious () {
return data[i + 1];
function nextPreview() {
return dot.charAt(index + 1);
}
/**
* Get a text description of the the current index in the data
* @return {String} desc
* Test whether given character is alphabetic or numeric
* @param {String} c
* @return {Boolean} isAlphaNumeric
*/
function pos() {
return '(char ' + i + ')';
var regexAlphaNumeric = /[a-zA-Z_0-9.#]/;
function isAlphaNumeric(c) {
return regexAlphaNumeric.test(c);
}
/**
* Skip whitespace and comments
* Merge all properties of object b into object b
* @param {Object} a
* @param {Object} b
* @return {Object} a
*/
function parseWhitespace() {
// skip whitespace
while (c && isWhitespace(c)) {
next();
function merge (a, b) {
if (!a) {
a = {};
}
// test for comment
var cNext = data[i + 1];
var cPrev = data[i - 1];
var c2 = c + cNext;
if (c2 == '/*') {
// block comment. skip until the block is closed
while (c && !(c == '*' && data[i + 1] == '/')) {
next();
}
next();
next();
parseWhitespace();
}
else if (c2 == '//' || (c == '#' && cPrev == '\n')) {
// line comment. skip until the next return
while (c && c != '\n') {
next();
if (b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
next();
parseWhitespace();
}
return a;
}
/**
* Parse a string
* The string may be enclosed by double quotes
* @return {String | undefined} value
* Add a node to the current graph object. If there is already a node with
* the same id, their attributes will be merged.
* @param {Object} node
*/
function parseString() {
parseWhitespace();
var name = '';
if (c == '"') {
next();
while (c && c != '"') {
name += c;
next();
function addNode(node) {
if (!graph.nodes) {
graph.nodes = {};
}
var current = graph.nodes[node.id];
if (current) {
// merge attributes
if (node.attr) {
current.attr = merge(current.attr, node.attr);
}
next(); // skip the closing quote
}
else {
while (c && !isWhitespace(c) && !isDelimeter(c)) {
name += c;
next();
}
// cast string to number or boolean
if (name.length) {
var number = Number(name);
if (!isNaN(number)) {
name = number;
}
else if (name == 'true') {
name = true;
}
else if (name == 'false') {
name = false;
}
else if (name == 'null') {
name = null;
}
// add
graph.nodes[node.id] = node;
if (nodeAttr) {
node.attr = merge(node.attr, nodeAttr);
}
}
return name;
}
/**
* Parse a value, can be a string, number, or boolean.
* The value may be enclosed by double quotes
* @return {String | Number | Boolean | undefined} value
* Add an edge to the current graph obect
* @param {Object} edge
*/
function parseValue() {
parseWhitespace();
if (c == '"') {
return parseString();
function addEdge(edge) {
if (!graph.edges) {
graph.edges = [];
}
else {
var value = parseString();
if (value != undefined) {
// cast string to number or boolean
var number = Number(value);
if (!isNaN(number)) {
value = number;
}
else if (value == 'true') {
value = true;
}
else if (value == 'false') {
value = false;
}
else if (value == 'null') {
value = null;
}
}
return value;
graph.edges.push(edge);
if (edgeAttr) {
edge.attr = merge(edge.attr, edgeAttr);
}
}
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
* @return {Object | undefined} attr
* Get next token in the current dot file.
* The token and token type are available as token and tokenType
*/
function parseAttributes() {
parseWhitespace();
function getToken() {
tokenType = TOKENTYPE.NULL;
token = '';
if (c == '[') {
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n') { // space, tab, enter
next();
var attr = {};
while (c && c != ']') {
parseWhitespace();
var name = parseString();
if (!name) {
throw new SyntaxError('Attribute name expected ' + pos());
}
}
parseWhitespace();
if (c != '=') {
throw new SyntaxError('Equal sign = expected ' + pos());
do {
var isComment = false;
// skip comment
if (c == '#') {
// find the previous non-space character
var i = index - 1;
while (dot[i] == ' ' || dot[i] == '\t') {
i--;
}
next();
var value = parseValue();
if (!value) {
throw new SyntaxError('Attribute value expected ' + pos());
if (dot[i] == '\n' || dot[i] == '') {
// the # is at the start of a line, this is indeed a line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
attr[name] = value;
parseWhitespace();
if (c ==',') {
}
if (c == '/' && nextPreview() == '/') {
// skip line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
if (c == '/' && nextPreview() == '*') {
// skip block comment
while (c != '') {
if (c == '*' && nextPreview() == '/') {
// end of block comment found. skip these last two characters
next();
next();
break;
}
else {
next();
}
isComment = true;
}
}
next();
return attr;
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n') { // space, tab, enter
next();
}
}
else {
return undefined;
while (isComment);
// check for end of dot file
if (c == '') {
// token is still empty
tokenType = TOKENTYPE.DELIMITER;
return;
}
}
/**
* Parse a directed or undirected arrow '->' or '--'
* @return {String | undefined} arrow
*/
function parseArrow() {
parseWhitespace();
// check for delimiters consisting of 2 characters
var c2 = c + nextPreview();
if (DELIMITERS[c2]) {
tokenType = TOKENTYPE.DELIMITER;
token = c2;
next();
next();
return;
}
// check for delimiters consisting of 1 character
if (DELIMITERS[c]) {
tokenType = TOKENTYPE.DELIMITER;
token = c;
next();
return;
}
if (c == '-') {
// check for an identifier (number or string)
// TODO: more precise parsing of numbers/strings
if (isAlphaNumeric(c) || c == '-') {
token += c;
next();
if (c == '>' || c == '-') {
var arrow = '-' + c;
while (isAlphaNumeric(c)) {
token += c;
next();
return arrow;
}
if (!isNaN(Number(token))) {
token = Number(token);
tokenType = TOKENTYPE.NUMBER;
}
else {
throw new SyntaxError('Arrow "->" or "--" expected ' + pos());
tokenType = TOKENTYPE.STRING;
}
return;
}
return undefined;
// check for a string
if (c == '"') {
next();
while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
token += c;
if (c == '"') { // skip the escape character
next();
}
next();
}
if (c != '"') {
throw newSyntaxError('End of string " expected');
}
next();
tokenType = TOKENTYPE.STRING;
return;
}
// something unknown is found, wrong characters, a syntax error
tokenType = TOKENTYPE.UNKNOWN;
while (c != '') {
token += c;
next();
}
throw new SyntaxError('Syntax error in part "' + token + '"');
}
/**
* Parse a line separator ';'
* @return {String | undefined} separator
* Parse a graph.
* @returns {Object} graph
*/
function parseSeparator() {
parseWhitespace();
function parseGraph() {
graph = {};
nodeAttr = null;
edgeAttr = null;
if (c == ';') {
next();
return ';';
first();
getToken();
// optional strict keyword
if (token == 'strict') {
graph.strict = true;
getToken();
}
return undefined;
}
// graph or digraph keyword
if (token == 'graph' || token == 'digraph') {
graph.type = token;
getToken();
}
/**
* Merge all properties of object b into object b
* @param {Object} a
* @param {Object} b
*/
function merge (a, b) {
if (a && b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
// graph id
if (tokenType == TOKENTYPE.STRING) {
graph.id = token;
getToken();
}
}
var nodeMap = {};
var edgeList = [];
// open angle bracket
if (token != '{') {
throw newSyntaxError('Angle bracket { expected');
}
getToken();
/**
* Register a node with attributes
* @param {String} id
* @param {Object} [attr]
*/
function addNode(id, attr) {
var node = {
id: String(id),
attr: attr || {}
};
if (!nodeMap[id]) {
nodeMap[id] = node;
// statements
parseStatements();
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
else {
merge(nodeMap[id].attr, node.attr);
getToken();
// end of file
if (token !== '') {
throw newSyntaxError('End of file expected');
}
getToken();
return graph;
}
/**
* Register an edge
* @param {String} from
* @param {String} to
* @param {String} type A string "->" or "--"
* @param {Object} [attr]
* Parse a list with statements.
*/
function addEdge(from, to, type, attr) {
edgeList.push({
from: String(from),
to: String(to),
type: type,
attr: attr || {}
});
}
function parseStatements () {
while (token !== '' && token != '}') {
if (tokenType != TOKENTYPE.STRING && tokenType != TOKENTYPE.NUMBER) {
throw newSyntaxError('String expected');
}
// find the opening curly bracket
next();
while (c && c != '{') {
next();
}
if (c != '{') {
throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos())
parseStatement();
if (token == ';') {
getToken();
}
}
}
next();
// parse all data until a closing curly bracket is encountered
while (c && c != '}') {
// parse node id and optional node attributes
var id = parseString();
if (id == undefined || id === '') {
throw new SyntaxError('String with id expected ' + pos());
/**
* Parse a single statement. Can be a an attribute statement, node
* statement, a series of node statements and edge statements, or a
* parameter.
*/
function parseStatement() {
var attr;
var id = token; // can be as string or a number
getToken();
// attribute statements
if (id == 'node') {
// node attributes
attr = parseAttributes();
if (attr) {
nodeAttr = merge(nodeAttr, attr);
}
}
var attr = parseAttributes();
addNode(id, attr);
// TODO: parse global attributes
// TODO: parse global attributes "graph", "node", "edge"
// parse arrow
var type = parseArrow();
while (type) {
// parse node id
var prevId = id;
id = parseString();
if (id == undefined) {
throw new SyntaxError('String with id expected ' + pos());
else if (id == 'edge') {
// edge attributes
attr = parseAttributes();
if (attr) {
edgeAttr = merge(edgeAttr, attr);
}
addNode(id);
// parse edge attributes and register edge
}
else if (id == 'graph') {
// graph attributes
attr = parseAttributes();
addEdge(prevId, id, type, attr);
if (attr) {
graph.attr = merge(graph.attr, attr);
}
}
else {
if (token == '=') {
// id statement
getToken();
if (!graph.attr) {
graph.attr = {};
}
graph.attr[id] = token;
getToken();
}
else {
// node statement
var node = {
id: String(id)
};
attr = parseAttributes();
if (attr) {
node.attr = attr;
}
addNode(node);
// edge statements
var from = id;
while (token == '->' || token == '--') {
var type = token;
getToken();
var to = token;
addNode({
id: String(to)
});
getToken();
attr = parseAttributes();
// create edge
var edge = {
from: String(from),
to: String(to),
type: type
};
if (attr) {
edge.attr = attr;
}
addEdge(edge);
// parse next arrow (optional)
type = parseArrow();
from = to;
}
}
}
}
// parse separator (optional)
parseSeparator();
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
* @return {Object | undefined} attr
*/
function parseAttributes() {
if (token == '[') {
getToken();
var attr = {};
while (token !== '' && token != ']') {
if (tokenType != TOKENTYPE.STRING) {
throw newSyntaxError('Attribute name expected');
}
var name = token;
parseWhitespace();
}
if (c != '}') {
throw new SyntaxError('Invalid data. Curly bracket } expected');
}
getToken();
if (token != '=') {
throw newSyntaxError('Equal sign = expected');
}
getToken();
// crop data between the curly brackets
var start = data.indexOf('{');
var end = data.indexOf('}', start);
var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined;
if (tokenType != TOKENTYPE.STRING && tokenType != TOKENTYPE.NUMBER) {
throw newSyntaxError('Attribute value expected');
}
var value = token;
attr[name] = value;
if (!text) {
throw new Error('Invalid data. no curly brackets containing data found');
}
getToken();
if (token ==',') {
getToken();
}
}
getToken();
// return the results
var nodeList = [];
for (id in nodeMap) {
if (nodeMap.hasOwnProperty(id)) {
nodeList.push(nodeMap[id]);
return attr;
}
else {
return undefined;
}
}
return {
nodes: nodeList,
edges: edgeList
}
};
/**
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graphData
*/
util.DOTToGraph = function (data) {
// parse the DOT file
var dotData = util.parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {
nodes: {},
edges: {}
}
};
/**
* Create a syntax error with extra information on current token and index.
* @param {String} message
* @returns {SyntaxError} err
*/
function newSyntaxError(message) {
return new SyntaxError(message + ', got "' + token + '" (char ' + index + ')');
}
/**
* Merge the properties of object b into object a, and replace non-supported
* attributes with supported properties.
* @param {Object} a
* @param {Object} b
* @param {Array} [ignore] Optional array with property names to be ignored
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graphData
*/
function merge (a, b, ignore) {
for (var prop in b) {
if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) {
a[prop] = b[prop];
function DOTToGraph (data) {
// parse the DOT file
var dotData = parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {}
};
// copy the nodes
if (dotData.nodes) {
for (var id in dotData.nodes) {
if (dotData.nodes.hasOwnProperty(id)) {
var node = {
id: id,
label: id
};
merge(node, dotData.nodes[id].attr);
graphData.nodes.push(node);
}
}
}
// TODO: Convert non supported attributes to properties supported by Graph
}
dotData.nodes.forEach(function (node) {
if (node.id.toLowerCase() == 'graph') {
merge(graphData.options, node.attr);
// copy the edges
if (dotData.edges) {
dotData.edges.forEach(function (dotEdge) {
var graphEdge = {
from: dotEdge.from,
to: dotEdge.to
};
merge(graphEdge, dotEdge.attr);
graphEdge.style = (dotEdge.type == '->') ? 'arrow-end' : 'line';
graphData.edges.push(graphEdge);
});
}
else if (node.id.toLowerCase() == 'node') {
merge(graphData.options.nodes, node.attr);
}
else if (node.id.toLowerCase() == 'edge') {
merge(graphData.options.edges, node.attr);
}
else {
var graphNode = {};
graphNode.id = node.id;
graphNode.label = node.id;
merge(graphNode, node.attr);
graphData.nodes.push(graphNode);
// copy the options
if (dotData.attr) {
graphData.options = dotData.attr;
}
});
dotData.edges.forEach(function (edge) {
var graphEdge = {};
graphEdge.from = edge.from;
graphEdge.to = edge.to;
graphEdge.label = edge.id;
graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line';
merge(graphEdge, edge.attr);
graphData.edges.push(graphEdge);
});
return graphData;
}
return graphData;
};
// exports
exports.parseDOT = parseDOT;
exports.DOTToGraph = DOTToGraph;
})(typeof util !== 'undefined' ? util : exports);
/**
* Canvas shapes used by the Graph

+ 6
- 6
vis.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save