UNPKG

genankjs

Version:

generate anki card

488 lines (377 loc) โ€ข 12.3 kB
# GenAnki.js [![npm version](https://badge.fury.io/js/genankjs.svg)](https://badge.fury.io/js/genankjs) [![downloads](https://img.shields.io/npm/dt/genankjs.svg)](https://www.npmjs.com/package/genankjs) [![license](https://img.shields.io/npm/l/genankjs.svg)](https://github.com/your-username/genankjs/blob/main/LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) > A powerful TypeScript library for generating Anki flashcard packages (.apkg files) programmatically, inspired by the Python [genanki](https://github.com/kerrickstaley/genanki) library. ## โœจ Features - ๐Ÿƒ **Create Anki packages** - Generate .apkg files programmatically - ๐ŸŽต **Rich media support** - Audio files (.mp3, .wav, etc.) - ๐Ÿ–ผ๏ธ **Image support** - Images (.jpg, .png, .gif, etc.) - ๐Ÿ“ **Built-in templates** - Basic, Cloze, Image Occlusion, and more - ๐ŸŽจ **Custom styling** - Full CSS customization support - ๐Ÿ“š **Multiple decks** - Organize cards into different decks - ๐Ÿท๏ธ **Tagging system** - Organize and categorize your cards - ๐Ÿ’ช **TypeScript first** - Full type safety and IntelliSense support - ๐Ÿงช **Well tested** - Comprehensive test coverage ## ๐Ÿ“ฆ Installation ```bash npm install genankjs ``` ## ๐Ÿš€ Quick Start ```typescript import { Package, Deck, Note, builtin } from 'genankjs'; // Create a new package const pkg = new Package(); // Create a deck const deck = new Deck({ name: 'Spanish Vocabulary', description: 'Basic Spanish words and phrases', }); // Create notes with the basic model const notes = [ new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: ['Hola', 'Hello'], tags: ['spanish', 'greetings'], }), new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: ['Gracias', 'Thank you'], tags: ['spanish', 'politeness'], }), ]; // Add notes to deck deck.addNotes(notes, builtin.BASIC_MODEL); // Add deck and model to package pkg.addDeck(deck); pkg.addModel(builtin.BASIC_MODEL); // Generate the .apkg file await pkg.writeToFile('spanish.apkg'); console.log('โœ… Package created successfully!'); ``` ## ๐Ÿ“š Table of Contents - [Core Classes](#-core-classes) - [Built-in Models](#-built-in-models) - [Media Support](#-media-support) - [Advanced Examples](#-advanced-examples) - [API Reference](#-api-reference) - [Development](#-development) - [Troubleshooting](#-troubleshooting) ## ๐Ÿ—๏ธ Core Classes ### Package The main container for creating .apkg files. ```typescript import { Package } from 'genankjs'; const pkg = new Package({ media: [], // Optional: array of media files }); // Add decks and models pkg.addDeck(deck); pkg.addModel(model); // Add media files pkg.addMedia({ name: 'audio.mp3', data: audioBuffer, }); // Generate package await pkg.writeToFile('output.apkg'); // or get as buffer const buffer = await pkg.writeToBuffer(); ``` ### Deck Represents a collection of flashcards. ```typescript import { Deck } from 'genankjs'; const deck = new Deck({ name: 'My Deck', description: 'Optional description', deckId: 123456789, // Optional: custom ID }); // Add notes deck.addNote(note, model); // model is optional deck.addNotes([note1, note2], model); // Get statistics console.log(`Notes: ${deck.getNoteCount()}`); console.log(`Cards: ${deck.getCardCount()}`); ``` ### Note Represents a single flashcard note. ```typescript import { Note } from 'genankjs'; const note = new Note({ modelId: model.modelId, fields: ['Front content', 'Back content'], tags: ['tag1', 'tag2'], guid: 'optional-custom-guid', }); // Manipulate fields note.setField(0, 'New front content'); console.log(note.getField(0)); // Manage tags note.addTag('new-tag'); note.removeTag('old-tag'); ``` ### Model Defines the structure and appearance of cards. ```typescript import { Model } from 'genankjs'; const model = new Model({ name: 'Custom Model', fields: [{ name: 'Question' }, { name: 'Answer' }, { name: 'Hint' }], templates: [ { name: 'Card 1', qfmt: '{{Question}}<br><small>{{Hint}}</small>', afmt: '{{FrontSide}}<hr id="answer">{{Answer}}', }, ], css: ` .card { font-family: Arial; font-size: 20px; text-align: center; color: black; background-color: white; } `, }); ``` ## ๐ŸŽฏ Built-in Models The library includes several pre-defined models similar to Anki's built-in note types: | Model | Description | Use Case | | ------------------------------------ | ------------------------------------- | ----------------------------- | | `BASIC_MODEL` | Simple front/back cards | Basic vocabulary, definitions | | `BASIC_AND_REVERSED_CARD_MODEL` | Creates both directions automatically | Language learning | | `BASIC_OPTIONAL_REVERSED_CARD_MODEL` | Optional reverse cards | Flexible vocabulary | | `BASIC_TYPE_IN_THE_ANSWER_MODEL` | Requires typing the answer | Active recall practice | | `CLOZE_MODEL` | Fill-in-the-blank cards | Context-based learning | | `IMAGE_OCCLUSION_MODEL` | Hide parts of images | Visual learning | ### Example Usage ```typescript import { builtin } from 'genankjs'; // Basic cards const basicNote = new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: ['What is the capital of France?', 'Paris'], }); // Cloze deletion const clozeNote = new Note({ modelId: builtin.CLOZE_MODEL.modelId, fields: ['The capital of {{c1::France}} is {{c2::Paris}}.', 'Geography fact'], }); // Add to deck deck.addNote(basicNote, builtin.BASIC_MODEL); deck.addNote(clozeNote, builtin.CLOZE_MODEL); // Don't forget to add models to package pkg.addModel(builtin.BASIC_MODEL); pkg.addModel(builtin.CLOZE_MODEL); ``` ## ๐ŸŽต Media Support Add audio, images, and other media files to your cards: ```typescript import { readFileSync } from 'fs'; // Read media files const audioData = readFileSync('pronunciation.mp3'); const imageData = readFileSync('diagram.jpg'); // Add to package pkg.addMedia({ name: 'pronunciation.mp3', data: audioData }); pkg.addMedia({ name: 'diagram.jpg', data: imageData }); // Reference in cards (media files are numbered starting from 0) const note = new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: [ 'How do you pronounce "bonjour"? [sound:0]', 'Bonjour <img src="1" style="max-width: 200px;">', ], }); ``` ### Supported Media Types - **Audio**: .mp3, .wav, .ogg, .flac - **Images**: .jpg, .png, .gif, .webp, .svg - **Video**: .mp4, .webm (limited support) ## ๐ŸŽจ Advanced Examples ### Custom Model with Multiple Templates ```typescript const languageModel = new Model({ name: 'Language Learning', fields: [ { name: 'Word' }, { name: 'Translation' }, { name: 'Example' }, { name: 'Audio' }, ], templates: [ { name: 'Recognition', qfmt: '{{Word}}<br>{{Audio}}', afmt: '{{FrontSide}}<hr id="answer">{{Translation}}<br><em>{{Example}}</em>', }, { name: 'Production', qfmt: '{{Translation}}', afmt: '{{FrontSide}}<hr id="answer">{{Word}}<br>{{Audio}}<br><em>{{Example}}</em>', }, ], css: ` .card { font-family: 'Segoe UI', sans-serif; font-size: 18px; text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; } .example { font-style: italic; color: #ccc; } `, }); ``` ### Conditional Templates ```typescript // Card will only be generated if "Add Reverse" field is not empty const template = { name: 'Reverse Card', qfmt: '{{#Add Reverse}}{{Back}}{{/Add Reverse}}', afmt: '{{FrontSide}}<hr id="answer">{{Front}}', }; ``` ### Bulk Import from CSV ```typescript import { readFileSync } from 'fs'; import { parse } from 'csv-parse/sync'; // Read CSV file const csvData = readFileSync('vocabulary.csv', 'utf8'); const records = parse(csvData, { columns: true }); // Create notes from CSV const notes = records.map( (record) => new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: [record.word, record.translation], tags: record.tags?.split(',') || [], }), ); // Add all notes to deck deck.addNotes(notes, builtin.BASIC_MODEL); ``` ## ๐Ÿ“‹ API Reference ### Core Types ```typescript interface Field { name: string; font?: string; size?: number; sticky?: boolean; rtl?: boolean; ord?: number; } interface Template { name: string; qfmt: string; // Question format afmt: string; // Answer format bqfmt?: string; // Browser question format bafmt?: string; // Browser answer format } interface MediaFile { name: string; data: Buffer | Uint8Array; } interface NoteConfig { modelId?: number; fields: string[]; tags?: string[]; guid?: string; } interface DeckConfig { deckId?: number; name: string; description?: string; } ``` ### Utility Functions ```typescript // Generate unique identifiers import { generateGuid, generateUniqueGuid } from 'genankjs'; const guid = generateGuid(); const uniqueGuid = generateUniqueGuid(); ``` ## ๐Ÿ› ๏ธ Development ### Building ```bash npm run build ``` ### Testing ```bash npm test # Run tests once npm run dev # Run tests in watch mode ``` ### Formatting ```bash npm run format # Format code npm run check-format # Check formatting ``` ## ๐Ÿ› Troubleshooting ### Duplicate Detection in Anki **Problem**: Anki shows "notes already present in your collection" and skips importing cards. **Why this happens**: - Anki detects duplicates based on the first field content - You might already have cards with the same content - The GUID or checksum matches existing notes **Solutions**: 1. **Use unique content** (recommended): ```typescript const note = new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: [`Hello - ${Date.now()}`, 'Hello'], tags: ['unique'], }); ``` 2. **Force unique GUIDs**: ```typescript import { generateUniqueGuid } from 'genankjs'; const note = new Note({ modelId: builtin.BASIC_MODEL.modelId, fields: ['Hello', 'Hello'], guid: generateUniqueGuid().toString(), }); ``` 3. **Use custom model IDs**: ```typescript const customModel = new Model({ modelId: Date.now(), // Unique timestamp-based ID name: 'My Custom Model', // ... rest of config }); ``` ### Common Issues | Issue | Solution | | -------------------- | -------------------------------------------------------------- | | "Invalid .apkg file" | Ensure all media files are included and field references match | | Large file sizes | Compress media files before adding | | Import errors | Use Anki 2.1+ and check error logs | | Missing cards | Verify model templates and field names | ### Getting Help - ๐Ÿ“– Check the [examples](examples/) directory - ๐Ÿงช Review test files for usage patterns - ๐Ÿ› Open an issue on GitHub for bugs - ๐Ÿ’ก Request features via GitHub discussions ## ๐Ÿ“„ License MIT License - see [LICENSE](LICENSE) file for details. ## ๐Ÿ™ Acknowledgments - Inspired by the Python [genanki](https://github.com/kerrickstaley/genanki) library by Kerrick Staley - Thanks to the Anki project for comprehensive file format documentation - Built with TypeScript and modern Node.js practices ## ๐Ÿค Contributing Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request --- <div align="center"> <strong>Made with โค๏ธ for the Anki community</strong> <br> <sub>If this project helped you, please consider giving it a โญ๏ธ</sub> </div>