UNPKG

@typecad/kicad-symbols

Version:

Intelligent fuzzy search for KiCad symbols with CLI interface

281 lines 10.3 kB
/** * Extractor for processing KiCad symbol files and extracting symbol definitions * Combines file reading with S-Expression parsing to extract structured symbol data */ import fs from 'node:fs'; import path from 'node:path'; import { SExpressionParser } from './SExpressionParser.js'; /** * Default extraction options */ const DEFAULT_EXTRACTION_OPTIONS = { includeWithoutDescription: true, includeWithoutKeywords: true, maxFileSize: 10 * 1024 * 1024, // 10MB limit encoding: 'utf8', continueOnError: true, strictValidation: false }; /** * KiCad symbol extractor for processing symbol files */ export class KiCadSymbolExtractor { parser; options; statistics = { filesProcessed: 0, filesWithErrors: 0, symbolsExtracted: 0, symbolsWithDescriptions: 0, symbolsWithKeywords: 0, processingTime: 0, errors: [] }; constructor(options = {}) { this.parser = new SExpressionParser(); this.options = { ...DEFAULT_EXTRACTION_OPTIONS, ...options }; this.resetStatistics(); } /** * Extract symbols from a list of symbol files * @param symbolFiles - Array of symbol file information * @returns Promise resolving to array of extracted symbols */ async extractSymbolsFromFiles(symbolFiles) { const startTime = Date.now(); this.resetStatistics(); const extractedSymbols = []; for (const fileInfo of symbolFiles) { try { const symbols = await this.extractSymbolsFromFile(fileInfo); extractedSymbols.push(...symbols); this.statistics.filesProcessed++; } catch (error) { this.handleFileError(fileInfo, error); if (!this.options.continueOnError) { throw error; } } } this.statistics.processingTime = Date.now() - startTime; return extractedSymbols; } /** * Extract symbols from a single file * @param fileInfo - Information about the symbol file * @returns Promise resolving to array of extracted symbols from the file */ async extractSymbolsFromFile(fileInfo) { // Check file size limit if (this.options.maxFileSize > 0 && fileInfo.size > this.options.maxFileSize) { throw new Error(`File size (${fileInfo.size} bytes) exceeds limit (${this.options.maxFileSize} bytes)`); } // Read file content let content; try { content = await fs.promises.readFile(fileInfo.filePath, { encoding: this.options.encoding }); } catch (error) { throw new Error(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`); } // Parse symbols from file content let symbolDefinitions; try { symbolDefinitions = this.parser.parseSymbolFile(content); } catch (error) { throw new Error(`Failed to parse symbols: ${error instanceof Error ? error.message : String(error)}`); } // Convert to extracted symbols const extractedSymbols = []; const libraryName = this.extractLibraryName(fileInfo); for (const symbolDef of symbolDefinitions) { try { const extractedSymbol = this.createExtractedSymbol(symbolDef, fileInfo, libraryName); // Apply filtering based on options if (this.shouldIncludeSymbol(extractedSymbol)) { extractedSymbols.push(extractedSymbol); this.statistics.symbolsExtracted++; if (extractedSymbol.hasDescription) { this.statistics.symbolsWithDescriptions++; } if (extractedSymbol.hasKeywords) { this.statistics.symbolsWithKeywords++; } } } catch (error) { const extractionError = { file: fileInfo.filePath, message: error instanceof Error ? error.message : String(error), symbolName: symbolDef.name, type: 'invalid_format' }; this.statistics.errors.push(extractionError); if (!this.options.continueOnError) { throw error; } } } return extractedSymbols; } /** * Get extraction statistics from the last operation * @returns Extraction statistics */ getStatistics() { return { ...this.statistics }; } /** * Create an ExtractedSymbol from a SymbolDefinition * @param symbolDef - Symbol definition from parser * @param fileInfo - File information * @param libraryName - Library name derived from file * @returns Extended symbol with metadata * @private */ createExtractedSymbol(symbolDef, fileInfo, libraryName) { const hasDescription = Boolean(symbolDef.description && symbolDef.description.trim().length > 0); const hasKeywords = Boolean(symbolDef.keywords && symbolDef.keywords.trim().length > 0); // Create combined search text const searchParts = []; if (symbolDef.description) { searchParts.push(symbolDef.description); } if (symbolDef.keywords) { searchParts.push(symbolDef.keywords); } searchParts.push(symbolDef.name); searchParts.push(libraryName); const searchText = searchParts.join(' ').toLowerCase(); return { ...symbolDef, library: libraryName, sourceFile: fileInfo.filePath, relativePath: fileInfo.relativePath, fileModified: fileInfo.lastModified, hasDescription, hasKeywords, searchText }; } /** * Extract library name from file information * @param fileInfo - File information * @returns Library name * @private */ extractLibraryName(fileInfo) { // Use the file name without extension as the library name return fileInfo.fileName; } /** * Check if a symbol should be included based on options * @param symbol - Symbol to check * @returns True if symbol should be included * @private */ shouldIncludeSymbol(symbol) { if (!this.options.includeWithoutDescription && !symbol.hasDescription) { return false; } if (!this.options.includeWithoutKeywords && !symbol.hasKeywords) { return false; } // Additional validation if strict mode is enabled if (this.options.strictValidation) { if (!symbol.name || symbol.name.trim().length === 0) { return false; } } return true; } /** * Handle file processing errors * @param fileInfo - File that caused the error * @param error - Error that occurred * @private */ handleFileError(fileInfo, error) { this.statistics.filesWithErrors++; let errorType = 'parse_error'; const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('Failed to read file')) { errorType = 'file_read_error'; } else if (errorMessage.includes('encoding') || errorMessage.includes('decode')) { errorType = 'encoding_error'; } else if (errorMessage.includes('Invalid KiCad symbol file')) { errorType = 'invalid_format'; } const extractionError = { file: fileInfo.filePath, message: errorMessage, type: errorType }; this.statistics.errors.push(extractionError); } /** * Reset extraction statistics * @private */ resetStatistics() { this.statistics = { filesProcessed: 0, filesWithErrors: 0, symbolsExtracted: 0, symbolsWithDescriptions: 0, symbolsWithKeywords: 0, processingTime: 0, errors: [] }; } /** * Get a human-readable summary of the extraction results * @returns Formatted summary string */ getExtractionSummary() { const stats = this.statistics; const processingTimeSeconds = (stats.processingTime / 1000).toFixed(2); const successRate = stats.filesProcessed > 0 ? ((stats.filesProcessed - stats.filesWithErrors) / stats.filesProcessed * 100).toFixed(1) : '0'; let summary = `Extraction Summary:\n`; summary += ` Files processed: ${stats.filesProcessed}\n`; summary += ` Files with errors: ${stats.filesWithErrors}\n`; summary += ` Success rate: ${successRate}%\n`; summary += ` Symbols extracted: ${stats.symbolsExtracted}\n`; summary += ` Symbols with descriptions: ${stats.symbolsWithDescriptions}\n`; summary += ` Symbols with keywords: ${stats.symbolsWithKeywords}\n`; summary += ` Processing time: ${processingTimeSeconds} seconds\n`; if (stats.errors.length > 0) { summary += `\nErrors encountered:\n`; const errorsByType = this.groupErrorsByType(); for (const [type, count] of Object.entries(errorsByType)) { summary += ` ${type}: ${count}\n`; } if (stats.errors.length <= 3) { summary += `\nError details:\n`; stats.errors.forEach(error => { summary += ` - ${path.basename(error.file)}: ${error.message}\n`; }); } } return summary; } /** * Group errors by type for summary reporting * @returns Object with error counts by type * @private */ groupErrorsByType() { const errorCounts = {}; for (const error of this.statistics.errors) { errorCounts[error.type] = (errorCounts[error.type] || 0) + 1; } return errorCounts; } } //# sourceMappingURL=KiCadSymbolExtractor.js.map