/** * Eine Frage auf Papier – der Baustein, den beide Fragendokumente teilen. * * ## Warum ein gemeinsamer Baustein, aber keine Wollmilchsau * * Fehlerprotokoll und Fragenliste geben dieselben Bestandteile wieder: * Nummer, Kapitel, Fragetext, Antwortmöglichkeiten, Musterantwort, * Abbildungen. Sie unterscheiden sich in genau zwei Fragen – ob die Lösung * dastehen darf und ob eine Erklärung dazukommt. Genau diese zwei stehen * deshalb in {@link Frageform}, und nichts weiter. Ein Baustein mit einem * Schalter je Denkbarkeit hätte einen Zustandsraum, den keine Prüfung mehr * abdeckt; beide Dokumente setzen ihre Form einmal für das ganze Dokument. * * ## Die Überschrift nennt das Kapitel, und das ist kein Schmuck * * Nachgemessen tragen die 575 Fragen des Katalogs nur **437 verschiedene** * amtliche Nummern; 227 Fragen sind von Doppelungen betroffen, eine Nummer * kommt dreimal vor. Eindeutig ist erst das Paar (Kapitel, Nummer) – innerhalb * eines Kapitels gibt es keine Doppelung. Ein Blatt, das nur „Frage 1.87“ * sagt, ist im amtlichen Werk nicht nachschlagbar. * * ## Warum die Antwortmöglichkeiten vollständig dastehen * * Auch dann, wenn die Lösung ausgewiesen ist. Nachgemessen nennen **302 der * 471** Erklärungen zu Auswahlfragen mindestens eine *falsche* Option beim * Buchstaben („Antwort b ist falsch: § 2 Abs. 1 setzt ein Mindestalter …“). * Ein Dokument, das nur die richtige Option abdruckt, macht seine eigenen * Begründungen unlesbar – und kürzt den amtlichen Wortlaut um 801 von 1430 * Optionstexten, ohne dass der Leser es erführe. */ import { kernelemente, type Frage, type RichText } from '../katalog'; import { absatz, absatzMitBeschriftung, bild, maskiert } from './dokument'; /** Wie viel eine Frage im Dokument preisgibt. */ export interface Frageform { /** * Wird ausgewiesen, welche Antwort richtig ist? * * `false` macht aus der Frage einen Bogen zum Bearbeiten. Die * Antwortmöglichkeiten stehen dann vollständig da, nur ohne Markierung – * weggelassen wird nichts. */ readonly loesungZeigen: boolean; /** Welche Erklärung dazukommt. `'keine'` lässt den Block ganz weg. */ readonly erklaerung: 'keine' | 'kurz' | 'voll'; } /** Was eine Erklärung beisteuert; deckungsgleich mit `content/erklaerungen.json`. */ export interface Frageerklaerung { readonly kurz: string; readonly text: string; readonly merksatz?: string; readonly fundstellen?: readonly string[]; } /** Alles, was eine Frage für das Papier braucht. */ export interface Papierfrage { readonly frage: Frage; /** Titel des Kapitels, für die Überschrift. */ readonly kapitelTitel: string; /** Bild-ID zu eingebetteter Datenadresse; fehlende Einträge werden benannt. */ readonly bilder: ReadonlyMap; /** Alternativtext je Bild-ID. */ readonly alttexte: ReadonlyMap; readonly erklaerung?: Frageerklaerung | undefined; } /** Kurzform für Überschriften, wo der Kapiteltitel zu lang wäre. */ export function frageKurzbezeichnung(frage: Frage): string { return `Kapitel ${frage.kapitel}, Frage ${frage.amtliche_nummer}`; } function bilderZu(ids: readonly string[], papier: Papierfrage): string { return ids .map((id) => bild(papier.bilder.get(id) ?? '', papier.alttexte.get(id) ?? '')) .join('\n'); } /** * Die Antwortmöglichkeiten. * * Das Kästchen ist U+2610 (BALLOT BOX) und steht **außerhalb** des amtlichen * Wortlauts, nämlich vor dem Buchstaben. Der Buchstabe selbst kommt aus * `option.label` und nicht aus einem Listenzähler – der Katalog nummeriert * a, b, c, und eine eigene Zählung liefe bei der ersten Abweichung falsch. */ function optionen(papier: Papierfrage, form: Frageform): string { const liste = papier.frage.optionen ?? []; if (liste.length === 0) { return ''; } const punkte = liste.map((option) => { const marke = form.loesungZeigen && option.korrekt ? '☑' : '☐'; const text = option.inhalt.text.trim(); const bilder = option.bilder.length > 0 ? `\n${bilderZu(option.bilder, papier)}` : ''; /* Bei Frage 3.05 haben zwei Optionen gar keinen Text – dort steht nur das Zeichen. Ein leerer Textknoten wäre dann alles, was ein Screenreader vorfände; der Alternativtext des Bildes trägt die Auskunft. */ const inhalt = text.length > 0 ? ` ${maskiert(text)}` : ''; /* Das Kästchen ist `aria-hidden`, weil „Wahlurne mit Haken“ als Ansage nichts nützt. Wo es aber die LÖSUNG trägt, darf die Auskunft nicht an ihm allein hängen: Das Fehlerprotokoll hat – anders als die Fragenliste – keinen Lösungsanhang, und wer das Blatt hört, erführe sonst nirgends, welche Antwort richtig war. Deshalb steht das Wort daneben, nur für die Ausgabe, und nur wenn die Lösung überhaupt ausgewiesen wird. */ const gelesen = form.loesungZeigen ? `${option.korrekt ? 'Richtig: ' : 'Falsch: '}` : ''; return `
  • ${gelesen}${maskiert(option.label)})${inhalt}${bilder}
  • `; }); const mehrfach = liste.filter((o) => o.korrekt).length > 1; const hinweis = form.loesungZeigen && mehrfach ? absatz('Bei dieser Frage sind mehrere Antworten richtig.', 'hinweis') : ''; return `\n${hinweis}`; } /** Die Musterantwort einer offenen Frage, samt der unterstrichenen Stellen. */ function musterantwort(text: RichText | undefined): string { if (text === undefined) { return absatz('Zu dieser Frage ist keine Musterantwort hinterlegt.', 'hinweis'); } const stellen = kernelemente(text); /* Der Vorbehalt ist derselbe wie am Bildschirm und steht bedingt: Bei 63 der 104 offenen Fragen ist gar nichts unterstrichen. Ein unbedingter Satz behauptete dort Markierungen, die das Dokument nicht enthält. */ const liste = stellen.length === 0 ? '' : `

    Im amtlichen Fragenkatalog ist hier unterstrichen:

    \n` + `\n` + absatz( 'Das ist die Hervorhebung des Katalogs, keine Vorgabe für Ihre Antwort. ' + 'Eine richtige Antwort kann anders formuliert sein.', 'hinweis', ); return `${absatzMitBeschriftung('Musterantwort:', text.text)}\n${liste}`; } /** * Die Erklärung – mit derselben Kennzeichnung wie am Bildschirm. * * Der abgrenzende Halbsatz „und nicht Teil des amtlichen Fragenkatalogs“ * steht hier vollständig, wortgleich zu `Erklaerungstafel.tsx`. Auf Papier * ist er nötiger als am Bildschirm: Dort steht die Anwendung daneben, hier * liegt womöglich ein einzelnes herausgelöstes Blatt vor jemandem. */ function erklaerungsblock(e: Frageerklaerung | undefined, form: Frageform): string { if (e === undefined || form.erklaerung === 'keine') { return ''; } const teile = [absatzMitBeschriftung('Kurz:', e.kurz)]; if (form.erklaerung === 'voll') { teile.push(absatz(e.text)); if (e.merksatz !== undefined && e.merksatz.trim().length > 0) { teile.push(absatzMitBeschriftung('Merksatz:', e.merksatz)); } const fundstellen = e.fundstellen ?? []; if (fundstellen.length > 0) { teile.push( `

    Im Gesetz nachlesen:

    \n`, ); } } teile.push( absatz( 'Diese Erklärung ist eine Ergänzung dieser Software und nicht Teil des amtlichen Fragenkatalogs.', 'hinweis', ), ); return teile.join('\n'); } /** * Eine vollständige Frage als HTML-Block. * * `ebene` ist die Überschriftenebene der Frage – 3, wenn darüber ein * Bereichs-`h2` steht. Ohne Sprünge, weil der Strukturbaum des PDF sonst * unbrauchbar wird (siehe `tests/druck-struktur.test.ts`). */ export function frageblock(papier: Papierfrage, form: Frageform, ebene: 2 | 3 = 3): string { const { frage } = papier; const h = `h${String(ebene)}`; const teile = [ `<${h}>${maskiert(frageKurzbezeichnung(frage))}`, absatz(frage.frage.text, 'frage__text'), ]; if (frage.bilder.length > 0) { teile.push(bilderZu(frage.bilder, papier)); } if (frage.typ === 'mc') { teile.push(optionen(papier, form)); } else if (form.loesungZeigen) { teile.push(musterantwort(frage.musterantwort)); } else { /* Ohne Lösung braucht eine offene Frage Platz zum Schreiben – sonst wäre der Bogen an dieser Stelle nicht zu bearbeiten. */ teile.push(''); } teile.push(erklaerungsblock(papier.erklaerung, form)); return `
    \n${teile.filter((t) => t !== '').join('\n')}\n
    `; }