Add exporter modules
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-30 00:59:51 +00:00
parent 15913bc173
commit 4e910cee06

View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
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();
}