/** * Laden und Prüfen der Themengliederung. * * Wie Erklärungen, Glossar und Normtexte eine eigene Datei: * * - Entwicklung: `/content/themen.json` * - Gepackt: `/resources/themen.json` * * Fehlt sie oder ist sie unbrauchbar, läuft die Anwendung ohne. Dann steht in * den Kapiteln II bis IV nur der Knopf für das ganze Kapitel – also genau der * Zustand vor 0.22.0. * * Die Prüfung hier betrifft nur die Form. Dass jede Frage der Kapitel II bis * IV in genau einer Gruppe liegt, sichert `data-pipeline/themen_bauen.py` vor * der Auslieferung – dort steht der Katalog zur Verfügung, hier nicht. */ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { app } from 'electron'; import { THEMEN_LEER, type Themen, type Themengruppe } from '../shared/themen'; import { sucheAufwaerts } from './katalog'; const DATEI = 'themen.json'; function ungueltig(nachricht: string): never { throw new Error(`Themen ungültig: ${nachricht}`); } function objekt(wert: unknown, pfad: string): Record { if (typeof wert !== 'object' || wert === null || Array.isArray(wert)) { ungueltig(`${pfad} ist kein Objekt.`); } return wert as Record; } function pflichttext(wert: unknown, pfad: string): string { if (typeof wert !== 'string' || wert.trim().length === 0) { ungueltig(`${pfad} fehlt oder ist leer.`); } return wert; } function gruppeLesen(roh: unknown, pfad: string): Themengruppe { const o = objekt(roh, pfad); const fragenRoh = o['fragen']; if (!Array.isArray(fragenRoh)) { ungueltig(`${pfad}.fragen fehlt oder ist keine Liste.`); } const fragen = (fragenRoh as readonly unknown[]).map((eintrag, i) => pflichttext(eintrag, `${pfad}.fragen[${String(i)}]`), ); if (fragen.length === 0) { /* Eine leere Gruppe erschiene als Knopf, der eine Sitzung ohne Fragen startet – schlimmer als eine fehlende Gruppe. */ ungueltig(`${pfad}.fragen ist leer.`); } return { id: pflichttext(o['id'], `${pfad}.id`), kapitel: pflichttext(o['kapitel'], `${pfad}.kapitel`), titel: pflichttext(o['titel'], `${pfad}.titel`), fragen, }; } function themenValidieren(roh: unknown): Themen { const o = objekt(roh, 'Die Datei'); const meta = objekt(o['meta'], 'meta'); const version = meta['version']; if (typeof version !== 'number' || !Number.isInteger(version) || version < 1) { ungueltig('meta.version fehlt oder ist keine ganze Zahl ab 1.'); } const gruppenRoh = o['gruppen']; if (!Array.isArray(gruppenRoh)) { ungueltig('gruppen fehlt oder ist keine Liste.'); } const gruppen = (gruppenRoh as readonly unknown[]).map((gruppe, i) => gruppeLesen(gruppe, `gruppen[${String(i)}]`), ); /* Doppelte Kennungen wären in der Anzeige mehrdeutig, doppelte Fragen hießen, dass ein Thema Fragen eines anderen mitzieht. */ const kennungen = new Set(); const fragen = new Set(); for (const gruppe of gruppen) { if (kennungen.has(gruppe.id)) { ungueltig(`Die Gruppenkennung „${gruppe.id}“ kommt mehrfach vor.`); } kennungen.add(gruppe.id); for (const frageId of gruppe.fragen) { if (fragen.has(frageId)) { ungueltig(`${frageId} steht in mehr als einer Gruppe.`); } fragen.add(frageId); } } return { meta: { version, stand: pflichttext(meta['stand'], 'meta.stand'), hinweis: pflichttext(meta['hinweis'], 'meta.hinweis'), }, gruppen, }; } /** Pfad der Themendatei – gepackt wie ungepackt. */ export function themenPfad(): string { if (app.isPackaged) { return join(process.resourcesPath, DATEI); } const relativ = join('content', DATEI); for (const start of [app.getAppPath(), __dirname, process.cwd()]) { const treffer = sucheAufwaerts(start, relativ); if (treffer) { return treffer; } } return join(app.getAppPath(), '..', relativ); } let speicher: Themen | null = null; export function themenLaden(): Themen { if (speicher !== null) { return speicher; } const pfad = themenPfad(); if (!existsSync(pfad)) { speicher = THEMEN_LEER; return speicher; } try { speicher = themenValidieren(JSON.parse(readFileSync(pfad, 'utf8'))); } catch (fehler: unknown) { console.error('Themen konnten nicht gelesen werden:', fehler); speicher = THEMEN_LEER; } return speicher; } /** Gegenstück für Tests. */ export function themenZuruecksetzen(): void { speicher = null; }