/* * 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 , May 2020 */ /* eslint-disable line-comment-position */ /* eslint-disable no-inline-comments */ import { Transform } from 'stream'; import { GraphStreamJSON } from '../interfaces/GraphStreamJSON'; import { GraphStreamCommand, translate_command } from '../enums/GraphStreamCommand'; import { validate_name } from '../Helper'; interface Stringable { // eslint-disable-next-line @typescript-eslint/naming-convention toString(): string; } export class GraphStream extends Transform { private _path: string[] = []; private _state: GraphStreamCommand | '' = ''; 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: (GraphStreamCommand|'')[] = []; 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 ${ translate_command (instr)} expected: ${states.map ((s) => translate_command (s)) .join (', ')} actual: ${translate_command (this._state)}`); } } // 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): string { const node_name = validate_name (name); this.write ({ type: 'cn', args: [ node_name ] }); return `${this.path}_${node_name}`; } public end_node (): void { this.write ({ type: 'en', args: [] }); } public create_graph (name: string, type: 'u'|'d'|'s' = 's'): void { const instr_type = `c${type}g` as GraphStreamCommand; this.write ({ type: instr_type, args: [ validate_name (name) ] }); } public end_graph (): void { this.write ({ type: 'eg', args: [] }); } public attributes (attrs: Record): void { 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}"`); } this.write ({ type: 'at', args: solved }); } public create_edge (origin: string, target: string): void { this.write ({ type: 'ce', args: [ origin, target ] }); } }