Creating first version of MCP server.
Some checks failed
CI Pipeline / Test and Build (20.x) (push) Failing after 3m22s
CI Pipeline / Code Quality Check (push) Failing after 11m52s
CI Pipeline / Security Audit (push) Failing after 11m53s
CI Pipeline / Test and Build (18.x) (push) Failing after 12m31s
CI Pipeline / Build Release Artifacts (push) Has been cancelled
CI Pipeline / Notification (push) Has been cancelled

This commit is contained in:
2025-07-22 07:11:59 +00:00
parent f10bf53522
commit bf088da9d5
38 changed files with 6398 additions and 9442 deletions

265
src/utils/file-manager.ts Normal file
View File

@ -0,0 +1,265 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { glob } from 'glob';
import { DiagramFormat, DiagramType } from '../types/diagram-types.js';
// Functional types
type FileConfig = Readonly<{
workspaceRoot: string;
}>;
type DiagramFileMetadata = Readonly<{
path: string;
relativePath: string;
format: DiagramFormat;
isDiagram: boolean;
stats: fs.Stats;
}>;
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
// Pure function to create file configuration
export const createFileConfig = (workspaceRoot?: string): FileConfig => ({
workspaceRoot: workspaceRoot || process.cwd()
});
// Pure function to get file extension mapping
const getFileExtensionMap = (): FileExtensionMap => ({
[DiagramFormat.DRAWIO]: 'drawio',
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
[DiagramFormat.DIO]: 'dio',
[DiagramFormat.XML]: 'xml'
});
// Pure function to get file extension for diagram format
export const getFileExtension = (format: DiagramFormat): string => {
const extensionMap = getFileExtensionMap();
return extensionMap[format] || 'drawio';
};
// Pure function to get diagram format from file path
export const getDiagramFormat = (filePath: string): DiagramFormat => {
const fileName = path.basename(filePath).toLowerCase();
if (fileName.endsWith('.drawio.svg')) {
return DiagramFormat.DRAWIO_SVG;
} else if (fileName.endsWith('.drawio.png')) {
return DiagramFormat.DRAWIO_PNG;
} else if (fileName.endsWith('.drawio')) {
return DiagramFormat.DRAWIO;
} else if (fileName.endsWith('.dio')) {
return DiagramFormat.DIO;
} else if (fileName.endsWith('.xml')) {
return DiagramFormat.XML;
}
return DiagramFormat.DRAWIO;
};
// Pure function to get search patterns for diagram files
const getDiagramSearchPatterns = (): readonly string[] => [
'**/*.drawio',
'**/*.drawio.svg',
'**/*.drawio.png',
'**/*.dio',
'**/*.xml'
];
// Pure function to get ignore patterns
const getIgnorePatterns = (): readonly string[] => [
'node_modules/**',
'.git/**',
'build/**',
'dist/**'
];
// Curried function to find diagram files
export const findDiagramFiles = (config: FileConfig) => async (): Promise<readonly string[]> => {
const patterns = getDiagramSearchPatterns();
const ignorePatterns = getIgnorePatterns();
const files: string[] = [];
for (const pattern of patterns) {
const matches = await glob(pattern, {
cwd: config.workspaceRoot,
ignore: [...ignorePatterns]
});
files.push(...matches);
}
return files.map(file => path.resolve(config.workspaceRoot, file));
};
// Pure function to create full file name with extension
const createFullFileName = (fileName: string, format: DiagramFormat): string => {
const extension = getFileExtension(format);
return fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
};
// Curried function to create diagram file
export const createDiagramFile = (config: FileConfig) =>
(fileName: string) =>
(content: string) =>
(format: DiagramFormat) =>
async (outputDir?: string): Promise<string> => {
const dir = outputDir || config.workspaceRoot;
const fullFileName = createFullFileName(fileName, format);
const filePath = path.join(dir, fullFileName);
// Ensure directory exists
await fs.ensureDir(path.dirname(filePath));
// Write file
await fs.writeFile(filePath, content, 'utf8');
return filePath;
};
// Higher-order function for file operations with error handling
const withFileErrorHandling = <T extends any[], R>(
operation: (...args: T) => Promise<R>
) => async (...args: T): Promise<R> => {
try {
return await operation(...args);
} catch (error) {
throw new Error(`File operation failed: ${error}`);
}
};
// Pure function to read diagram file content
export const readDiagramFile = withFileErrorHandling(async (filePath: string): Promise<string> => {
if (!await fs.pathExists(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
return await fs.readFile(filePath, 'utf8');
});
// Pure function to update existing diagram file
export const updateDiagramFile = withFileErrorHandling(async (filePath: string, content: string): Promise<void> => {
if (!await fs.pathExists(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
await fs.writeFile(filePath, content, 'utf8');
});
// Curried function to convert diagram format
export const convertDiagramFormat = (config: FileConfig) =>
(sourcePath: string) =>
(targetFormat: DiagramFormat) =>
async (targetPath?: string): Promise<string> => {
const content = await readDiagramFile(sourcePath);
const sourceDir = path.dirname(sourcePath);
const baseName = path.basename(sourcePath, path.extname(sourcePath));
const newExtension = getFileExtension(targetFormat);
const newFileName = targetPath || path.join(sourceDir, `${baseName}.${newExtension}`);
const createFile = createDiagramFile(config);
await createFile(path.basename(newFileName))(content)(targetFormat)(path.dirname(newFileName));
return newFileName;
};
// Pure function to check if file is a diagram file
export const isDiagramFile = async (filePath: string): Promise<boolean> => {
const fileName = path.basename(filePath).toLowerCase();
const isDiagramExtension = fileName.endsWith('.drawio') ||
fileName.endsWith('.drawio.svg') ||
fileName.endsWith('.drawio.png') ||
fileName.endsWith('.dio');
if (isDiagramExtension) {
return true;
}
if (fileName.endsWith('.xml')) {
return await isDrawioXml(filePath);
}
return false;
};
// Pure function to check if XML file is a draw.io diagram
export const isDrawioXml = async (filePath: string): Promise<boolean> => {
try {
const content = await fs.readFile(filePath, 'utf8');
return content.includes('mxGraphModel') || content.includes('mxfile');
} catch {
return false;
}
};
// Curried function to ensure workspace directory exists
export const ensureWorkspaceDir = (config: FileConfig) => async (subDir?: string): Promise<string> => {
const targetDir = subDir ? path.join(config.workspaceRoot, subDir) : config.workspaceRoot;
await fs.ensureDir(targetDir);
return targetDir;
};
// Curried function to get relative path from workspace root
export const getRelativePath = (config: FileConfig) => (filePath: string): string => {
return path.relative(config.workspaceRoot, filePath);
};
// Higher-order function to create file metadata
const createFileMetadata = (config: FileConfig) => async (filePath: string): Promise<DiagramFileMetadata> => ({
path: filePath,
relativePath: getRelativePath(config)(filePath),
format: getDiagramFormat(filePath),
isDiagram: await isDiagramFile(filePath),
stats: await fs.stat(filePath)
});
// Curried function to get all diagram files with their metadata
export const getDiagramFilesWithMetadata = (config: FileConfig) => async (): Promise<readonly DiagramFileMetadata[]> => {
const files = await findDiagramFiles(config)();
const createMetadata = createFileMetadata(config);
return Promise.all(files.map(createMetadata));
};
// Curried function to create validated file configuration
export const createValidatedFileConfig = async (workspaceRoot?: string): Promise<FileConfig> => {
const config = createFileConfig(workspaceRoot);
// Ensure workspace exists
await ensureWorkspaceDir(config)();
return config;
};
// Utility functions for functional composition
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduceRight((acc, fn) => fn(acc), value);
// Higher-order function for file operations with retry logic
export const withRetry = <T extends any[], R>(
operation: (...args: T) => Promise<R>,
maxRetries: number = 3,
delay: number = 1000
) => async (...args: T): Promise<R> => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation(...args);
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) break;
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError!;
};
// Functional file operations with retry
export const readDiagramFileWithRetry = withRetry(readDiagramFile);
export const updateDiagramFileWithRetry = withRetry(updateDiagramFile);

View File

@ -0,0 +1,366 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs-extra';
import { DiagramFormat } from '../types/diagram-types.js';
const execAsync = promisify(exec);
// Functional types
type VSCodeConfig = Readonly<{
workspaceRoot: string;
extensionId: string;
}>;
type ExtensionStatus = Readonly<{
isInstalled: boolean;
isVSCodeAvailable: boolean;
version?: string;
}>;
type SetupResult = Readonly<{
success: boolean;
message: string;
}>;
type FileExtensionMap = Readonly<Record<DiagramFormat, string>>;
type PlatformCommandMap = Readonly<Record<string, string>>;
// Pure function to create VSCode configuration
export const createVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => ({
workspaceRoot: workspaceRoot || process.cwd(),
extensionId: 'hediet.vscode-drawio'
});
// Pure function to get file extension mapping
const getFileExtensionMap = (): FileExtensionMap => ({
[DiagramFormat.DRAWIO]: 'drawio',
[DiagramFormat.DRAWIO_SVG]: 'drawio.svg',
[DiagramFormat.DRAWIO_PNG]: 'drawio.png',
[DiagramFormat.DIO]: 'dio',
[DiagramFormat.XML]: 'xml'
});
// Pure function to get file extension for diagram format
export const getFileExtension = (format: DiagramFormat): string => {
const extensionMap = getFileExtensionMap();
return extensionMap[format] || 'drawio';
};
// Pure function to get platform-specific file explorer commands
const getPlatformCommandMap = (targetPath: string): PlatformCommandMap => ({
'win32': `explorer "${targetPath}"`,
'darwin': `open "${targetPath}"`,
'linux': `xdg-open "${targetPath}"`
});
// Pure function to get platform-specific file explorer command
const getFileExplorerCommand = (targetPath: string): string => {
const platform = process.platform;
const commandMap = getPlatformCommandMap(targetPath);
const command = commandMap[platform];
if (!command) {
throw new Error(`Unsupported platform: ${platform}`);
}
return command;
};
// Higher-order function for command execution with error handling
const withCommandErrorHandling = <T extends any[], R>(
operation: (...args: T) => Promise<R>
) => async (...args: T): Promise<R> => {
try {
return await operation(...args);
} catch (error) {
throw new Error(`Command execution failed: ${error}`);
}
};
// Pure function to execute VSCode command
export const executeVSCodeCommand = withCommandErrorHandling(async (command: string, args?: readonly string[]): Promise<string> => {
const fullCommand = args ? `${command} ${args.join(' ')}` : command;
const { stdout } = await execAsync(fullCommand);
return stdout;
});
// Curried function to open diagram file in VSCode
export const openDiagramInVSCode = (config: VSCodeConfig) => async (filePath: string): Promise<void> => {
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.resolve(config.workspaceRoot, filePath);
await executeVSCodeCommand('code', [absolutePath]);
};
// Curried function to check if draw.io extension is installed
export const isDrawioExtensionInstalled = (config: VSCodeConfig) => async (): Promise<boolean> => {
try {
const stdout = await executeVSCodeCommand('code', ['--list-extensions']);
return stdout.includes(config.extensionId);
} catch (error) {
console.warn('Could not check VSCode extensions:', error);
return false;
}
};
// Curried function to install draw.io extension
export const installDrawioExtension = (config: VSCodeConfig) => async (): Promise<void> => {
await executeVSCodeCommand('code', ['--install-extension', config.extensionId]);
};
// Curried function to open VSCode workspace
export const openWorkspace = (config: VSCodeConfig) => async (): Promise<void> => {
await executeVSCodeCommand('code', [config.workspaceRoot]);
};
// Pure function to check if VSCode is available
export const isVSCodeAvailable = async (): Promise<boolean> => {
try {
await executeVSCodeCommand('code', ['--version']);
return true;
} catch (error) {
return false;
}
};
// Pure function to get VSCode version
export const getVSCodeVersion = async (): Promise<string> => {
const stdout = await executeVSCodeCommand('code', ['--version']);
return stdout.split('\n')[0];
};
// Curried function to create and open new diagram in VSCode
export const createAndOpenDiagram = (config: VSCodeConfig) =>
(fileName: string) =>
(content: string) =>
(format: DiagramFormat) =>
async (outputDir?: string): Promise<string> => {
const dir = outputDir || config.workspaceRoot;
const extension = getFileExtension(format);
const fullFileName = fileName.endsWith(extension) ? fileName : `${fileName}.${extension}`;
const filePath = path.join(dir, fullFileName);
// Write file
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf8');
// Open in VSCode
await openDiagramInVSCode(config)(filePath);
return filePath;
};
// Curried function to get workspace folders
export const getWorkspaceFolders = (config: VSCodeConfig) => async (): Promise<readonly string[]> => {
try {
// This would require VSCode API integration
// For now, return current workspace
return [config.workspaceRoot];
} catch (error) {
console.warn('Could not get workspace folders:', error);
return [config.workspaceRoot];
}
};
// Pure function to show notification (placeholder for VSCode API)
export const showNotification = async (
message: string,
type: 'info' | 'warning' | 'error' = 'info'
): Promise<void> => {
try {
// This would require VSCode extension API
// For now, just log to console
console.log(`[${type.toUpperCase()}] ${message}`);
} catch (error) {
console.warn('Could not show notification:', error);
}
};
// Curried function to open file explorer at specific path
export const openFileExplorer = (config: VSCodeConfig) => async (dirPath?: string): Promise<void> => {
const targetPath = dirPath || config.workspaceRoot;
const command = getFileExplorerCommand(targetPath);
await executeVSCodeCommand(command);
};
// Pure function to refresh workspace (placeholder for VSCode API)
export const refreshWorkspace = async (): Promise<void> => {
try {
// This would require VSCode API integration
// For now, just log
console.log('Workspace refresh requested');
} catch (error) {
console.warn('Could not refresh workspace:', error);
}
};
// Curried function to check VSCode extension status
export const checkExtensionStatus = (config: VSCodeConfig) => async (): Promise<ExtensionStatus> => {
const isVSCodeAvail = await isVSCodeAvailable();
if (!isVSCodeAvail) {
return {
isInstalled: false,
isVSCodeAvailable: false
};
}
const isInstalled = await isDrawioExtensionInstalled(config)();
const version = isVSCodeAvail ? await getVSCodeVersion() : undefined;
return {
isInstalled,
isVSCodeAvailable: isVSCodeAvail,
version
};
};
// Curried function to setup VSCode environment for draw.io
export const setupVSCodeEnvironment = (config: VSCodeConfig) => async (): Promise<SetupResult> => {
try {
const status = await checkExtensionStatus(config)();
if (!status.isVSCodeAvailable) {
return {
success: false,
message: 'VSCode is not available. Please install VSCode first.'
};
}
if (!status.isInstalled) {
await installDrawioExtension(config)();
return {
success: true,
message: 'Draw.io extension installed successfully.'
};
}
return {
success: true,
message: 'VSCode environment is ready for draw.io.'
};
} catch (error) {
return {
success: false,
message: `Failed to setup VSCode environment: ${error}`
};
}
};
// Curried function to open multiple diagrams in VSCode
export const openMultipleDiagrams = (config: VSCodeConfig) => async (filePaths: readonly string[]): Promise<readonly string[]> => {
const results: string[] = [];
const openDiagram = openDiagramInVSCode(config);
for (const filePath of filePaths) {
try {
await openDiagram(filePath);
results.push(`Successfully opened: ${filePath}`);
} catch (error) {
results.push(`Failed to open ${filePath}: ${error}`);
}
}
return results;
};
// Pure function to create VSCode workspace configuration for draw.io
export const createWorkspaceConfig = (config: VSCodeConfig): Readonly<Record<string, any>> => ({
'hediet.vscode-drawio': {
'local-storage': path.join(config.workspaceRoot, '.vscode', 'drawio-storage'),
'theme': 'automatic',
'online-url': 'https://embed.diagrams.net/',
'offline': false
}
});
// Curried function to save VSCode workspace settings
export const saveWorkspaceSettings = (config: VSCodeConfig) => async (settings: Readonly<Record<string, any>>): Promise<void> => {
const settingsPath = path.join(config.workspaceRoot, '.vscode', 'settings.json');
try {
await fs.ensureDir(path.dirname(settingsPath));
let existingSettings = {};
if (await fs.pathExists(settingsPath)) {
const content = await fs.readFile(settingsPath, 'utf8');
existingSettings = JSON.parse(content);
}
const mergedSettings = { ...existingSettings, ...settings };
await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
} catch (error) {
throw new Error(`Failed to save workspace settings: ${error}`);
}
};
// Utility functions for functional composition
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduceRight((acc, fn) => fn(acc), value);
// Higher-order function for operations with retry logic
export const withRetry = <T extends any[], R>(
operation: (...args: T) => Promise<R>,
maxRetries: number = 3,
delay: number = 1000
) => async (...args: T): Promise<R> => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation(...args);
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) break;
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError!;
};
// Higher-order function for operations with timeout
export const withTimeout = <T extends any[], R>(
operation: (...args: T) => Promise<R>,
timeoutMs: number = 30000
) => async (...args: T): Promise<R> => {
return Promise.race([
operation(...args),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
)
]);
};
// Functional VSCode operations with retry and timeout
export const openDiagramInVSCodeWithRetry = (config: VSCodeConfig) =>
withRetry(openDiagramInVSCode(config));
export const setupVSCodeEnvironmentWithTimeout = (config: VSCodeConfig) =>
withTimeout(setupVSCodeEnvironment(config));
// Pure function to validate VSCode configuration
export const validateVSCodeConfig = (config: VSCodeConfig): boolean => {
return typeof config.workspaceRoot === 'string' &&
config.workspaceRoot.length > 0 &&
typeof config.extensionId === 'string' &&
config.extensionId.length > 0;
};
// Pure function to create validated VSCode configuration
export const createValidatedVSCodeConfig = (workspaceRoot?: string): VSCodeConfig => {
const config = createVSCodeConfig(workspaceRoot);
if (!validateVSCodeConfig(config)) {
throw new Error('Invalid VSCode configuration');
}
return config;
};

476
src/utils/xml-parser.ts Normal file
View File

@ -0,0 +1,476 @@
import * as xml2js from 'xml2js';
import { DiagramData, DiagramElement, DiagramConnection, DiagramType, DiagramFormat } from '../types/diagram-types.js';
// Functional types
type ParserConfig = Readonly<{
explicitArray: boolean;
mergeAttrs: boolean;
normalize: boolean;
normalizeTags: boolean;
trim: boolean;
}>;
type BuilderConfig = Readonly<{
xmldec: Readonly<{ version: string; encoding: string }>;
renderOpts: Readonly<{ pretty: boolean; indent: string }>;
}>;
type MxGraphModel = Readonly<{
mxGraphModel: any;
}>;
type CellData = Readonly<{
id: string;
value?: string;
style?: string;
vertex?: string;
edge?: string;
source?: string;
target?: string;
parent?: string;
mxGeometry?: any;
}>;
type StyleMap = Readonly<Record<string, string>>;
// Pure function to create default parser configuration
export const createParserConfig = (): ParserConfig => ({
explicitArray: false,
mergeAttrs: true,
normalize: true,
normalizeTags: true,
trim: true
});
// Pure function to create default builder configuration
export const createBuilderConfig = (): BuilderConfig => ({
xmldec: { version: '1.0', encoding: 'UTF-8' },
renderOpts: { pretty: true, indent: ' ' }
});
// Pure function to create parser instance
export const createParser = (config: ParserConfig = createParserConfig()): xml2js.Parser =>
new xml2js.Parser(config);
// Pure function to create builder instance
export const createBuilder = (config: BuilderConfig = createBuilderConfig()): xml2js.Builder =>
new xml2js.Builder(config);
// Pure function to get default style mappings
const getDefaultStyleMap = (): StyleMap => ({
'bpmn-start-event': 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#d5e8d4;strokeColor=#82b366;',
'bpmn-end-event': 'ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#f8cecc;strokeColor=#b85450;',
'bpmn-task': 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;',
'bpmn-gateway': 'rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;',
'uml-class': 'swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;fillColor=#dae8fc;strokeColor=#6c8ebf;',
'uml-actor': 'shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#d5e8d4;strokeColor=#82b366;',
'er-entity': 'whiteSpace=wrap;html=1;align=center;treeFolding=1;treeMoving=1;newEdgeStyle={"edgeStyle":"entityRelationEdgeStyle","startArrow":"none","endArrow":"none","segment":10,"curved":1};fillColor=#e1d5e7;strokeColor=#9673a6;',
'rectangle': 'rounded=0;whiteSpace=wrap;html=1;',
'ellipse': 'ellipse;whiteSpace=wrap;html=1;',
'diamond': 'rhombus;whiteSpace=wrap;html=1;',
'triangle': 'triangle;whiteSpace=wrap;html=1;'
});
// Pure function to get default style for element type
export const getDefaultStyle = (elementType: string): string => {
const styleMap = getDefaultStyleMap();
return styleMap[elementType] || styleMap['rectangle'];
};
// Pure function to get default connection style
export const getDefaultConnectionStyle = (): string =>
'edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;';
// Pure function to generate unique ID
export const generateId = (): string =>
Math.random().toString(36).substr(2, 9);
// Pure function to generate etag for draw.io
export const generateEtag = (): string =>
Math.random().toString(36).substr(2, 16);
// Higher-order function for XML operations with error handling
const withXMLErrorHandling = <T extends any[], R>(
operation: (...args: T) => Promise<R>
) => async (...args: T): Promise<R> => {
try {
return await operation(...args);
} catch (error) {
throw new Error(`XML operation failed: ${error}`);
}
};
// Pure function to extract properties from cell
const extractCellProperties = (cell: CellData): Readonly<Record<string, any>> => {
const properties: Record<string, any> = {};
if (cell.style) properties.style = cell.style;
if (cell.value) properties.value = cell.value;
if (cell.vertex) properties.vertex = cell.vertex;
if (cell.edge) properties.edge = cell.edge;
return properties;
};
// Pure function to infer element type from cell data
const inferElementType = (cell: CellData): string => {
const style = cell.style || '';
// BPMN elements
if (style.includes('bpmn')) return 'bpmn-element';
if (style.includes('startEvent')) return 'bpmn-start-event';
if (style.includes('endEvent')) return 'bpmn-end-event';
if (style.includes('task')) return 'bpmn-task';
if (style.includes('gateway')) return 'bpmn-gateway';
// UML elements
if (style.includes('uml')) return 'uml-element';
if (style.includes('class')) return 'uml-class';
if (style.includes('actor')) return 'uml-actor';
if (style.includes('usecase')) return 'uml-usecase';
// Database elements
if (style.includes('entity')) return 'er-entity';
if (style.includes('table')) return 'db-table';
// Network elements
if (style.includes('router')) return 'network-router';
if (style.includes('switch')) return 'network-switch';
if (style.includes('server')) return 'network-server';
// Generic shapes
if (style.includes('rectangle')) return 'rectangle';
if (style.includes('ellipse')) return 'ellipse';
if (style.includes('rhombus')) return 'diamond';
if (style.includes('triangle')) return 'triangle';
return 'generic-shape';
};
// Pure function to create DiagramElement from cell data
const createElementFromCell = (cell: CellData): DiagramElement => {
const geometry = cell.mxGeometry || {};
return {
id: cell.id,
type: inferElementType(cell),
label: cell.value || '',
style: cell.style || '',
geometry: {
x: parseFloat(geometry.x) || 0,
y: parseFloat(geometry.y) || 0,
width: parseFloat(geometry.width) || 100,
height: parseFloat(geometry.height) || 50
},
properties: extractCellProperties(cell)
};
};
// Pure function to create DiagramConnection from cell data
const createConnectionFromCell = (cell: CellData): DiagramConnection => ({
id: cell.id,
source: cell.source || '',
target: cell.target || '',
label: cell.value || '',
style: cell.style || '',
properties: extractCellProperties(cell)
});
// Pure function to infer diagram type from elements and connections
const inferDiagramType = (elements: readonly DiagramElement[], connections: readonly DiagramConnection[]): DiagramType => {
const elementTypes = elements.map(e => e.type);
// Check for BPMN elements
if (elementTypes.some(type => type.startsWith('bpmn-'))) {
return DiagramType.BPMN_PROCESS;
}
// Check for UML elements
if (elementTypes.some(type => type.startsWith('uml-'))) {
if (elementTypes.includes('uml-class')) return DiagramType.UML_CLASS;
if (elementTypes.includes('uml-actor')) return DiagramType.UML_USE_CASE;
return DiagramType.UML_CLASS; // Default UML type
}
// Check for ER elements
if (elementTypes.some(type => type.startsWith('er-') || type.startsWith('db-'))) {
return DiagramType.ER_DIAGRAM;
}
// Check for network elements
if (elementTypes.some(type => type.startsWith('network-'))) {
return DiagramType.NETWORK_TOPOLOGY;
}
// Default to flowchart
return DiagramType.FLOWCHART;
};
// Pure function to extract elements and connections from mxGraphModel
const extractElementsAndConnections = (mxGraphModel: MxGraphModel): Readonly<{ elements: DiagramElement[], connections: DiagramConnection[] }> => {
const elements: DiagramElement[] = [];
const connections: DiagramConnection[] = [];
// Parse cells from mxGraphModel
const root = mxGraphModel.mxGraphModel.root;
const cells = Array.isArray(root.mxCell) ? root.mxCell : [root.mxCell];
for (const cell of cells) {
if (!cell || cell.id === '0' || cell.id === '1') continue; // Skip root cells
if (cell.edge) {
// This is a connection/edge
connections.push(createConnectionFromCell(cell));
} else {
// This is an element/vertex
elements.push(createElementFromCell(cell));
}
}
return { elements, connections };
};
// Curried function to extract mxGraphModel from parsed XML
const extractMxGraphModel = (parser: xml2js.Parser) => async (result: any): Promise<MxGraphModel> => {
if (result.mxfile) {
// Standard .drawio format
const diagram = Array.isArray(result.mxfile.diagram)
? result.mxfile.diagram[0]
: result.mxfile.diagram;
return await parser.parseStringPromise(diagram.mxGraphModel || diagram._);
} else if (result.mxGraphModel) {
// Direct mxGraphModel format
return result;
} else {
throw new Error('Invalid draw.io XML format');
}
};
// Main function to parse draw.io XML content to DiagramData
export const parseDrawioXML = withXMLErrorHandling(async (xmlContent: string): Promise<DiagramData> => {
const parser = createParser();
const result = await parser.parseStringPromise(xmlContent);
// Handle different draw.io XML formats
const mxGraphModel = await extractMxGraphModel(parser)(result);
const { elements, connections } = extractElementsAndConnections(mxGraphModel);
return {
elements,
connections,
metadata: {
type: inferDiagramType(elements, connections),
format: DiagramFormat.DRAWIO,
created: new Date().toISOString(),
modified: new Date().toISOString(),
version: '1.0'
}
};
});
// Pure function to create cell from DiagramElement
const createCellFromElement = (element: DiagramElement): CellData => ({
id: element.id,
value: element.label || '',
style: element.style || getDefaultStyle(element.type),
vertex: '1',
parent: '1',
mxGeometry: {
x: element.geometry?.x || 0,
y: element.geometry?.y || 0,
width: element.geometry?.width || 100,
height: element.geometry?.height || 50,
as: 'geometry'
}
});
// Pure function to create cell from DiagramConnection
const createCellFromConnection = (connection: DiagramConnection): CellData => ({
id: connection.id,
value: connection.label || '',
style: connection.style || getDefaultConnectionStyle(),
edge: '1',
parent: '1',
source: connection.source,
target: connection.target,
mxGeometry: {
relative: '1',
as: 'geometry'
}
});
// Pure function to create cells array from diagram data
const createCellsFromDiagramData = (diagramData: DiagramData): readonly CellData[] => {
const cells: CellData[] = [
{ id: '0' },
{ id: '1', parent: '0' }
];
// Add elements
const elementCells = diagramData.elements.map(createCellFromElement);
cells.push(...elementCells);
// Add connections
const connectionCells = diagramData.connections.map(createCellFromConnection);
cells.push(...connectionCells);
return cells;
};
// Pure function to create mxGraphModel structure
const createMxGraphModel = (cells: readonly CellData[]): Readonly<Record<string, any>> => ({
mxGraphModel: {
dx: '1422',
dy: '794',
grid: '1',
gridSize: '10',
guides: '1',
tooltips: '1',
connect: '1',
arrows: '1',
fold: '1',
page: '1',
pageScale: '1',
pageWidth: '827',
pageHeight: '1169',
math: '0',
shadow: '0',
root: {
mxCell: [...cells]
}
}
});
// Pure function to create mxfile structure
const createMxFile = (mxGraphModel: Readonly<Record<string, any>>, builder: xml2js.Builder): Readonly<Record<string, any>> => ({
mxfile: {
host: 'Electron',
modified: new Date().toISOString(),
agent: 'Mozilla/5.0',
etag: generateEtag(),
version: '24.7.17',
type: 'device',
diagram: {
id: generateId(),
name: 'Page-1',
mxGraphModel: builder.buildObject(mxGraphModel).replace('<?xml version="1.0" encoding="UTF-8"?>', '')
}
}
});
// Main function to convert DiagramData to draw.io XML format
export const generateDrawioXML = (diagramData: DiagramData): string => {
const builder = createBuilder();
const cells = createCellsFromDiagramData(diagramData);
const mxGraphModel = createMxGraphModel(cells);
const mxfile = createMxFile(mxGraphModel, builder);
return builder.buildObject(mxfile);
};
// Pure function to validate XML content
export const validateDrawioXML = async (xmlContent: string): Promise<boolean> => {
try {
await parseDrawioXML(xmlContent);
return true;
} catch {
return false;
}
};
// Curried function to convert between different XML formats
export const convertXMLFormat = (targetFormat: DiagramFormat) => async (xmlContent: string): Promise<string> => {
const diagramData = await parseDrawioXML(xmlContent);
diagramData.metadata = { ...diagramData.metadata, format: targetFormat };
return generateDrawioXML(diagramData);
};
// Utility functions for functional composition
export const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
export const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
fns.reduceRight((acc, fn) => fn(acc), value);
// Higher-order function for operations with retry logic
export const withRetry = <T extends any[], R>(
operation: (...args: T) => Promise<R>,
maxRetries: number = 3,
delay: number = 1000
) => async (...args: T): Promise<R> => {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation(...args);
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) break;
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw lastError!;
};
// Functional XML operations with retry
export const parseDrawioXMLWithRetry = withRetry(parseDrawioXML);
export const validateDrawioXMLWithRetry = withRetry(validateDrawioXML);
// Pure function to create diagram metadata
export const createDiagramMetadata = (
type: DiagramType,
format: DiagramFormat = DiagramFormat.DRAWIO
): Readonly<DiagramData['metadata']> => ({
type,
format,
created: new Date().toISOString(),
modified: new Date().toISOString(),
version: '1.0'
});
// Pure function to update diagram metadata
export const updateDiagramMetadata = (
metadata: DiagramData['metadata'],
updates: Partial<DiagramData['metadata']>
): Readonly<DiagramData['metadata']> => ({
...metadata,
...updates,
modified: new Date().toISOString()
});
// Higher-order function to transform diagram data
export const transformDiagramData = <T>(
transformer: (data: DiagramData) => T
) => (data: DiagramData): T => transformer(data);
// Pure function to merge diagram data
export const mergeDiagramData = (
base: DiagramData,
additional: DiagramData
): DiagramData => ({
elements: [...base.elements, ...additional.elements],
connections: [...base.connections, ...additional.connections],
metadata: updateDiagramMetadata(base.metadata, {
type: base.metadata.type // Keep base type
})
});
// Pure function to filter diagram elements by type
export const filterElementsByType = (elementType: string) => (data: DiagramData): DiagramData => ({
...data,
elements: data.elements.filter(element => element.type === elementType),
metadata: updateDiagramMetadata(data.metadata, {})
});
// Pure function to map over diagram elements
export const mapDiagramElements = <T>(
mapper: (element: DiagramElement) => T
) => (data: DiagramData): T[] => data.elements.map(mapper);
// Pure function to map over diagram connections
export const mapDiagramConnections = <T>(
mapper: (connection: DiagramConnection) => T
) => (data: DiagramData): T[] => data.connections.map(mapper);