This commit is contained in:
160
src/exporters/dotExporter.ts
Normal file
160
src/exporters/dotExporter.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import * as fs from 'fs';
|
||||
import { DependencyGraph, GraphNode, GraphEdge, GraphCluster, ExportOptions } from '../types';
|
||||
|
||||
export class DOTExporter {
|
||||
private graph: DependencyGraph;
|
||||
private options: Required<ExportOptions>;
|
||||
|
||||
constructor(graph: DependencyGraph, options: ExportOptions) {
|
||||
this.graph = graph;
|
||||
this.options = {
|
||||
format: options.format,
|
||||
outputPath: options.outputPath,
|
||||
includeMetadata: options.includeMetadata ?? false,
|
||||
layout: options.layout ?? 'dot',
|
||||
rankDir: options.rankDir ?? 'TB'
|
||||
};
|
||||
}
|
||||
|
||||
export(): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('digraph typeflow {');
|
||||
lines.push(' graph [');
|
||||
lines.push(` rankdir=${this.options.rankDir}`);
|
||||
lines.push(' splines=true');
|
||||
lines.push(' overlap=false');
|
||||
lines.push(' nodesep=0.5');
|
||||
lines.push(' ranksep=0.5');
|
||||
lines.push(' ]');
|
||||
lines.push('');
|
||||
lines.push(' node [');
|
||||
lines.push(' shape=box');
|
||||
lines.push(' style=filled');
|
||||
lines.push(' fontname="Helvetica"');
|
||||
lines.push(' fontsize=10');
|
||||
lines.push(' ]');
|
||||
lines.push('');
|
||||
lines.push(' edge [');
|
||||
lines.push(' fontname="Helvetica"');
|
||||
lines.push(' fontsize=8');
|
||||
lines.push(' ]');
|
||||
lines.push('');
|
||||
|
||||
if (this.graph.clusters.length > 0) {
|
||||
for (const cluster of this.graph.clusters) {
|
||||
this.writeCluster(lines, cluster);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
for (const edge of this.graph.edges) {
|
||||
this.writeEdge(lines, edge);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
for (const node of this.graph.nodes.values()) {
|
||||
this.writeNode(lines, node);
|
||||
}
|
||||
|
||||
if (this.options.includeMetadata) {
|
||||
lines.push('');
|
||||
lines.push(' subgraph cluster_metadata {');
|
||||
lines.push(' label="Metadata"');
|
||||
lines.push(' style=dashed');
|
||||
lines.push(` nodeCount [label="Nodes: ${this.graph.metadata.nodeCount}"]`);
|
||||
lines.push(` edgeCount [label="Edges: ${this.graph.metadata.edgeCount}"]`);
|
||||
lines.push(` createdAt [label="Created: ${this.graph.metadata.createdAt}"]`);
|
||||
lines.push(' }');
|
||||
}
|
||||
|
||||
lines.push('}');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
private writeCluster(lines: string[], cluster: GraphCluster): void {
|
||||
lines.push(` subgraph ${cluster.id} {`);
|
||||
lines.push(` label="${cluster.label}"`);
|
||||
if (cluster.style) {
|
||||
lines.push(` style="${cluster.style}"`);
|
||||
}
|
||||
lines.push(' color=lightgray');
|
||||
|
||||
for (const nodeId of cluster.nodeIds) {
|
||||
lines.push(` ${this.escapeId(nodeId)}`);
|
||||
}
|
||||
|
||||
lines.push(' }');
|
||||
}
|
||||
|
||||
private writeNode(lines: string[], node: GraphNode): void {
|
||||
const nodeId = this.escapeId(node.id);
|
||||
const label = this.getNodeLabel(node);
|
||||
const fillcolor = this.getNodeColor(node.kind);
|
||||
|
||||
lines.push(` ${nodeId} [`);
|
||||
lines.push(` label="${label}"`);
|
||||
lines.push(` fillcolor="${fillcolor}"`);
|
||||
lines.push(' ]');
|
||||
}
|
||||
|
||||
private writeEdge(lines: string[], edge: GraphEdge): void {
|
||||
const sourceId = this.escapeId(edge.source);
|
||||
const targetId = this.escapeId(edge.target);
|
||||
|
||||
const props: string[] = [];
|
||||
|
||||
if (edge.style) {
|
||||
props.push(`style="${edge.style}"`);
|
||||
}
|
||||
|
||||
if (edge.label) {
|
||||
props.push(`label="${edge.label}"`);
|
||||
}
|
||||
|
||||
if (edge.weight !== undefined) {
|
||||
props.push(`weight=${edge.weight}`);
|
||||
}
|
||||
|
||||
const propsStr = props.length > 0 ? ` [${props.join(', ')}]` : '';
|
||||
|
||||
lines.push(` ${sourceId} -> ${targetId}${propsStr}`);
|
||||
}
|
||||
|
||||
private getNodeLabel(node: GraphNode): string {
|
||||
const name = node.label;
|
||||
const fileName = node.filePath.split('/').pop() || node.filePath;
|
||||
const line = node.lineNumber;
|
||||
|
||||
return `${name}\\n(${fileName}:${line})`;
|
||||
}
|
||||
|
||||
private getNodeColor(kind: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
interface: '#e1f5fe',
|
||||
type_alias: '#f3e5f5',
|
||||
class: '#e8f5e9',
|
||||
enum: '#fff3e0',
|
||||
function: '#fce4ec',
|
||||
import: '#ffebee',
|
||||
external: '#fafafa'
|
||||
};
|
||||
|
||||
return colors[kind] || '#ffffff';
|
||||
}
|
||||
|
||||
private escapeId(id: string): string {
|
||||
return id.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^(\d)/, '_$1');
|
||||
}
|
||||
|
||||
toFile(): void {
|
||||
const content = this.export();
|
||||
fs.writeFileSync(this.options.outputPath, content, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
export function exportToDOT(graph: DependencyGraph, options: ExportOptions): string {
|
||||
const exporter = new DOTExporter(graph, options);
|
||||
return exporter.export();
|
||||
}
|
||||
Reference in New Issue
Block a user