UNPKG

spex-mcp

Version:

MCP server for Figma SpeX plugin and Cursor AI integration

186 lines (159 loc) 5.98 kB
/** * 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 }; }