diff --git a/packages/cards-core/package.json b/packages/cards-core/package.json deleted file mode 100644 index 4c9e2364d..000000000 --- a/packages/cards-core/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@mana/cards-core", - "version": "0.1.0", - "private": true, - "sideEffects": false, - "description": "Pure utilities for the Cardecky product: types, FSRS wrapper, Cloze parser, Markdown render. Consumed by both the mana cards module and the cardecky.mana.how standalone app.", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "type-check": "tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest", - "clean": "rm -rf dist" - }, - "dependencies": { - "@mana/local-store": "workspace:*", - "@mana/shared-privacy": "workspace:*", - "isomorphic-dompurify": "^3.7.1", - "marked": "^17.0.5", - "ts-fsrs": "^5.3.2" - }, - "devDependencies": { - "@types/node": "^24.10.1", - "typescript": "^5.9.3", - "vitest": "^4.1.3" - } -} diff --git a/packages/cards-core/src/card-reviews.test.ts b/packages/cards-core/src/card-reviews.test.ts deleted file mode 100644 index 86db2bd73..000000000 --- a/packages/cards-core/src/card-reviews.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { subIndexesFor } from './card-reviews'; - -describe('subIndexesFor', () => { - it('basic → [0]', () => { - expect(subIndexesFor({ type: 'basic', fields: { front: 'a', back: 'b' } })).toEqual([0]); - }); - - it('type-in → [0]', () => { - expect(subIndexesFor({ type: 'type-in', fields: { front: 'a', back: 'b' } })).toEqual([0]); - }); - - it('basic-reverse → [0, 1]', () => { - expect(subIndexesFor({ type: 'basic-reverse', fields: { front: 'a', back: 'b' } })).toEqual([ - 0, 1, - ]); - }); - - it('cloze → cluster indexes', () => { - expect( - subIndexesFor({ - type: 'cloze', - fields: { text: '{{c1::Berlin}} ist Hauptstadt von {{c2::Deutschland}}.' }, - }) - ).toEqual([1, 2]); - }); - - it('cloze with no clusters falls back to [1]', () => { - expect(subIndexesFor({ type: 'cloze', fields: { text: '' } })).toEqual([1]); - expect(subIndexesFor({ type: 'cloze', fields: { text: 'no clozes here' } })).toEqual([1]); - }); - - it('cloze deduplicates repeated clusters', () => { - expect( - subIndexesFor({ - type: 'cloze', - fields: { text: '{{c1::a}} und {{c1::b}} und {{c2::c}}' }, - }) - ).toEqual([1, 2]); - }); - - it('phase-2 types stub to [0] (no crash)', () => { - expect(subIndexesFor({ type: 'image-occlusion', fields: {} })).toEqual([0]); - expect(subIndexesFor({ type: 'audio', fields: {} })).toEqual([0]); - expect(subIndexesFor({ type: 'multiple-choice', fields: {} })).toEqual([0]); - }); -}); diff --git a/packages/cards-core/src/card-reviews.ts b/packages/cards-core/src/card-reviews.ts deleted file mode 100644 index 0c1186cd4..000000000 --- a/packages/cards-core/src/card-reviews.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Per-card-type review fan-out. - * - * Different card types produce different numbers of learnable units: - * - basic / type-in: one (subIndex 0) - * - basic-reverse: two (0=front→back, 1=back→front) - * - cloze: one per distinct cluster (subIndex = cluster idx) - */ - -import { clusterIndexes } from './cloze'; -import type { CardFields, CardType } from './types'; - -export function subIndexesFor(input: { type: CardType; fields: CardFields }): number[] { - switch (input.type) { - case 'basic': - case 'type-in': - return [0]; - case 'basic-reverse': - return [0, 1]; - case 'cloze': { - const text = input.fields.text ?? ''; - const idx = clusterIndexes(text); - return idx.length > 0 ? idx : [1]; - } - case 'image-occlusion': - case 'audio': - case 'multiple-choice': - return [0]; - } -} diff --git a/packages/cards-core/src/cloze.test.ts b/packages/cards-core/src/cloze.test.ts deleted file mode 100644 index 11c08e523..000000000 --- a/packages/cards-core/src/cloze.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { tokenize, clusterIndexes, clusters, renderCloze } from './cloze'; - -describe('tokenize', () => { - it('splits plain text and clusters', () => { - const tokens = tokenize('A {{c1::B}} C'); - expect(tokens).toEqual([ - { kind: 'text', value: 'A ' }, - { kind: 'cluster', index: 1, answer: 'B', hint: undefined }, - { kind: 'text', value: ' C' }, - ]); - }); - - it('captures hints', () => { - const [t] = tokenize('{{c1::Berlin::Hauptstadt}}'); - expect(t).toEqual({ kind: 'cluster', index: 1, answer: 'Berlin', hint: 'Hauptstadt' }); - }); - - it('handles multiple clusters in one source', () => { - const tokens = tokenize('{{c1::Berlin}} ist die Hauptstadt von {{c2::Deutschland}}.'); - const indexes = tokens.filter((t) => t.kind === 'cluster').map((t) => (t as any).index); - expect(indexes).toEqual([1, 2]); - }); - - it('passes through unmatched braces', () => { - const tokens = tokenize('foo {bar} baz'); - expect(tokens).toEqual([{ kind: 'text', value: 'foo {bar} baz' }]); - }); - - it('survives multi-line input', () => { - const tokens = tokenize('Line 1\n{{c1::x}}\nLine 3'); - expect(tokens.length).toBe(3); - expect((tokens[1] as any).answer).toBe('x'); - }); -}); - -describe('clusterIndexes', () => { - it('returns ascending unique indexes', () => { - expect(clusterIndexes('{{c2::a}} {{c1::b}} {{c2::c}} {{c3::d}}')).toEqual([1, 2, 3]); - }); - - it('returns empty for plain text', () => { - expect(clusterIndexes('no clozes here')).toEqual([]); - }); -}); - -describe('clusters', () => { - it('groups answers under their cluster', () => { - const result = clusters('{{c1::a}} {{c1::b}} {{c2::c}}'); - expect(result).toEqual([ - { index: 1, answers: ['a', 'b'] }, - { index: 2, answers: ['c'] }, - ]); - }); -}); - -describe('renderCloze', () => { - it('blanks the hidden cluster on front, reveals on back', () => { - const r = renderCloze('{{c1::Berlin}} ist die Hauptstadt von {{c2::Deutschland}}.', 1); - expect(r.front).toContain('[…]'); - expect(r.front).toContain('Deutschland'); - expect(r.back).toContain('Berlin'); - expect(r.back).toContain('cloze-active'); - expect(r.answer).toBe('Berlin'); - }); - - it('uses hint when present', () => { - const r = renderCloze('{{c1::Berlin::Hauptstadt}} ist eine Stadt.', 1); - expect(r.front).toContain('[Hauptstadt]'); - }); - - it('blanks every occurrence of the hidden cluster', () => { - const r = renderCloze('{{c1::x}} und {{c1::x}}', 1); - const blanks = r.front.match(/cloze-blank/g) ?? []; - expect(blanks.length).toBe(2); - }); - - it('escapes HTML in user content', () => { - const r = renderCloze('{{c1::