@typecad/kicad-symbols
Version:
Intelligent fuzzy search for KiCad symbols with CLI interface
289 lines • 10.9 kB
JavaScript
/**
* Converter for transforming KiCad symbol data into ComponentRecord format
* Maintains compatibility with the existing search engine and data structures
*/
/**
* Default conversion options
*/
const DEFAULT_CONVERSION_OPTIONS = {
defaultManufacturer: 'KiCad',
defaultPackage: 'Symbol',
includeFilePath: true,
includeMetadata: true,
generateSearchableExtra: true
};
/**
* Converter for KiCad symbols to ComponentRecord format
*/
export class KiCadSymbolConverter {
options;
statistics = {
symbolsConverted: 0,
symbolsWithDescriptions: 0,
symbolsWithKeywords: 0,
uniqueLibraries: 0,
conversionTime: 0,
libraryDistribution: {}
};
constructor(options = {}) {
this.options = { ...DEFAULT_CONVERSION_OPTIONS, ...options };
this.resetStatistics();
}
/**
* Convert array of extracted symbols to ComponentRecord format
* @param extractedSymbols - Array of extracted symbols
* @returns Array of component records
*/
convertSymbolsToComponentRecords(extractedSymbols) {
const startTime = Date.now();
this.resetStatistics();
const componentRecords = [];
const libraryCount = {};
for (const symbol of extractedSymbols) {
try {
const componentRecord = this.convertSingleSymbol(symbol);
componentRecords.push(componentRecord);
this.statistics.symbolsConverted++;
if (symbol.hasDescription) {
this.statistics.symbolsWithDescriptions++;
}
if (symbol.hasKeywords) {
this.statistics.symbolsWithKeywords++;
}
// Track library distribution
libraryCount[symbol.library] = (libraryCount[symbol.library] || 0) + 1;
}
catch (error) {
console.warn(`Failed to convert symbol ${symbol.name} from ${symbol.library}: ${error instanceof Error ? error.message : String(error)}`);
continue;
}
}
this.statistics.uniqueLibraries = Object.keys(libraryCount).length;
this.statistics.libraryDistribution = libraryCount;
this.statistics.conversionTime = Date.now() - startTime;
return componentRecords;
}
/**
* Convert a single extracted symbol to ComponentRecord format
* @param symbol - Extracted symbol to convert
* @returns Component record
* @private
*/
convertSingleSymbol(symbol) {
// Generate unique identifier in library:symbol format
const lcsc = `${symbol.library}:${symbol.name}`;
// Use library as category, with optional mapping
const category = this.options.categoryMapping?.[symbol.library] || symbol.library;
// Build description with fallback
const description = this.buildDescription(symbol);
// Build extra data object
const extraData = this.buildExtraData(symbol);
// Create formatted timestamp
const timestamp = new Date().toISOString();
const componentRecord = {
lcsc,
category_id: symbol.library,
category,
subcategory: 'Schematic Symbol',
mfr: symbol.name,
package: this.options.defaultPackage,
joints: '0', // Symbols don't have physical joints
manufacturer: this.options.defaultManufacturer,
basic: '1', // All KiCad symbols are considered basic
preferred: '1', // All KiCad symbols are preferred for schematic use
description,
datasheet: '', // KiCad symbols typically don't include datasheets
stock: '∞', // Unlimited availability for symbols
last_on_stock: timestamp,
price: '[]', // Free symbols
extra: JSON.stringify(extraData),
assembly_process: 'N/A', // Not applicable for symbols
min_order_qty: '0',
attrition_qty: '0'
};
return componentRecord;
}
/**
* Build description from symbol data with fallback options
* @param symbol - Extracted symbol
* @returns Description string
* @private
*/
buildDescription(symbol) {
const parts = [];
// Primary description from symbol
if (symbol.description && symbol.description.trim()) {
parts.push(symbol.description.trim());
}
// Add library context if no description
if (parts.length === 0) {
parts.push(`Symbol from ${symbol.library} library`);
}
// Add keywords as additional context if available and not already in description
if (symbol.keywords && symbol.keywords.trim()) {
const keywords = symbol.keywords.trim();
const description = parts[0].toLowerCase();
// Only add keywords if they provide additional information
if (!description.includes(keywords.toLowerCase())) {
parts.push(`Keywords: ${keywords}`);
}
}
return parts.join(' - ');
}
/**
* Build extra data object with symbol metadata
* @param symbol - Extracted symbol
* @returns Extra data object
* @private
*/
buildExtraData(symbol) {
const extraData = {
symbolType: 'schematic',
library: symbol.library,
source: 'local_files'
};
// Include original description if different from processed description
if (symbol.description) {
extraData.originalDescription = symbol.description;
}
// Include keywords
if (symbol.keywords) {
extraData.keywords = symbol.keywords;
// Split keywords for easier searching
extraData.keywordList = symbol.keywords.split(/\s+/).filter(k => k.length > 0);
}
// Include file metadata if enabled
if (this.options.includeFilePath) {
extraData.sourceFile = symbol.relativePath;
extraData.fileModified = symbol.fileModified.toISOString();
}
// Include processing metadata if enabled
if (this.options.includeMetadata) {
extraData.hasDescription = symbol.hasDescription;
extraData.hasKeywords = symbol.hasKeywords;
extraData.processingDate = new Date().toISOString();
}
// Include searchable data if enabled
if (this.options.generateSearchableExtra) {
extraData.searchText = symbol.searchText;
extraData.searchTerms = this.generateSearchTerms(symbol);
}
// Include all properties for advanced users
if (symbol.properties && symbol.properties.length > 0) {
extraData.properties = symbol.properties.map(prop => ({
name: prop.name,
value: prop.value
}));
}
return extraData;
}
/**
* Generate search terms from symbol data
* @param symbol - Extracted symbol
* @returns Array of search terms
* @private
*/
generateSearchTerms(symbol) {
const terms = new Set();
// Add symbol name parts
const nameParts = symbol.name.split(/[_\-\s]+/).filter(part => part.length > 1);
nameParts.forEach(part => terms.add(part.toLowerCase()));
// Add library name
terms.add(symbol.library.toLowerCase());
// Add description words
if (symbol.description) {
const descWords = symbol.description
.split(/\s+/)
.filter(word => word.length > 2)
.map(word => word.toLowerCase().replace(/[^a-z0-9]/g, ''));
descWords.forEach(word => {
if (word.length > 2) {
terms.add(word);
}
});
}
// Add keywords
if (symbol.keywords) {
const keywords = symbol.keywords
.split(/\s+/)
.filter(keyword => keyword.length > 1)
.map(keyword => keyword.toLowerCase());
keywords.forEach(keyword => terms.add(keyword));
}
return Array.from(terms);
}
/**
* Get conversion statistics from the last operation
* @returns Conversion statistics
*/
getStatistics() {
return { ...this.statistics };
}
/**
* Reset conversion statistics
* @private
*/
resetStatistics() {
this.statistics = {
symbolsConverted: 0,
symbolsWithDescriptions: 0,
symbolsWithKeywords: 0,
uniqueLibraries: 0,
conversionTime: 0,
libraryDistribution: {}
};
}
/**
* Get a human-readable summary of the conversion results
* @returns Formatted summary string
*/
getConversionSummary() {
const stats = this.statistics;
const conversionTimeSeconds = (stats.conversionTime / 1000).toFixed(2);
const averagePerSymbol = stats.symbolsConverted > 0
? (stats.conversionTime / stats.symbolsConverted).toFixed(2)
: '0';
let summary = `Conversion Summary:\n`;
summary += ` Symbols converted: ${stats.symbolsConverted}\n`;
summary += ` Symbols with descriptions: ${stats.symbolsWithDescriptions}\n`;
summary += ` Symbols with keywords: ${stats.symbolsWithKeywords}\n`;
summary += ` Unique libraries: ${stats.uniqueLibraries}\n`;
summary += ` Conversion time: ${conversionTimeSeconds} seconds\n`;
summary += ` Average time per symbol: ${averagePerSymbol} ms\n`;
if (stats.uniqueLibraries > 0) {
summary += `\nTop 10 Libraries by Symbol Count:\n`;
const sortedLibraries = Object.entries(stats.libraryDistribution)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
sortedLibraries.forEach(([library, count]) => {
summary += ` ${library}: ${count} symbols\n`;
});
}
return summary;
}
/**
* Validate that a ComponentRecord is properly formatted
* @param record - Component record to validate
* @returns True if valid
* @static
*/
static validateComponentRecord(record) {
// Check required fields
const requiredFields = ['lcsc', 'category', 'description', 'manufacturer'];
for (const field of requiredFields) {
if (!record[field] ||
String(record[field]).trim().length === 0) {
return false;
}
}
// Validate JSON fields
try {
JSON.parse(record.extra);
}
catch {
return false;
}
return true;
}
}
//# sourceMappingURL=KiCadSymbolConverter.js.map