diff --git a/lib/Element.ts b/lib/Element.ts index d380bfc..160c16c 100644 --- a/lib/Element.ts +++ b/lib/Element.ts @@ -13,6 +13,9 @@ export class Element { } 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.parent_name = parent; } diff --git a/lib/Graph.ts b/lib/Graph.ts index 399441f..6582c77 100644 --- a/lib/Graph.ts +++ b/lib/Graph.ts @@ -1,9 +1,16 @@ import { Element } from './Element'; import { Edge } from './Edge'; import { Node } from './Node'; -import { GraphStyles } from './Styles'; +import { GraphStyles, NodeStyles } from './Styles'; import { Color } from './Color'; +interface NodeOptions { + name: string; + label: string; + style: NodeStyles; + color: Color; +} + export class Graph extends Element { public children: Array = []; public nodes: Array = []; @@ -13,7 +20,7 @@ export class Graph extends Element { public color?: Color; // eslint-disable-next-line @typescript-eslint/naming-convention - public toString (level = 0): string { + public toString (): string { const header = this.parent ? `subgraph cluster_${this.full_name}` : `digraph ${this.full_name}`; @@ -23,54 +30,61 @@ export class Graph extends Element { if (this.style) attributes.push ({ name: 'style', value: this.style.toString () }); - let attrs = `\n ${attributes.map ((v) => `${v.name} = ${v.value}`) - .join ('\n ')}\n`; - let children = `\n ${this.children.map ((c) => c.toString (level + 1)) - .join ('\n ')}\n`; - let nodes = `\n ${this.nodes.map ((c) => c.toString ()) - .join ('\n ')}\n`; - let edges = `\n ${this.edges.map ((c) => c.toString ()) - .join ('\n ')}\n`; + let attrs = `\n${attributes.map ((v) => `${v.name} = ${v.value}`) + .join ('\n')}\n`; + let children = `\n${this.children.map ((c) => c.toString ()) + .join ('\n')}\n`; + let nodes = `\n${this.nodes.map ((c) => c.toString ()) + .join ('\n')}\n`; + let edges = `\n${this.edges.map ((c) => c.toString ()) + .join ('\n')}\n`; - if (attrs === '\n \n') + if (attrs === '\n\n') attrs = ''; - if (children === '\n \n') + if (children === '\n\n') children = ''; - if (nodes === '\n \n') + if (nodes === '\n\n') nodes = ''; - if (edges === '\n \n') + if (edges === '\n\n') edges = ''; - return `${header} {${attrs}${children}${nodes}${edges}}` - .replace (/\n/gu, `\n${' '.repeat (level)}`) + const indented = `${attrs}${children}${nodes}${edges}` + .replace (/\n$/u, '') + .replace (/^/gmu, ' ') + .split ('\n') + .map ((v) => v.trimRight ()) + .join ('\n'); + + return `${header} {${indented}\n}` .replace (/^\s+$/gmu, ''); } - public add_node (constructor: ((g: 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; - } + public add_node (constructor: ((n: Node) => void) | string): string { 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); return node.full_name; } 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); - constructor (graph); + + if (typeof constructor === 'string') + graph.name = constructor; + else + constructor (graph); + this.children.push (graph); return graph.full_name; } public add_edge (origin: string, target: string): void { - this.edges.push ( - new Edge (`${this.full_name}_${origin}`, `${this.full_name}_${target}`) - ); + this.edges.push (new Edge (origin, target)); } } diff --git a/lib/Node.ts b/lib/Node.ts index b04e136..815712e 100644 --- a/lib/Node.ts +++ b/lib/Node.ts @@ -9,7 +9,7 @@ export class Node extends Element { public style?: NodeStyles; public color?: Color; - public constructor (name: string, parent?: string, label?: string) { + public constructor (name: string, parent: string, label?: string) { super (name, parent); this.label = label; } @@ -28,19 +28,36 @@ export class Node extends Element { // eslint-disable-next-line @typescript-eslint/naming-convention public toString (): string { const attributes = []; + if (this.label || this.is_table) { + attributes.push ({ + name: 'label', + value: this.is_table + ? this.serialized_table + : this.label + }); + } if (this.style) attributes.push ({ name: 'style', value: this.style.toString () }); if (this.color) attributes.push ({ name: 'color', value: this.color.toString () }); - const attrs = attributes.map ((v) => `${v.name}="${v.value}"`) - .join (','); + const attrs = attributes.map ((v) => { + 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}`; } } diff --git a/test/Graph.ts b/test/Graph.ts index 1e5466a..e591755 100644 --- a/test/Graph.ts +++ b/test/Graph.ts @@ -1,16 +1,27 @@ import test from 'ava'; -import { Graph, GraphStyles, Color } from '../lib'; +import { Graph, GraphStyles, Color, NodeStyles } from '../lib'; const result = `digraph foo { subgraph cluster_foo_baz { color = #ff0000 style = bold - foo_baz_asd [label=] + 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=] - foo_foo [label=] + foo_baz [label="baz"] + foo_foo [label="foo"] foo_foo -> foo_baz }`; @@ -22,13 +33,31 @@ test ('serialize', (t) => { g.add_graph ((graph) => { graph.name = 'baz'; 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.color = Color.red; }); - g.add_node ('baz'); - g.add_node ('foo'); - g.add_edge ('foo', 'baz'); + const baz = g.add_node ('baz'); + const foo = g.add_node ('foo'); + g.add_edge (foo, baz); const serialized = g.toString (); diff --git a/test/Node.ts b/test/Node.ts index e897fca..829cace 100644 --- a/test/Node.ts +++ b/test/Node.ts @@ -2,12 +2,12 @@ import test from 'ava'; import { NodeStyles, Node, Color } from '../lib'; const serialized_simple -= 'bar_foo [label=,style="dashed",color="#00ff00"]'; += 'bar_foo [label="baz", style="dashed", color="#00ff00"]'; const serialized_table = `bar_foo [label=< -
foobarbaz
barbazfoo
bazfoobar
>,style="invis",color="#00ff00"]`; +>, style="invis", color="#00ff00"]`; test ('serialize simple', (t) => { const g = new Node ('foo', 'bar', 'baz'); @@ -47,3 +47,10 @@ test ('serialize table', (t) => { t.is (g.full_name, 'bar_foo'); 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' }); +});