// Macht aus Skript.md ein PDF in A4. // // Aufruf: node skript-pdf.mjs [--quelle Skript-kurz.md] [--fuer-noah] // --quelle andere Markdown-Datei aus diesem Ordner, Vorgabe Skript.md // --fuer-noah lässt die Arbeitszeilen oben weg (wer schreibt, Grundlage) // // Chrome läuft wie in slides/exportieren.mjs über das DevTools-Protokoll: // eine Instanz, am Ende samt Hilfsprozessen beendet, spätestens nach 60 s. import { spawn } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const HIER = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const { marked } = require(join(HIER, "..", "Dashboard", "vendor", "marked.min.js")); const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; const ziel = process.argv[2]; if (!ziel) { console.error("Aufruf: node skript-pdf.mjs [--fuer-noah]"); process.exit(1); } const fuerNoah = process.argv.includes("--fuer-noah"); const qi = process.argv.indexOf("--quelle"); const quelle = qi > 0 ? process.argv[qi + 1] : "Skript.md"; let text = readFileSync(join(HIER, quelle), "utf-8"); if (fuerNoah) { text = text .split("\n") .filter((z) => !z.startsWith("**Grundlage:**")) .map((z) => z.startsWith("**Stand:**") ? z.replace(/(Entwurf \d+\.).*/, "$1") : z) .join("\n"); } // Jeder Slide-Block (Überschrift, Knackpunkt, Sätze) bleibt auf einer Seite, // und ein Teil beginnt nie mit seinem Kasten allein unten auf der Seite. // Chrome hält sich beim Drucken nicht verlässlich an break-after: avoid, // an break-inside: avoid auf einem Container schon. function bloecke(html) { html = html.replace(/(]*>[\s\S]*?<\/h3>\s*

[\s\S]*?<\/p>\s*

    [\s\S]*?<\/ul>)/g, '
    $1
    '); html = html.replace(/(]*>[\s\S]*?<\/h2>\s*
    [\s\S]*?<\/blockquote>\s*
    [\s\S]*?<\/div>)/g, '
    $1
    '); return html; } const logo = pathToFileURL(join(HIER, "slides", "bilder", "logo.png")).href; const seite = ` HiveGrid Webinar: Skript ${bloecke(marked.parse(text))} `; const arbeit = mkdtempSync(join(tmpdir(), "hivegrid-skript-")); const htmlDatei = join(arbeit, "skript.html"); writeFileSync(htmlDatei, seite); const profil = mkdtempSync(join(tmpdir(), "hivegrid-skript-chrome-")); const chrome = spawn(CHROME, [ "--headless", "--disable-gpu", "--no-first-run", "--no-default-browser-check", "--allow-file-access-from-files", "--remote-debugging-pipe", `--user-data-dir=${profil}`, "about:blank", ], { stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"], detached: true }); let beendet = false; const beenden = () => { if (beendet) return; beendet = true; try { process.kill(-chrome.pid, "SIGKILL"); } catch { /* schon weg */ } rmSync(profil, { recursive: true, force: true }); rmSync(arbeit, { recursive: true, force: true }); }; const offen = new Map(); const warter = []; const abbrechen = (grund) => { for (const f of offen.values()) f({ error: { message: grund } }); offen.clear(); for (const w of warter.splice(0)) w.nein(new Error(grund)); }; const notbremse = setTimeout(() => { abbrechen("Zeitgrenze von 60 Sekunden erreicht"); beenden(); }, 60000); chrome.on("exit", () => abbrechen("Chrome wurde beendet")); try { const raus = chrome.stdio[3]; let puffer = ""; let n = 0; chrome.stdio[4].on("data", (stueck) => { puffer += stueck.toString(); let i; while ((i = puffer.indexOf("\0")) >= 0) { const m = JSON.parse(puffer.slice(0, i)); puffer = puffer.slice(i + 1); if (m.id && offen.has(m.id)) { offen.get(m.id)(m); offen.delete(m.id); continue; } const w = warter.findIndex((x) => x.passt(m)); if (w >= 0) warter.splice(w, 1)[0].ok(m); } }); const senden = (method, params = {}, sessionId) => new Promise((ok, nein) => { const id = ++n; offen.set(id, (m) => (m.error ? nein(new Error(`${method}: ${m.error.message}`)) : ok(m.result))); raus.write(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }) + "\0"); }); const warten = (passt) => new Promise((ok, nein) => warter.push({ passt, ok, nein })); const { targetId } = await senden("Target.createTarget", { url: "about:blank" }); const { sessionId } = await senden("Target.attachToTarget", { targetId, flatten: true }); await senden("Page.enable", {}, sessionId); const geladen = warten((m) => m.method === "Page.loadEventFired" && m.sessionId === sessionId); await senden("Page.navigate", { url: pathToFileURL(htmlDatei).href }, sessionId); await geladen; await senden("Runtime.evaluate", { expression: "document.fonts.ready.then(() => true)", awaitPromise: true }, sessionId); const { data } = await senden("Page.printToPDF", { printBackground: true, preferCSSPageSize: true, displayHeaderFooter: true, headerTemplate: "
    ", footerTemplate: '
    Webinar-Skript, Seite von
    ', }, sessionId); writeFileSync(resolve(ziel), Buffer.from(data, "base64")); console.log("Skript-PDF:", resolve(ziel)); } finally { clearTimeout(notbremse); beenden(); }