/** * Glossar der Fachbegriffe des Waffenrechts. * * Das Waffenrecht arbeitet mit Wörtern, die im Alltag etwas anderes bedeuten. * „Führen" heißt nicht lenken, sondern die tatsächliche Gewalt außerhalb der * eigenen Wohnung ausüben. „Erwerben" hat mit Kaufen nichts zu tun. Wer das * nicht weiß, versteht die Frage nicht – unabhängig davon, wie gut er den * Stoff kann. * * Genau darauf zielt WCAG 3.1.3 (Ungewöhnliche Wörter, Stufe AAA): Es muss * einen Weg geben, die Bedeutung nachzuschlagen. 3.1.4 verlangt dasselbe für * Abkürzungen; beide sind im Prüfplan dieses Projekts als Zusage aufgeführt. * * ## Herkunft der Definitionen * * Wo das Gesetz einen Begriff selbst bestimmt, wird diese Bestimmung * wiedergegeben – nicht eine eigene Umschreibung davon. Die * waffenrechtlichen Begriffe stehen in Anlage 1 Abschnitt 2 WaffG, die * technischen in Abschnitt 1. Gesetzestexte sind nach § 5 Abs. 1 UrhG * gemeinfrei. * * Jede Fundstelle wird wie bei den Erklärungen maschinell gegen den amtlichen * Gesetzestext geprüft (`data-pipeline/pruefe_glossar.py`). */ import type { Fundstelle } from './erklaerungen'; /** * Art des Eintrags. * * Abkürzungen werden getrennt geführt: Sie brauchen keine Erläuterung, * sondern die aufgelöste Form – und WCAG 3.1.4 verlangt genau das. */ export type Begriffsart = 'begriff' | 'abkuerzung'; export interface Glossareintrag { /** Anzeigeform, etwa „Führen" oder „WaffG". */ readonly begriff: string; readonly art: Begriffsart; /** * Die Bedeutung in einem Satz. Bei Abkürzungen die aufgelöste Form. * * Steht für sich allein: Wer nur diesen Satz liest, muss die Frage * verstehen können. */ readonly kurz: string; /** Ausführlichere Erläuterung, wo ein Satz nicht reicht. */ readonly text?: string; /** * Wortformen, unter denen der Begriff im Katalog vorkommt. * * Nötig, weil Deutsch flektiert: „Führen" erscheint als „führt", * „geführt", „des Führens". Ohne die Formen bliebe der Begriff in genau * den Fragen unerkannt, in denen er gebraucht wird. */ readonly varianten: readonly string[]; readonly fundstellen: readonly Fundstelle[]; /** Warum es zu diesem Begriff keine gesetzliche Bestimmung gibt. */ readonly ohneFundstelleGrund?: string; /** Verwandte Begriffe, die beim Verständnis helfen. */ readonly siehe?: readonly string[]; } export interface GlossarMeta { readonly version: number; readonly stand: string; readonly gesetzesstand: Readonly>; readonly hinweis: string; } export interface Glossar { readonly meta: GlossarMeta; readonly eintraege: readonly Glossareintrag[]; } /** * Zeichen, die in einem deutschen Wort vorkommen. * * `\b` aus der regulären Ausdruckssprache taugt hier nicht: Es kennt nur * ASCII-Wortzeichen. In „Schießstätte" gälte damit jedes ß und ä als * Wortgrenze, und „Stätte" wäre plötzlich ein eigenes Wort. */ const WORTZEICHEN = 'A-Za-zÄÖÜäöüßẞ0-9'; /** Escaped, was in einem regulären Ausdruck Bedeutung hätte. */ function maskiert(wert: string): string { return wert.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); } function ausdruckFuer(eintrag: Glossareintrag): RegExp { // Längere Formen zuerst, damit „Kurzwaffe" nicht von „Waffe" verdeckt wird, // falls beide als Variante desselben Eintrags stehen. const alternativen = [...eintrag.varianten] .sort((a, b) => b.length - a.length) .map(maskiert) .join('|'); return new RegExp(`(?>(); for (const { eintrag, formen } of treffer) { for (const form of formen) { if (eintrag.varianten.includes(form)) { const bisher = genau.get(form) ?? new Set(); bisher.add(eintrag.begriff); genau.set(form, bisher); } } } return treffer .filter(({ eintrag, formen }) => formen.some((form) => { const beansprucht = genau.get(form); return beansprucht === undefined || beansprucht.has(eintrag.begriff); }), ) .map(({ eintrag }) => eintrag); } /** * Vorbereitetes Glossar – die Ausdrücke werden einmal gebaut, nicht je Frage. */ export interface Begriffssuche { readonly eintraege: readonly Glossareintrag[]; /** Findet die Begriffe, die in einem Text tatsächlich vorkommen. */ readonly imText: (text: string) => Glossareintrag[]; } /** * Bereitet die Suche vor. * * Einträge ohne Varianten werden übergangen: Ein Ausdruck aus einer leeren * Alternativenliste würde auf jede Stelle passen und das ganze Glossar an * jede Frage hängen. */ export function begriffssucheBauen(glossar: Glossar): Begriffssuche { const vorbereitet = glossar.eintraege .filter((eintrag) => eintrag.varianten.length > 0) .map((eintrag) => ({ eintrag, ausdruck: ausdruckFuer(eintrag) })); return { eintraege: glossar.eintraege, imText: (text: string): Glossareintrag[] => { const treffer = vorbereitet .map(({ eintrag, ausdruck }) => { /* `lastIndex` zurücksetzen: Der Ausdruck ist global und wird über alle Aufrufe hinweg wiederverwendet. */ ausdruck.lastIndex = 0; return { eintrag, formen: [...text.matchAll(ausdruck)].map((m) => m[0]) }; }) .filter(({ formen }) => formen.length > 0); return eindeutigMachen(treffer).sort((a, b) => a.begriff.localeCompare(b.begriff, 'de')); }, }; } /** Leeres Glossar – die Anwendung läuft auch ohne. */ export const GLOSSAR_LEER: Glossar = Object.freeze({ meta: Object.freeze({ version: 0, stand: '', gesetzesstand: Object.freeze({}), hinweis: '', }), eintraege: Object.freeze([]), });