/** * Erzeugt public/anleitung.html aus ANLEITUNG.md. * * ANLEITUNG.md ist die einzige Quelle des Handbuchs; die HTML-Fassung wird nie von Hand * bearbeitet (sie steht in .gitignore). Aufruf: `npm run anleitung` — läuft zusätzlich * automatisch vor jedem `npm run build` (prebuild) und damit auch im Docker-Image. * * Abbildung Markdown → HTML: * # Titel → Kopfzeile der Seite (vor „–" = H1, danach = Untertitel) * ## Inhaltsverzeichnis → nav.toc, die Sprungziele werden auf #s1..#sN umgeschrieben * ## N. Titel →
mit nummeriertem Abschnittskopf * --- → entfällt (reine Trennlinie in der Markdown-Ansicht) * > Text → Hinweisbox .callout * > [!WARNUNG] Text → .callout warn (auch [!WARNING]) * > [!ACHTUNG] Text → .callout danger (auch [!CAUTION]) * > [!HINWEIS] Text → .callout (auch [!NOTE]) */ import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { marked } from 'marked'; const wurzel = join(dirname(fileURLToPath(import.meta.url)), '..'); const QUELLE = join(wurzel, 'ANLEITUNG.md'); const VORLAGE = join(wurzel, 'scripts', 'anleitung.template.html'); const ZIEL = join(wurzel, 'public', 'anleitung.html'); const ALERTS = { WARNUNG: 'callout warn', WARNING: 'callout warn', ACHTUNG: 'callout danger', CAUTION: 'callout danger', DANGER: 'callout danger', HINWEIS: 'callout', NOTE: 'callout', }; /** Blockquotes werden zu Hinweisboxen, optional mit Variante über [!WARNUNG] o. ä. */ function calloutsUmschreiben(html) { return html.replace(/
\s*([\s\S]*?)\s*<\/blockquote>/g, (_, inhalt) => { let klasse = 'callout'; const markierung = inhalt.match(/^

\s*\[!([A-ZÄÖÜ]+)\]\s*/); if (markierung && ALERTS[markierung[1]]) { klasse = ALERTS[markierung[1]]; inhalt = inhalt.replace(markierung[0], '

'); } // Eine einzelne Textzeile braucht kein

— das spart eine Zeile Abstand in der Box. const einzeln = inhalt.match(/^

([\s\S]*?)<\/p>$/); return `

${einzeln ? einzeln[1] : inhalt}
`; }); } /** Markdown-Sprungziele wie #7-drucken auf die Abschnitts-IDs #s7 umschreiben. */ function ankerUmschreiben(html) { return html.replace(/href="#(\d+)-[^"]*"/g, (_, nummer) => `href="#s${nummer}"`); } function einrücken(html, stufe = ' ') { return html.split('\n').map((z) => (z.trim() ? stufe + z : z)).join('\n'); } const markdown = readFileSync(QUELLE, 'utf8'); // 1. Seitentitel aus der H1 const h1 = markdown.match(/^#\s+(.+)$/m); if (!h1) throw new Error('ANLEITUNG.md: H1-Überschrift (# Titel) fehlt'); const [titelKopf, titelUnter = ''] = h1[1].split(/\s+[–—-]\s+/, 2); // 2. In Blöcke je H2 zerlegen const bloecke = markdown .split(/^##\s+/m) .slice(1) .map((b) => { const umbruch = b.indexOf('\n'); return { titel: b.slice(0, umbruch).trim(), rumpf: b.slice(umbruch + 1).replace(/^\s*---\s*$/gm, '').trim(), }; }); // 3. Inhaltsverzeichnis: die Nummern der Listenpunkte werden zu #s const tocBlock = bloecke.find((b) => b.titel.toLowerCase() === 'inhaltsverzeichnis'); if (!tocBlock) throw new Error('ANLEITUNG.md: Abschnitt „## Inhaltsverzeichnis" fehlt'); const toc = [...tocBlock.rumpf.matchAll(/^\s*(\d+)\.\s*\[([^\]]+)\]/gm)] .map(([, nr, text]) => `
  • ${text}
  • `) .join('\n'); if (!toc) throw new Error('ANLEITUNG.md: Inhaltsverzeichnis enthält keine Einträge'); // 4. Nummerierte Abschnitte const abschnitte = []; for (const block of bloecke) { const kopf = block.titel.match(/^(\d+)\.\s+(.*)$/); if (!kopf) continue; // Inhaltsverzeichnis und alles ohne Nummer const [, nummer, titel] = kopf; const rumpf = ankerUmschreiben(calloutsUmschreiben(marked.parse(block.rumpf))); abschnitte.push( ` \n` + `
    \n` + `

    ${nummer} ${titel}

    \n` + `${einrücken(rumpf.trim())}\n` + `
    ` ); } if (!abschnitte.length) throw new Error('ANLEITUNG.md: keine nummerierten Abschnitte (## 1. …) gefunden'); const html = readFileSync(VORLAGE, 'utf8') .replace('{{H1}}', titelKopf.trim()) .replace('{{SUBTITLE}}', titelUnter.trim()) .replace('{{TOC}}', toc) .replace('{{SECTIONS}}', abschnitte.join('\n\n')); writeFileSync(ZIEL, html, 'utf8'); console.log(`anleitung.html erzeugt — ${abschnitte.length} Abschnitte, ${html.length} Zeichen`);