This commit is contained in:
126
src/exporters/graphmlExporter.ts
Normal file
126
src/exporters/graphmlExporter.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import * as fs from 'fs';
|
||||
import { DependencyGraph, GraphNode, GraphEdge, ExportOptions } from '../types';
|
||||
|
||||
export class GraphMLExporter {
|
||||
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('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
lines.push('<graphml xmlns="http://graphml.graphdrawing.org/xmlns">');
|
||||
lines.push(' <key id="d0" for="node" attr.name="label" attr.type="string"/>');
|
||||
lines.push(' <key id="d1" for="node" attr.name="kind" attr.type="string"/>');
|
||||
lines.push(' <key id="d2" for="node" attr.name="filePath" attr.type="string"/>');
|
||||
lines.push(' <key id="d3" for="node" attr.name="lineNumber" attr.type="int"/>');
|
||||
lines.push(' <key id="d4" for="edge" attr.name="style" attr.type="string"/>');
|
||||
lines.push(' <key id="d5" for="edge" attr.name="label" attr.type="string"/>');
|
||||
lines.push('');
|
||||
lines.push(' <graph id="G" edgedefault="directed">');
|
||||
|
||||
if (this.options.includeMetadata) {
|
||||
lines.push(` <data key="d6">${this.escapeXml(this.graph.metadata.createdAt)}</data>`);
|
||||
lines.push(` <data key="d7">${this.graph.metadata.nodeCount}</data>`);
|
||||
lines.push(` <data key="d8">${this.graph.metadata.edgeCount}</data>`);
|
||||
}
|
||||
|
||||
for (const node of this.graph.nodes.values()) {
|
||||
this.writeNode(lines, node);
|
||||
}
|
||||
|
||||
for (const edge of this.graph.edges) {
|
||||
this.writeEdge(lines, edge);
|
||||
}
|
||||
|
||||
lines.push(' </graph>');
|
||||
lines.push('</graphml>');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
private writeNode(lines: string[], node: GraphNode): void {
|
||||
const nodeId = this.getNodeId(node.id);
|
||||
|
||||
lines.push(` <node id="${nodeId}">`);
|
||||
lines.push(` <data key="d0">${this.escapeXml(node.label)}</data>`);
|
||||
lines.push(` <data key="d1">${this.escapeXml(node.kind)}</data>`);
|
||||
lines.push(` <data key="d2">${this.escapeXml(node.filePath)}</data>`);
|
||||
lines.push(` <data key="d3">${node.lineNumber}</data>`);
|
||||
|
||||
for (const [key, value] of Object.entries(node.metadata)) {
|
||||
if (typeof value === 'string') {
|
||||
lines.push(` <data key="${this.getDataKey(key)}">${this.escapeXml(value)}</data>`);
|
||||
} else if (typeof value === 'number') {
|
||||
lines.push(` <data key="${this.getDataKey(key)}">${value}</data>`);
|
||||
} else if (typeof value === 'boolean') {
|
||||
lines.push(` <data key="${this.getDataKey(key)}">${value}</data>`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(' </node>');
|
||||
}
|
||||
|
||||
private writeEdge(lines: string[], edge: GraphEdge): void {
|
||||
const sourceId = this.getNodeId(edge.source);
|
||||
const targetId = this.getNodeId(edge.target);
|
||||
|
||||
lines.push(` <edge source="${sourceId}" target="${targetId}">`);
|
||||
|
||||
if (edge.style) {
|
||||
lines.push(` <data key="d4">${this.escapeXml(edge.style)}</data>`);
|
||||
}
|
||||
|
||||
if (edge.label) {
|
||||
lines.push(` <data key="d5">${this.escapeXml(edge.label)}</data>`);
|
||||
}
|
||||
|
||||
if (edge.weight !== undefined) {
|
||||
lines.push(` <data key="${this.getDataKey('weight')}">${edge.weight}</data>`);
|
||||
}
|
||||
|
||||
lines.push(' </edge>');
|
||||
}
|
||||
|
||||
private getNodeId(id: string): string {
|
||||
return `n${id.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
||||
}
|
||||
|
||||
private getDataKey(key: string): string {
|
||||
const keyMap: Record<string, string> = {
|
||||
weight: 'd9',
|
||||
dependencies: 'd10'
|
||||
};
|
||||
return keyMap[key] || 'd9';
|
||||
}
|
||||
|
||||
private escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
toFile(): void {
|
||||
const content = this.export();
|
||||
fs.writeFileSync(this.options.outputPath, content, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
export function exportToGraphML(graph: DependencyGraph, options: ExportOptions): string {
|
||||
const exporter = new GraphMLExporter(graph, options);
|
||||
return exporter.export();
|
||||
}
|
||||
Reference in New Issue
Block a user