${maskiert(plan.augenbraue)}
/** * Das Gerüst gedruckter Dokumente – reine Funktionen, ohne Electron und ohne DOM. * * Ein Dokument dieser Anwendung entsteht als vollständige, in sich * geschlossene HTML-Datei. Kein Skript, keine externe Adresse, kein Nachladen: * Was hier herauskommt, ist genau das, was gedruckt wird. * * **Die Quellenangabe ist kein Beiwerk.** Der amtliche Fragenkatalog ist ein * amtliches Werk; wer ihn wiedergibt, muss die Quelle nennen (§ 63 UrhG) und * darf ihn nicht ändern (§ 62 UrhG). Die Pflicht hängt an der * Vervielfältigung, nicht an der Weitergabe – ein PDF, das nur auf dem * eigenen Rechner liegt, ist davon nicht ausgenommen, und weitergeben lässt * es sich ohnehin jederzeit. * * Deshalb nimmt {@link dokumentBauen} die Quellenangabe als Pflichtfeld * entgegen. Es gibt keinen Schalter, sie wegzulassen, und keinen Pfad, auf * dem ein Dokument ohne sie entstehen könnte. Sie steht an zwei Stellen: als * Block am Anfang und – über die Fußzeile von `printToPDF` – auf jedem * einzelnen Blatt. Beides ist nötig, weil gedruckte Seiten getrennt werden. */ import { druckStil, type Schriftgroesse } from './stil'; /** * Maskiert Text für die Einbettung in HTML. * * Gilt für **jeden** Wert, der aus Daten stammt – auch für die aus dem * eigenen Katalog. Eine Ausnahme „das ist doch unser eigener Text“ hält * genau so lange, bis jemand ein Profil „Max & Moritz“ anlegt. */ export function maskiert(wert: string): string { return wert .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } /** Angaben zur Herkunft, wie sie ins Dokument müssen. */ export interface Quellenangabe { /** Wortlaut aus `katalog.json`, unverändert. */ readonly amtlich: string; /** Herausgeber des amtlichen Werks. */ readonly herausgeber: string; /** Katalogstand als ISO-Datum. */ readonly stand: string; /** Bezugsadresse – als Text, nie als Verweis. */ readonly quelleUrl: string; /** * Wie viel des amtlichen Werks dieses Dokument wiedergibt. * * Hieß bis Fassung 0.18.0 schlicht `'auszug' | 'kein amtlicher wortlaut'`, * und der Satz zu `'auszug'` lautete unbedingt „Dieses Dokument gibt nur * einen Teil des amtlichen Fragenkatalogs wieder.“ Für eine Fragenliste * über den ganzen Katalog wäre das nachweislich falsch: 575 von 575 * Fragen und 1430 von 1430 Antwortmöglichkeiten sind kein Teil, sondern * das Ganze. * * Deshalb wird jetzt **gezählt statt behauptet**. Aus den Zahlen bildet * {@link quellensatz} den Satz; ob er „alle“ oder „einen Teil“ sagt, * entscheidet die Zählung und nicht der Aufrufer. */ readonly umfang: Werkumfang; } /** * Was ein Dokument vom amtlichen Werk wiedergibt – in Zahlen. * * Zwei Achsen, weil keine Formulierung beide trägt: wie viele **Fragen** * enthalten sind, und ob bei ihnen die **Lösung** ausgewiesen ist. Eine * Fragenliste über den ganzen Katalog ohne Lösungsanhang gibt alle Fragen * vollständig wieder und lässt zugleich jede Lösungsmarkierung weg; ein Satz, * der nur eine der beiden Achsen nennt, verschwiege die andere. */ export type Werkumfang = | { /** Das Dokument enthält keinen amtlichen Wortlaut (nur der Lernbericht). */ readonly art: 'kein amtlicher wortlaut'; } | { readonly art: 'wiedergabe'; /** Wiedergegebene Fragen. */ readonly fragen: number; /** Fragen im amtlichen Katalog insgesamt. */ readonly fragenGesamt: number; /** * Sind die richtigen Antworten im Dokument ausgewiesen? * * `'alle'` – bei jeder Frage. `'keine'` – bei keiner; dann ist der * Bogen zum Bearbeiten gedacht. Ein Mittelding gibt es nicht: Beide * Dokumente entscheiden das für das ganze Dokument, nicht je Frage. */ readonly loesungen: 'alle' | 'keine'; /** * Sind bei Auswahlfragen alle Antwortmöglichkeiten abgedruckt? * * Ein Dokument, das nur die richtige Option zeigt, kürzt den amtlichen * Wortlaut erheblich – gemessen 801 von 1430 Optionstexten. Das muss * dastehen, sonst tritt das Dokument vollständiger auf, als es ist. */ readonly optionen: 'alle' | 'nur die richtigen'; }; export interface Dokumentbauplan { /** Erscheint als `
Dieses Dokument enthält keinen Wortlaut des amtlichen Fragenkatalogs; ' + 'genannt werden lediglich dessen Kapitel- und Abschnittsbezeichnungen.
' ); } const { fragen, fragenGesamt, loesungen, optionen } = umfang; const alle = fragen >= fragenGesamt; const eine = fragen === 1; /* „alle 575“ statt „575 der 575“: Wer den ganzen Katalog vor sich hat, soll das lesen und nicht selbst vergleichen müssen. Der Singular ist erreichbar – ein einziger Fehler ergibt ein Dokument mit einer Frage. */ const menge = alle ? `alle ${String(fragenGesamt)} Fragen` : eine ? `1 der ${String(fragenGesamt)} Fragen` : `${String(fragen)} der ${String(fragenGesamt)} Fragen`; const zweiter = loesungen === 'alle' ? optionen === 'alle' ? 'Die Antwortmöglichkeiten sind vollständig wiedergegeben; die jeweils richtige ist gekennzeichnet.' : 'Von den Antwortmöglichkeiten ist nur die jeweils richtige wiedergegeben; die übrigen fehlen.' : 'Welche Antwort richtig ist, weist dieses Dokument nicht aus – die amtliche Kennzeichnung fehlt hier vollständig.'; return ( `Dieses Dokument gibt ${menge} des amtlichen Fragenkatalogs wieder. ${zweiter}
\n` + 'Die Bildbeschreibungen zu den Prüf- und Zulassungszeichen stammen nicht aus dem ' + 'amtlichen Fragenkatalog; sie sind eine Ergänzung dieser Software.
' ); } function quellenblock(quelle: Quellenangabe): string { return `Amtlicher Fragenkatalog: ${maskiert(quelle.amtlich)}
Herausgeber: ${maskiert(quelle.herausgeber)}. Stand: ${maskiert(quelle.stand)}. Bezug: ${maskiert(quelle.quelleUrl)}
${quellensatz(quelle.umfang)}Erklärungen, Bildbeschreibungen, Glossar und die Gestaltung dieses Dokuments sind eigener Inhalt der Waffensachkunde-Lernsoftware, © 2026 Olaf Willerding, EUPL-1.2.
${maskiert(plan.augenbraue)}
${maskiert(text)}
`; } /** * Ein Absatz mit fett vorangestellter Beschriftung: „**Kurz:** …“. * * Es gibt ihn, damit kein Aufrufer Auszeichnung in eine Zeichenkette * schreiben muss, die anschließend maskiert wird. Beide Teile kommen roh * herein und werden hier einzeln maskiert; das `` entsteht an der * einen Stelle, an der es entstehen darf. */ export function absatzMitBeschriftung(beschriftung: string, text: string, klasse?: string): string { const attribut = klasse === undefined ? '' : ` class="${klasse}"`; return `${maskiert(beschriftung)} ${maskiert(text)}
`; }