/** * Laden und Prüfen der mitgelieferten Normtexte. * * Wie Erklärungen und Glossar eine eigene Datei: * * - Entwicklung: `/content/normtexte.json` * - Gepackt: `/resources/normtexte.json` * * Fehlt sie oder ist sie unbrauchbar, läuft die Anwendung ohne. Die * Fundstellen stehen dann wie bisher als Zitat da – der Lernbetrieb hängt * nicht daran. * * Erzeugt wird die Datei von `data-pipeline/normtexte_bauen.py` aus * `content/gesetze/index.json`. Die Prüfung hier betrifft nur die Form; dass * der Wortlaut der amtliche ist, sichert die Pipeline, die ihn unverändert * übernimmt und die verlustfreie Zerlegung der Anlagen nachrechnet. */ import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { app } from 'electron'; import { NORMTEXTE_LEER, type Anlagenblock, type Gesetzestext, type Normtext, type Normtexte, } from '../shared/normtexte'; import { sucheAufwaerts } from './katalog'; const DATEI = 'normtexte.json'; function ungueltig(nachricht: string): never { throw new Error(`Normtexte 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.length === 0) { ungueltig(`${pfad} fehlt oder ist leer.`); } return wert; } function textabbildung(wert: unknown, pfad: string): Record { const roh = objekt(wert, pfad); const abbildung: Record = {}; for (const [schluessel, text] of Object.entries(roh)) { /* Leere Absätze gibt es im amtlichen Text: „(weggefallen)" steht dort teils als leere Zeichenkette. Sie zu verwerfen wäre eine Lücke, die niemand erwartet – sie durchzulassen ist die Wahrheit. */ if (typeof text !== 'string') { ungueltig(`${pfad}.${schluessel} ist kein Text.`); } abbildung[schluessel] = text; } return abbildung; } function blockLesen(roh: unknown, pfad: string): Anlagenblock { const o = objekt(roh, pfad); const marke = o['marke']; if (marke !== null && typeof marke !== 'string') { ungueltig(`${pfad}.marke muss Text oder null sein.`); } const wegRoh = o['pfad']; if (!Array.isArray(wegRoh)) { ungueltig(`${pfad}.pfad fehlt oder ist keine Liste.`); } const weg = (wegRoh as readonly unknown[]).map((teil, i) => pflichttext(teil, `${pfad}.pfad[${String(i)}]`), ); const text = o['text']; if (typeof text !== 'string') { ungueltig(`${pfad}.text fehlt.`); } return { marke, pfad: weg, text }; } function normLesen(roh: unknown, pfad: string): Normtext { const o = objekt(roh, pfad); const istAnlage = o['istAnlage']; if (typeof istAnlage !== 'boolean') { ungueltig(`${pfad}.istAnlage fehlt.`); } /* Der Titel darf leer sein: Manche Anlagen tragen im amtlichen XML gar keinen – SprengG Anlage II etwa. Einen zu erfinden wäre schlechter. */ const titel = o['titel']; if (typeof titel !== 'string') { ungueltig(`${pfad}.titel fehlt.`); } if (istAnlage) { const bloeckeRoh = o['bloecke']; if (!Array.isArray(bloeckeRoh)) { ungueltig(`${pfad}.bloecke fehlt oder ist keine Liste.`); } return { titel, istAnlage: true, bloecke: (bloeckeRoh as readonly unknown[]).map((block, i) => blockLesen(block, `${pfad}.bloecke[${String(i)}]`), ), }; } const text = o['text']; if (text !== undefined && typeof text !== 'string') { ungueltig(`${pfad}.text ist kein Text.`); } return { titel, istAnlage: false, absaetze: textabbildung(o['absaetze'], `${pfad}.absaetze`), ...(text === undefined ? {} : { text }), }; } function gesetzLesen(roh: unknown, pfad: string): Gesetzestext { const o = objekt(roh, pfad); const normenRoh = objekt(o['normen'], `${pfad}.normen`); const normen: Record = {}; for (const [name, norm] of Object.entries(normenRoh)) { normen[name] = normLesen(norm, `${pfad}.normen["${name}"]`); } return { bezeichnung: pflichttext(o['bezeichnung'], `${pfad}.bezeichnung`), stand: pflichttext(o['stand'], `${pfad}.stand`), quelle: pflichttext(o['quelle'], `${pfad}.quelle`), normen, }; } function normtexteValidieren(roh: unknown): Normtexte { 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 standRoh = objekt(meta['gesetzesstand'], 'meta.gesetzesstand'); const gesetzesstand: Record = {}; for (const [kuerzel, stand] of Object.entries(standRoh)) { gesetzesstand[kuerzel] = pflichttext(stand, `meta.gesetzesstand.${kuerzel}`); } const gesetzeRoh = objekt(o['gesetze'], 'gesetze'); const gesetze: Record = {}; for (const [kuerzel, gesetz] of Object.entries(gesetzeRoh)) { gesetze[kuerzel] = gesetzLesen(gesetz, `gesetze.${kuerzel}`); } return { meta: { version, stand: pflichttext(meta['stand'], 'meta.stand'), hinweis: pflichttext(meta['hinweis'], 'meta.hinweis'), gesetzesstand, }, gesetze, }; } /** Pfad der Normtextdatei – gepackt wie ungepackt. */ export function normtextePfad(): 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: Normtexte | null = null; export function normtexteLaden(): Normtexte { if (speicher !== null) { return speicher; } const pfad = normtextePfad(); if (!existsSync(pfad)) { speicher = NORMTEXTE_LEER; return speicher; } try { speicher = normtexteValidieren(JSON.parse(readFileSync(pfad, 'utf8'))); } catch (fehler: unknown) { console.error('Normtexte konnten nicht gelesen werden:', fehler); speicher = NORMTEXTE_LEER; } return speicher; } /** Gegenstück für Tests. */ export function normtexteZuruecksetzen(): void { speicher = null; }