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