feat: add Node.js/TypeScript integration with clean function interface
- Add SorobanGenerator class with python-shell bridge
- No CLI arguments - clean TypeScript function calls
- Support for Express.js and Next.js integration
- Include comprehensive API documentation
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a8a01a8db3
commit
fb1b0470cf
|
|
@ -0,0 +1,259 @@
|
|||
# Soroban Flashcard Generator - Node.js API Documentation
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install python-shell
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { SorobanGenerator } from './soroban-generator-bridge';
|
||||
|
||||
const generator = new SorobanGenerator();
|
||||
const result = await generator.generate({
|
||||
range: '0-99',
|
||||
cardsPerPage: 6
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SorobanGenerator`
|
||||
|
||||
The main class for generating flashcards from Node.js/TypeScript.
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
new SorobanGenerator(projectRoot?: string)
|
||||
```
|
||||
|
||||
- `projectRoot` (optional): Path to the soroban-abacus-flashcards directory. Defaults to `../../` from the module location.
|
||||
|
||||
#### Methods
|
||||
|
||||
##### `generate(config: FlashcardConfig): Promise<FlashcardResult>`
|
||||
|
||||
Generate flashcards with the specified configuration.
|
||||
|
||||
**Parameters:**
|
||||
- `config`: Configuration object (see FlashcardConfig below)
|
||||
|
||||
**Returns:** Promise resolving to:
|
||||
```typescript
|
||||
{
|
||||
pdf: string; // Base64 encoded PDF
|
||||
count: number; // Number of flashcards generated
|
||||
numbers: number[]; // Array of numbers (limited to first 100)
|
||||
}
|
||||
```
|
||||
|
||||
##### `generateBuffer(config: FlashcardConfig): Promise<Buffer>`
|
||||
|
||||
Generate flashcards and return as Node.js Buffer.
|
||||
|
||||
**Parameters:**
|
||||
- `config`: Configuration object
|
||||
|
||||
**Returns:** Promise resolving to Buffer containing PDF data
|
||||
|
||||
##### `initialize(): Promise<void>`
|
||||
|
||||
Initialize a persistent Python process for better performance when generating multiple PDFs.
|
||||
|
||||
##### `close(): Promise<void>`
|
||||
|
||||
Clean up the persistent Python process.
|
||||
|
||||
### Configuration Interface
|
||||
|
||||
```typescript
|
||||
interface FlashcardConfig {
|
||||
// Required
|
||||
range: string; // e.g., "0-99" or "1,2,5,10"
|
||||
|
||||
// Optional
|
||||
step?: number; // Increment (default: 1)
|
||||
cardsPerPage?: number; // 1-30+ (default: 6)
|
||||
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
|
||||
orientation?: 'portrait' | 'landscape';
|
||||
margins?: {
|
||||
top?: string; // e.g., "0.5in"
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
};
|
||||
gutter?: string; // Space between cards (default: "5mm")
|
||||
shuffle?: boolean; // Randomize order
|
||||
seed?: number; // Random seed for deterministic shuffle
|
||||
showCutMarks?: boolean; // Show cutting guides
|
||||
showRegistration?: boolean; // Show alignment marks
|
||||
fontFamily?: string; // Font name (default: "DejaVu Sans")
|
||||
fontSize?: string; // Font size (default: "48pt")
|
||||
columns?: string | number; // "auto" or specific number
|
||||
showEmptyColumns?: boolean;
|
||||
hideInactiveBeads?: boolean;
|
||||
beadShape?: 'diamond' | 'circle' | 'square';
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
|
||||
coloredNumerals?: boolean; // Color numerals to match beads
|
||||
scaleFactor?: number; // 0.1 to 1.0 (default: 0.9)
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Generation
|
||||
|
||||
```typescript
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// Simple 0-9 flashcards
|
||||
const result = await generator.generate({
|
||||
range: '0-9'
|
||||
});
|
||||
```
|
||||
|
||||
### Skip Counting
|
||||
|
||||
```typescript
|
||||
// Count by 5s from 0 to 100
|
||||
const result = await generator.generate({
|
||||
range: '0-100',
|
||||
step: 5,
|
||||
cardsPerPage: 12
|
||||
});
|
||||
```
|
||||
|
||||
### Educational Colors
|
||||
|
||||
```typescript
|
||||
// Place-value coloring for learning
|
||||
const result = await generator.generate({
|
||||
range: '0-999',
|
||||
colorScheme: 'place-value',
|
||||
coloredNumerals: true,
|
||||
showCutMarks: true
|
||||
});
|
||||
```
|
||||
|
||||
### Express.js Route
|
||||
|
||||
```typescript
|
||||
app.post('/api/flashcards', async (req, res) => {
|
||||
try {
|
||||
const generator = new SorobanGenerator();
|
||||
const config = {
|
||||
range: req.body.range || '0-9',
|
||||
cardsPerPage: req.body.cardsPerPage || 6,
|
||||
colorScheme: req.body.colorScheme || 'monochrome',
|
||||
...req.body
|
||||
};
|
||||
|
||||
const result = await generator.generate(config);
|
||||
|
||||
if (req.query.format === 'json') {
|
||||
// Return metadata
|
||||
res.json({
|
||||
count: result.count,
|
||||
numbers: result.numbers
|
||||
});
|
||||
} else {
|
||||
// Return PDF
|
||||
const pdfBuffer = Buffer.from(result.pdf, 'base64');
|
||||
res.contentType('application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=flashcards.pdf');
|
||||
res.send(pdfBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Next.js API Route
|
||||
|
||||
```typescript
|
||||
// pages/api/flashcards.ts
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { SorobanGenerator } from '@/lib/soroban-generator-bridge';
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const generator = new SorobanGenerator();
|
||||
const result = await generator.generate(req.body);
|
||||
const pdfBuffer = Buffer.from(result.pdf, 'base64');
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=flashcards.pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
For generating multiple PDFs, use persistent mode:
|
||||
|
||||
```typescript
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// Initialize once
|
||||
await generator.initialize();
|
||||
|
||||
// Generate multiple PDFs quickly
|
||||
for (const config of configs) {
|
||||
const result = await generator.generate(config);
|
||||
// Process result...
|
||||
}
|
||||
|
||||
// Clean up when done
|
||||
await generator.close();
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 14+
|
||||
- Python 3.8+
|
||||
- Typst (installed via `brew install typst`)
|
||||
- qpdf (optional, for PDF optimization)
|
||||
|
||||
## Error Handling
|
||||
|
||||
The generator will throw errors for:
|
||||
- Missing Python installation
|
||||
- Missing Typst installation
|
||||
- Invalid configuration
|
||||
- Typst compilation errors
|
||||
|
||||
Always wrap calls in try/catch blocks:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await generator.generate(config);
|
||||
} catch (error) {
|
||||
console.error('Generation failed:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
All interfaces and types are included in the module. Import them as needed:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
SorobanGenerator,
|
||||
FlashcardConfig,
|
||||
FlashcardResult
|
||||
} from './soroban-generator-bridge';
|
||||
```
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Example TypeScript server using the Soroban generator
|
||||
* This shows how to call the PDF generation directly from TypeScript
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { SorobanGenerator } from './soroban-generator';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Initialize the generator once
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// Direct usage - just call the function from TypeScript
|
||||
app.post('/api/generate-pdf', async (req, res) => {
|
||||
try {
|
||||
// Call the generator directly from TypeScript
|
||||
const pdfBuffer = await generator.generate({
|
||||
range: req.body.range || '0-9',
|
||||
cardsPerPage: req.body.cardsPerPage || 6,
|
||||
colorScheme: req.body.colorScheme || 'monochrome',
|
||||
showCutMarks: req.body.showCutMarks || false,
|
||||
// ... other config options
|
||||
});
|
||||
|
||||
// Send PDF directly to client
|
||||
res.contentType('application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=flashcards.pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Example: Generate specific sets programmatically
|
||||
app.get('/api/presets/:preset', async (req, res) => {
|
||||
try {
|
||||
let config;
|
||||
|
||||
switch (req.params.preset) {
|
||||
case 'basic':
|
||||
config = { range: '0-9' };
|
||||
break;
|
||||
case 'counting-by-5':
|
||||
config = { range: '0-100', step: 5 };
|
||||
break;
|
||||
case 'place-value':
|
||||
config = {
|
||||
range: '0-999',
|
||||
colorScheme: 'place-value' as const,
|
||||
coloredNumerals: true
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return res.status(404).json({ error: 'Unknown preset' });
|
||||
}
|
||||
|
||||
// Direct function call from TypeScript
|
||||
const pdfBuffer = await generator.generate(config);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Simple usage in any async function
|
||||
async function generateFlashcardsDirectly() {
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// Just call it like any TypeScript function
|
||||
const pdfBuffer = await generator.generate({
|
||||
range: '0-99',
|
||||
cardsPerPage: 6,
|
||||
colorScheme: 'place-value',
|
||||
coloredNumerals: true
|
||||
});
|
||||
|
||||
// Now you have the PDF as a Buffer, use it however you want
|
||||
return pdfBuffer;
|
||||
}
|
||||
|
||||
// Next.js API route example
|
||||
export async function nextJsApiRoute(req: any, res: any) {
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// Direct call from TypeScript
|
||||
const pdf = await generator.generate(req.body);
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.send(pdf);
|
||||
}
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log('Server running on http://localhost:3000');
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "soroban-flashcards-node",
|
||||
"version": "1.0.0",
|
||||
"description": "Node.js TypeScript interface for Soroban Flashcard Generator",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "ts-node-dev --respawn example-server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"python-shell": "^5.0.0",
|
||||
"express": "^4.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/express": "^4.17.0",
|
||||
"typescript": "^5.0.0",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* TypeScript wrapper using python-shell for clean function interface
|
||||
* No CLI arguments - just function calls with objects
|
||||
*/
|
||||
|
||||
import { PythonShell } from 'python-shell';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface FlashcardConfig {
|
||||
range: string;
|
||||
step?: number;
|
||||
cardsPerPage?: number;
|
||||
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
|
||||
orientation?: 'portrait' | 'landscape';
|
||||
margins?: {
|
||||
top?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
};
|
||||
gutter?: string;
|
||||
shuffle?: boolean;
|
||||
seed?: number;
|
||||
showCutMarks?: boolean;
|
||||
showRegistration?: boolean;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
columns?: string | number;
|
||||
showEmptyColumns?: boolean;
|
||||
hideInactiveBeads?: boolean;
|
||||
beadShape?: 'diamond' | 'circle' | 'square';
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
|
||||
coloredNumerals?: boolean;
|
||||
scaleFactor?: number;
|
||||
}
|
||||
|
||||
export interface FlashcardResult {
|
||||
pdf: string; // base64 encoded
|
||||
count: number;
|
||||
numbers: number[];
|
||||
}
|
||||
|
||||
export class SorobanGenerator {
|
||||
private pythonShell: PythonShell | null = null;
|
||||
private projectRoot: string;
|
||||
|
||||
constructor(projectRoot?: string) {
|
||||
this.projectRoot = projectRoot || path.join(__dirname, '../../');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize persistent Python process for better performance
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.pythonShell) return;
|
||||
|
||||
this.pythonShell = new PythonShell(
|
||||
path.join(this.projectRoot, 'src', 'bridge.py'),
|
||||
{
|
||||
mode: 'json',
|
||||
pythonPath: 'python3',
|
||||
pythonOptions: ['-u'], // Unbuffered
|
||||
scriptPath: this.projectRoot,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate flashcards - clean function interface
|
||||
*/
|
||||
async generate(config: FlashcardConfig): Promise<FlashcardResult> {
|
||||
// One-shot mode if not initialized
|
||||
if (!this.pythonShell) {
|
||||
return new Promise((resolve, reject) => {
|
||||
PythonShell.run(
|
||||
path.join(this.projectRoot, 'src', 'bridge.py'),
|
||||
{
|
||||
mode: 'json',
|
||||
pythonPath: 'python3',
|
||||
scriptPath: this.projectRoot,
|
||||
args: [],
|
||||
},
|
||||
(err, results) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (results && results[0]) {
|
||||
const result = results[0] as any;
|
||||
if (result.error) {
|
||||
reject(new Error(result.error));
|
||||
} else {
|
||||
resolve(result as FlashcardResult);
|
||||
}
|
||||
} else {
|
||||
reject(new Error('No result from Python'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Send config as JSON
|
||||
PythonShell.defaultOptions = {};
|
||||
const shell = new PythonShell(
|
||||
path.join(this.projectRoot, 'src', 'bridge.py'),
|
||||
{
|
||||
mode: 'json',
|
||||
pythonPath: 'python3',
|
||||
scriptPath: this.projectRoot,
|
||||
}
|
||||
);
|
||||
|
||||
shell.send(config);
|
||||
shell.end((err, code, signal) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Persistent mode
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.pythonShell) {
|
||||
reject(new Error('Not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = (message: any) => {
|
||||
if (message.error) {
|
||||
reject(new Error(message.error));
|
||||
} else {
|
||||
resolve(message as FlashcardResult);
|
||||
}
|
||||
this.pythonShell?.removeListener('message', handler);
|
||||
};
|
||||
|
||||
this.pythonShell.on('message', handler);
|
||||
this.pythonShell.send(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and return as Buffer
|
||||
*/
|
||||
async generateBuffer(config: FlashcardConfig): Promise<Buffer> {
|
||||
const result = await this.generate(config);
|
||||
return Buffer.from(result.pdf, 'base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up Python process
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.pythonShell) {
|
||||
this.pythonShell.end(() => {});
|
||||
this.pythonShell = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage - just like calling a regular TypeScript function
|
||||
async function example() {
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// Just call it like a function!
|
||||
const result = await generator.generate({
|
||||
range: '0-99',
|
||||
cardsPerPage: 6,
|
||||
colorScheme: 'place-value',
|
||||
coloredNumerals: true,
|
||||
showCutMarks: true
|
||||
});
|
||||
|
||||
// You get back a clean result object
|
||||
console.log(`Generated ${result.count} flashcards`);
|
||||
|
||||
// Convert to Buffer if needed
|
||||
const pdfBuffer = Buffer.from(result.pdf, 'base64');
|
||||
|
||||
// Or use persistent mode for better performance
|
||||
await generator.initialize();
|
||||
|
||||
// Now calls are faster
|
||||
const result2 = await generator.generate({ range: '0-9' });
|
||||
const result3 = await generator.generate({ range: '10-19' });
|
||||
|
||||
await generator.close();
|
||||
}
|
||||
|
||||
// Express example - clean function calls
|
||||
export function expressRoute(app: any) {
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
app.post('/api/flashcards', async (req, res) => {
|
||||
try {
|
||||
// Just pass the config object directly!
|
||||
const result = await generator.generate(req.body);
|
||||
|
||||
// Send back JSON or PDF
|
||||
if (req.query.format === 'json') {
|
||||
res.json(result);
|
||||
} else {
|
||||
const pdfBuffer = Buffer.from(result.pdf, 'base64');
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Node.js TypeScript wrapper for Soroban Flashcard Generator
|
||||
* Calls Python functions via child_process
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
|
||||
export interface FlashcardConfig {
|
||||
range: string;
|
||||
step?: number;
|
||||
cardsPerPage?: number;
|
||||
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5';
|
||||
orientation?: 'portrait' | 'landscape';
|
||||
margins?: {
|
||||
top?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
};
|
||||
gutter?: string;
|
||||
shuffle?: boolean;
|
||||
seed?: number;
|
||||
showCutMarks?: boolean;
|
||||
showRegistration?: boolean;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
columns?: string | number;
|
||||
showEmptyColumns?: boolean;
|
||||
hideInactiveBeads?: boolean;
|
||||
beadShape?: 'diamond' | 'circle' | 'square';
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating';
|
||||
coloredNumerals?: boolean;
|
||||
scaleFactor?: number;
|
||||
}
|
||||
|
||||
export class SorobanGenerator {
|
||||
private pythonPath: string;
|
||||
private generatorPath: string;
|
||||
private projectRoot: string;
|
||||
|
||||
constructor(projectRoot?: string) {
|
||||
// Find Python executable
|
||||
this.pythonPath = this.findPython();
|
||||
|
||||
// Set project root (where the Python scripts are)
|
||||
this.projectRoot = projectRoot || path.join(__dirname, '../../');
|
||||
this.generatorPath = path.join(this.projectRoot, 'src', 'generate.py');
|
||||
}
|
||||
|
||||
private findPython(): string {
|
||||
// Try common Python commands
|
||||
const pythonCommands = ['python3', 'python'];
|
||||
|
||||
for (const cmd of pythonCommands) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, { encoding: 'utf8' });
|
||||
if (version.includes('Python 3')) {
|
||||
return cmd;
|
||||
}
|
||||
} catch {
|
||||
// Continue to next command
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Python 3 not found. Please install Python 3.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate flashcards and return PDF as Buffer
|
||||
*/
|
||||
async generate(config: FlashcardConfig): Promise<Buffer> {
|
||||
// Create temp output file
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'soroban-'));
|
||||
const outputPath = path.join(tempDir, 'flashcards.pdf');
|
||||
|
||||
try {
|
||||
// Build command line arguments
|
||||
const args = [
|
||||
this.generatorPath,
|
||||
'--output', outputPath,
|
||||
'--range', config.range,
|
||||
];
|
||||
|
||||
if (config.step) args.push('--step', config.step.toString());
|
||||
if (config.cardsPerPage) args.push('--cards-per-page', config.cardsPerPage.toString());
|
||||
if (config.paperSize) args.push('--paper-size', config.paperSize);
|
||||
if (config.orientation) args.push('--orientation', config.orientation);
|
||||
if (config.gutter) args.push('--gutter', config.gutter);
|
||||
if (config.shuffle) args.push('--shuffle');
|
||||
if (config.seed !== undefined) args.push('--seed', config.seed.toString());
|
||||
if (config.showCutMarks) args.push('--cut-marks');
|
||||
if (config.showRegistration) args.push('--registration');
|
||||
if (config.fontFamily) args.push('--font-family', config.fontFamily);
|
||||
if (config.fontSize) args.push('--font-size', config.fontSize);
|
||||
if (config.columns !== undefined) args.push('--columns', config.columns.toString());
|
||||
if (config.showEmptyColumns) args.push('--show-empty-columns');
|
||||
if (config.hideInactiveBeads) args.push('--hide-inactive-beads');
|
||||
if (config.beadShape) args.push('--bead-shape', config.beadShape);
|
||||
if (config.colorScheme) args.push('--color-scheme', config.colorScheme);
|
||||
if (config.coloredNumerals) args.push('--colored-numerals');
|
||||
if (config.scaleFactor !== undefined) args.push('--scale-factor', config.scaleFactor.toString());
|
||||
|
||||
if (config.margins) {
|
||||
const m = config.margins;
|
||||
const marginStr = `${m.top || '0.5in'},${m.right || '0.5in'},${m.bottom || '0.5in'},${m.left || '0.5in'}`;
|
||||
args.push('--margins', marginStr);
|
||||
}
|
||||
|
||||
// Execute Python script
|
||||
await this.executePython(args);
|
||||
|
||||
// Read generated PDF
|
||||
const pdfBuffer = await fs.readFile(outputPath);
|
||||
return pdfBuffer;
|
||||
|
||||
} finally {
|
||||
// Clean up temp directory
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate flashcards and save to file
|
||||
*/
|
||||
async generateToFile(config: FlashcardConfig, outputPath: string): Promise<void> {
|
||||
const pdfBuffer = await this.generate(config);
|
||||
await fs.writeFile(outputPath, pdfBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate flashcards and return as base64 string
|
||||
*/
|
||||
async generateBase64(config: FlashcardConfig): Promise<string> {
|
||||
const pdfBuffer = await this.generate(config);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
private executePython(args: string[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(this.pythonPath, args, {
|
||||
cwd: this.projectRoot,
|
||||
env: { ...process.env, PYTHONPATH: this.projectRoot }
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Python process failed with code ${code}: ${stderr}`));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', (err) => {
|
||||
reject(new Error(`Failed to start Python process: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all dependencies are installed
|
||||
*/
|
||||
async checkDependencies(): Promise<{ python: boolean; typst: boolean; qpdf: boolean }> {
|
||||
const checks = {
|
||||
python: false,
|
||||
typst: false,
|
||||
qpdf: false
|
||||
};
|
||||
|
||||
try {
|
||||
execSync(`${this.pythonPath} --version`);
|
||||
checks.python = true;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
execSync('typst --version');
|
||||
checks.typst = true;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
execSync('qpdf --version');
|
||||
checks.qpdf = true;
|
||||
} catch {}
|
||||
|
||||
return checks;
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage for Express/Next.js/etc
|
||||
export async function expressExample() {
|
||||
const generator = new SorobanGenerator();
|
||||
|
||||
// In your Express route handler:
|
||||
// app.post('/api/generate', async (req, res) => {
|
||||
// try {
|
||||
// const pdfBuffer = await generator.generate(req.body);
|
||||
// res.contentType('application/pdf');
|
||||
// res.send(pdfBuffer);
|
||||
// } catch (error) {
|
||||
// res.status(500).json({ error: error.message });
|
||||
// }
|
||||
// });
|
||||
}
|
||||
Loading…
Reference in New Issue