aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/server/anki/parser.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/anki/parser.test.ts')
-rw-r--r--src/server/anki/parser.test.ts868
1 files changed, 0 insertions, 868 deletions
diff --git a/src/server/anki/parser.test.ts b/src/server/anki/parser.test.ts
deleted file mode 100644
index aeee62c..0000000
--- a/src/server/anki/parser.test.ts
+++ /dev/null
@@ -1,868 +0,0 @@
-import { randomBytes } from "node:crypto";
-import { mkdir, rm, writeFile } from "node:fs/promises";
-import { tmpdir } from "node:os";
-import { join } from "node:path";
-import { DatabaseSync } from "node:sqlite";
-import { deflateRawSync } from "node:zlib";
-import { afterAll, beforeAll, describe, expect, it } from "vitest";
-import {
- type AnkiPackage,
- listAnkiPackageContents,
- mapAnkiToKioku,
- parseAnkiPackage,
-} from "./parser.js";
-
-/**
- * Create a minimal ZIP file with the given entries
- */
-function createZip(entries: Map<string, Buffer>): Buffer {
- const chunks: Buffer[] = [];
- const centralDirectory: Buffer[] = [];
- let offset = 0;
-
- for (const [name, data] of entries) {
- const nameBuffer = Buffer.from(name, "utf8");
- const compressedData = deflateRawSync(data);
-
- // Local file header
- const localHeader = Buffer.alloc(30 + nameBuffer.length);
- localHeader.writeUInt32LE(0x04034b50, 0); // signature
- localHeader.writeUInt16LE(20, 4); // version needed
- localHeader.writeUInt16LE(0, 6); // flags
- localHeader.writeUInt16LE(8, 8); // compression method (deflate)
- localHeader.writeUInt16LE(0, 10); // mod time
- localHeader.writeUInt16LE(0, 12); // mod date
- localHeader.writeUInt32LE(0, 14); // crc32 (not validated in our parser)
- localHeader.writeUInt32LE(compressedData.length, 18); // compressed size
- localHeader.writeUInt32LE(data.length, 22); // uncompressed size
- localHeader.writeUInt16LE(nameBuffer.length, 26); // file name length
- localHeader.writeUInt16LE(0, 28); // extra field length
- nameBuffer.copy(localHeader, 30);
-
- // Central directory entry
- const centralEntry = Buffer.alloc(46 + nameBuffer.length);
- centralEntry.writeUInt32LE(0x02014b50, 0); // signature
- centralEntry.writeUInt16LE(20, 4); // version made by
- centralEntry.writeUInt16LE(20, 6); // version needed
- centralEntry.writeUInt16LE(0, 8); // flags
- centralEntry.writeUInt16LE(8, 10); // compression method
- centralEntry.writeUInt16LE(0, 12); // mod time
- centralEntry.writeUInt16LE(0, 14); // mod date
- centralEntry.writeUInt32LE(0, 16); // crc32
- centralEntry.writeUInt32LE(compressedData.length, 20); // compressed size
- centralEntry.writeUInt32LE(data.length, 24); // uncompressed size
- centralEntry.writeUInt16LE(nameBuffer.length, 28); // file name length
- centralEntry.writeUInt16LE(0, 30); // extra field length
- centralEntry.writeUInt16LE(0, 32); // comment length
- centralEntry.writeUInt16LE(0, 34); // disk number
- centralEntry.writeUInt16LE(0, 36); // internal attributes
- centralEntry.writeUInt32LE(0, 38); // external attributes
- centralEntry.writeUInt32LE(offset, 42); // offset of local header
- nameBuffer.copy(centralEntry, 46);
-
- centralDirectory.push(centralEntry);
- chunks.push(localHeader, compressedData);
- offset += localHeader.length + compressedData.length;
- }
-
- // Central directory
- const centralDirOffset = offset;
- const centralDirBuffer = Buffer.concat(centralDirectory);
- chunks.push(centralDirBuffer);
-
- // End of central directory
- const endRecord = Buffer.alloc(22);
- endRecord.writeUInt32LE(0x06054b50, 0); // signature
- endRecord.writeUInt16LE(0, 4); // disk number
- endRecord.writeUInt16LE(0, 6); // disk with central dir
- endRecord.writeUInt16LE(entries.size, 8); // entries on this disk
- endRecord.writeUInt16LE(entries.size, 10); // total entries
- endRecord.writeUInt32LE(centralDirBuffer.length, 12); // central dir size
- endRecord.writeUInt32LE(centralDirOffset, 16); // central dir offset
- endRecord.writeUInt16LE(0, 20); // comment length
- chunks.push(endRecord);
-
- return Buffer.concat(chunks);
-}
-
-/**
- * Create a test Anki SQLite database
- */
-function createTestAnkiDb(dbPath: string): void {
- const db = new DatabaseSync(dbPath);
-
- // Create tables
- db.exec(`
- CREATE TABLE col (
- id INTEGER PRIMARY KEY,
- crt INTEGER NOT NULL,
- mod INTEGER NOT NULL,
- scm INTEGER NOT NULL,
- ver INTEGER NOT NULL,
- dty INTEGER NOT NULL,
- usn INTEGER NOT NULL,
- ls INTEGER NOT NULL,
- conf TEXT NOT NULL,
- models TEXT NOT NULL,
- decks TEXT NOT NULL,
- dconf TEXT NOT NULL,
- tags TEXT NOT NULL
- )
- `);
-
- db.exec(`
- CREATE TABLE notes (
- id INTEGER PRIMARY KEY,
- guid TEXT NOT NULL,
- mid INTEGER NOT NULL,
- mod INTEGER NOT NULL,
- usn INTEGER NOT NULL,
- tags TEXT NOT NULL,
- flds TEXT NOT NULL,
- sfld TEXT NOT NULL,
- csum INTEGER NOT NULL,
- flags INTEGER NOT NULL,
- data TEXT NOT NULL
- )
- `);
-
- db.exec(`
- CREATE TABLE cards (
- id INTEGER PRIMARY KEY,
- nid INTEGER NOT NULL,
- did INTEGER NOT NULL,
- ord INTEGER NOT NULL,
- mod INTEGER NOT NULL,
- usn INTEGER NOT NULL,
- type INTEGER NOT NULL,
- queue INTEGER NOT NULL,
- due INTEGER NOT NULL,
- ivl INTEGER NOT NULL,
- factor INTEGER NOT NULL,
- reps INTEGER NOT NULL,
- lapses INTEGER NOT NULL,
- left INTEGER NOT NULL,
- odue INTEGER NOT NULL,
- odid INTEGER NOT NULL,
- flags INTEGER NOT NULL,
- data TEXT NOT NULL
- )
- `);
-
- // Insert collection data
- const decks = {
- "1": { id: 1, name: "Default", desc: "" },
- "1234567890123": {
- id: 1234567890123,
- name: "Test Deck",
- desc: "A test deck",
- },
- };
-
- const models = {
- "9876543210987": {
- id: 9876543210987,
- name: "Basic",
- flds: [{ name: "Front" }, { name: "Back" }],
- tmpls: [{ name: "Card 1", qfmt: "{{Front}}", afmt: "{{Back}}" }],
- },
- };
-
- const insertCol = db.prepare(`
- INSERT INTO col (id, crt, mod, scm, ver, dty, usn, ls, conf, models, decks, dconf, tags)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
- insertCol.run(
- 1,
- 1600000000,
- 1600000001000,
- 1600000000000,
- 11,
- 0,
- -1,
- 0,
- "{}",
- JSON.stringify(models),
- JSON.stringify(decks),
- "{}",
- "{}",
- );
-
- // Insert test notes
- const insertNote = db.prepare(`
- INSERT INTO notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
-
- // Note 1: Simple card
- insertNote.run(
- 1000000000001,
- "abc123",
- 9876543210987,
- 1600000001,
- -1,
- " vocabulary test ",
- "Hello\x1fWorld",
- "Hello",
- 12345,
- 0,
- "",
- );
-
- // Note 2: Card with multiple tags
- insertNote.run(
- 1000000000002,
- "def456",
- 9876543210987,
- 1600000002,
- -1,
- " japanese kanji n5 ",
- "日本語\x1fJapanese",
- "日本語",
- 67890,
- 0,
- "",
- );
-
- // Note 3: Card with no tags
- insertNote.run(
- 1000000000003,
- "ghi789",
- 9876543210987,
- 1600000003,
- -1,
- "",
- "Question\x1fAnswer",
- "Question",
- 11111,
- 0,
- "",
- );
-
- // Insert test cards
- const insertCard = db.prepare(`
- INSERT INTO cards (id, nid, did, ord, mod, usn, type, queue, due, ivl, factor, reps, lapses, left, odue, odid, flags, data)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
-
- // Card for note 1 (new card)
- insertCard.run(
- 2000000000001,
- 1000000000001,
- 1234567890123,
- 0,
- 1600000001,
- -1,
- 0,
- 0,
- 1,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- "",
- );
-
- // Card for note 2 (review card)
- insertCard.run(
- 2000000000002,
- 1000000000002,
- 1234567890123,
- 0,
- 1600000002,
- -1,
- 2,
- 2,
- 100,
- 30,
- 2500,
- 5,
- 1,
- 0,
- 0,
- 0,
- 0,
- "",
- );
-
- // Card for note 3 (learning card)
- insertCard.run(
- 2000000000003,
- 1000000000003,
- 1234567890123,
- 0,
- 1600000003,
- -1,
- 1,
- 1,
- 1600100000,
- 1,
- 2500,
- 1,
- 0,
- 1001,
- 0,
- 0,
- 0,
- "",
- );
-
- db.close();
-}
-
-describe("Anki Parser", () => {
- let tempDir: string;
- let testApkgPath: string;
-
- beforeAll(async () => {
- // Create temp directory
- tempDir = join(tmpdir(), `kioku-test-${randomBytes(8).toString("hex")}`);
- await mkdir(tempDir, { recursive: true });
-
- // Create test database
- const dbPath = join(tempDir, "collection.anki2");
- createTestAnkiDb(dbPath);
-
- // Read the database file
- const { readFile } = await import("node:fs/promises");
- const dbBuffer = await readFile(dbPath);
-
- // Create media file (empty JSON object)
- const mediaBuffer = Buffer.from("{}", "utf8");
-
- // Create ZIP with database and media
- const zipEntries = new Map<string, Buffer>();
- zipEntries.set("collection.anki2", dbBuffer);
- zipEntries.set("media", mediaBuffer);
-
- const zipBuffer = createZip(zipEntries);
-
- // Write the .apkg file
- testApkgPath = join(tempDir, "test.apkg");
- await writeFile(testApkgPath, zipBuffer);
- });
-
- afterAll(async () => {
- // Clean up
- await rm(tempDir, { recursive: true, force: true });
- });
-
- describe("listAnkiPackageContents", () => {
- it("should list files in the package", async () => {
- const files = await listAnkiPackageContents(testApkgPath);
-
- expect(files).toContain("collection.anki2");
- expect(files).toContain("media");
- });
- });
-
- describe("parseAnkiPackage", () => {
- let result: AnkiPackage;
-
- beforeAll(async () => {
- result = await parseAnkiPackage(testApkgPath);
- });
-
- it("should parse decks correctly", () => {
- expect(result.decks.length).toBe(2);
-
- const testDeck = result.decks.find((d) => d.name === "Test Deck");
- expect(testDeck).toBeDefined();
- expect(testDeck?.description).toBe("A test deck");
-
- const defaultDeck = result.decks.find((d) => d.name === "Default");
- expect(defaultDeck).toBeDefined();
- });
-
- it("should parse models correctly", () => {
- expect(result.models.length).toBe(1);
-
- const basicModel = result.models[0];
- expect(basicModel).toBeDefined();
- expect(basicModel?.name).toBe("Basic");
- expect(basicModel?.fields).toEqual(["Front", "Back"]);
- expect(basicModel?.templates.length).toBe(1);
- expect(basicModel?.templates[0]?.name).toBe("Card 1");
- expect(basicModel?.templates[0]?.qfmt).toBe("{{Front}}");
- expect(basicModel?.templates[0]?.afmt).toBe("{{Back}}");
- });
-
- it("should parse notes correctly", () => {
- expect(result.notes.length).toBe(3);
-
- // Note 1
- const note1 = result.notes.find((n) => n.guid === "abc123");
- expect(note1).toBeDefined();
- expect(note1?.fields).toEqual(["Hello", "World"]);
- expect(note1?.tags).toEqual(["vocabulary", "test"]);
- expect(note1?.sfld).toBe("Hello");
-
- // Note 2
- const note2 = result.notes.find((n) => n.guid === "def456");
- expect(note2).toBeDefined();
- expect(note2?.fields).toEqual(["日本語", "Japanese"]);
- expect(note2?.tags).toEqual(["japanese", "kanji", "n5"]);
-
- // Note 3 (no tags)
- const note3 = result.notes.find((n) => n.guid === "ghi789");
- expect(note3).toBeDefined();
- expect(note3?.tags).toEqual([]);
- });
-
- it("should parse cards correctly", () => {
- expect(result.cards.length).toBe(3);
-
- // New card
- const card1 = result.cards.find((c) => c.nid === 1000000000001);
- expect(card1).toBeDefined();
- expect(card1?.type).toBe(0); // new
- expect(card1?.reps).toBe(0);
-
- // Review card
- const card2 = result.cards.find((c) => c.nid === 1000000000002);
- expect(card2).toBeDefined();
- expect(card2?.type).toBe(2); // review
- expect(card2?.ivl).toBe(30);
- expect(card2?.reps).toBe(5);
- expect(card2?.lapses).toBe(1);
-
- // Learning card
- const card3 = result.cards.find((c) => c.nid === 1000000000003);
- expect(card3).toBeDefined();
- expect(card3?.type).toBe(1); // learning
- });
-
- it("should throw error for non-existent file", async () => {
- await expect(parseAnkiPackage("/non/existent/file.apkg")).rejects.toThrow(
- "File not found",
- );
- });
-
- it("should throw error for invalid package without database", async () => {
- // Create a ZIP without a database
- const zipEntries = new Map<string, Buffer>();
- zipEntries.set("media", Buffer.from("{}", "utf8"));
- const invalidZip = createZip(zipEntries);
-
- const invalidPath = join(tempDir, "invalid.apkg");
- await writeFile(invalidPath, invalidZip);
-
- await expect(parseAnkiPackage(invalidPath)).rejects.toThrow(
- "No Anki database found",
- );
- });
- });
-
- describe("ZIP extraction", () => {
- it("should handle uncompressed entries", async () => {
- // Create a ZIP with uncompressed entries
- const name = Buffer.from("test.txt", "utf8");
- const data = Buffer.from("Hello, World!", "utf8");
-
- // Local file header (uncompressed)
- const localHeader = Buffer.alloc(30 + name.length);
- localHeader.writeUInt32LE(0x04034b50, 0);
- localHeader.writeUInt16LE(20, 4);
- localHeader.writeUInt16LE(0, 6);
- localHeader.writeUInt16LE(0, 8); // no compression
- localHeader.writeUInt16LE(0, 10);
- localHeader.writeUInt16LE(0, 12);
- localHeader.writeUInt32LE(0, 14);
- localHeader.writeUInt32LE(data.length, 18);
- localHeader.writeUInt32LE(data.length, 22);
- localHeader.writeUInt16LE(name.length, 26);
- localHeader.writeUInt16LE(0, 28);
- name.copy(localHeader, 30);
-
- // Central directory
- const centralEntry = Buffer.alloc(46 + name.length);
- centralEntry.writeUInt32LE(0x02014b50, 0);
- centralEntry.writeUInt16LE(20, 4);
- centralEntry.writeUInt16LE(20, 6);
- centralEntry.writeUInt16LE(0, 8);
- centralEntry.writeUInt16LE(0, 10);
- centralEntry.writeUInt16LE(0, 12);
- centralEntry.writeUInt16LE(0, 14);
- centralEntry.writeUInt32LE(0, 16);
- centralEntry.writeUInt32LE(data.length, 20);
- centralEntry.writeUInt32LE(data.length, 24);
- centralEntry.writeUInt16LE(name.length, 28);
- centralEntry.writeUInt16LE(0, 30);
- centralEntry.writeUInt16LE(0, 32);
- centralEntry.writeUInt16LE(0, 34);
- centralEntry.writeUInt16LE(0, 36);
- centralEntry.writeUInt32LE(0, 38);
- centralEntry.writeUInt32LE(0, 42);
- name.copy(centralEntry, 46);
-
- // End of central directory
- const endRecord = Buffer.alloc(22);
- endRecord.writeUInt32LE(0x06054b50, 0);
- endRecord.writeUInt16LE(0, 4);
- endRecord.writeUInt16LE(0, 6);
- endRecord.writeUInt16LE(1, 8);
- endRecord.writeUInt16LE(1, 10);
- endRecord.writeUInt32LE(centralEntry.length, 12);
- endRecord.writeUInt32LE(localHeader.length + data.length, 16);
- endRecord.writeUInt16LE(0, 20);
-
- const zipBuffer = Buffer.concat([
- localHeader,
- data,
- centralEntry,
- endRecord,
- ]);
-
- const testPath = join(tempDir, "uncompressed.zip");
- await writeFile(testPath, zipBuffer);
-
- const files = await listAnkiPackageContents(testPath);
- expect(files).toContain("test.txt");
- });
- });
-
- describe("mapAnkiToKioku", () => {
- let pkg: AnkiPackage;
-
- beforeAll(async () => {
- pkg = await parseAnkiPackage(testApkgPath);
- });
-
- it("should map decks correctly", () => {
- const result = mapAnkiToKioku(pkg);
-
- // Should have Test Deck (skipping Default deck by default)
- expect(result.length).toBe(1);
-
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
- expect(testDeckData).toBeDefined();
- expect(testDeckData?.deck.description).toBe("A test deck");
- });
-
- it("should include default deck when skipDefaultDeck is false", () => {
- // Create a package with cards in both default and test decks
- const pkgWithDefault: AnkiPackage = {
- notes: [
- {
- id: 1,
- guid: "test1",
- mid: 1,
- mod: 0,
- tags: [],
- fields: ["Front1", "Back1"],
- sfld: "Front1",
- },
- {
- id: 2,
- guid: "test2",
- mid: 1,
- mod: 0,
- tags: [],
- fields: ["Front2", "Back2"],
- sfld: "Front2",
- },
- ],
- cards: [
- {
- id: 1,
- nid: 1,
- did: 1, // Default deck
- ord: 0,
- mod: 0,
- type: 0,
- queue: 0,
- due: 0,
- ivl: 0,
- factor: 0,
- reps: 0,
- lapses: 0,
- },
- {
- id: 2,
- nid: 2,
- did: 2, // Test deck
- ord: 0,
- mod: 0,
- type: 0,
- queue: 0,
- due: 0,
- ivl: 0,
- factor: 0,
- reps: 0,
- lapses: 0,
- },
- ],
- decks: [
- { id: 1, name: "Default", description: "" },
- { id: 2, name: "Test Deck", description: "" },
- ],
- models: [
- {
- id: 1,
- name: "Basic",
- fields: ["Front", "Back"],
- templates: [
- { name: "Card 1", qfmt: "{{Front}}", afmt: "{{Back}}" },
- ],
- },
- ],
- };
-
- // With skipDefaultDeck = true (default), should only have Test Deck
- const resultSkip = mapAnkiToKioku(pkgWithDefault);
- expect(resultSkip.length).toBe(1);
- expect(resultSkip[0]?.deck.name).toBe("Test Deck");
-
- // With skipDefaultDeck = false, should have both decks
- const result = mapAnkiToKioku(pkgWithDefault, { skipDefaultDeck: false });
- const deckNames = result.map((d) => d.deck.name);
- expect(deckNames).toContain("Default");
- expect(deckNames).toContain("Test Deck");
- });
-
- it("should map note fields to front/back", () => {
- const result = mapAnkiToKioku(pkg);
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
-
- expect(testDeckData?.cards.length).toBe(3);
-
- // Card 1: Hello/World
- const card1 = testDeckData?.cards.find((c) => c.front === "Hello");
- expect(card1).toBeDefined();
- expect(card1?.back).toBe("World");
-
- // Card 2: 日本語/Japanese
- const card2 = testDeckData?.cards.find((c) => c.front === "日本語");
- expect(card2).toBeDefined();
- expect(card2?.back).toBe("Japanese");
-
- // Card 3: Question/Answer
- const card3 = testDeckData?.cards.find((c) => c.front === "Question");
- expect(card3).toBeDefined();
- expect(card3?.back).toBe("Answer");
- });
-
- it("should map card states correctly", () => {
- const result = mapAnkiToKioku(pkg);
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
-
- // New card (Hello/World)
- const newCard = testDeckData?.cards.find((c) => c.front === "Hello");
- expect(newCard?.state).toBe(0); // New
-
- // Review card (日本語/Japanese)
- const reviewCard = testDeckData?.cards.find((c) => c.front === "日本語");
- expect(reviewCard?.state).toBe(2); // Review
- expect(reviewCard?.reps).toBe(5);
- expect(reviewCard?.lapses).toBe(1);
-
- // Learning card (Question/Answer)
- const learningCard = testDeckData?.cards.find(
- (c) => c.front === "Question",
- );
- expect(learningCard?.state).toBe(1); // Learning
- });
-
- it("should map scheduling data correctly", () => {
- const result = mapAnkiToKioku(pkg);
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
-
- // Review card has interval of 30 days
- const reviewCard = testDeckData?.cards.find((c) => c.front === "日本語");
- expect(reviewCard?.scheduledDays).toBe(30);
- expect(reviewCard?.stability).toBe(30); // Stability approximates interval
- expect(reviewCard?.elapsedDays).toBe(30);
-
- // New card has no interval
- const newCard = testDeckData?.cards.find((c) => c.front === "Hello");
- expect(newCard?.scheduledDays).toBe(0);
- expect(newCard?.stability).toBe(0);
- expect(newCard?.elapsedDays).toBe(0);
- });
-
- it("should convert Anki factor to FSRS difficulty", () => {
- const result = mapAnkiToKioku(pkg);
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
-
- // Review card has factor 2500 (default ease)
- const reviewCard = testDeckData?.cards.find((c) => c.front === "日本語");
- // Factor 2500 should map to a moderate difficulty (around 5)
- expect(reviewCard?.difficulty).toBeGreaterThan(0);
- expect(reviewCard?.difficulty).toBeLessThan(10);
-
- // New card has factor 0, should have difficulty 0
- const newCard = testDeckData?.cards.find((c) => c.front === "Hello");
- expect(newCard?.difficulty).toBe(0);
- });
-
- it("should set due date for cards", () => {
- const result = mapAnkiToKioku(pkg);
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
-
- // All cards should have valid due dates
- for (const card of testDeckData?.cards || []) {
- expect(card.due).toBeInstanceOf(Date);
- expect(card.due.getTime()).not.toBeNaN();
- }
- });
-
- it("should set lastReview for reviewed cards", () => {
- const result = mapAnkiToKioku(pkg);
- const testDeckData = result.find((d) => d.deck.name === "Test Deck");
-
- // Review card has been reviewed
- const reviewCard = testDeckData?.cards.find((c) => c.front === "日本語");
- expect(reviewCard?.lastReview).toBeInstanceOf(Date);
-
- // New card has not been reviewed
- const newCard = testDeckData?.cards.find((c) => c.front === "Hello");
- expect(newCard?.lastReview).toBeNull();
- });
-
- it("should strip HTML tags from fields", () => {
- // Create a mock package with HTML in fields
- const htmlPkg: AnkiPackage = {
- notes: [
- {
- id: 1,
- guid: "test1",
- mid: 1,
- mod: 0,
- tags: [],
- fields: ["<b>Bold</b> text", "<div>Answer</div><br/>Line 2"],
- sfld: "Bold text",
- },
- ],
- cards: [
- {
- id: 1,
- nid: 1,
- did: 2,
- ord: 0,
- mod: 0,
- type: 0,
- queue: 0,
- due: 0,
- ivl: 0,
- factor: 0,
- reps: 0,
- lapses: 0,
- },
- ],
- decks: [{ id: 2, name: "HTML Test", description: "" }],
- models: [
- {
- id: 1,
- name: "Basic",
- fields: ["Front", "Back"],
- templates: [
- { name: "Card 1", qfmt: "{{Front}}", afmt: "{{Back}}" },
- ],
- },
- ],
- };
-
- const result = mapAnkiToKioku(htmlPkg);
- const card = result[0]?.cards[0];
-
- expect(card?.front).toBe("Bold text");
- expect(card?.back).toBe("Answer\nLine 2");
- });
-
- it("should decode HTML entities", () => {
- const htmlPkg: AnkiPackage = {
- notes: [
- {
- id: 1,
- guid: "test1",
- mid: 1,
- mod: 0,
- tags: [],
- fields: [
- "&lt;code&gt; &amp; &quot;quotes&quot;",
- "&nbsp;spaced&nbsp;",
- ],
- sfld: "code",
- },
- ],
- cards: [
- {
- id: 1,
- nid: 1,
- did: 2,
- ord: 0,
- mod: 0,
- type: 0,
- queue: 0,
- due: 0,
- ivl: 0,
- factor: 0,
- reps: 0,
- lapses: 0,
- },
- ],
- decks: [{ id: 2, name: "Entity Test", description: "" }],
- models: [
- {
- id: 1,
- name: "Basic",
- fields: ["Front", "Back"],
- templates: [
- { name: "Card 1", qfmt: "{{Front}}", afmt: "{{Back}}" },
- ],
- },
- ],
- };
-
- const result = mapAnkiToKioku(htmlPkg);
- const card = result[0]?.cards[0];
-
- expect(card?.front).toBe('<code> & "quotes"');
- expect(card?.back).toBe("spaced");
- });
-
- it("should handle empty package", () => {
- const emptyPkg: AnkiPackage = {
- notes: [],
- cards: [],
- decks: [],
- models: [],
- };
-
- const result = mapAnkiToKioku(emptyPkg);
- expect(result).toEqual([]);
- });
-
- it("should skip cards with missing notes", () => {
- const incompletePkg: AnkiPackage = {
- notes: [], // No notes
- cards: [
- {
- id: 1,
- nid: 999, // Non-existent note
- did: 2,
- ord: 0,
- mod: 0,
- type: 0,
- queue: 0,
- due: 0,
- ivl: 0,
- factor: 0,
- reps: 0,
- lapses: 0,
- },
- ],
- decks: [{ id: 2, name: "Test", description: "" }],
- models: [],
- };
-
- const result = mapAnkiToKioku(incompletePkg);
- // Should have deck but no cards
- expect(result).toEqual([]);
- });
- });
-});