graphviz-builder/lib/classes/GraphStream.ts

161 lines
4.2 KiB
TypeScript
Raw Normal View History

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';
import { GraphStreamCommand } from '../enums/GraphStreamCommand';
interface Stringable {
// eslint-disable-next-line @typescript-eslint/naming-convention
toString(): string;
}
export class GraphStream extends Transform {
private _path: string[] = [];
private _state = '';
private _directional = false;
public get path (): string {
return this._path.join ('_');
}
private get level (): string {
return ' '.repeat (this._path.length);
}
private expect_state (instr: GraphStreamCommand): void {
const states = [];
if ([
'cug',
'cdg'
].includes (instr))
states.push ('');
if ([
'eg',
'cn',
'csg',
'ce'
].includes (instr))
states.push ('en', 'at', 'eg', 'cug', 'cdg', 'csg', 'ce');
switch (instr) {
case 'en':
states.push ('cn', 'at');
break;
case 'at':
states.push ('cn', 'cug', 'cdg', 'csg');
break;
default:
break;
}
if (!states.includes (this._state)) {
throw new Error (`invalid state to execute command ${instr}
expected: ${states.join (', ')}`);
}
}
// 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
this.push (
`${this.level}subgraph cluster_${this.path}_${instr.args[0]} {\n`
);
this._path.push (instr.args[0]);
break;
case 'eg': // end graph
this._path.pop ();
this.push (`${this.level}}\n\n`);
break;
case 'cn': // create node
this.push (`${this.level}${this.path}_${instr.args[0]}`);
break;
case 'en': // end node
this.push ('\n');
break;
case 'at': // add attributes
if (this._state === 'cn') {
this.push (` [${instr.args.join (', ')}]`);
}
else {
this.push (`${this.level}${
instr.args.join (`\n${this.level}`)
.replace (/"/gu, '')
}\n\n`);
}
break;
case 'ce':
this.push (`${this.level}${
instr.args[0]
} -${this._directional ? '>' : '-'} ${instr.args[1]}\n`);
break;
default:
break;
}
this._state = instr.type;
callback ();
}
public write (
instr: GraphStreamJSON
): boolean {
return super.write (JSON.stringify (instr), 'utf-8');
}
public create_node (name: string): boolean {
return this.write ({ type: 'cn', args: [ name ] });
}
public end_node (): boolean {
return this.write ({ type: 'en', args: [] });
}
public create_graph (name: string, type: 'u'|'d'|'s' = 's'): boolean {
const instr_type = `c${type}g` as GraphStreamCommand;
return this.write ({ type: instr_type, args: [ name ] });
}
public end_graph (): boolean {
return this.write ({ type: 'eg', args: [] });
}
public attributes (attrs: Record<string, Stringable>): boolean {
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}"`);
}
return this.write ({ type: 'at', args: solved });
}
public create_edge (origin: string, target: string): boolean {
return this.write ({
type: 'ce',
args: [
origin,
target
]
});
}
}