spex-mcp
Version:
MCP server for Figma SpeX plugin and Cursor AI integration
186 lines (159 loc) • 5.98 kB
JavaScript
/**
* SVG to Vector Drawable Conversion Utility
*
* This module provides functionality to convert SVG content to Android Vector Drawable XML format
* using the vd-tool library.
*/
import { writeFile, unlink, mkdtemp, readFile } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';
import { vdConvert } from 'vd-tool';
/**
* Converts SVG content to Android Vector Drawable XML format
*
* @param {string} svgContent - The SVG content to convert
* @param {Object} options - Conversion options
* @param {string} [options.name] - Optional name for the vector drawable
* @param {string} [options.tint] - Optional tint color to apply to the vector drawable
* @returns {Promise<Object>} - Conversion result with vectorDrawable XML and optional warnings
* @throws {Error} - If conversion fails
*/
export async function convertSvgToVectorDrawable(svgContent, options = {}) {
// Input validation
if (!svgContent || typeof svgContent !== 'string') {
throw new Error('SVG content is required and must be a string');
}
if (svgContent.trim().length === 0) {
throw new Error('SVG content cannot be empty');
}
// Basic SVG format validation
if (!svgContent.includes('<svg') || !svgContent.includes('</svg>')) {
throw new Error('Invalid SVG format: must contain <svg> tags');
}
let tempDir;
let tempSvgFile;
let tempVdFile;
try {
// Create temporary directory
tempDir = await mkdtemp(join(tmpdir(), 'svg-to-vd-'));
tempSvgFile = join(tempDir, 'input.svg');
tempVdFile = join(tempDir, 'output.xml');
// Write SVG content to temporary file
await writeFile(tempSvgFile, svgContent, 'utf8');
// Use vd-tool library directly for conversion
try {
// The vdConvert function might use a different output file naming convention
// Let's check the documentation and adjust accordingly
await vdConvert(tempSvgFile, {
outDir: tempDir
// Note: vd-tool might not support outFileName directly
// It typically uses the input filename with .xml extension
});
// The output file might be named differently, let's try to find it
tempVdFile = join(tempDir, 'input.xml');
} catch (conversionError) {
// Handle vd-tool specific errors
const errorMessage = conversionError.message || 'Unknown conversion error';
throw new Error(`SVG to Vector Drawable conversion failed: ${errorMessage}`);
}
// Read the generated Vector Drawable XML
let vectorDrawableContent;
try {
vectorDrawableContent = await readFile(tempVdFile, 'utf8');
} catch (readError) {
throw new Error('Failed to read converted Vector Drawable file');
}
// Apply customizations if provided
if (options.name || options.tint) {
vectorDrawableContent = applyCustomizations(vectorDrawableContent, options);
}
// Clean up temporary files
await cleanup(tempSvgFile, tempVdFile, tempDir);
return {
vectorDrawable: vectorDrawableContent,
warnings: [] // Will be populated with any conversion warnings in future iterations
};
} catch (error) {
// Clean up temporary files in case of error
if (tempSvgFile || tempVdFile || tempDir) {
await cleanup(tempSvgFile, tempVdFile, tempDir).catch(() => {
// Ignore cleanup errors
});
}
throw error;
}
}
/**
* Applies customizations to the Vector Drawable XML
*
* @param {string} vectorDrawableXml - The Vector Drawable XML content
* @param {Object} options - Customization options
* @param {string} [options.name] - Optional name for the vector drawable
* @param {string} [options.tint] - Optional tint color to apply to the vector drawable
* @returns {string} - Customized Vector Drawable XML
*/
function applyCustomizations(vectorDrawableXml, options) {
let customizedXml = vectorDrawableXml;
// Apply tint if specified
if (options.tint) {
// Add tint attribute to the vector element
customizedXml = customizedXml.replace(
/<vector([^>]*)>/,
`<vector$1 android:tint="${options.tint}">`
);
}
// Note: The name customization would typically be handled at the file level
// when saving the Vector Drawable, not within the XML content itself
return customizedXml;
}
/**
* Cleans up temporary files and directories
*
* @param {string} svgFile - Path to temporary SVG file
* @param {string} vdFile - Path to temporary Vector Drawable file
* @param {string} tempDir - Path to temporary directory
*/
async function cleanup(svgFile, vdFile, tempDir) {
try {
if (svgFile) await unlink(svgFile).catch(() => {});
if (vdFile) await unlink(vdFile).catch(() => {});
if (tempDir) {
const { rm } = await import('fs/promises');
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
} catch (error) {
// Ignore cleanup errors
}
}
/**
* Validates SVG content format
*
* @param {string} svgContent - The SVG content to validate
* @returns {Object} - Validation result with isValid boolean and errors array
*/
export function validateSvgContent(svgContent) {
const errors = [];
if (!svgContent || typeof svgContent !== 'string') {
errors.push('SVG content is required and must be a string');
} else {
if (svgContent.trim().length === 0) {
errors.push('SVG content cannot be empty');
}
if (!svgContent.includes('<svg')) {
errors.push('SVG content must contain an opening <svg> tag');
}
if (!svgContent.includes('</svg>')) {
errors.push('SVG content must contain a closing </svg> tag');
}
// Check for basic XML structure
const openTags = (svgContent.match(/<svg[^>]*>/g) || []).length;
const closeTags = (svgContent.match(/<\/svg>/g) || []).length;
if (openTags !== closeTags) {
errors.push('SVG content has mismatched <svg> tags');
}
}
return {
isValid: errors.length === 0,
errors
};
}