2020-05-17 17:17:39 +02:00
|
|
|
/*
|
|
|
|
* Copyright (C) Sapphirecode - All Rights Reserved
|
|
|
|
* This file is part of graphviz-builder which is released under MIT.
|
|
|
|
* See file 'LICENSE' for full license details.
|
|
|
|
* Created by Timo Hocker <timo@scode.ovh>, May 2020
|
|
|
|
*/
|
|
|
|
|
2020-05-06 20:24:37 +02:00
|
|
|
/* eslint-disable line-comment-position */
|
|
|
|
/* eslint-disable no-inline-comments */
|
|
|
|
import { Transform } from 'stream';
|
|
|
|
import { GraphStreamJSON } from '../interfaces/GraphStreamJSON';
|
2020-05-08 09:44:37 +02:00
|
|
|
import {
|
|
|
|
GraphStreamCommand,
|
|
|
|
translate_command
|
|
|
|
} from '../enums/GraphStreamCommand';
|
2020-05-07 15:09:25 +02:00
|
|
|
import { validate_name } from '../Helper';
|
2020-05-06 20:24:37 +02:00
|
|
|
|
|
|
|
interface Stringable {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
toString(): string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class GraphStream extends Transform {
|
2020-07-17 13:29:03 +02:00
|
|
|
private _node_count = 0;
|
2020-05-06 20:24:37 +02:00
|
|
|
private _path: string[] = [];
|
2020-06-05 11:46:58 +02:00
|
|
|
private _state: (GraphStreamCommand | '')[] = [
|
|
|
|
'',
|
|
|
|
''
|
|
|
|
];
|
|
|
|
|
2020-05-06 20:24:37 +02:00
|
|
|
private _directional = false;
|
|
|
|
|
|
|
|
public get path (): string {
|
|
|
|
return this._path.join ('_');
|
|
|
|
}
|
|
|
|
|
2020-07-17 13:29:03 +02:00
|
|
|
public get node_count (): number {
|
|
|
|
return this._node_count;
|
|
|
|
}
|
|
|
|
|
2020-05-06 20:24:37 +02:00
|
|
|
private get level (): string {
|
|
|
|
return ' '.repeat (this._path.length);
|
|
|
|
}
|
|
|
|
|
2021-05-02 11:30:21 +02:00
|
|
|
private finish_node (): void {
|
2020-06-05 11:46:58 +02:00
|
|
|
if (
|
|
|
|
[
|
|
|
|
'ce',
|
|
|
|
'cn'
|
|
|
|
].includes (this._state[1])
|
|
|
|
)
|
|
|
|
this.push ('\n');
|
|
|
|
}
|
|
|
|
|
2020-05-06 20:24:37 +02:00
|
|
|
private expect_state (instr: GraphStreamCommand): void {
|
2020-05-08 09:44:37 +02:00
|
|
|
const states: (GraphStreamCommand|'')[] = [];
|
2020-05-06 20:24:37 +02:00
|
|
|
if ([
|
|
|
|
'cug',
|
|
|
|
'cdg'
|
|
|
|
].includes (instr))
|
|
|
|
states.push ('');
|
|
|
|
|
|
|
|
if ([
|
|
|
|
'eg',
|
|
|
|
'cn',
|
|
|
|
'csg',
|
|
|
|
'ce'
|
|
|
|
].includes (instr))
|
2020-06-05 11:46:58 +02:00
|
|
|
states.push ('at', 'eg', 'cug', 'cdg', 'csg', 'ce', 'cn');
|
2020-05-06 20:24:37 +02:00
|
|
|
|
|
|
|
switch (instr) {
|
|
|
|
case 'at':
|
2020-06-05 11:46:58 +02:00
|
|
|
states.push ('cn', 'cug', 'cdg', 'csg', 'ce');
|
2020-05-06 20:24:37 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2020-06-05 11:46:58 +02:00
|
|
|
if (!states.includes (this._state[1])) {
|
2020-05-08 09:44:37 +02:00
|
|
|
throw new Error (`invalid state to execute command ${
|
|
|
|
translate_command (instr)}
|
|
|
|
expected: ${states.map ((s) => translate_command (s))
|
|
|
|
.join (', ')}
|
2020-06-05 11:46:58 +02:00
|
|
|
actual: ${translate_command (this._state[1])}`);
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention, complexity, max-lines-per-function
|
|
|
|
public _transform (
|
|
|
|
chunk: string,
|
|
|
|
encoding: string,
|
|
|
|
callback: ((error?: Error) => unknown)
|
|
|
|
): void {
|
|
|
|
const instr = JSON.parse (chunk) as GraphStreamJSON;
|
|
|
|
this.expect_state (instr.type);
|
|
|
|
switch (instr.type) {
|
|
|
|
case 'cug': // create unordered graph
|
|
|
|
this._path.push (instr.args[0]);
|
|
|
|
this.push (`graph ${this.path} {\n`);
|
|
|
|
this._directional = false;
|
|
|
|
break;
|
|
|
|
case 'cdg': // create directional graph
|
|
|
|
this._path.push (instr.args[0]);
|
|
|
|
this.push (`digraph ${this.path} {\n`);
|
|
|
|
this._directional = true;
|
|
|
|
break;
|
|
|
|
case 'csg': // create subgraph
|
2020-06-05 11:46:58 +02:00
|
|
|
this.finish_node ();
|
2020-05-06 20:24:37 +02:00
|
|
|
this.push (
|
|
|
|
`${this.level}subgraph cluster_${this.path}_${instr.args[0]} {\n`
|
|
|
|
);
|
|
|
|
this._path.push (instr.args[0]);
|
|
|
|
break;
|
|
|
|
case 'eg': // end graph
|
2020-06-05 11:46:58 +02:00
|
|
|
this.finish_node ();
|
2020-05-06 20:24:37 +02:00
|
|
|
this._path.pop ();
|
|
|
|
this.push (`${this.level}}\n\n`);
|
|
|
|
break;
|
|
|
|
case 'cn': // create node
|
2020-06-05 11:46:58 +02:00
|
|
|
this.finish_node ();
|
2020-05-06 20:24:37 +02:00
|
|
|
this.push (`${this.level}${this.path}_${instr.args[0]}`);
|
2020-07-17 13:29:03 +02:00
|
|
|
this._node_count++;
|
2020-05-06 20:24:37 +02:00
|
|
|
break;
|
|
|
|
case 'at': // add attributes
|
2020-06-05 11:46:58 +02:00
|
|
|
if ([
|
|
|
|
'cn',
|
|
|
|
'ce'
|
|
|
|
].includes (this._state[1])) {
|
|
|
|
this.push (` [${instr.args.join (', ')}]\n`);
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.push (`${this.level}${
|
|
|
|
instr.args.join (`\n${this.level}`)
|
|
|
|
.replace (/"/gu, '')
|
|
|
|
}\n\n`);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'ce':
|
2020-06-05 11:46:58 +02:00
|
|
|
this.finish_node ();
|
2020-05-06 20:24:37 +02:00
|
|
|
this.push (`${this.level}${
|
|
|
|
instr.args[0]
|
2020-06-05 11:46:58 +02:00
|
|
|
} -${this._directional ? '>' : '-'} ${instr.args[1]}`);
|
2020-05-06 20:24:37 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2020-06-05 11:46:58 +02:00
|
|
|
this._state.push (instr.type);
|
|
|
|
this._state.shift ();
|
2020-05-06 20:24:37 +02:00
|
|
|
callback ();
|
|
|
|
}
|
|
|
|
|
|
|
|
public write (
|
|
|
|
instr: GraphStreamJSON
|
|
|
|
): boolean {
|
|
|
|
return super.write (JSON.stringify (instr), 'utf-8');
|
|
|
|
}
|
|
|
|
|
2020-05-07 15:09:25 +02:00
|
|
|
public create_node (name: string): string {
|
|
|
|
const node_name = validate_name (name);
|
|
|
|
this.write ({ type: 'cn', args: [ node_name ] });
|
|
|
|
return `${this.path}_${node_name}`;
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
|
2020-06-05 11:46:58 +02:00
|
|
|
/**
|
|
|
|
* @deprecated calling this method is not needed anymore
|
|
|
|
*/
|
2020-05-07 15:09:25 +02:00
|
|
|
public end_node (): void {
|
2020-06-05 11:46:58 +02:00
|
|
|
// this.write ({ type: 'en', args: [] });
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
|
2021-05-02 11:30:21 +02:00
|
|
|
public create_graph (name: string, type: 'd' | 's' | 'u' = 's'): void {
|
2020-05-06 20:24:37 +02:00
|
|
|
const instr_type = `c${type}g` as GraphStreamCommand;
|
2020-05-07 15:09:25 +02:00
|
|
|
this.write ({ type: instr_type, args: [ validate_name (name) ] });
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
|
2020-05-07 15:09:25 +02:00
|
|
|
public end_graph (): void {
|
|
|
|
this.write ({ type: 'eg', args: [] });
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
|
2020-05-07 15:09:25 +02:00
|
|
|
public attributes (attrs: Record<string, Stringable>): void {
|
2020-05-06 20:24:37 +02:00
|
|
|
const solved = [];
|
|
|
|
for (const attr of Object.keys (attrs)) {
|
|
|
|
const val = attrs[attr].toString ();
|
|
|
|
if ((/\n/u).test (val))
|
|
|
|
solved.push (`${attr} = <${val}>`);
|
|
|
|
else
|
|
|
|
solved.push (`${attr} = "${val}"`);
|
|
|
|
}
|
2020-05-07 15:09:25 +02:00
|
|
|
this.write ({ type: 'at', args: solved });
|
2020-05-06 20:24:37 +02:00
|
|
|
}
|
|
|
|
|
2020-05-07 15:09:25 +02:00
|
|
|
public create_edge (origin: string, target: string): void {
|
|
|
|
this.write ({
|
2020-05-06 20:24:37 +02:00
|
|
|
type: 'ce',
|
|
|
|
args: [
|
|
|
|
origin,
|
|
|
|
target
|
|
|
|
]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|