mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-28 15:44:34 +00:00
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>
This commit is contained in:
parent
4708c69d9c
commit
f7470d333c
113 changed files with 25817 additions and 9 deletions
145
tools/extract/mluvnice.mjs
Normal file
145
tools/extract/mluvnice.mjs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { parseArray, trimNulls } from '../lib/cparse.mjs';
|
||||
|
||||
/**
|
||||
* Czech grammar ("Mluvnice"), 20 exercises in one lesson.
|
||||
*
|
||||
* Unlike the other subjects, mluvnice.c passes its instructions as literal
|
||||
* Czech strings rather than i18n keys, so they are copied verbatim from the
|
||||
* `build_mluvnice_exercise_page` switch below.
|
||||
*/
|
||||
|
||||
/** One row per `case` in `build_mluvnice_exercise_page`, in the same order. */
|
||||
const EXERCISES = [
|
||||
['typed', 'Pravopis – i/y po obojetných souhláskách', 'Doplňte i/í nebo y/ý.', null],
|
||||
['choice', 'Pravopis – velká/malá písmena', 'Vyberte správnou variantu.', null],
|
||||
['typed', 'Slovní druhy', 'Určete slovní druh podtržených slov.',
|
||||
'Do pole napište název slovního druhu.'],
|
||||
['typed', 'Mluvnické kategorie – podstatná jména',
|
||||
'Určete rod, číslo a pád podtržených slov.',
|
||||
'Vyplňte tři pole: rod, číslo a pád.'],
|
||||
['typed', 'Mluvnické kategorie – slovesa',
|
||||
'Určete osobu, číslo, čas a způsob u těchto sloves.',
|
||||
'U rozkazovacího a podmiňovacího způsobu napište do pole čas „neurčitý“.'],
|
||||
['reveal', 'Slovotvorba', 'Rozeberte slova na morfémy.',
|
||||
'Určete předponu, kořen, příponu a koncovku.'],
|
||||
['typed', 'Větné členy', 'Určete větný člen podtržených výrazů.', null],
|
||||
['typed', 'Druhy vedlejších vět', 'Určete druh vedlejší věty.', null],
|
||||
['typed', 'Synonyma', 'Ke každému slovu napište synonymum.', null],
|
||||
['typed', 'Antonyma', 'Napište opak.', null],
|
||||
['typed', 'Pravopis – s/z na začátku slova', 'Vyberte správnou předponu s-/z-.', null],
|
||||
['reveal', 'Skladba – najděte chybu',
|
||||
'Každá věta obsahuje jednu chybu. Najděte ji a opravte.', null],
|
||||
['typed', 'Tvarosloví – správný tvar', 'Dejte slovo do správného tvaru.', null],
|
||||
['choice', 'Výběr ze čtyř možností', 'Vyberte správnou odpověď.', null],
|
||||
['reveal', 'Přímá a nepřímá řeč', 'Přepište přímou řeč na nepřímou.', null],
|
||||
['reveal', 'Slovní zásoba – rozdíly', 'Vysvětlete rozdíl mezi těmito dvojicemi slov.', null],
|
||||
['reveal', 'Interpunkce', 'Doplňte čárky tam, kde patří.', null],
|
||||
['typed', 'Obrazná pojmenování',
|
||||
'Určete, o jaký druh obrazného pojmenování jde.', null],
|
||||
['typed', 'Stylistika – slohové útvary',
|
||||
'Přiřaďte ukázku ke správnému slohovému útvaru.', null],
|
||||
['choice', 'Souhrnné opakování', 'Vyberte správnou odpověď.', null],
|
||||
];
|
||||
|
||||
/** Only exercise 14 ships per-question explanations. */
|
||||
const CHOICE_EXPLANATIONS = { 14: 'e14_expl' };
|
||||
|
||||
/** `MluvItem`: prompt, shown, nblank, hint[5], answers[5]. */
|
||||
function fieldsItem(row, label) {
|
||||
const [prompt, shown, nblank, hints, answers] = row;
|
||||
if (typeof nblank !== 'number' || nblank < 1) {
|
||||
throw new Error(`${label}: expected at least one blank, got ${nblank}`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'fields',
|
||||
prompt: String(prompt),
|
||||
fields: Array.from({ length: nblank }, (_, i) => ({
|
||||
label: String(hints[i] ?? ''),
|
||||
answers: String(answers[i] ?? '').split('|').map((a) => a.trim()).filter(Boolean),
|
||||
})),
|
||||
...(shown ? { solution: String(shown) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function choiceItem(row, expl, label) {
|
||||
const [prompt, options, nOptions, correct] = row;
|
||||
const list = trimNulls(options ?? []).map(String);
|
||||
const n = typeof nOptions === 'number' ? nOptions : list.length;
|
||||
|
||||
if (typeof correct !== 'number' || correct < 0 || correct >= n) {
|
||||
throw new Error(`${label}: correct index ${correct} out of range for ${n} options`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'choice',
|
||||
prompt: String(prompt),
|
||||
options: list.slice(0, n),
|
||||
correct,
|
||||
...(expl ? { hint: String(expl) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function extractMluvnice(desktopDir) {
|
||||
const src = readFileSync(`${desktopDir}/mluvnice.c`, 'utf8');
|
||||
const names = parseArray(src, 'mluv_names').map(String);
|
||||
|
||||
if (names.length !== EXERCISES.length) {
|
||||
throw new Error(`mluv_names has ${names.length} entries but the extractor knows ${EXERCISES.length}`);
|
||||
}
|
||||
|
||||
const tasks = EXERCISES.map(([type, heading, subtitle, note], index) => {
|
||||
const number = index + 1;
|
||||
const label = `mluvnice ex${number}`;
|
||||
const base = {
|
||||
number,
|
||||
// Short label for the map bubble; the page shows the full heading.
|
||||
title: names[index],
|
||||
heading,
|
||||
subtitleKey: null,
|
||||
subtitle,
|
||||
...(note ? { note } : {}),
|
||||
};
|
||||
|
||||
if (type === 'choice') {
|
||||
const explName = CHOICE_EXPLANATIONS[number];
|
||||
const expls = explName ? parseArray(src, explName).map(String) : [];
|
||||
return {
|
||||
...base,
|
||||
items: parseArray(src, `e${number}_qs`)
|
||||
.map((row, i) => choiceItem(row, expls[i], `${label} q${i + 1}`)),
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'reveal') {
|
||||
return {
|
||||
...base,
|
||||
items: parseArray(src, `e${number}_items`).map(([prompt, solution]) => ({
|
||||
kind: 'reveal',
|
||||
prompt: String(prompt),
|
||||
solution: String(solution),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
items: parseArray(src, `e${number}_items`)
|
||||
.map((row, i) => fieldsItem(row, `${label} item ${i + 1}`)),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'mluvnice',
|
||||
subject: 'czech',
|
||||
number: 1,
|
||||
title: { cs: 'Mluvnice', en: 'Czech grammar' },
|
||||
subtitle: {
|
||||
cs: 'Vyberte cvičení a dokončete je.',
|
||||
en: 'Pick an exercise and complete it.',
|
||||
},
|
||||
slides: [],
|
||||
tasks,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue