@typecad/kicad-symbols
Version:
Intelligent fuzzy search for KiCad symbols with CLI interface
281 lines • 10.3 kB
JavaScript
/**
* 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