/** * Laden und Prüfen des Glossars. * * Wie die Erklärungen eigener redaktioneller Inhalt, wie diese in einer * eigenen Datei: * * - Entwicklung: `/content/glossar.json` * - Gepackt: `/resources/glossar.json` * * Fehlt es oder ist es unbrauchbar, läuft die Anwendung ohne. Der Lernbetrieb * hängt nicht daran. * * Die Prüfung hier betrifft nur die Form. Ob eine Fundstelle im Gesetz * existiert, klärt `data-pipeline/pruefe_glossar.py` vor der Auslieferung – * dort steht der Gesetzestext zur Verfügung, hier nicht. */ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { app } from 'electron'; import { GESETZE, type Fundstelle, type Gesetzeskuerzel } from '../shared/erklaerungen'; import { GLOSSAR_LEER, type Begriffsart, type Glossar, type Glossareintrag, } from '../shared/glossar'; import { sucheAufwaerts } from './katalog'; const DATEI = 'glossar.json'; const NORM_MUSTER = /^(§ \d+[a-z]?|Anlage( [0-9IVX]+)?)$/u; const GLIEDERUNG_MUSTER = /^[0-9]{1,3}[a-z]?$/u; const BUCHSTABE_MUSTER = /^[a-z]{1,2}$/u; const ARTEN: readonly Begriffsart[] = ['begriff', 'abkuerzung']; function ungueltig(nachricht: string): never { throw new Error(`Glossar 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 kuerteText(wert: unknown, pfad: string): string | undefined { return wert === undefined ? undefined : pflichttext(wert, pfad); } function textliste(wert: unknown, pfad: string): string[] { if (!Array.isArray(wert)) { ungueltig(`${pfad} fehlt oder ist keine Liste.`); } return (wert as readonly unknown[]).map((eintrag, i) => pflichttext(eintrag, `${pfad}[${String(i)}]`), ); } function gliederung(wert: unknown, pfad: string, muster: RegExp): string | undefined { if (wert === undefined) { return undefined; } if (typeof wert !== 'string' || !muster.test(wert)) { ungueltig(`${pfad} hat eine unerwartete Form.`); } return wert; } /** Baut ein Feld nur ein, wenn es einen Wert hat (`exactOptionalPropertyTypes`). */ function optional( schluessel: K, wert: string | undefined, ): Record | Record { return wert === undefined ? {} : ({ [schluessel]: wert } as Record); } function fundstelleLesen(roh: unknown, pfad: string): Fundstelle { const o = objekt(roh, pfad); const gesetz = o['gesetz']; if (typeof gesetz !== 'string' || !(GESETZE as readonly string[]).includes(gesetz)) { ungueltig(`${pfad}.gesetz ist kein bekanntes Gesetz.`); } const norm = pflichttext(o['norm'], `${pfad}.norm`); if (!NORM_MUSTER.test(norm)) { ungueltig(`${pfad}.norm muss „§ 12“ oder „Anlage 1“ lauten.`); } return { gesetz: gesetz as Gesetzeskuerzel, norm, ...optional('absatz', gliederung(o['absatz'], `${pfad}.absatz`, GLIEDERUNG_MUSTER)), ...optional('nummer', gliederung(o['nummer'], `${pfad}.nummer`, GLIEDERUNG_MUSTER)), ...optional('buchstabe', gliederung(o['buchstabe'], `${pfad}.buchstabe`, BUCHSTABE_MUSTER)), ...optional('satz', gliederung(o['satz'], `${pfad}.satz`, GLIEDERUNG_MUSTER)), ...optional('stelle', kuerteText(o['stelle'], `${pfad}.stelle`)), }; } function eintragLesen(roh: unknown, pfad: string): Glossareintrag { const o = objekt(roh, pfad); const art = o['art']; if (typeof art !== 'string' || !(ARTEN as readonly string[]).includes(art)) { ungueltig(`${pfad}.art muss „begriff“ oder „abkuerzung“ sein.`); } /* Leer ist zulässig: Der Eintrag erscheint dann nur in der Glossaransicht und wird an keiner Frage angezeigt. Bei Abkürzungen, die nur in den Erklärungen vorkommen, ist genau das richtig – eine erfundene Wortform wäre schlechter. Die Begriffssuche übergeht solche Einträge. */ const varianten = textliste(o['varianten'], `${pfad}.varianten`); const fundstellenRoh = o['fundstellen']; if (!Array.isArray(fundstellenRoh)) { ungueltig(`${pfad}.fundstellen fehlt oder ist keine Liste.`); } const fundstellen = (fundstellenRoh as readonly unknown[]).map((eintrag, i) => fundstelleLesen(eintrag, `${pfad}.fundstellen[${String(i)}]`), ); const grund = kuerteText(o['ohneFundstelleGrund'], `${pfad}.ohneFundstelleGrund`); if (fundstellen.length === 0 && grund === undefined) { ungueltig(`${pfad}: ohne Fundstelle muss ein Grund angegeben sein.`); } const text = kuerteText(o['text'], `${pfad}.text`); const siehe = o['siehe'] === undefined ? undefined : textliste(o['siehe'], `${pfad}.siehe`); return { begriff: pflichttext(o['begriff'], `${pfad}.begriff`), art: art as Begriffsart, kurz: pflichttext(o['kurz'], `${pfad}.kurz`), varianten, fundstellen, ...(text === undefined ? {} : { text }), ...(grund === undefined ? {} : { ohneFundstelleGrund: grund }), ...(siehe === undefined ? {} : { siehe }), }; } function glossarValidieren(roh: unknown): Glossar { 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 gesetzesstandRoh = objekt(meta['gesetzesstand'], 'meta.gesetzesstand'); const gesetzesstand: Record = {}; for (const [kuerzel, stand] of Object.entries(gesetzesstandRoh)) { gesetzesstand[kuerzel] = pflichttext(stand, `meta.gesetzesstand.${kuerzel}`); } const eintraegeRoh = o['eintraege']; if (!Array.isArray(eintraegeRoh)) { ungueltig('eintraege fehlt oder ist keine Liste.'); } const eintraege = (eintraegeRoh as readonly unknown[]).map((eintrag, i) => eintragLesen(eintrag, `eintraege[${String(i)}]`), ); /* Doppelte Begriffe wären in der Anzeige verwirrend und in „siehe“ mehrdeutig. */ const gesehen = new Set(); for (const eintrag of eintraege) { if (gesehen.has(eintrag.begriff)) { ungueltig(`„${eintrag.begriff}“ kommt mehrfach vor.`); } gesehen.add(eintrag.begriff); } return { meta: { version, stand: pflichttext(meta['stand'], 'meta.stand'), gesetzesstand, hinweis: pflichttext(meta['hinweis'], 'meta.hinweis'), }, eintraege, }; } /** Pfad der Glossardatei – gepackt wie ungepackt. */ export function glossarPfad(): 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: Glossar | null = null; export function glossarLaden(): Glossar { if (speicher !== null) { return speicher; } const pfad = glossarPfad(); if (!existsSync(pfad)) { speicher = GLOSSAR_LEER; return speicher; } try { speicher = glossarValidieren(JSON.parse(readFileSync(pfad, 'utf8'))); } catch (fehler: unknown) { console.error('Glossar konnte nicht gelesen werden:', fehler); speicher = GLOSSAR_LEER; } return speicher; }