fixes
This commit is contained in:
parent
2e34757a1a
commit
73db0afc1f
@ -13,6 +13,9 @@ export class Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public constructor (name: string, parent = '') {
|
public constructor (name: string, parent = '') {
|
||||||
|
const regex = /^[a-z_][a-z_0-9]+$/iu;
|
||||||
|
if (!regex.test (name))
|
||||||
|
throw new Error ('invalid name specified');
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.parent_name = parent;
|
this.parent_name = parent;
|
||||||
}
|
}
|
||||||
|
74
lib/Graph.ts
74
lib/Graph.ts
@ -1,9 +1,16 @@
|
|||||||
import { Element } from './Element';
|
import { Element } from './Element';
|
||||||
import { Edge } from './Edge';
|
import { Edge } from './Edge';
|
||||||
import { Node } from './Node';
|
import { Node } from './Node';
|
||||||
import { GraphStyles } from './Styles';
|
import { GraphStyles, NodeStyles } from './Styles';
|
||||||
import { Color } from './Color';
|
import { Color } from './Color';
|
||||||
|
|
||||||
|
interface NodeOptions {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
style: NodeStyles;
|
||||||
|
color: Color;
|
||||||
|
}
|
||||||
|
|
||||||
export class Graph extends Element {
|
export class Graph extends Element {
|
||||||
public children: Array<Graph> = [];
|
public children: Array<Graph> = [];
|
||||||
public nodes: Array<Node> = [];
|
public nodes: Array<Node> = [];
|
||||||
@ -13,7 +20,7 @@ export class Graph extends Element {
|
|||||||
public color?: Color;
|
public color?: Color;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
public toString (level = 0): string {
|
public toString (): string {
|
||||||
const header = this.parent
|
const header = this.parent
|
||||||
? `subgraph cluster_${this.full_name}`
|
? `subgraph cluster_${this.full_name}`
|
||||||
: `digraph ${this.full_name}`;
|
: `digraph ${this.full_name}`;
|
||||||
@ -23,54 +30,61 @@ export class Graph extends Element {
|
|||||||
if (this.style)
|
if (this.style)
|
||||||
attributes.push ({ name: 'style', value: this.style.toString () });
|
attributes.push ({ name: 'style', value: this.style.toString () });
|
||||||
|
|
||||||
let attrs = `\n ${attributes.map ((v) => `${v.name} = ${v.value}`)
|
let attrs = `\n${attributes.map ((v) => `${v.name} = ${v.value}`)
|
||||||
.join ('\n ')}\n`;
|
.join ('\n')}\n`;
|
||||||
let children = `\n ${this.children.map ((c) => c.toString (level + 1))
|
let children = `\n${this.children.map ((c) => c.toString ())
|
||||||
.join ('\n ')}\n`;
|
.join ('\n')}\n`;
|
||||||
let nodes = `\n ${this.nodes.map ((c) => c.toString ())
|
let nodes = `\n${this.nodes.map ((c) => c.toString ())
|
||||||
.join ('\n ')}\n`;
|
.join ('\n')}\n`;
|
||||||
let edges = `\n ${this.edges.map ((c) => c.toString ())
|
let edges = `\n${this.edges.map ((c) => c.toString ())
|
||||||
.join ('\n ')}\n`;
|
.join ('\n')}\n`;
|
||||||
|
|
||||||
if (attrs === '\n \n')
|
if (attrs === '\n\n')
|
||||||
attrs = '';
|
attrs = '';
|
||||||
if (children === '\n \n')
|
if (children === '\n\n')
|
||||||
children = '';
|
children = '';
|
||||||
if (nodes === '\n \n')
|
if (nodes === '\n\n')
|
||||||
nodes = '';
|
nodes = '';
|
||||||
if (edges === '\n \n')
|
if (edges === '\n\n')
|
||||||
edges = '';
|
edges = '';
|
||||||
|
|
||||||
return `${header} {${attrs}${children}${nodes}${edges}}`
|
const indented = `${attrs}${children}${nodes}${edges}`
|
||||||
.replace (/\n/gu, `\n${' '.repeat (level)}`)
|
.replace (/\n$/u, '')
|
||||||
|
.replace (/^/gmu, ' ')
|
||||||
|
.split ('\n')
|
||||||
|
.map ((v) => v.trimRight ())
|
||||||
|
.join ('\n');
|
||||||
|
|
||||||
|
return `${header} {${indented}\n}`
|
||||||
.replace (/^\s+$/gmu, '');
|
.replace (/^\s+$/gmu, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
public add_node (constructor: ((g: Node) => void) | string): string {
|
public add_node (constructor: ((n: Node) => void) | string): string {
|
||||||
if (typeof constructor === 'string') {
|
|
||||||
this.nodes.push (new Node (constructor, this.full_name, constructor));
|
|
||||||
return this.nodes[this.nodes.length - 1].full_name;
|
|
||||||
}
|
|
||||||
const node = new Node ('unnamed', this.full_name);
|
const node = new Node ('unnamed', this.full_name);
|
||||||
constructor (node);
|
|
||||||
|
if (typeof constructor === 'string') {
|
||||||
|
node.name = constructor;
|
||||||
|
node.label = constructor;
|
||||||
|
}
|
||||||
|
else { constructor (node); }
|
||||||
|
|
||||||
this.nodes.push (node);
|
this.nodes.push (node);
|
||||||
return node.full_name;
|
return node.full_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public add_graph (constructor: ((g: Graph) => void) | string): string {
|
public add_graph (constructor: ((g: Graph) => void) | string): string {
|
||||||
if (typeof constructor === 'string') {
|
|
||||||
this.children.push (new Graph (constructor, this.full_name));
|
|
||||||
return this.children[this.children.length - 1].full_name;
|
|
||||||
}
|
|
||||||
const graph = new Graph ('unnamed', this.full_name);
|
const graph = new Graph ('unnamed', this.full_name);
|
||||||
constructor (graph);
|
|
||||||
|
if (typeof constructor === 'string')
|
||||||
|
graph.name = constructor;
|
||||||
|
else
|
||||||
|
constructor (graph);
|
||||||
|
|
||||||
this.children.push (graph);
|
this.children.push (graph);
|
||||||
return graph.full_name;
|
return graph.full_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public add_edge (origin: string, target: string): void {
|
public add_edge (origin: string, target: string): void {
|
||||||
this.edges.push (
|
this.edges.push (new Edge (origin, target));
|
||||||
new Edge (`${this.full_name}_${origin}`, `${this.full_name}_${target}`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
lib/Node.ts
33
lib/Node.ts
@ -9,7 +9,7 @@ export class Node extends Element {
|
|||||||
public style?: NodeStyles;
|
public style?: NodeStyles;
|
||||||
public color?: Color;
|
public color?: Color;
|
||||||
|
|
||||||
public constructor (name: string, parent?: string, label?: string) {
|
public constructor (name: string, parent: string, label?: string) {
|
||||||
super (name, parent);
|
super (name, parent);
|
||||||
this.label = label;
|
this.label = label;
|
||||||
}
|
}
|
||||||
@ -28,19 +28,36 @@ export class Node extends Element {
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
public toString (): string {
|
public toString (): string {
|
||||||
const attributes = [];
|
const attributes = [];
|
||||||
|
if (this.label || this.is_table) {
|
||||||
|
attributes.push ({
|
||||||
|
name: 'label',
|
||||||
|
value: this.is_table
|
||||||
|
? this.serialized_table
|
||||||
|
: this.label
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.style)
|
if (this.style)
|
||||||
attributes.push ({ name: 'style', value: this.style.toString () });
|
attributes.push ({ name: 'style', value: this.style.toString () });
|
||||||
if (this.color)
|
if (this.color)
|
||||||
attributes.push ({ name: 'color', value: this.color.toString () });
|
attributes.push ({ name: 'color', value: this.color.toString () });
|
||||||
|
|
||||||
const attrs = attributes.map ((v) => `${v.name}="${v.value}"`)
|
const attrs = attributes.map ((v) => {
|
||||||
.join (',');
|
const d = (/\n/u).test (v.value as string)
|
||||||
|
? [
|
||||||
|
'<',
|
||||||
|
'>'
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'"',
|
||||||
|
'"'
|
||||||
|
];
|
||||||
|
return `${v.name}=${d[0]}${v.value}${d[1]}`;
|
||||||
|
})
|
||||||
|
.join (', ');
|
||||||
|
|
||||||
|
if (attributes.length > 0)
|
||||||
|
return `${this.full_name} [${attrs}]`;
|
||||||
|
|
||||||
if (this.is_table || typeof this.label !== 'undefined') {
|
|
||||||
return `${this.full_name} [label=<${this.is_table
|
|
||||||
? this.serialized_table
|
|
||||||
: this.label}>${attributes.length > 0 ? `,${attrs}` : ''}]`;
|
|
||||||
}
|
|
||||||
return `${this.full_name}`;
|
return `${this.full_name}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,27 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import { Graph, GraphStyles, Color } from '../lib';
|
import { Graph, GraphStyles, Color, NodeStyles } from '../lib';
|
||||||
|
|
||||||
const result = `digraph foo {
|
const result = `digraph foo {
|
||||||
subgraph cluster_foo_baz {
|
subgraph cluster_foo_baz {
|
||||||
color = #ff0000
|
color = #ff0000
|
||||||
style = bold
|
style = bold
|
||||||
|
|
||||||
foo_baz_asd [label=<asd>]
|
subgraph cluster_foo_baz_nested {
|
||||||
|
color = #808080
|
||||||
|
style = dotted
|
||||||
|
|
||||||
|
subgraph cluster_foo_baz_nested_unnamed {
|
||||||
|
color = #808080
|
||||||
|
style = dotted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo_baz_asd [label="asd"]
|
||||||
|
foo_baz_test [style="bold", color="#808080"]
|
||||||
}
|
}
|
||||||
|
|
||||||
foo_baz [label=<baz>]
|
foo_baz [label="baz"]
|
||||||
foo_foo [label=<foo>]
|
foo_foo [label="foo"]
|
||||||
|
|
||||||
foo_foo -> foo_baz
|
foo_foo -> foo_baz
|
||||||
}`;
|
}`;
|
||||||
@ -22,13 +33,31 @@ test ('serialize', (t) => {
|
|||||||
g.add_graph ((graph) => {
|
g.add_graph ((graph) => {
|
||||||
graph.name = 'baz';
|
graph.name = 'baz';
|
||||||
graph.add_node ('asd');
|
graph.add_node ('asd');
|
||||||
|
graph.add_node ((n) => {
|
||||||
|
n.name = 'test';
|
||||||
|
n.style = NodeStyles.bold;
|
||||||
|
n.color = Color.gray;
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
graph.add_graph ((g) => {
|
||||||
|
g.style = GraphStyles.dotted;
|
||||||
|
g.color = Color.gray;
|
||||||
|
g.name = 'nested';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow, max-nested-callbacks
|
||||||
|
g.add_graph ((g) => {
|
||||||
|
g.style = GraphStyles.dotted;
|
||||||
|
g.color = Color.gray;
|
||||||
|
});
|
||||||
|
});
|
||||||
graph.style = GraphStyles.bold;
|
graph.style = GraphStyles.bold;
|
||||||
graph.color = Color.red;
|
graph.color = Color.red;
|
||||||
});
|
});
|
||||||
|
|
||||||
g.add_node ('baz');
|
const baz = g.add_node ('baz');
|
||||||
g.add_node ('foo');
|
const foo = g.add_node ('foo');
|
||||||
g.add_edge ('foo', 'baz');
|
g.add_edge (foo, baz);
|
||||||
|
|
||||||
const serialized = g.toString ();
|
const serialized = g.toString ();
|
||||||
|
|
||||||
|
11
test/Node.ts
11
test/Node.ts
@ -2,12 +2,12 @@ import test from 'ava';
|
|||||||
import { NodeStyles, Node, Color } from '../lib';
|
import { NodeStyles, Node, Color } from '../lib';
|
||||||
|
|
||||||
const serialized_simple
|
const serialized_simple
|
||||||
= 'bar_foo [label=<baz>,style="dashed",color="#00ff00"]';
|
= 'bar_foo [label="baz", style="dashed", color="#00ff00"]';
|
||||||
const serialized_table = `bar_foo [label=<<table>
|
const serialized_table = `bar_foo [label=<<table>
|
||||||
<tr><td>foo</td><td>bar</td><td>baz</td></tr>
|
<tr><td>foo</td><td>bar</td><td>baz</td></tr>
|
||||||
<tr><td>bar</td><td>baz</td><td>foo</td></tr>
|
<tr><td>bar</td><td>baz</td><td>foo</td></tr>
|
||||||
<tr><td>baz</td><td>foo</td><td>bar</td></tr>
|
<tr><td>baz</td><td>foo</td><td>bar</td></tr>
|
||||||
</table>>,style="invis",color="#00ff00"]`;
|
</table>>, style="invis", color="#00ff00"]`;
|
||||||
|
|
||||||
test ('serialize simple', (t) => {
|
test ('serialize simple', (t) => {
|
||||||
const g = new Node ('foo', 'bar', 'baz');
|
const g = new Node ('foo', 'bar', 'baz');
|
||||||
@ -47,3 +47,10 @@ test ('serialize table', (t) => {
|
|||||||
t.is (g.full_name, 'bar_foo');
|
t.is (g.full_name, 'bar_foo');
|
||||||
t.is (serialized, serialized_table);
|
t.is (serialized, serialized_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test ('adhere to naming convention', (t) => {
|
||||||
|
t.throws (() => {
|
||||||
|
const n = new Node ('invalid.name', 'parent');
|
||||||
|
return n.toString ();
|
||||||
|
}, { message: 'invalid name specified' });
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user