what have i done

This commit is contained in:
2025-10-15 15:05:23 +02:00
parent c5733897ea
commit 1015e9e18f
123 changed files with 28542 additions and 1030 deletions

5
scripts/csound-parser/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
dist/
downloaded-opcodes/
*.log
.DS_Store

View File

@ -0,0 +1,277 @@
# Csound Manual Parser
A robust parser that converts the Csound reference manual into TypeScript reference files for use in code editors with hover tooltips and searchable documentation.
## Features
- Downloads markdown files directly from GitHub
- Parses 1000+ Csound opcodes
- Extracts structured data (syntax, parameters, examples)
- Generates organized TypeScript files by category
- Creates a main aggregator file with lookup functions
- Handles edge cases and provides detailed error reporting
## Installation
```bash
cd scripts/csound-parser
pnpm install
```
## Usage
### Option 1: Download and Parse (Recommended)
Download fresh markdown files from GitHub and parse them:
```bash
pnpm run download
```
This will:
1. Download all opcode markdown files from the Csound manual repository
2. Parse them into structured data
3. Generate TypeScript reference files in `src/lib/csound-reference/`
### Option 2: Parse Local Files
If you already have the Csound manual cloned locally:
```bash
pnpm run parse -- --input=/path/to/csound-manual/docs/opcodes
```
### Custom Output Directory
```bash
pnpm run parse -- --output=/custom/output/path
```
## Output Structure
The parser generates:
```
src/lib/csound-reference/
├── types.ts # Type definitions
├── csoundReference.ts # Main aggregator
├── signal-generators-basic-oscillators.ts
├── signal-modifiers-standard-filters.ts
├── mathematical-operations-trigonometric-functions.ts
└── ... (100+ category files)
```
Each category file contains an array of `CsoundReference` objects:
```typescript
export const signalGeneratorsBasicOscillators: CsoundReference[] = [
{
name: 'oscil',
type: 'opcode',
category: 'Signal Generators:Basic Oscillators',
description: 'A simple oscillator without any interpolation.',
syntax: 'ares = oscil(xamp, xcps [, ifn, iphs])\nkres = oscil(kamp, kcps [, ifn, iphs])',
rates: ['a-rate', 'k-rate', 'i-rate'],
parameters: [
{
name: 'ifn',
description: 'function table number. Requires a wrap-around guard point.',
type: 'initialization'
},
// ...
],
seeAlso: ['oscili', 'poscil']
},
// ...
]
```
## Integration
Import in your application:
```typescript
import {
allCsoundReferences,
getCsoundReference,
getCsoundReferencesByCategory
} from './lib/csound-reference/csoundReference'
// Get a specific opcode
const oscil = getCsoundReference('oscil')
// Get all oscillators
const oscillators = getCsoundReferencesByCategory('Signal Generators:Basic Oscillators')
// Search all references
const filtered = allCsoundReferences.filter(ref =>
ref.description.includes('filter')
)
```
## Use Cases
### 1. Hover Tooltips in CodeMirror
Create a tooltip extension similar to the GLSL tooltips:
```typescript
import { hoverTooltip } from '@codemirror/view'
import { getCsoundReference } from './lib/csound-reference/csoundReference'
export const csoundTooltip = hoverTooltip((view, pos) => {
const word = getWordAt(view, pos)
if (!word) return null
const reference = getCsoundReference(word.text)
if (!reference) return null
return {
pos: word.from,
end: word.to,
above: true,
create() {
const dom = document.createElement('div')
dom.innerHTML = `
<div class="tooltip-header">
<strong>${reference.name}</strong>
<span>${reference.type}</span>
</div>
<div class="tooltip-description">${reference.description}</div>
${reference.syntax ? `<pre>${reference.syntax}</pre>` : ''}
`
return { dom }
}
}
})
```
### 2. Searchable Help Panel
Create a reference panel with fuzzy search:
```typescript
import { allCsoundReferences } from './lib/csound-reference/csoundReference'
import Fuse from 'fuse.js'
const fuse = new Fuse(allCsoundReferences, {
keys: ['name', 'description', 'category'],
threshold: 0.4
})
const results = fuse.search('oscillator')
```
### 3. Autocomplete
Use the reference data for intelligent autocomplete:
```typescript
import { allCsoundReferences } from './lib/csound-reference/csoundReference'
const completions = allCsoundReferences.map(ref => ({
label: ref.name,
type: ref.type,
info: ref.description,
detail: ref.syntax
}))
```
## Data Structure
### CsoundReference Interface
```typescript
interface CsoundReference {
name: string // Opcode name (e.g., "oscil")
type: 'opcode' | 'keyword' | 'header' | 'constant'
category: string // Full category path
description: string // Brief description
syntax?: string // Syntax examples
example?: string // Code example
rates?: string[] // ['a-rate', 'k-rate', 'i-rate']
parameters?: {
name: string
description: string
type: 'initialization' | 'performance'
}[]
seeAlso?: string[] // Related opcodes
}
```
## Parser Architecture
### 1. Downloader (`downloader.ts`)
- Fetches markdown files from GitHub API
- Handles rate limiting and retries
- Downloads to `downloaded-opcodes/` directory
### 2. Parser (`parser.ts`)
- Parses markdown frontmatter (id, category)
- Extracts sections using regex
- Extracts parameters from initialization/performance sections
- Handles modern and classic syntax variants
- Detects rate types (a-rate, k-rate, i-rate)
### 3. Generator (`generator.ts`)
- Groups opcodes by category
- Sanitizes category names for file names
- Generates TypeScript files with proper escaping
- Creates main aggregator with imports
- Provides lookup functions
## Troubleshooting
### Download Fails
If the GitHub download fails, manually clone the repository:
```bash
git clone https://github.com/csound/manual.git
cd manual
git checkout develop
```
Then run the parser with the local path:
```bash
pnpm run parse -- --input=../manual/docs/opcodes
```
### Missing Categories
Some opcodes may not have categories defined. The parser will skip these and log warnings.
### Parse Errors
The parser is robust and will continue parsing even if individual files fail. Check the console output for warnings about skipped files.
## Extending the Parser
### Adding New Extraction
To extract additional information from the markdown files, modify `parser.ts`:
```typescript
extractNewField(content: string): string {
const section = this.extractSection(content, 'NewSection')
// Parse the section content
return parsed
}
```
Then add the field to the `CsoundReference` interface in `types.ts`.
### Custom Categories
To reorganize categories, modify the `category` field in the parser or create a mapping function in the generator.
## Performance
- Parsing 1000+ opcodes: ~2-5 seconds
- Generating TypeScript files: ~1-2 seconds
- Download from GitHub: ~30-60 seconds (network dependent)
## License
MIT

View File

@ -0,0 +1,45 @@
import * as fs from 'fs';
import { CsoundManualParser } from './parser';
const parser = new CsoundManualParser();
const content = fs.readFileSync('downloaded-opcodes/moogladder.md', 'utf-8');
console.log('=== Testing moogladder.md ===\n');
// Test extractSection
const syntaxSection = parser.extractSection(content, 'Syntax');
console.log('Syntax section length:', syntaxSection.length);
console.log('First 200 chars:', syntaxSection.substring(0, 200));
console.log('\n---\n');
// Look for ### Initialization
const initMatch = syntaxSection.match(/###\s+Initialization\s*\n([\s\S]*?)(?=\n###|\n##|$)/i);
console.log('Initialization match:', initMatch ? 'FOUND' : 'NOT FOUND');
if (initMatch) {
console.log('Init section content (first 300 chars):');
console.log(initMatch[1].substring(0, 300));
console.log('\n---\n');
// Test the parameter regex
const paramRegex = /_([a-zA-Z0-9_,\s/]+)_\s*[-–—]+\s*([^\n]+(?:\n(?!_)[^\n]+)*)/g;
const params = Array.from(initMatch[1].matchAll(paramRegex));
console.log(`Found ${params.length} parameters`);
params.forEach((p, i) => {
console.log(` Param ${i + 1}: name="${p[1]}", desc="${p[2].substring(0, 60)}..."`);
});
}
// Test extractParameters directly
const initParams = parser.extractParameters(content, 'Initialization');
const perfParams = parser.extractParameters(content, 'Performance');
console.log('\n=== extractParameters results ===');
console.log(`Initialization params: ${initParams.length}`);
initParams.forEach(p => {
console.log(` - ${p.name}: ${p.description.substring(0, 60)}...`);
});
console.log(`\nPerformance params: ${perfParams.length}`);
perfParams.forEach(p => {
console.log(` - ${p.name}: ${p.description.substring(0, 60)}...`);
});

View File

@ -0,0 +1,30 @@
import * as fs from 'fs';
const content = fs.readFileSync('downloaded-opcodes/moogladder.md', 'utf-8');
console.log('Full content length:', content.length);
console.log('\n=== Testing extractSection regex ===\n');
const regex = /##\s+Syntax\s*\n([\s\S]*?)(?=\n##|$)/i;
const match = content.match(regex);
if (match) {
console.log('Match found!');
console.log('Captured content length:', match[1].length);
console.log('\nFull captured content:');
console.log('---START---');
console.log(match[1]);
console.log('---END---');
} else {
console.log('No match found');
}
// Also check what comes after
const syntaxIndex = content.indexOf('## Syntax');
const examplesIndex = content.indexOf('## Examples');
console.log('\n=== Indices ===');
console.log('## Syntax at:', syntaxIndex);
console.log('## Examples at:', examplesIndex);
console.log('Distance:', examplesIndex - syntaxIndex);
console.log('\nContent between (first 500 chars):');
console.log(content.substring(syntaxIndex, syntaxIndex + 500));

View File

@ -0,0 +1,123 @@
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
interface GitHubFile {
name: string;
download_url: string;
type: string;
}
export class GitHubDownloader {
private apiBase = 'https://api.github.com/repos/csound/manual/contents';
private branch = 'develop';
private downloadDir: string;
constructor(downloadDir: string) {
this.downloadDir = downloadDir;
}
private async fetchJson<T>(url: string): Promise<T> {
return new Promise((resolve, reject) => {
https.get(
url,
{
headers: {
'User-Agent': 'Csound-Parser',
},
},
(res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(new Error(`Failed to parse JSON: ${err}`));
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
}
).on('error', reject);
});
}
private async downloadFile(url: string, destPath: string): Promise<void> {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
if (res.headers.location) {
this.downloadFile(res.headers.location, destPath).then(resolve).catch(reject);
return;
}
}
const fileStream = fs.createWriteStream(destPath);
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
resolve();
});
fileStream.on('error', (err) => {
fs.unlink(destPath, () => {});
reject(err);
});
}).on('error', reject);
});
}
async downloadOpcodes(): Promise<number> {
console.log('Fetching opcode list from GitHub...');
const url = `${this.apiBase}/docs/opcodes?ref=${this.branch}`;
const files = await this.fetchJson<GitHubFile[]>(url);
const markdownFiles = files.filter(f => f.type === 'file' && f.name.endsWith('.md'));
console.log(`Found ${markdownFiles.length} markdown files`);
if (!fs.existsSync(this.downloadDir)) {
fs.mkdirSync(this.downloadDir, { recursive: true });
}
let downloaded = 0;
let failed = 0;
for (const file of markdownFiles) {
const destPath = path.join(this.downloadDir, file.name);
try {
await this.downloadFile(file.download_url, destPath);
downloaded++;
if (downloaded % 50 === 0) {
console.log(`Downloaded ${downloaded}/${markdownFiles.length}...`);
}
} catch (err) {
console.error(`Failed to download ${file.name}:`, err);
failed++;
}
}
console.log(`\nDownloaded ${downloaded} files, ${failed} failed`);
return downloaded;
}
async downloadCategories(): Promise<void> {
console.log('Downloading categories.py...');
const url = 'https://raw.githubusercontent.com/csound/manual/develop/categories.py';
const destPath = path.join(this.downloadDir, 'categories.py');
await this.downloadFile(url, destPath);
console.log('Categories file downloaded');
}
}

View File

@ -0,0 +1,207 @@
import * as fs from 'fs';
import * as path from 'path';
import type { CsoundReference } from './types';
export class TypeScriptGenerator {
private outputDir: string;
constructor(outputDir: string) {
this.outputDir = outputDir;
}
sanitizeCategoryName(category: string): string {
return category
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
sanitizeVariableName(category: string): string {
return category
.replace(/[^a-zA-Z0-9]+/g, ' ')
.split(' ')
.map((word, index) =>
index === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
)
.join('')
.replace(/^[0-9]/, '_$&');
}
escapeString(str: string): string {
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
generateReferenceObject(ref: CsoundReference, indent: string = ' '): string {
const lines: string[] = [];
lines.push(`{`);
lines.push(`${indent}name: '${this.escapeString(ref.name)}',`);
lines.push(`${indent}type: '${ref.type}',`);
lines.push(`${indent}category: '${this.escapeString(ref.category)}',`);
lines.push(`${indent}description: '${this.escapeString(ref.description)}',`);
if (ref.syntax) {
lines.push(`${indent}syntax: '${this.escapeString(ref.syntax)}',`);
}
if (ref.example) {
const escapedExample = this.escapeString(ref.example);
lines.push(`${indent}example: '${escapedExample}',`);
}
if (ref.rates && ref.rates.length > 0) {
lines.push(`${indent}rates: [${ref.rates.map(r => `'${r}'`).join(', ')}],`);
}
if (ref.parameters && ref.parameters.length > 0) {
lines.push(`${indent}parameters: [`);
for (const param of ref.parameters) {
lines.push(`${indent} {`);
lines.push(`${indent} name: '${this.escapeString(param.name)}',`);
lines.push(`${indent} description: '${this.escapeString(param.description)}',`);
lines.push(`${indent} type: '${param.type}'`);
lines.push(`${indent} },`);
}
lines.push(`${indent}],`);
}
if (ref.seeAlso && ref.seeAlso.length > 0) {
lines.push(`${indent}seeAlso: [${ref.seeAlso.map(s => `'${this.escapeString(s)}'`).join(', ')}]`);
}
lines.push(`}`);
return lines.join('\n');
}
generateCategoryFile(
category: string,
references: CsoundReference[],
fileName: string
): string {
const varName = this.sanitizeVariableName(category);
const lines: string[] = [];
lines.push(`import type { CsoundReference } from './types'`);
lines.push(``);
lines.push(`// ${category}`);
lines.push(`export const ${varName}: CsoundReference[] = [`);
for (const ref of references) {
const refLines = this.generateReferenceObject(ref, ' ');
lines.push(refLines + ',');
}
lines.push(`]`);
lines.push(``);
return lines.join('\n');
}
generateMainFile(categories: Map<string, CsoundReference[]>): string {
const lines: string[] = [];
lines.push(`import type { CsoundReference } from './types'`);
const imports: string[] = [];
const varNames: string[] = [];
for (const [category] of categories) {
const fileName = this.sanitizeCategoryName(category);
const varName = this.sanitizeVariableName(category);
imports.push(`import { ${varName} } from './${fileName}'`);
varNames.push(varName);
}
lines.push(...imports);
lines.push(``);
lines.push(`export type { CsoundReference }`);
lines.push(``);
lines.push(`export const allCsoundReferences: CsoundReference[] = [`);
for (const varName of varNames) {
lines.push(` ...${varName},`);
}
lines.push(`]`);
lines.push(``);
lines.push(`export const csoundReferenceMap = new Map<string, CsoundReference>()`);
lines.push(`allCsoundReferences.forEach(ref => {`);
lines.push(` csoundReferenceMap.set(ref.name, ref)`);
lines.push(`})`);
lines.push(``);
lines.push(`export function getCsoundReference(name: string): CsoundReference | undefined {`);
lines.push(` return csoundReferenceMap.get(name)`);
lines.push(`}`);
lines.push(``);
lines.push(`export function getCsoundReferencesByCategory(category: string): CsoundReference[] {`);
lines.push(` return allCsoundReferences.filter(ref => ref.category === category)`);
lines.push(`}`);
lines.push(``);
lines.push(`export {`);
for (const varName of varNames) {
lines.push(` ${varName},`);
}
lines.push(`}`);
lines.push(``);
return lines.join('\n');
}
writeCategoryFiles(categories: Map<string, CsoundReference[]>): void {
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}
let fileCount = 0;
for (const [category, references] of categories) {
const fileName = this.sanitizeCategoryName(category);
const filePath = path.join(this.outputDir, `${fileName}.ts`);
const content = this.generateCategoryFile(category, references, fileName);
fs.writeFileSync(filePath, content, 'utf-8');
fileCount++;
console.log(`Generated: ${fileName}.ts (${references.length} opcodes)`);
}
console.log(`\nGenerated ${fileCount} category files`);
}
writeMainFile(categories: Map<string, CsoundReference[]>): void {
const filePath = path.join(this.outputDir, 'csoundReference.ts');
const content = this.generateMainFile(categories);
fs.writeFileSync(filePath, content, 'utf-8');
console.log(`Generated: csoundReference.ts`);
}
copyTypes(): void {
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}
const sourceTypes = path.join(__dirname, 'types.ts');
const destTypes = path.join(this.outputDir, 'types.ts');
if (fs.existsSync(sourceTypes)) {
fs.copyFileSync(sourceTypes, destTypes);
console.log(`Copied: types.ts`);
}
}
generateAll(categories: Map<string, CsoundReference[]>): void {
console.log(`\nGenerating TypeScript files...`);
this.copyTypes();
this.writeCategoryFiles(categories);
this.writeMainFile(categories);
console.log(`\nDone!`);
}
}

View File

@ -0,0 +1,92 @@
#!/usr/bin/env node
import * as path from 'path';
import { CsoundManualParser } from './parser';
import { TypeScriptGenerator } from './generator';
import { GitHubDownloader } from './downloader';
const DOWNLOAD_DIR = path.join(__dirname, 'downloaded-opcodes');
const OUTPUT_DIR = path.join(__dirname, '../../src/lib/csound-reference');
async function main() {
const args = process.argv.slice(2);
const shouldDownload = args.includes('--download') || args.includes('-d');
const inputDir = args.find(arg => arg.startsWith('--input='))?.split('=')[1] || DOWNLOAD_DIR;
const outputDir = args.find(arg => arg.startsWith('--output='))?.split('=')[1] || OUTPUT_DIR;
console.log('='.repeat(60));
console.log('Csound Manual Parser');
console.log('='.repeat(60));
if (shouldDownload) {
console.log('\nStep 1: Downloading markdown files from GitHub...');
const downloader = new GitHubDownloader(DOWNLOAD_DIR);
try {
await downloader.downloadOpcodes();
await downloader.downloadCategories();
} catch (err) {
console.error('Download failed:', err);
console.log('\nYou can also manually clone the repository:');
console.log(' git clone https://github.com/csound/manual.git');
console.log(' Then run: npm run parse -- --input=manual/docs/opcodes');
process.exit(1);
}
} else {
console.log(`\nUsing existing files from: ${inputDir}`);
console.log('(Use --download or -d to download fresh files from GitHub)');
}
console.log('\nStep 2: Parsing markdown files...');
const parser = new CsoundManualParser();
try {
parser.parseDirectory(inputDir);
} catch (err) {
console.error('Parsing failed:', err);
console.log('\nMake sure the input directory exists and contains .md files');
console.log(`Input directory: ${inputDir}`);
process.exit(1);
}
const opcodeCount = parser.getOpcodes().length;
console.log(`\nParsed ${opcodeCount} opcodes`);
if (opcodeCount === 0) {
console.error('No opcodes found. Check the input directory.');
process.exit(1);
}
console.log('\nStep 3: Grouping by category...');
const categoriesMap = parser.getReferencesByCategory();
console.log(`Found ${categoriesMap.size} categories`);
const topCategories = Array.from(categoriesMap.entries())
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 10);
console.log('\nTop 10 categories by opcode count:');
for (const [category, refs] of topCategories) {
console.log(` - ${category}: ${refs.length} opcodes`);
}
console.log('\nStep 4: Generating TypeScript files...');
const generator = new TypeScriptGenerator(outputDir);
generator.generateAll(categoriesMap);
console.log('\n' + '='.repeat(60));
console.log('Success!');
console.log('='.repeat(60));
console.log(`\nGenerated files in: ${outputDir}`);
console.log(`Total opcodes: ${opcodeCount}`);
console.log(`Total categories: ${categoriesMap.size}`);
console.log('\nYou can now import the reference in your application:');
console.log(` import { allCsoundReferences, getCsoundReference } from './lib/csound-reference/csoundReference'`);
}
if (require.main === module) {
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
}

View File

@ -0,0 +1,26 @@
{
"name": "csound-manual-parser",
"version": "1.0.0",
"description": "Parser for Csound manual to generate TypeScript reference files",
"main": "index.ts",
"scripts": {
"parse": "tsx index.ts",
"download": "tsx index.ts --download",
"build": "tsc"
},
"keywords": [
"csound",
"parser",
"documentation"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"fuse.js": "^7.1.0"
}
}

View File

@ -0,0 +1,245 @@
import * as fs from 'fs';
import * as path from 'path';
import type { ParsedOpcode, CsoundReference } from './types';
interface FrontMatter {
id?: string;
category?: string;
}
export class CsoundManualParser {
private opcodes: Map<string, ParsedOpcode> = new Map();
parseFrontMatter(content: string): FrontMatter {
const frontMatterMatch = content.match(/^<!--\s*\n([\s\S]*?)\n-->/);
if (!frontMatterMatch) return {};
const frontMatter: FrontMatter = {};
const lines = frontMatterMatch[1].split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length > 0) {
const trimmedKey = key.trim();
const value = valueParts.join(':').trim();
if (trimmedKey === 'id') frontMatter.id = value;
if (trimmedKey === 'category') frontMatter.category = value;
}
}
return frontMatter;
}
extractFirstParagraph(content: string): string {
const withoutFrontMatter = content.replace(/^<!--[\s\S]*?-->\n/, '');
const withoutTitle = withoutFrontMatter.replace(/^#\s+.*\n/, '');
const lines = withoutTitle.split('\n');
const paragraphLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '') {
if (paragraphLines.length > 0) break;
continue;
}
if (trimmed.startsWith('#')) break;
paragraphLines.push(trimmed);
}
return paragraphLines.join(' ').trim();
}
extractSection(content: string, sectionTitle: string): string {
const regex = new RegExp(`##\\s+${sectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n##(?!#)|$)`, 'i');
const match = content.match(regex);
return match ? match[1].trim() : '';
}
extractSyntax(content: string): { modern?: string; classic?: string } {
const syntaxSection = this.extractSection(content, 'Syntax');
if (!syntaxSection) return {};
const modernMatch = syntaxSection.match(/===\s*"Modern"[\s\S]*?```[\s\S]*?\n([\s\S]*?)```/);
const classicMatch = syntaxSection.match(/===\s*"Classic"[\s\S]*?```[\s\S]*?\n([\s\S]*?)```/);
return {
modern: modernMatch ? modernMatch[1].trim() : undefined,
classic: classicMatch ? classicMatch[1].trim() : undefined,
};
}
extractParameters(content: string, subsectionTitle: string): { name: string; description: string }[] {
const syntaxSection = this.extractSection(content, 'Syntax');
const subsectionRegex = new RegExp(`###\\s+${subsectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n###|\\n##|$)`, 'i');
const match = syntaxSection.match(subsectionRegex);
if (!match) return [];
const subsectionContent = match[1];
const params: { name: string; description: string }[] = [];
const paramMatches = subsectionContent.matchAll(/_([a-zA-Z0-9_,\s/]+)_\s*[-–—]+\s*([^\n]+(?:\n(?!_)[^\n]+)*)/g);
for (const paramMatch of paramMatches) {
const names = paramMatch[1]
.split(/[,/]/)
.map(n => n.trim().replace(/^_+|_+$/g, ''))
.filter(n => n.length > 0);
const description = paramMatch[2].trim().replace(/\s+/g, ' ');
for (const name of names) {
params.push({ name, description });
}
}
return params;
}
extractExample(content: string): string | undefined {
const exampleSection = this.extractSection(content, 'Examples');
if (!exampleSection) return undefined;
const codeMatch = exampleSection.match(/```[\s\S]*?\n([\s\S]*?)```/);
return codeMatch ? codeMatch[1].trim() : undefined;
}
extractSeeAlso(content: string): string[] {
const seeAlsoSection = this.extractSection(content, 'See also');
if (!seeAlsoSection) return [];
const links = seeAlsoSection.matchAll(/\[([^\]]+)\]/g);
return Array.from(links, match => match[1]);
}
parseMarkdownFile(filePath: string, content: string): ParsedOpcode | null {
try {
const frontMatter = this.parseFrontMatter(content);
if (!frontMatter.id || !frontMatter.category) {
console.warn(`Skipping ${filePath}: missing id or category`);
return null;
}
const description = this.extractFirstParagraph(content);
const syntax = this.extractSyntax(content);
const initParams = this.extractParameters(content, 'Initialization');
const perfParams = this.extractParameters(content, 'Performance');
const example = this.extractExample(content);
const seeAlso = this.extractSeeAlso(content);
return {
id: frontMatter.id,
category: frontMatter.category,
title: frontMatter.id,
description,
syntaxModern: syntax.modern,
syntaxClassic: syntax.classic,
initParams,
perfParams,
example,
seeAlso,
rawContent: content,
};
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);
return null;
}
}
parseDirectory(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
throw new Error(`Directory not found: ${dirPath}`);
}
const files = fs.readdirSync(dirPath);
let parsed = 0;
let skipped = 0;
for (const file of files) {
if (!file.endsWith('.md')) continue;
const filePath = path.join(dirPath, file);
const content = fs.readFileSync(filePath, 'utf-8');
const opcode = this.parseMarkdownFile(file, content);
if (opcode) {
this.opcodes.set(opcode.id, opcode);
parsed++;
} else {
skipped++;
}
}
console.log(`Parsed ${parsed} opcodes, skipped ${skipped}`);
}
convertToReference(opcode: ParsedOpcode): CsoundReference {
const syntax = opcode.syntaxModern || opcode.syntaxClassic;
const parameters = [
...opcode.initParams.map(p => ({ ...p, type: 'initialization' as const })),
...opcode.perfParams.map(p => ({ ...p, type: 'performance' as const })),
];
const rates = this.extractRates(syntax || '');
return {
name: opcode.id,
type: 'opcode',
category: opcode.category,
description: opcode.description,
syntax,
example: opcode.example,
rates,
parameters: parameters.length > 0 ? parameters : undefined,
seeAlso: opcode.seeAlso.length > 0 ? opcode.seeAlso : undefined,
};
}
private extractRates(syntax: string): string[] {
const rates: Set<string> = new Set();
if (/\bares\b|\basin\b|\basig\b/.test(syntax)) rates.add('a-rate');
if (/\bkres\b|\bkamp\b|\bkcps\b|\bkin\b/.test(syntax)) rates.add('k-rate');
if (/\bires\b|\bifn\b|\biphs\b/.test(syntax)) rates.add('i-rate');
return Array.from(rates);
}
getOpcodes(): ParsedOpcode[] {
return Array.from(this.opcodes.values());
}
getOpcodesByCategory(): Map<string, ParsedOpcode[]> {
const categories = new Map<string, ParsedOpcode[]>();
for (const opcode of this.opcodes.values()) {
const category = opcode.category;
if (!categories.has(category)) {
categories.set(category, []);
}
categories.get(category)!.push(opcode);
}
return categories;
}
getReferences(): CsoundReference[] {
return this.getOpcodes().map(op => this.convertToReference(op));
}
getReferencesByCategory(): Map<string, CsoundReference[]> {
const categories = new Map<string, CsoundReference[]>();
for (const opcode of this.opcodes.values()) {
const category = opcode.category;
if (!categories.has(category)) {
categories.set(category, []);
}
categories.get(category)!.push(this.convertToReference(opcode));
}
return categories;
}
}

352
scripts/csound-parser/pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,352 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
fuse.js:
specifier: ^7.1.0
version: 7.1.0
devDependencies:
'@types/node':
specifier: ^20.0.0
version: 20.19.21
tsx:
specifier: ^4.0.0
version: 4.20.6
typescript:
specifier: ^5.0.0
version: 5.9.3
packages:
'@esbuild/aix-ppc64@0.25.11':
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.11':
resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.11':
resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.11':
resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.11':
resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.11':
resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.11':
resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.11':
resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.11':
resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.11':
resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.11':
resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.11':
resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.11':
resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.11':
resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.11':
resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.11':
resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.11':
resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.11':
resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.11':
resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.11':
resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.11':
resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.11':
resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.11':
resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.11':
resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.11':
resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.11':
resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@types/node@20.19.21':
resolution: {integrity: sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==}
esbuild@0.25.11:
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fuse.js@7.1.0:
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
engines: {node: '>=10'}
get-tsconfig@4.12.0:
resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
tsx@4.20.6:
resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
engines: {node: '>=18.0.0'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@esbuild/aix-ppc64@0.25.11':
optional: true
'@esbuild/android-arm64@0.25.11':
optional: true
'@esbuild/android-arm@0.25.11':
optional: true
'@esbuild/android-x64@0.25.11':
optional: true
'@esbuild/darwin-arm64@0.25.11':
optional: true
'@esbuild/darwin-x64@0.25.11':
optional: true
'@esbuild/freebsd-arm64@0.25.11':
optional: true
'@esbuild/freebsd-x64@0.25.11':
optional: true
'@esbuild/linux-arm64@0.25.11':
optional: true
'@esbuild/linux-arm@0.25.11':
optional: true
'@esbuild/linux-ia32@0.25.11':
optional: true
'@esbuild/linux-loong64@0.25.11':
optional: true
'@esbuild/linux-mips64el@0.25.11':
optional: true
'@esbuild/linux-ppc64@0.25.11':
optional: true
'@esbuild/linux-riscv64@0.25.11':
optional: true
'@esbuild/linux-s390x@0.25.11':
optional: true
'@esbuild/linux-x64@0.25.11':
optional: true
'@esbuild/netbsd-arm64@0.25.11':
optional: true
'@esbuild/netbsd-x64@0.25.11':
optional: true
'@esbuild/openbsd-arm64@0.25.11':
optional: true
'@esbuild/openbsd-x64@0.25.11':
optional: true
'@esbuild/openharmony-arm64@0.25.11':
optional: true
'@esbuild/sunos-x64@0.25.11':
optional: true
'@esbuild/win32-arm64@0.25.11':
optional: true
'@esbuild/win32-ia32@0.25.11':
optional: true
'@esbuild/win32-x64@0.25.11':
optional: true
'@types/node@20.19.21':
dependencies:
undici-types: 6.21.0
esbuild@0.25.11:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.11
'@esbuild/android-arm': 0.25.11
'@esbuild/android-arm64': 0.25.11
'@esbuild/android-x64': 0.25.11
'@esbuild/darwin-arm64': 0.25.11
'@esbuild/darwin-x64': 0.25.11
'@esbuild/freebsd-arm64': 0.25.11
'@esbuild/freebsd-x64': 0.25.11
'@esbuild/linux-arm': 0.25.11
'@esbuild/linux-arm64': 0.25.11
'@esbuild/linux-ia32': 0.25.11
'@esbuild/linux-loong64': 0.25.11
'@esbuild/linux-mips64el': 0.25.11
'@esbuild/linux-ppc64': 0.25.11
'@esbuild/linux-riscv64': 0.25.11
'@esbuild/linux-s390x': 0.25.11
'@esbuild/linux-x64': 0.25.11
'@esbuild/netbsd-arm64': 0.25.11
'@esbuild/netbsd-x64': 0.25.11
'@esbuild/openbsd-arm64': 0.25.11
'@esbuild/openbsd-x64': 0.25.11
'@esbuild/openharmony-arm64': 0.25.11
'@esbuild/sunos-x64': 0.25.11
'@esbuild/win32-arm64': 0.25.11
'@esbuild/win32-ia32': 0.25.11
'@esbuild/win32-x64': 0.25.11
fsevents@2.3.3:
optional: true
fuse.js@7.1.0: {}
get-tsconfig@4.12.0:
dependencies:
resolve-pkg-maps: 1.0.0
resolve-pkg-maps@1.0.0: {}
tsx@4.20.6:
dependencies:
esbuild: 0.25.11
get-tsconfig: 4.12.0
optionalDependencies:
fsevents: 2.3.3
typescript@5.9.3: {}
undici-types@6.21.0: {}

View File

@ -0,0 +1,71 @@
import {
allCsoundReferences,
getCsoundReference,
getCsoundReferencesByCategory
} from '../../src/lib/csound-reference/csoundReference';
console.log('='.repeat(60));
console.log('Csound Reference Output Test');
console.log('='.repeat(60));
console.log(`\nTotal opcodes: ${allCsoundReferences.length}`);
console.log('\n--- Testing getCsoundReference() ---');
const oscil = getCsoundReference('oscil');
if (oscil) {
console.log(`\nFound oscil:`);
console.log(` Name: ${oscil.name}`);
console.log(` Category: ${oscil.category}`);
console.log(` Description: ${oscil.description.substring(0, 80)}...`);
console.log(` Syntax: ${oscil.syntax?.substring(0, 80)}...`);
console.log(` Rates: ${oscil.rates?.join(', ')}`);
console.log(` Parameters: ${oscil.parameters?.length || 0}`);
}
const moogladder = getCsoundReference('moogladder');
if (moogladder) {
console.log(`\nFound moogladder:`);
console.log(` Name: ${moogladder.name}`);
console.log(` Category: ${moogladder.category}`);
console.log(` Description: ${moogladder.description}`);
console.log(` Syntax: ${moogladder.syntax?.substring(0, 100)}...`);
console.log(` Rates: ${moogladder.rates?.join(', ')}`);
console.log(` Parameters: ${moogladder.parameters?.length || 0}`);
if (moogladder.parameters) {
console.log(` Parameters detail:`);
moogladder.parameters.forEach(p => {
console.log(` - ${p.name} (${p.type}): ${p.description.substring(0, 60)}...`);
});
}
}
console.log('\n--- Testing getCsoundReferencesByCategory() ---');
const oscillators = getCsoundReferencesByCategory('Signal Generators:Basic Oscillators');
console.log(`\nBasic Oscillators (${oscillators.length} opcodes):`);
oscillators.slice(0, 5).forEach(op => {
console.log(` - ${op.name}: ${op.description.substring(0, 60)}...`);
});
console.log('\n--- Sample categories ---');
const categories = new Set(allCsoundReferences.map(ref => ref.category));
const categoryArray = Array.from(categories).sort();
console.log(`\nTotal categories: ${categories.size}`);
console.log('First 10 categories:');
categoryArray.slice(0, 10).forEach(cat => {
const count = allCsoundReferences.filter(r => r.category === cat).length;
console.log(` - ${cat} (${count} opcodes)`);
});
console.log('\n--- Checking for opcodes with parameters ---');
const opcodesWithParams = allCsoundReferences.filter(ref => ref.parameters && ref.parameters.length > 0);
console.log(`\nOpcodes with parameters: ${opcodesWithParams.length} out of ${allCsoundReferences.length}`);
if (opcodesWithParams.length > 0) {
console.log('\nFirst 5 opcodes with parameters:');
opcodesWithParams.slice(0, 5).forEach(op => {
console.log(` - ${op.name}: ${op.parameters?.length} parameters`);
});
}
console.log('\n' + '='.repeat(60));
console.log('Test Complete!');
console.log('='.repeat(60));

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"types": ["node"]
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,35 @@
export interface CsoundReference {
name: string
type: 'opcode' | 'keyword' | 'header' | 'constant'
category: string
description: string
syntax?: string
example?: string
rates?: string[]
parameters?: {
name: string
description: string
type: 'initialization' | 'performance'
}[]
seeAlso?: string[]
}
export interface ParsedOpcode {
id: string
category: string
title: string
description: string
syntaxModern?: string
syntaxClassic?: string
initParams: { name: string; description: string }[]
perfParams: { name: string; description: string }[]
example?: string
seeAlso: string[]
rawContent: string
}
export interface CategoryGroup {
categoryKey: string
categoryName: string
opcodes: CsoundReference[]
}