import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { BaseElement } from '../core/element.js';
import { Relationship } from '../core/relationship.js';
export interface ModelExportOptions {
modelId?: string;
modelName?: string;
modelPurpose?: string;
includeViews?: boolean;
viewName?: string;
viewDocumentation?: string;
}
export class ModelXmlGenerator {
private templatePath: string;
constructor() {
// Get current file directory in ES module
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
this.templatePath = join(__dirname, 'templates');
}
/**
* Generate complete ArchiMate model XML
*/
generate(
elements: BaseElement[],
relationships: Relationship[],
options: ModelExportOptions = {}
): string {
const template = this.loadTemplate('model-template.xml');
// Set default values
const modelId = options.modelId || `model-${Date.now()}`;
const modelName = options.modelName || 'ArchiMate Model';
const modelPurpose = options.modelPurpose || 'Generated ArchiMate model from MCP server';
// Generate sections
const elementsXml = this.generateElementsSection(elements);
const relationshipsXml = this.generateRelationshipsSection(relationships);
const viewsXml = options.includeViews
? this.generateViewsSection(elements, relationships, options)
: '';
// Substitute template variables
return template
.replace('{{MODEL_ID}}', this.escapeXml(modelId))
.replace('{{MODEL_NAME}}', this.escapeXml(modelName))
.replace('{{MODEL_PURPOSE}}', this.escapeXml(modelPurpose))
.replace('{{ELEMENTS}}', elementsXml)
.replace('{{RELATIONSHIPS}}', relationshipsXml)
.replace('{{VIEWS}}', viewsXml);
}
private generateElementsSection(elements: BaseElement[]): string {
if (elements.length === 0) return '';
const elementTemplate = this.loadTemplate('element-template.xml');
let elementsXml = '\n <!-- ELEMENTS -->\n';
// Group elements by layer for better organization
const elementsByLayer = this.groupElementsByLayer(elements);
for (const [layer, layerElements] of elementsByLayer) {
elementsXml += `\n <!-- ${layer.toUpperCase()} LAYER ELEMENTS -->\n`;
for (const element of layerElements) {
const elementXml = elementTemplate
.replace('{{ELEMENT_TYPE}}', element.type)
.replace('{{ELEMENT_NAME}}', this.escapeXml(element.name))
.replace('{{ELEMENT_ID}}', this.escapeXml(element.id))
.replace('{{ELEMENT_DOCUMENTATION}}', this.escapeXml(`${element.type} element: ${element.name}`));
elementsXml += elementXml + '\n';
}
}
return elementsXml;
}
private generateRelationshipsSection(relationships: Relationship[]): string {
if (relationships.length === 0) return '';
const relationshipTemplate = this.loadTemplate('relationship-template.xml');
let relationshipsXml = '\n <!-- RELATIONSHIPS -->\n';
for (const relationship of relationships) {
const relationshipXml = relationshipTemplate
.replace('{{RELATIONSHIP_TYPE}}', relationship.type)
.replace('{{RELATIONSHIP_NAME}}', '') // Relationships typically don't have names in ArchiMate
.replace('{{RELATIONSHIP_ID}}', this.escapeXml(relationship.id))
.replace('{{SOURCE_ID}}', this.escapeXml(relationship.source))
.replace('{{TARGET_ID}}', this.escapeXml(relationship.target));
relationshipsXml += relationshipXml + '\n';
}
return relationshipsXml;
}
private generateViewsSection(
elements: BaseElement[],
relationships: Relationship[],
options: ModelExportOptions
): string {
// This will be enhanced when ViewXmlGenerator is implemented
// For now, return a placeholder or basic view structure
const viewTemplate = this.loadTemplate('view-template.xml');
const viewId = `view-${Date.now()}`;
const viewName = options.viewName || 'Generated View';
const viewDocumentation = options.viewDocumentation || 'Auto-generated diagram view';
return viewTemplate
.replace('{{VIEWS_FOLDER_ID}}', `folder-views-${Date.now()}`)
.replace('{{VIEW_NAME}}', this.escapeXml(viewName))
.replace('{{VIEW_ID}}', viewId)
.replace('{{VIEW_DOCUMENTATION}}', this.escapeXml(viewDocumentation))
.replace('{{VIEW_VISUAL_ELEMENTS}}', '<!-- Visual elements will be generated by ViewXmlGenerator -->')
.replace('{{VIEW_CONNECTIONS}}', '<!-- Connections will be generated by ViewXmlGenerator -->');
}
private groupElementsByLayer(elements: BaseElement[]): Map<string, BaseElement[]> {
const grouped = new Map<string, BaseElement[]>();
for (const element of elements) {
const layer = element.layer;
if (!grouped.has(layer)) {
grouped.set(layer, []);
}
grouped.get(layer)!.push(element);
}
return grouped;
}
private loadTemplate(templateName: string): string {
try {
const templatePath = join(this.templatePath, templateName);
return readFileSync(templatePath, 'utf-8');
} catch (error) {
throw new Error(`Failed to load template ${templateName}: ${error}`);
}
}
private escapeXml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}