diff --git a/src/exporters/graphmlExporter.ts b/src/exporters/graphmlExporter.ts new file mode 100644 index 0000000..bd61199 --- /dev/null +++ b/src/exporters/graphmlExporter.ts @@ -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; + + 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(''); + lines.push(''); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(''); + lines.push(' '); + + if (this.options.includeMetadata) { + lines.push(` ${this.escapeXml(this.graph.metadata.createdAt)}`); + lines.push(` ${this.graph.metadata.nodeCount}`); + lines.push(` ${this.graph.metadata.edgeCount}`); + } + + 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(' '); + lines.push(''); + + return lines.join('\n'); + } + + private writeNode(lines: string[], node: GraphNode): void { + const nodeId = this.getNodeId(node.id); + + lines.push(` `); + lines.push(` ${this.escapeXml(node.label)}`); + lines.push(` ${this.escapeXml(node.kind)}`); + lines.push(` ${this.escapeXml(node.filePath)}`); + lines.push(` ${node.lineNumber}`); + + for (const [key, value] of Object.entries(node.metadata)) { + if (typeof value === 'string') { + lines.push(` ${this.escapeXml(value)}`); + } else if (typeof value === 'number') { + lines.push(` ${value}`); + } else if (typeof value === 'boolean') { + lines.push(` ${value}`); + } + } + + lines.push(' '); + } + + private writeEdge(lines: string[], edge: GraphEdge): void { + const sourceId = this.getNodeId(edge.source); + const targetId = this.getNodeId(edge.target); + + lines.push(` `); + + if (edge.style) { + lines.push(` ${this.escapeXml(edge.style)}`); + } + + if (edge.label) { + lines.push(` ${this.escapeXml(edge.label)}`); + } + + if (edge.weight !== undefined) { + lines.push(` ${edge.weight}`); + } + + lines.push(' '); + } + + private getNodeId(id: string): string { + return `n${id.replace(/[^a-zA-Z0-9]/g, '_')}`; + } + + private getDataKey(key: string): string { + const keyMap: Record = { + weight: 'd9', + dependencies: 'd10' + }; + return keyMap[key] || 'd9'; + } + + private escapeXml(str: string): string { + return str + .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(); +}