/** * In den mitgelieferten Gesetzen blättern und suchen. * * ## Was diese Datei beantwortet * * „Was steht eigentlich in § 13 WaffG?“ Bis Fassung 0.26.7 war der amtliche * Wortlaut nur über eine Erklärung erreichbar: Wer eine Frage bearbeitete, * konnte die Fundstelle darunter aufklappen. Wer eine Vorschrift **suchte**, * ohne die passende Frage zu kennen, kam nicht an sie heran – obwohl 148 * Normen aus sieben Gesetzen im Paket liegen. * * ## Warum die Suche hier steht und nicht in der Ansicht * * Damit sie prüfbar ist. Was gesucht wird und in welcher Reihenfolge die * Treffer stehen, ist eine Aussage über den Inhalt – kein Aussehen. Die * Ansicht bekommt fertige Listen und entscheidet nur noch über die Darstellung. * * ## Was nicht gesucht wird * * Der Wortlaut wird nicht verändert, nicht gekürzt und nicht umsortiert * (§ 5 Abs. 1 UrhG, und der Grundsatz dieses Projekts dazu). Gesucht wird * über Bezeichnung, Überschrift und Text; ausgegeben wird der Fundort, nicht * ein zurechtgeschnittener Ausschnitt. */ import { GESETZ_BEZEICHNUNG, type Gesetzeskuerzel } from './erklaerungen'; import type { Normtext, Normtexte } from './normtexte'; /** Ein Gesetz, wie die Ansicht es in der Auswahl braucht. */ export interface Gesetzeseintrag { readonly kuerzel: Gesetzeskuerzel; readonly bezeichnung: string; /** Änderungsstand, wörtlich aus dem amtlichen XML. */ readonly stand: string; readonly quelle: string; readonly normenGesamt: number; } /** Eine Norm in der Liste. */ export interface Normeintrag { readonly gesetz: Gesetzeskuerzel; /** „§ 12“ oder „Anlage 1“. */ readonly norm: string; readonly titel: string; readonly istAnlage: boolean; /** * Warum diese Norm in der Trefferliste steht. * * Die Ansicht sagt es dazu: Ein Treffer, der nur im Fließtext steckt, ist * für den Suchenden etwas anderes als einer in der Überschrift – und wer * nicht sieht, warum etwas gefunden wurde, sucht weiter. */ readonly treffer: 'bezeichnung' | 'ueberschrift' | 'text' | null; } /** Die Gesetze in der Reihenfolge, in der sie angeboten werden. */ export function gesetzeAuflisten(normtexte: Normtexte): Gesetzeseintrag[] { return ( Object.entries(normtexte.gesetze) .map(([kuerzel, gesetz]) => ({ kuerzel: kuerzel as Gesetzeskuerzel, bezeichnung: gesetz.bezeichnung, stand: gesetz.stand, quelle: gesetz.quelle, normenGesamt: Object.keys(gesetz.normen).length, })) /* Nach der ausgeschriebenen Bezeichnung, nicht nach dem Kürzel: In der Auswahl steht die Bezeichnung, und eine Liste, die anders sortiert ist als sie aussieht, ist keine Liste, sondern ein Rätsel. */ .sort((a, b) => a.bezeichnung.localeCompare(b.bezeichnung, 'de')) ); } /** * Die Sortiernummer einer Norm. * * „§ 2“ muss vor „§ 10“ stehen – alphabetisch stünde es dahinter. Gelesen * wird die erste Zahlengruppe; „§ 13a“ reiht sich hinter „§ 13“ ein, weil der * Rest als Zeichenkette nachentscheidet. Anlagen kommen nach den Paragrafen, * so wie sie auch im Gesetz stehen. */ function sortierschluessel(eintrag: Normeintrag): [number, number, string] { const zahl = /\d+/u.exec(eintrag.norm); return [eintrag.istAnlage ? 1 : 0, zahl === null ? 0 : Number(zahl[0]), eintrag.norm]; } function nachOrdnung(a: Normeintrag, b: Normeintrag): number { const [aA, aZ, aT] = sortierschluessel(a); const [bA, bZ, bT] = sortierschluessel(b); return aA - bA || aZ - bZ || aT.localeCompare(bT, 'de'); } /** Der ganze Text einer Norm, für die Suche zusammengelegt. */ function volltext(norm: Normtext): string { const teile: string[] = [norm.titel]; if (norm.text !== undefined) { teile.push(norm.text); } for (const absatz of Object.values(norm.absaetze ?? {})) { teile.push(absatz); } for (const block of norm.bloecke ?? []) { teile.push(block.text); } return teile.join('\n'); } /** * Vereinheitlicht einen Suchbegriff. * * Kleinschreibung und das Paragrafenzeichen weg: Wer „§13“ eingibt, meint * „§ 13“, und wer „13“ eingibt, meint es auch. Ein Suchfeld, das an einem * Leerzeichen scheitert, wird nicht wieder benutzt. */ function falten(text: string): string { return text.toLocaleLowerCase('de').replace(/§/gu, ' ').replace(/\s+/gu, ' ').trim(); } /** * Die Normen eines Gesetzes – gefiltert, wenn gesucht wird. * * Ohne Suchbegriff steht die vollständige Liste in der Ordnung des Gesetzes. * Mit Suchbegriff stehen die Treffer in derselben Ordnung: Wer in einem * Gesetz sucht, will wissen, **wo** etwas steht – eine nach Trefferqualität * umsortierte Liste zerrisse den Zusammenhang, den die Nummerierung stiftet. */ export function normenAuflisten( normtexte: Normtexte, gesetz: Gesetzeskuerzel, suche = '', ): Normeintrag[] { const eintrag = normtexte.gesetze[gesetz]; if (eintrag === undefined) { return []; } const gesucht = falten(suche); const alle = Object.entries(eintrag.normen).map(([norm, text]) => { const treffer: Normeintrag['treffer'] = gesucht.length === 0 ? null : falten(norm).includes(gesucht) ? 'bezeichnung' : falten(text.titel).includes(gesucht) ? 'ueberschrift' : falten(volltext(text)).includes(gesucht) ? 'text' : null; return { gesetz, norm, titel: text.titel, istAnlage: text.istAnlage, treffer, }; }); const gefiltert = gesucht.length === 0 ? alle : alle.filter((n) => n.treffer !== null); return gefiltert.sort(nachOrdnung); } /** * Wie viele Normen jedes Gesetz zu einem Suchbegriff beisteuert. * * Damit die Gesetzesauswahl sagen kann, wo etwas zu finden ist, statt den * Suchenden sieben Gesetze der Reihe nach durchklicken zu lassen. */ export function trefferJeGesetz( normtexte: Normtexte, suche: string, ): ReadonlyMap { const zahlen = new Map(); for (const kuerzel of Object.keys(normtexte.gesetze) as Gesetzeskuerzel[]) { zahlen.set(kuerzel, normenAuflisten(normtexte, kuerzel, suche).length); } return zahlen; } /** Die ausgeschriebene Bezeichnung – auch für ein Kürzel ohne Normtext. */ export function bezeichnungVon(kuerzel: Gesetzeskuerzel): string { return GESETZ_BEZEICHNUNG[kuerzel]; }