/**
* SVG Exporter
* Server-side SVG extraction and metadata injection
*
* Purpose: Pre-process SVG artifacts for enhanced export
* Use Cases: Add metadata, optimize viewBox, sanitize attributes
*/
import { ArtifactMetadata } from '../types.js';
/**
* Handles server-side SVG extraction and metadata enrichment
*/
export class SVGExporter {
/**
* Extract SVG element from HTML content
* Used for server-side SVG pre-processing
*
* @param htmlContent - Full HTML artifact content
* @returns SVG element string or null if not found
*/
extractSVG(htmlContent: string): string | null {
// Match SVG element with all nested content
const svgMatch = htmlContent.match(/<svg[^>]*>[\s\S]*?<\/svg>/i);
return svgMatch ? svgMatch[0] : null;
}
/**
* Add RDF metadata to SVG element
* Injects metadata as <metadata> element with RDF/Dublin Core
*
* @param svgContent - SVG element string
* @param metadata - Artifact metadata
* @returns SVG with embedded metadata
*/
addMetadata(svgContent: string, metadata: Partial<ArtifactMetadata>): string {
const title = metadata.title || 'CTS Artifact';
const description = metadata.description || 'Generated by CTS MCP Server';
const version = '3.2.0'; // CTS MCP Server version
const timestamp = new Date().toISOString();
const metadataXML = `
<metadata>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<rdf:Description rdf:about="">
<dc:title>${this.escapeXML(title)}</dc:title>
<dc:description>${this.escapeXML(description)}</dc:description>
<dc:creator>CTS MCP Server v${this.escapeXML(version)}</dc:creator>
<dc:date>${timestamp}</dc:date>
<dc:format>image/svg+xml</dc:format>
</rdf:Description>
</rdf:RDF>
</metadata>`;
// Insert metadata after opening <svg> tag
return svgContent.replace(/(<svg[^>]*>)/, `$1${metadataXML}`);
}
/**
* Optimize SVG viewBox for export
* Calculates bounding box and sets viewBox attribute
*
* @param svgContent - SVG element string
* @returns SVG with optimized viewBox
*/
optimizeViewBox(svgContent: string): string {
// Check if viewBox already exists
if (/viewBox=/.test(svgContent)) {
return svgContent;
}
// Extract width and height attributes
const widthMatch = svgContent.match(/width="(\d+)"/);
const heightMatch = svgContent.match(/height="(\d+)"/);
if (widthMatch && heightMatch) {
const width = widthMatch[1];
const height = heightMatch[1];
const viewBox = `viewBox="0 0 ${width} ${height}"`;
// Add viewBox attribute
return svgContent.replace(/(<svg[^>]*)>/, `$1 ${viewBox}>`);
}
return svgContent;
}
/**
* Ensure SVG has proper XML namespace
* Adds xmlns attribute if missing
*
* @param svgContent - SVG element string
* @returns SVG with xmlns attribute
*/
ensureNamespace(svgContent: string): string {
if (/xmlns=/.test(svgContent)) {
return svgContent;
}
return svgContent.replace(/(<svg)/, '$1 xmlns="http://www.w3.org/2000/svg"');
}
/**
* Process SVG for export
* Applies all optimizations and metadata in one pass
*
* @param svgContent - Raw SVG content
* @param metadata - Artifact metadata
* @returns Processed SVG ready for export
*/
processSVGForExport(svgContent: string, metadata: Partial<ArtifactMetadata>): string {
let processed = svgContent;
// Ensure namespace
processed = this.ensureNamespace(processed);
// Optimize viewBox
processed = this.optimizeViewBox(processed);
// Add metadata
processed = this.addMetadata(processed, metadata);
return processed;
}
/**
* Extract SVG from HTML and process for export
* Convenience method combining extraction and processing
*
* @param htmlContent - Full HTML artifact
* @param metadata - Artifact metadata
* @returns Processed SVG or null if no SVG found
*/
extractAndProcess(htmlContent: string, metadata: Partial<ArtifactMetadata>): string | null {
const svg = this.extractSVG(htmlContent);
if (!svg) {
return null;
}
return this.processSVGForExport(svg, metadata);
}
/**
* Escape XML special characters
* Prevents XML injection in metadata
*
* @param text - Text to escape
* @returns Escaped text safe for XML
*/
private escapeXML(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}