Open Deck Format
Open Deck Format
This document defines a human-readable deck format for flashcard decks that can live as either a directory or a zip archive. The design goal is to support a large range of Anki-like decks without copying Anki's HTML/CSS/template model into the authoring format.
The core rule is simple:
Deck files describe learning content and intent. The app owns rendering.
That means a deck can say "this is the term", "this is the sentence", "this is audio", "this answer should be typed", or "this region is hidden". It should not say "make a centered div with 72px text and this CSS class". Pretty defaults, platform-specific layout, accessibility, dark mode, and responsive behavior are app responsibilities.
Non-Goals
- No deck-authored HTML templates.
- No deck-authored CSS.
- No JavaScript as the default extension point.
- No required database.
- No hidden binary package format.
- No attempt to preserve every imported deck layout exactly.
Package Shape
A deck is a directory:
my-deck/
deck.yaml
notes/
basics.yaml
chapter-01.yaml
assets/
images/
audio/
video/
The same directory can be zipped:
my-deck.zip
A conforming reader should treat a zip exactly like the directory it contains.
File Roles
deck.yaml
deck.yaml is the deck manifest. It declares the deck identity.
format: open-deck
id: rust-book
title: The Rust Book
description: Cards based on The Rust Programming Language book.
language: en
Optional manifest fields:
license: MIT OR Apache-2.0
notes/*.yaml
Note files contain authorable content. They may define file-level defaults and a list of notes.
Decks may split notes across files in any useful way:
notes/
0001-0250.yaml
0251-0500.yaml
grammar.yaml
chapter-03.yaml
Readers should load note files in stable lexical path order, then load notes in the order they appear inside each file. If deck order matters, choose filenames that sort in the intended order, such as zero-padded numeric prefixes.
defaults:
deck: rust-book/ch03
tags: [ch03, data-types]
notes:
- id: rust-scalar-categories
type: prompt_response
prompt: Which four scalar type categories does Rust have?
answer: Integers, floating-point numbers, Booleans, and characters.
tags: [definition]
assets/
Assets are ordinary files referenced by relative path.
image: assets/images/knee.png
audio: assets/audio/word.mp3
video: assets/video/demo.mp4
Readers should reject paths that escape the deck root.
Core Concepts
Deck Type
There are three top-level note types:
type: prompt_response | cloze | occlusion
These classify the review interaction. Subject matter should usually be modeled with decks and tags.
Shared Fields
Every note may use these fields.
id: stable-note-id
type: prompt_response
deck: rust-book/ch03
tags: [tag-one, tag-two]
language: en
Keep review fields focused on what the learner sees or what the scheduler needs.
Authoring history, import details, and source file paths belong in provenance,
not in the core note shape.
Review Behavior
Review behavior describes how the learner answers without defining layout.
answer_mode: reveal | typed
Recommended default:
answer_mode: reveal
Content
Visible content should use either Markdown text or a small list of generic content blocks. Use Markdown text for ordinary cards. Use blocks when a card has several learner-facing pieces that should not be flattened into one blob.
Simple string:
prompt: What is ownership?
Markdown block:
prompt: |
What happens when this code runs?
```rust
println!("{}", [1, 2, 3][1]);
```
Block list:
prompt:
- role: main
text: "\u79c1"
language: ja
media:
- kind: audio
src: assets/audio/watashi.mp3
label: Word audio
- role: context
label: Sentence
text: "\u79c1\u306f\u30a2\u30f3\u3067\u3059\u3002"
language: ja
media:
- kind: audio
src: assets/audio/watashi-sentence.mp3
label: Sentence audio
Block fields:
role: main | context | support | note
label: string
text: markdown-string
runs: [inline-run]
language: language-code
media: [media-ref]
A block must contain at least one of text, runs, or media. Use text for
ordinary Markdown. Use runs only when a card needs small inline relationships
that Markdown cannot express cleanly, such as text above or below an exact span.
Do not set both text and runs on the same block.
Use media on a block when the file belongs to that specific piece of content, such as word audio, sentence audio, or an example image.
role tells the renderer the weight of the block, not its subject matter:
main: the primary thing to answer or remember.context: material needed to understand the prompt or answer.support: ordinary supporting information.note: lower-emphasis extra information.
label is optional. Use it when flattening would lose meaning, such as
distinguishing "Reading", "Frequency", "Source sentence", or "Compiler error".
Labels are deck content, not renderer commands.
Readers may render a block list as native grouped text, as labeled rows, or as plain Markdown-like sections. The app decides layout, spacing, script rendering, inline above/below text, and emphasis.
Inline Runs
Inline runs are plain text spans with small generic presentation hints. They are not a template language, annotation engine, or subject-matter model. They store what appears attached to text, not why.
runs:
- Plain text before
- text: "\u79c1"
above: "\u308f\u305f\u3057"
marks: [strong]
- "\u306f\u30a2\u30f3\u3067\u3059\u3002"
Run fields:
text: string
marks: [strong | emphasis | code | strike | highlight]
above: string
below: string
link: url
Run text, above, and below are plain text, not Markdown. Use Markdown
text blocks for paragraphs, code fences, lists, formulas, and ordinary links.
Use runs only for the small cases where the exact span matters.
Media References
Media references are semantic. The app decides replay button style, placement, image sizing, captions, and lazy loading.
Media may appear on a note or inside an individual content block. Prefer block-level media when the media belongs to a specific block. Use note-level media when the media belongs to the whole card or there is no useful block to attach it to.
media:
- kind: audio
src: assets/audio/word.mp3
label: Word audio
role: main
- kind: image
src: assets/images/example.webp
role: support
alt: Person being threatened
Allowed media kinds:
kind: image | audio | video
SVG files may be referenced as kind: image.
role and label on media are optional. They help the renderer group media
with nearby content without inventing media fields like word_audio or
sentence_audio. Block-level media already inherits the surrounding block's
role, so a block-level media item often needs only kind, src, and maybe
label or alt.
Visual Demonstrations
Some cards need to show how something is produced over time: drawing a character, sketching a diagram, constructing a proof, playing a chord, or folding a shape. Model that as ordinary media first.
answer:
- role: main
text: "\u4f60"
language: zh-Hans
media:
- kind: video
src: assets/video/ni-writing-demo.mp4
label: Writing demo
role: support
This keeps the format generic. The deck stores learner-facing content and optional demonstration media. The app decides playback controls, animation style, native drawing surfaces, script-aware stroke renderers, and fallbacks.
Do not add renderer-specific libraries, JavaScript, CDN URLs, canvas settings,
stroke colors, or widget configuration to the deck. If native ordered-step
rendering becomes a common real need, define one generic media form for ordered
visual sequences later. Do not add subject-specific fields such as
hanzi_writer, stroke_order, or kanji_svg to the core note shape.
Type 1: prompt_response
Use prompt_response for ordinary recall and problem-solving cards:
- Front/back cards.
- Vocabulary.
- Definitions.
- Dates and facts.
- Geography.
- Classification.
- Media identification.
- Structured fact cards.
- Problems, cases, proofs, and worked examples.
- Code diagnostics and query questions.
Required Fields
type: prompt_response
prompt: content
answer: content
Additional Fields
hint: content
media: [media-ref]
references:
- title: string
url: url
locator: string
Example: Simple QA
- id: oxygen-symbol
type: prompt_response
prompt: What is the chemical symbol for oxygen?
answer: O
Example: Japanese Vocabulary
- id: jp-warui
type: prompt_response
prompt:
- role: main
runs:
- text: "\u60aa"
above: "\u308f\u308b"
- "\u3044"
language: ja
media:
- kind: audio
src: assets/audio/warui.mp3
label: Word audio
- role: context
label: Sentence
text: "\u3042\u306e\u4eba\u306f\u60aa\u3044\u4eba\u3067\u3059\u3002"
language: ja
media:
- kind: audio
src: assets/audio/warui-sentence.mp3
label: Sentence audio
answer:
- role: main
label: Meaning
text: bad
- role: support
label: Reading
text: warui
- role: support
label: Sentence meaning
text: That person is a bad person.
- role: support
label: Illustration
media:
- kind: image
src: assets/images/bad-person.webp
alt: Person being threatened
Example: Structured Fact As Separate Cards
- id: france-capital
type: prompt_response
prompt: What is the capital of France?
answer: Paris
- id: france-country
type: prompt_response
prompt: Paris is the capital of which country?
answer: France
- id: france-flag
type: prompt_response
prompt: Which country uses this flag?
media:
- kind: image
src: assets/images/france.svg
alt: Flag of France
answer: France
Use separate notes when one source item should create multiple review prompts. This is intentionally boring: it avoids introducing a template or card-generator layer into the deck format.
- id: artwork-ernst-artist
type: prompt_response
prompt:
- role: main
label: Artwork
media:
- kind: image
src: assets/images/europe-after-rain.jpg
alt: Europe After the Rain II
answer:
- role: main
label: Artist
text: Max Ernst
- id: artwork-ernst-title
type: prompt_response
prompt:
- role: main
label: Artwork
media:
- kind: image
src: assets/images/europe-after-rain.jpg
alt: Europe After the Rain II
answer:
- role: main
label: Title
text: Europe After the Rain II
Example: Code Diagnostic
- id: rust-double-mut-borrow
type: prompt_response
prompt: |
Why does this fail?
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
```
answer: |
It creates two simultaneous mutable references to `s`.
Rust allows either one mutable reference or any number of immutable
references, but not overlapping mutable references.
Common mistakes:
- Thinking the error is caused by `println!` formatting.
- Thinking the borrow ends immediately after `r1` is created.
Example: Math Problem
- id: derivative-x2
type: prompt_response
prompt: |
Find the derivative.
$f(x) = x^2$
answer: |
$f'(x) = 2x$
- Apply the power rule.
- Multiply by the exponent and subtract one from the exponent.
Type 2: cloze
Use cloze when the prompt is a source passage with inline hidden spans.
Required Fields
type: cloze
text: content-with-cloze-markers
Additional Fields
context: content
extra: content
media: [media-ref]
Cloze markers use this form:
{{id::answer}}
{{id::answer::hint}}
Repeated IDs belong to the same group.
Example
- id: rust-ownership-cloze
type: cloze
text: |
In Rust, each value has {{c1::one owner::count + noun}} at a time,
and when the owner goes out of scope, the value is {{c2::dropped::cleanup action}}.
extra: |
This is the core ownership rule that lets Rust avoid a garbage collector.
Type 3: occlusion
Use occlusion when the card hides regions of an image or diagram.
Required Fields
type: occlusion
image:
src: assets/images/diagram.png
masks:
- id: label-1
shape:
kind: rect
x: 0
y: 0
w: 100
h: 50
answer: string
Additional Fields
image:
src: path
alt: string
width: number
height: number
masks:
- id: string
answer: string
hint: string
group: string
shape:
kind: rect | ellipse | polygon
x: number
y: number
w: number
h: number
points: [[number, number]]
context: content
extra: content
Coordinates should use the natural image coordinate system. The renderer can scale to screen size.
Example
- id: knee-ligaments
type: occlusion
image:
src: assets/images/knee.png
alt: Knee ligament diagram
width: 1200
height: 900
masks:
- id: acl
answer: Anterior cruciate ligament
hint: ACL
shape:
kind: rect
x: 510
y: 320
w: 180
h: 70
- id: patella
answer: Patella
shape:
kind: ellipse
x: 440
y: 160
w: 150
h: 120
extra: |
Review the relative position of each structure, not only the label name.
References
References point to learner-facing source material. Use them when the app should be able to show "read more", citations, or a friendly source link.
references:
- title: The Rust Programming Language
url: https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
locator: Chapter 4, References and Borrowing
Do not use references for raw generator bookkeeping. A local source file path
such as rust-book/src/ch04-02-references-and-borrowing.md is usually not useful
to a learner. Put that in provenance instead.
Provenance
provenance is optional maintainer metadata. It can help generated decks,
imports, audits, and regeneration, but native review behavior must not depend on
it.
provenance:
source_path: rust-book/src/ch04-02-references-and-borrowing.md
source_lines: 10-30
generator: rust-book-card-generator
Readers may ignore provenance.
Importers should produce ordinary native notes. If they need to preserve source
system details, they should put that data in provenance, not in review fields.
Imported HTML templates, CSS, JavaScript, renderer preferences, and widget
configuration are not native deck content. Preserve the useful learner-facing
facts, media, links, and audit metadata instead.
Validation Rules
A validator should check:
deck.yamlexists.formatis supported.- Every note has a stable
id. - Every note has a supported
type. - Required fields for the type are present.
- Asset references exist and stay inside the deck root.
- IDs are unique within the deck.
- Prompt-response notes have a prompt and answer.
- Content blocks, when used, have a supported role and at least one of
text,runs, ormedia. - Content blocks do not set both
textandruns. - Inline runs, when used, are non-empty and use supported marks.
- Media is valid on both notes and content blocks.
- Occlusion masks have valid geometry.
- Cloze notes have at least one cloze marker.
This repository includes a small validator:
bun tools/validate-open-deck.ts kaishi-open-deck
Warnings, not hard errors:
- Missing optional alt text.
- Very large media files.
Renderer Contract
The renderer must provide good defaults for:
- Front and back layout.
- Mobile and desktop responsive sizing.
- Dark mode.
- Audio controls.
- Image sizing.
- Inline above/below text where available.
- Code blocks.
- Math/formula text where available.
- Accessibility labels.
- Empty optional fields.
The renderer should not require deck authors to manage layout.
Format Identifier
The format string identifies the Open Deck format:
format: open-deck
Compatibility rules:
- Canonical validators should reject unknown schema fields so typos and stale fields are caught early.
- Readers may ignore unknown keys inside
provenance. - Readers must reject unknown required
typevalues. - Unknown note types are invalid.
- Native deck behavior must not depend on
provenance.
Minimum Reader Support
A conforming reader should support:
- Directory and zip loading.
deck.yaml.notes/*.yaml.- Local assets.
prompt_response.cloze.occlusion.- Validation errors with file paths and note IDs.
Do not build a template engine.
Native Markdown Rendering
Card content may use Markdown for ordinary rich text, code fences, links, lists,
and formulas. Card content may also be a list of generic labeled blocks whose
text values are Markdown. Markdown is authoring syntax, not a layout or
template system.
Blocks that use runs bypass Markdown. Run text is already a small inline
content tree, so readers should render it directly as native text spans.
Readers should parse Markdown into an app-owned content tree before rendering:
Markdown content
-> content tree
-> native renderer
For block lists, readers should parse each block's text through the same
Markdown pipeline, or render its runs directly. Then they should render the
block role and label with app-owned native components.
Readers should render that tree with platform-native UI components where practical. For example, iOS can render the tree with SwiftUI views, Android can render it with Compose views, and desktop apps can render it with their native UI toolkit. Electron desktop apps may render sanitized HTML as an implementation detail, but deck content should still be parsed through the same content tree and must not become deck-authored HTML.
The content tree should stay small and semantic:
paragraph
text
emphasis
strong
inline_code
code_block(language, text)
link
image
bullet_list
ordered_list
blockquote
math_inline
math_block
Readers should not support raw HTML, custom CSS, deck-authored JavaScript, or renderer-specific Markdown extensions.
Syntax highlighting, if supported, should be returned as spans over a code block rather than HTML:
code_span(start, end, role)
The native renderer maps span roles to platform theme colors.
Formula support should be isolated behind math_inline and math_block.
Readers may render formulas with a native math renderer, pre-rendered images, or
plain TeX fallback. Formula support should not require cards to use HTML.