npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

genankjs

v1.0.6

Published

generate anki card

Downloads

19

Readme

GenAnki.js

npm version downloads license TypeScript

A powerful TypeScript library for generating Anki flashcard packages (.apkg files) programmatically, inspired by the Python 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

npm install genankjs

🚀 Quick Start

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

Package

The main container for creating .apkg files.

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.

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.

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.

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

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:

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

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

// 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

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

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

// Generate unique identifiers
import { generateGuid, generateUniqueGuid } from 'genankjs';

const guid = generateGuid();
const uniqueGuid = generateUniqueGuid();

🛠️ Development

Building

npm run build

Testing

npm test        # Run tests once
npm run dev     # Run tests in watch mode

Formatting

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):
const note = new Note({
  modelId: builtin.BASIC_MODEL.modelId,
  fields: [`Hello - ${Date.now()}`, 'Hello'],
  tags: ['unique'],
});
  1. Force unique GUIDs:
import { generateUniqueGuid } from 'genankjs';

const note = new Note({
  modelId: builtin.BASIC_MODEL.modelId,
  fields: ['Hello', 'Hello'],
  guid: generateUniqueGuid().toString(),
});
  1. Use custom model IDs:
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 directory
  • 🧪 Review test files for usage patterns
  • 🐛 Open an issue on GitHub for bugs
  • 💡 Request features via GitHub discussions

📄 License

MIT License - see LICENSE file for details.

🙏 Acknowledgments

  • Inspired by the Python 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