Graduately/tools/extract/deutsch.mjs
FBukovina f7470d333c Add the Next.js web app and shared lesson content.
Move lessons out of the C sources into JSON so the web client can use the same material as the desktop app, and point release builds at desktop/.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 16:06:15 +02:00

249 lines
7.4 KiB
JavaScript

import { readFileSync } from 'node:fs';
import { parseArray, trimNulls } from '../lib/cparse.mjs';
/**
* Deutsch Unit 1 ("Neue Freunde").
*
* The desktop app defines each exercise's data as a `const` array next to its
* `build_*` function; the task order and titles come from
* `build_exercise_page` in main.c and `u1_ex_names` in globals.c.
*/
const opt = (v) => (v === null || v === undefined || v === '' ? undefined : String(v));
/** Split the pipe-separated accepted-answer lists used by TypedQ. */
const alternatives = (s) => String(s).split('|').map((a) => a.trim()).filter(Boolean);
/** `{before, after, answer}` becomes a single-gap fill sentence. */
function verbFill(row, meaning, extra = {}) {
const [before, after, answer] = row;
return {
kind: 'fill',
segments: [before ?? '', after ?? ''],
answers: [String(answer)],
input: 'select',
...(meaning ? { meaning: String(meaning) } : {}),
...extra,
};
}
export function extractDeutschUnit1(desktopDir) {
const u1 = readFileSync(`${desktopDir}/exercise_u1.c`, 'utf8');
const common = readFileSync(`${desktopDir}/exercise_common.c`, 'utf8');
const tasks = [];
/* 1 — Dialog: three dialogues sharing one word pool, one gap per line. */
{
const meanings = parseArray(common, 'ex1_meaning');
const pool = parseArray(common, 'ex1_pool');
const dialogues = [
['Sich vorstellen', parseArray(common, 'ex1_d1')],
['Begrüßung', parseArray(common, 'ex1_d2')],
['Verabschiedung', parseArray(common, 'ex1_d3')],
];
let i = 0;
const items = dialogues.map(([title, rows]) => ({
kind: 'dialogue',
title,
rows: rows.map(([speaker, before, answer, after]) => ({
speaker: speaker ?? '',
before: before ?? '',
answer: String(answer),
after: after ?? '',
meaning: opt(meanings[i++]),
})),
}));
tasks.push({ number: 1, title: 'Dialog', subtitleKey: 'sub_dialog', pool, items });
}
/* 2 — Sätze bilden */
{
const meanings = parseArray(common, 'ex2_meaning');
tasks.push({
number: 2,
title: 'Sätze bilden',
subtitleKey: 'sub_assembly',
items: parseArray(common, 'ex2_items').map(([prompt, words], i) => ({
kind: 'assembly',
prompt: opt(prompt) ?? '',
words: trimNulls(words).map(String),
meaning: opt(meanings[i]),
})),
});
}
/* 3 + 9 — multiple choice */
const choiceTask = (number, title, subtitleKey, qsName, meanName) => {
const meanings = parseArray(u1, meanName);
return {
number,
title,
subtitleKey,
items: parseArray(u1, qsName).map(([prompt, options, n, correct], i) => ({
kind: 'choice',
prompt: String(prompt),
options: trimNulls(options).map(String).slice(0, n),
correct,
meaning: opt(meanings[i]),
})),
};
};
tasks.push(choiceTask(3, 'Was ist richtig?', 'sub_choice_num', 'ex3_questions', 'ex3_meaning'));
/* 4 — Freie Antwort */
tasks.push({
number: 4,
title: 'Freie Antwort',
subtitleKey: 'sub_free',
items: parseArray(u1, 'ex4_questions').map(([question, questionCs, sample, sampleCs]) => ({
kind: 'free',
question: String(question),
questionCs: opt(questionCs),
sample: String(sample),
sampleCs: opt(sampleCs),
})),
});
/* 5 — Zahlen: digits shown as the clue, pick the German word. */
{
const meanings = parseArray(u1, 'ex5_meaning');
tasks.push({
number: 5,
title: 'Zahlen',
subtitleKey: 'sub_zahlen',
pool: parseArray(u1, 'ex5_pool'),
items: parseArray(u1, 'ex5_data').map(([digits, answer], i) => ({
kind: 'fill',
segments: ['', ''],
answers: [String(answer)],
input: 'select',
clue: String(digits),
meaning: opt(meanings[i]),
})),
});
}
/* 6 — Wie viel?: the emoji is repeated `count` times as the clue. */
{
const meanings = parseArray(u1, 'ex6_meaning');
tasks.push({
number: 6,
title: 'Wie viel?',
subtitleKey: 'sub_wieviel',
pool: parseArray(u1, 'ex6_pool'),
items: parseArray(u1, 'ex6_data').map(([emoji, count, noun, answer], i) => ({
kind: 'fill',
segments: ['', ` ${noun}`],
answers: [String(answer)],
input: 'select',
emoji: Array.from({ length: count }, () => emoji).join(' '),
meaning: opt(meanings[i]),
})),
});
}
/* 7 — Zahlenreihe */
{
const meanings = parseArray(u1, 'ex7_meaning');
tasks.push({
number: 7,
title: 'Zahlenreihe',
subtitleKey: 'sub_reihe',
pool: parseArray(u1, 'ex7_pool'),
items: parseArray(u1, 'ex7_data').map((row, i) => verbFill(row, meanings[i])),
});
}
/* 8 — Verb einsetzen */
{
const meanings = parseArray(u1, 'ex8_meaning');
tasks.push({
number: 8,
title: 'Verb einsetzen',
subtitleKey: 'sub_verb',
pool: parseArray(u1, 'ex8_pool'),
items: parseArray(u1, 'ex8_data').map((row, i) => verbFill(row, meanings[i])),
});
}
tasks.push(choiceTask(9, 'Wer? Wie? Wo?', 'sub_wer', 'ex9_questions', 'ex9_meaning'));
/* 10 — Wörter trennen: split a run-together string into words. */
{
const meanings = parseArray(u1, 'ex10_meaning');
tasks.push({
number: 10,
title: 'Wörter trennen',
subtitleKey: 'sub_assembly',
items: parseArray(u1, 'ex10_items').map(([prompt, words], i) => ({
kind: 'assembly',
prompt: opt(prompt) ?? '',
words: trimNulls(words).map(String),
meaning: opt(meanings[i]),
})),
});
}
/* 11 + 13 — sort items into groups */
const assignTask = (number, title, subtitleKey, itemsName, groupsName, meanName) => {
const meanings = parseArray(u1, meanName);
return {
number,
title,
subtitleKey,
items: [{
kind: 'assign',
groups: parseArray(u1, groupsName).map(String),
items: parseArray(u1, itemsName).map(([emoji, label, group], i) => ({
emoji: emoji ?? null,
label: String(label),
group,
meaning: opt(meanings[i]),
})),
}],
};
};
tasks.push(assignTask(11, 'Grußformen', 'sub_gruss', 'ex11_items', 'ex11_groups', 'ex11_meaning'));
/* 12 — Was macht er/sie? */
{
const meanings = parseArray(u1, 'ex12_meaning');
tasks.push({
number: 12,
title: 'Was macht er/sie?',
subtitleKey: 'sub_bild',
pool: parseArray(u1, 'ex12_pool'),
items: parseArray(u1, 'ex12_data').map(([emoji, before, after, answer], i) =>
verbFill([before, after, answer], meanings[i], { emoji: String(emoji) })),
});
}
tasks.push(assignTask(13, 'Länder', 'sub_land', 'ex13_items', 'ex13_groups', 'ex13_meaning'));
/* Vocabulary trainer branch: one section per textbook page. */
const vocab = parseArray(u1, 'u1_trans_sections').map(([header, arrayRef]) => {
const name = arrayRef?.__c;
if (!name) throw new Error(`vocab section '${header}' has no data array`);
return {
header: String(header),
items: parseArray(u1, name).map(([prompt, answers, meaning]) => ({
prompt: String(prompt),
answers: alternatives(answers),
meaning: opt(meaning),
})),
};
});
return {
id: 'unit-01',
subject: 'deutsch',
number: 1,
title: { cs: 'Neue Freunde', en: 'Neue Freunde' },
slides: [],
tasks,
vocab,
};
}