Skip to content

A CLI tool and TypeScript library for syncing Markdown to Anki flashcards.

License

Notifications You must be signed in to change notification settings

kitschpatrol/yanki

Repository files navigation

yanki

NPM Package yanki License: MIT

A CLI tool and TypeScript library for syncing Markdown to Anki flashcards.

Important

Yanki is feature-complete but will remain zero-versioned until it's been thoroughly tested. Please exercise caution and make backups of your Anki notes until the 1.0.0 release.

Table of contents

Overview

Yanki simply syncs a folder of Markdown notes to Anki.

The primary novelty of its approach is in how Markdown is translated into Anki notes, and how folders are translated into Anki decks:

  • One Markdown file maps to one Anki note.

  • The structure of a Markdown note determines the type of Anki note it becomes, so no extra syntax or Anki-specific markup is required — just pure Markdown.

  • The parent folder of your Markdown note determines its deck name in Anki, with any intermediate hierarchies created as needed.

This package provides both a stand-alone CLI tool and a TypeScript library for ease of integration in other projects.

The library powers the yanki-obsidian Obsidian plugin. And for lower-level access to Anki, please take a look at the yanki-connect TypeScript library.

The "Y" prefix in "Yanki" is in the "Yet another" naming tradition; a nod to Anki's robust and occasionally duplicative ecosystem of third-party tools. (Also, appropriately, Yankī are a variety of truant youth.)

Quick start

Install Node.js if you haven't already. (Yanki requires Node 18.19.0+, 20.5.0+, or newer.)

Assuming you have a folder of Markdown note files, the Anki app is open and has the the Anki-Connect add-on installed:

npx yanki ./folder-of-markdown

This will turn the folder's Markdown files into Anki notes and send them up to the Anki database.

Features

One Markdown file = one Anki note

Avoid the complexity of mixing and matching multi-note and single-note syntaxes. One local Markdown file always yields one Anki note.

Local folder hierarchy = Anki deck hierarchy

Yanki uses the source Markdown file's parent directory name as the deck name. Complex folder hierarchies are also supported — Anki decks will be created and nested as needed to match the structure of the local file system.

Embrace of Anki's default note types

More note types, more problems.

Yanki only supports turning Markdown into the Basic, Basic (and reversed card), Basic (type in the answer), and Cloze note types that ship as defaults in the Anki App.

Infer Anki note type from Markdown structure

Since the number of supported note types is small, the type of Anki note to create from a given document can be inferred from a few simple rules about the structure of the Markdown.

For example, a Basic note is any Markdown file with a --- horizontal rule splitting the front and back of the card:

I'm the front.

---

I'm the back.

That's it, no extra metadata or Anki-specific markup is required. You can add whatever additional Markdown syntax you'd like to style the note.

The structural cues for all four supported note types are described later in this document.

Tags in frontmatter

Optionally, you can add a tags array to your Markdown file's frontmatter and have it automatically synchronized to the Anki database. Frontmatter is also used to store the Anki note's ID after an initial synchronization.

Intelligent synchronization

Your local Markdown files are the single point of truth for what will and up in Anki, but Yanki knows to leave your other Anki notes alone.

When you edit a local markdown note, Yanki makes every effort to update rather than delete it in the Anki database so that progress is preserved.

But when you do want to delete something, it's as simple as deleting the local Markdown note from the file system and running yanki sync to remove it from the Anki database. Protections are in place to prevent deleting Anki notes that weren't initially created by Yanki.

If you use AnkiWeb to sync your notes to the cloud, Yanki will also trigger this next step in the sync, automating the flow from Markdown → Anki → AnkiWeb in one shot.

Existing notes are untouched

Yanki tags the notes it's in charge of with a hidden field, so it will never touch your existing Anki notes. (But please exercise caution until the 1.0.0 release...)

Fancy markdown

An extended palette of markdown syntax is available out of the box:

Custom styles

Yanki uses Anki's built-in CSS stylesheet to style cards by default, but it makes it simple to set a custom stylesheet for all of your Yanki-managed notes without having to go on a click-quest in the Anki user interface.

Filename management

The "one Markdown file = one Anki note" can make for a lot of individual files, and thinking up and renaming files as content is revised can be tedious. So, if you want, Yanki can manage the names of your note files based on their content.

Yanki looks inside each note, and extracts either the text of the "prompt" (e.g. the front of the card in most cases), or the "response" (e.g. the back of the card in most cases) to use as the filename. Truncation, deduplication, and sanitization are all taken care of.

Edge cases are carefully managed to ensure that there's always some kind of best-effort semantically valuable file name assigned.

Media asset synchronization

Yanki can sync images, videos, and audio files embedded in your notes with Anki's media asset management system. At your option, it can sync local assets, or assets linked via URL, or both, or none.

Yanki automatically manages clean-up of synced media assets when you delete your notes, or when specific assets are removed from your notes.

Both wiki-style ![[something.png]] and ![markdown](style.png) asset embedding syntaxes are supported.

Markdown note types

Yanki automatically infers the type of Note you'd like to create in Anki based on the presence or absence of certain elements in your Markdown files.

The rules were designed with the semantic and visual nature of Markdown in mind.

The most minimal examples to "trigger" different note types are shown below, but the implementation can handle additional weirdness and will generally do the right thing if it encounters elements that might indicate conflicting note types.

You're free to use additional Markdown in your note files to style and structure the front and back of your flashcards.

Basic

A basic card is created from any file with a ---:

This is the front of the card

---

This is the back of the card

Basic (and reversed card)

Doubling up the --- identifies the note as being reversible (and will result in the generation of two cards in Anki).

Mnemonic: Twice the --- for twice the cards.

Sometimes the answer is the question

---

---

Sometimes the question is the answer

Yanki also supports adding "extra" content that will appear on the the back of both generated cards:

Sometimes the answer is the question

---

---

Sometimes the question is the answer

---

This will appear on the back of both generated cards

Basic (type in the answer)

If the last statement in the Markdown file is _emphasized like this_, it becomes the type-in-the-answer text in Anki.

Mnemonic: The syntax resembles a _blank to be filled in_.

Jazz isn't dead

_It just smells funny_

Cloze

Text that is ~~struck through~~ with the somewhat esoteric double-tilde syntax will be hidden in the resulting cloze card:

Mnemonic: The ~~strike through~~ implies redaction.

All will be ~~revealed~~.

Multiple clozes are supported, which will create additional cards. You can add a --- to include back-of-card information as well. Hints are also supported, and are indicated by giving the hint text _emphasis_ at the end of the cloze strike-through:

~~All~~ will be ~~revealed _here's a hint_~~.

---

Additional revelations on the back of the card.

By default, clozes are numbered incrementally if there are more than one on a card, for example the Markdown:

~~All~~ will be ~~revealed _here's a hint_~~.

Is turned into the following Anki syntax behind the scenes:

{{c1::All}} will be {{c2::revealed::<em>here's a hint</em>}}

In rare cases, you might want to take control over cloze numbering — perhaps you want to reveal multiple clozes simultaneously on a single card, or group certain clozes together on a particularly cloze-heavy note.

To support this, Yanki offers some optional extra syntax. A leading one- or two-digit a number at the front of your clozed content will be interpreted as the cloze number:

For example:

~~1 All~~ will be ~~1 revealed _here's a hint_~~.

Becomes the following Anki syntax.

{{c1::All}} will be {{c1::revealed::<em>here's a hint</em>}}

The difference is subtle, but note the matching {{c1s. This markup yields a single card where both All and revealed are revealed simultaneously.

Note: While you can encloze images, math equations, and other inline-styled syntax, clozing over multiple lines or block elements is not currently supported.

Getting started

Dependencies

The yanki CLI tool requires Node 18+. The exported TypeScript / JavaScript APIs are isomorphic, and can run in both browser-based and Node runtime environments. The Yanki library is ESM-only, is implemented in TypeScript, and bundles a complete set of type definitions.

The tool has been tested on Windows, macOS, and Linux.

Prerequisites:

  • The Anki desktop app

  • The Anki-Connect add-on

    If you need to install it, select Tools → Add-ons from the menu, click Get Add-ons..., and then enter the code 2055492159 in the field to get Anki-Connect.

    Anki-Connect may ask for your permission in the Anki application to connect to Obsidian on the first sync.

    If the automatic permission request fails, you might need to configure Anki-Connect to accept connections from your origin.

    In Anki, select Tools → Add-ons from the menu, then select AnkiConnect from the list, and click the Config button in the lower right. In the ensuing modal, add the host and port from which you're attempting to connect to to the webCorsOriginList array.

Installation

Invoke directly:

npx yanki ./folder-to-sync/**/*.md

...or install globally:

npm install --global yanki

...or install locally in your JavaScript or TypeScript project to use the exported APIs:

npm install --save-dev yanki

Usage

Basics

Setup

Create a folder of Markdown files that you'd like to use as Anki notes. (See the section on Markdown notes for details on how to structure your document to create different card types in Anki.)

Launch the Anki desktop app. Ensure that the Anki-Connect add-on is installed and set up.

Create

Pass the path to a folder of Markdown notes to turn them into Anki notes and send them to the Anki database:

yanki ./your-deck-folder

You'll now see your Markdown files as HTML-rendered notes in the Anki desktop app.

Update

Edit the Markdown file locally (not in Anki!) and run yanki ./your-deck-folder again.

Delete

Delete the Markdown file locally (not in Anki!) and run yanki ./your-deck-folder again.

CLI

All available commands and options for advanced use cases are described below, but shouldn't typically be necessary.

Command: yanki

Run a Yanki command. Defaults to sync if a command is not provided.

This section lists top-level commands for yanki.

If no command is provided, yanki sync is run by default.

Usage:

yanki [command]
Command Argument Description
sync <directory> [options] Perform a one-way synchronization from a local directory of Markdown files to the Anki database. Any Markdown files in subdirectories are included as well. (Default command.)
list [options] Utility command to list Yanki-created notes in the Anki database.
delete [options] Utility command to manually delete Yanki-created notes in the Anki database. This is for advanced use cases, usually the sync command takes care of deleting files from Anki Database once they're removed from the local file system.
style [options] Utility command to set the CSS stylesheet for all present and future Yanki-created notes.

See the sections below for more information on each subcommand.

Subcommand: yanki sync

Perform a one-way synchronization from a local directory of Markdown files to the Anki database. Any Markdown files in subdirectories are included as well.

Usage:

yanki sync <directory> [options]
Positional Argument Description Type
directory The path to the local directory of Markdown files to sync. (Required.) string
Option Description Type Default
--dry-run
-d
Run without making any changes to the Anki database. See a report of what would have been done. boolean false
--namespace
-n
Advanced option for managing multiple Yanki synchronization groups. Case insensitive. See the readme for more information. string "Yanki"
--anki-connect Host and port of the Anki-Connect server. The default is usually fine. See the Anki-Connect documentation for more information. string "http://127.0.0.1:8765"
--anki-auto-launch
-l
Attempt to open the Anki desktop app if it's not already running. (Experimental, macOS only.) boolean false
--anki-web
-w
Automatically sync any changes to AnkiWeb after Yanki has finished syncing locally. If false, only local Anki data is updated and you must manually invoke a sync to AnkiWeb. This is the equivalent of pushing the "sync" button in the Anki app. boolean true
--manage-filenames
-m
Rename local note files to match their content. Useful if you want to feel have semantically reasonable note file names without managing them by hand. The "prompt" option will attempt to create the filename based on the "front" of the card, while "response" will prioritize the "back", "Cloze", or "type in the answer" portions of the card. Truncation, sanitization, and deduplication are taken care of. "off" "prompt" "response" "off"
--max-filename-length If manage-filenames is enabled, this option specifies the maximum length of the filename in characters. number 60
--sync-media
-s
Sync image, video, and audio assets to Anki's media storage system. Clean up is managed automatically. The all argument will save both local and remote assets to Anki, while local will only save local assets, remote will only save remote assets, and off will not save any assets. "off" "all" "local" "remote" "local"
--strict-line-breaks
-b
Set to false to treat single newlines in Markdown as line breaks. boolean true
--json Output the sync report as JSON. boolean false
--verbose Enable verbose logging. boolean false
--help
-h
Show help boolean
--version
-v
Show version number boolean

Subcommand: yanki list

Utility command to list Yanki-created notes in the Anki database.

Usage:

yanki list [options]
Option Description Type Default
--namespace
-n
Advanced option to list notes in a specific namespace. Case insensitive. Notes from the default internal namespace are listed by default. Pass '*' to list all Yanki-created notes in the Anki database. string "Yanki"
--anki-connect Host and port of the Anki-Connect server. The default is usually fine. See the Anki-Connect documentation for more information. string "http://127.0.0.1:8765"
--anki-auto-launch
-l
Attempt to open the Anki desktop app if it's not already running. (Experimental, macOS only.) boolean false
--json Output the list of notes as JSON to stdout. boolean false
--help
-h
Show help boolean
--version
-v
Show version number boolean

Subcommand: yanki delete

Utility command to manually delete Yanki-created notes in the Anki database. This is for advanced use cases, usually the sync command takes care of deleting files from Anki Database once they're removed from the local file system.

Usage:

yanki delete [options]
Option Description Type Default
--dry-run
-d
Run without making any changes to the Anki database. See a report of what would have been done. boolean false
--namespace
-n
Advanced option to list notes in a specific namespace. Case insensitive. Notes from the default internal namespace are listed by default. If you've synced notes to multiple namespaces, Pass '*' to delete all Yanki-created notes in the Anki database. string "Yanki"
--anki-connect Host and port of the Anki-Connect server. The default is usually fine. See the Anki-Connect documentation for more information. string "http://127.0.0.1:8765"
--anki-auto-launch
-l
Attempt to open the Anki desktop app if it's not already running. (Experimental, macOS only.) boolean false
--anki-web
-w
Automatically sync any changes to AnkiWeb after Yanki has finished syncing locally. If false, only local Anki data is updated and you must manually invoke a sync to AnkiWeb. This is the equivalent of pushing the "sync" button in the Anki app. boolean true
--json Output the list of deleted notes as JSON to stdout. boolean false
--verbose Enable verbose logging. boolean false
--help
-h
Show help boolean
--version
-v
Show version number boolean

Subcommand: yanki style

Utility command to set the CSS stylesheet for all present and future Yanki-created notes.

Usage:

yanki style [options]
Option Description Type Default
--dry-run
-d
Run without making any changes to the Anki database. See a report of what would have been done. boolean false
--css
-c
Path to the CSS stylesheet to set for all Yanki-created notes. If not provided, the default Anki stylesheet is used. string
--anki-connect Host and port of the Anki-Connect server. The default is usually fine. See the Anki-Connect documentation for more information. string "http://127.0.0.1:8765"
--anki-auto-launch
-l
Attempt to open the Anki desktop app if it's not already running. (Experimental, macOS only.) boolean false
--anki-web
-w
Automatically sync any changes to AnkiWeb after Yanki has finished syncing locally. If false, only local Anki data is updated and you must manually invoke a sync to AnkiWeb. This is the equivalent of pushing the "sync" button in the Anki app. boolean true
--json Output the list of updated note types / models as JSON to stdout. boolean false
--verbose Enable verbose logging. boolean false
--help
-h
Show help boolean
--version
-v
Show version number boolean

Library

API

This package also exposes an API for integrating syncing capability programmatically in other contexts. (For example, in the yanki-obsidian plugin.)

The primary functions of interest are:

function getNoteFromMarkdown(
  markdown: string,
  options?: Partial<GetNoteFromMarkdownOptions>,
): Promise<YankiNote>

function syncNotes(
  allLocalNotes: YankiNote[],
  options?: PartialDeep<SyncOptions>,
): Promise<SyncNotesResult>

function syncFiles(
  allLocalFilePaths: string[],
  options?: PartialDeep<SyncFilesOptions>,
): Promise<SyncFilesResult>

function listNotes(options?: PartialDeep<ListOptions>): Promise<ListResult>

function cleanNotes(options?: PartialDeep<CleanOptions>): Promise<CleanResult>

function setStyle(options?: PartialDeep<StyleOptions>): Promise<StyleResult>

See the source code for additional exports and inline documentation.

Advanced features

Namespaces

For simplicity's sake, Yanki anticipates syncing one folder of notes on one machine as its primary use case. Every note uploaded to Anki has a "namespace" string in a hidden field. Yanki uses this field to identify the notes it's in charge of, and to identify notes for deletion if they are present in the Anki database but missing in the collection of local markdown notes.

For example, if you run:

yanki sync ./important-cards

Followed by:

yanki sync ./more-important-cards

Any notes synced from the important-cards folder will be deleted from Anki by the second command to sync more-important-cards, because they share the same default namespace, and because your local notes are a single source of truth for the sync system.

If you don't want this behavior, then you can pass the --namespace flag to explicitly state that you want the commands to execute in separate namespaces:

yanki sync ./important-cards --namespace "Foo"
yanki sync ./more-important-cards --namespace "Bar"

This will let both directories sync and co-exist.

But in general, a better solution would be to give them a common parent directory and, just sync that:

# Move to a common parent
mv ./important-cards ~/cards/imporant-cards
mv ./more-important-cards ~/cards/more-important-cards

# Sync the parent
yanki sync ~/cards

This makes ~/cards the single source of truth, and spares the cognitive overhead of namespaces.

Yanki will put the cards in eponymous decks, so you'll still have a clean separation of the two groups of cards in Anki.

Media asset synchronization

By default, Yanki will automatically copy local image, media, and audio media linked in your notes' markdown to Anki's media storage system. With an option, it can do the same for remote assets.

Note that there Anki's underlying implementation requires media assets to be less than 100 MB in size. There are also internal limits around filename length, but Yanki manages the filename for you internally, so this is not a concern.

Support file formats / codecs is a bit nuanced, and some variations have been observed between what is officially supported according to the Anki source code and what actually works in practice on different platforms.

The subset of the officially supported formats that have shown the broadest compatibility in testing Yanki are:

  • Image: jpeg, png, svg
  • Audio: mp3
  • Video: mp4, gif

See the document on file formats for additional details and a full compatibility matrix.

Styles

The yanki style command lets you assign a CSS stylesheet to all Yanki-managed notes.

Because of how styles work in Anki and how Yanki manages card models, assigned style is a global operation to all Yanki-managed cards. (Though your other Anki cards, as always, will be untouched.)

Your custom stylesheets can take advantage of several CSS classes added to the top-level <div> of each card by Yanki.

The classes include:

  • yanki: Shared by all cards generated by Yanki
  • namespace-$name: The namespace associated with the card
  • front | back: The "side" of the card"
  • model-$name: The name of the card type / model, one of the following. They're wordy, but consistent with Anki's default model naming scheme:
    • model-yanki-basic
    • model-yanki-basic-and-reversed-card
    • model-yanki-basic-type-in-the-answer
    • model-yanki-cloze

For example, the front of a basic card would look like:

<div class="yanki namespace-yanki front model-yanki-basic">
  <!-- The rest of the card's markup is here -->
</div>

Browser environments

The Yanki TypeScript / JavaScript library is idempotent, so you can run it in a browser you'd like.

There's one exception, the syncFiles(...) function, which by default relies on file system access to work.

To retain syncFiles(...)'s utility in a browser environment, has the optional arguments readFile, writeFile, and rename, which are implemented by node:fs/promises by default in Node environments.

Running Yanki in a browser environment requires implementing and passing readFile, writeFile, and rename implementations to syncFiles(...) that are suited to your particular use case. (A warning will be provided if you neglect to do so.)

The rest of the library should work fine in both contexts without special measures.

Background

Implementation notes

The excellent unified / remark / rehype libraries are used extensively to traverse and render the underlying Markdown ASTs.

Anki-Connect provides access to Anki's database.

For type safety, access to Anki-Connect is managed through my wrapper library, yanki-connect.

Behind the scenes, Yanki creates new note type models to match the four default Anki types. It keeps track of the notes it has ownership of via a hidden YankiNamespace field in each note.

Linux testing was performed with Debian 12 and Ubuntu 22, both running on an arm64 virtual machine. Anki does not officially support arm64 linux, but a workaround was able to get Anki up and running. Getting obsidian:// links working on Linux required some manual steps.

Other projects

Known issues

  • Intra-note links do not update after automatic renaming, e.g. via the --manage-filenames flag, potentially resulting in broken links.

The future

Possible features on the horizon:

  • Command to convert markdown notes to Anki packages (.apkg files), instead of syncing directly with Anki.
  • Sync and store Anki's review statistics to the Markdown file's frontmatter. This could be useful to both "back up" progress made in Anki, and to afford integration with other local tools. (In exchange for some extra noise in the frontmatter.)
  • Model migrations.
  • Include a built-in CSS stylesheet, since Anki's defaults can't always anticipate the kinds of HTML you're likely to generate from Markdown.
  • Mermaid diagrams support. (Unlikely since rendering to an SVG seems to require a browser. See rehype-mermaid and remark-mermaidjs.)
  • It would be nice to find a way to talk to the Anki database that doesn't require the Anki app to be running, but my research hasn't yet turned up anything as robust and reliable as Anki-Connect for this purpose.

Maintainers

@kitschpatrol

Acknowledgments

Thanks to Alex Yatskov for creating Anki-Connect.

Thanks to the unified team for their superb ecosystem of AST tools.

Contributing

Issues and pull requests are welcome.

License

MIT © Eric Mika