@typecad/kicad-symbols
Version:
Intelligent fuzzy search for KiCad symbols with CLI interface
282 lines • 9.7 kB
JavaScript
/**
* S-Expression parser for KiCad symbol files (.kicad_sym)
* Parses the nested s-expression format used by KiCad symbol libraries
*/
/**
* S-Expression parser for KiCad symbol files
*/
export class SExpressionParser {
position = 0;
content = '';
/**
* Parse a KiCad symbol file content into symbol definitions
* @param content - The file content as a string
* @returns Array of symbol definitions found in the file
*/
parseSymbolFile(content) {
this.content = content;
this.position = 0;
const rootExpression = this.parseExpression();
if (!rootExpression || rootExpression.type !== 'list') {
throw new Error('Invalid KiCad symbol file: Root element must be a list');
}
// Check if this is a kicad_symbol_lib
if (!rootExpression.children || rootExpression.children.length === 0) {
throw new Error('Invalid KiCad symbol file: Empty root element');
}
const firstChild = rootExpression.children[0];
if (firstChild.type !== 'atom' || firstChild.value !== 'kicad_symbol_lib') {
throw new Error('Invalid KiCad symbol file: Expected kicad_symbol_lib');
}
// Extract all symbol definitions
const symbols = [];
for (const child of rootExpression.children) {
if (child.type === 'list' && child.children && child.children.length > 0) {
const firstToken = child.children[0];
if (firstToken.type === 'atom' && firstToken.value === 'symbol') {
const symbolDef = this.parseSymbolDefinition(child);
if (symbolDef) {
symbols.push(symbolDef);
}
}
}
}
return symbols;
}
/**
* Parse a single s-expression from the current position
* @returns Parsed s-expression or null if end of input
*/
parseExpression() {
this.skipWhitespace();
if (this.position >= this.content.length) {
return null;
}
const char = this.content[this.position];
if (char === '(') {
return this.parseList();
}
else if (char === ')') {
return null; // End of list
}
else {
return this.parseAtom();
}
}
/**
* Parse a list s-expression (starts with '(')
* @returns Parsed list expression
*/
parseList() {
this.position++; // Skip opening '('
const children = [];
while (this.position < this.content.length) {
this.skipWhitespace();
if (this.position >= this.content.length) {
throw new Error('Unexpected end of input: missing closing parenthesis');
}
if (this.content[this.position] === ')') {
this.position++; // Skip closing ')'
break;
}
const child = this.parseExpression();
if (child) {
children.push(child);
}
}
return {
type: 'list',
children
};
}
/**
* Parse an atom s-expression (string or quoted string)
* @returns Parsed atom expression
*/
parseAtom() {
this.skipWhitespace();
if (this.content[this.position] === '"') {
return this.parseQuotedString();
}
else {
return this.parseUnquotedAtom();
}
}
/**
* Parse a quoted string atom
* @returns Parsed quoted string
*/
parseQuotedString() {
this.position++; // Skip opening quote
let value = '';
while (this.position < this.content.length) {
const char = this.content[this.position];
if (char === '"') {
this.position++; // Skip closing quote
break;
}
else if (char === '\\' && this.position + 1 < this.content.length) {
// Handle escape sequences
this.position++;
const nextChar = this.content[this.position];
switch (nextChar) {
case 'n':
value += '\n';
break;
case 't':
value += '\t';
break;
case 'r':
value += '\r';
break;
case '\\':
value += '\\';
break;
case '"':
value += '"';
break;
default:
value += nextChar;
}
this.position++;
}
else {
value += char;
this.position++;
}
}
return {
type: 'atom',
value
};
}
/**
* Parse an unquoted atom
* @returns Parsed unquoted atom
*/
parseUnquotedAtom() {
let value = '';
while (this.position < this.content.length) {
const char = this.content[this.position];
if (char === '(' || char === ')' || this.isWhitespace(char)) {
break;
}
value += char;
this.position++;
}
return {
type: 'atom',
value
};
}
/**
* Parse a symbol definition from an s-expression
* @param symbolExpr - The symbol s-expression
* @returns Parsed symbol definition or null if invalid
*/
parseSymbolDefinition(symbolExpr) {
if (symbolExpr.type !== 'list' || !symbolExpr.children || symbolExpr.children.length < 2) {
return null;
}
// Second element should be the symbol name
const nameExpr = symbolExpr.children[1];
if (nameExpr.type !== 'atom' || !nameExpr.value) {
return null;
}
const symbolName = nameExpr.value;
const properties = [];
let description;
let keywords;
// Parse all child expressions to find properties
for (let i = 2; i < symbolExpr.children.length; i++) {
const child = symbolExpr.children[i];
if (child.type === 'list' && child.children && child.children.length > 0) {
const firstToken = child.children[0];
if (firstToken.type === 'atom' && firstToken.value === 'property') {
const property = this.parseProperty(child);
if (property) {
properties.push(property);
// Extract description and keywords for easy access
if (property.name === 'Description') {
description = property.value;
}
else if (property.name === 'ki_keywords') {
keywords = property.value;
}
}
}
}
}
return {
name: symbolName,
properties,
description,
keywords
};
}
/**
* Parse a property from an s-expression
* @param propertyExpr - The property s-expression
* @returns Parsed property or null if invalid
*/
parseProperty(propertyExpr) {
if (propertyExpr.type !== 'list' || !propertyExpr.children || propertyExpr.children.length < 3) {
return null;
}
// First element is 'property'
// Second element is property name
// Third element is property value
const nameExpr = propertyExpr.children[1];
const valueExpr = propertyExpr.children[2];
if (nameExpr.type !== 'atom' || !nameExpr.value ||
valueExpr.type !== 'atom' || !valueExpr.value) {
return null;
}
const property = {
name: nameExpr.value,
value: valueExpr.value
};
// Parse additional property attributes if present
for (let i = 3; i < propertyExpr.children.length; i++) {
const child = propertyExpr.children[i];
if (child.type === 'list' && child.children && child.children.length > 0) {
const firstToken = child.children[0];
if (firstToken.type === 'atom' && firstToken.value === 'effects') {
// Property effects can include font size, hide flag, etc.
// For now, we'll just note that effects exist
property.effects = {};
}
}
}
return property;
}
/**
* Skip whitespace characters and comments
*/
skipWhitespace() {
while (this.position < this.content.length) {
const char = this.content[this.position];
if (this.isWhitespace(char)) {
this.position++;
}
else if (char === ';') {
// Skip comment line
while (this.position < this.content.length && this.content[this.position] !== '\n') {
this.position++;
}
}
else {
break;
}
}
}
/**
* Check if character is whitespace
* @param char - Character to check
* @returns True if whitespace
*/
isWhitespace(char) {
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
}
}
//# sourceMappingURL=SExpressionParser.js.map