import { Hono } from 'hono'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import matter from 'gray-matter'; import { marked } from 'marked'; import { safeRel } from '../files.js'; // ÖFFENTLICH: Versionsverlauf eines Library-Beitrags aus der Git-History. // Der Container hat das Repo unter /site gemountet + git installiert. Wir // holen alte Fassungen on-demand (kein Vorbauen) und zeigen sie auf der Site. const execFileP = promisify(execFile); const SITE_DIR = process.env.SITE_DIR || '/site'; const git = (...args) => execFileP('git', ['-C', SITE_DIR, ...args], { maxBuffer: 10 * 1024 * 1024 }); const US = '\x1f'; // Feldtrenner (Unit Separator) — kommt in Commit-Daten nicht vor. const history = new Hono(); // Liste der Versionen: neueste zuerst. history.get('/', async (c) => { let rel; try { rel = safeRel(c.req.query('path')); } catch { return c.json({ error: 'Ungültiger Pfad' }, 400); } try { const { stdout } = await git( 'log', '--follow', `--format=%H${US}%h${US}%aI${US}%an${US}%s`, '--', `content/${rel}`); const versions = stdout.trim().split('\n').filter(Boolean).map((line) => { const [rev, short, date, author, subject] = line.split(US); return { rev, short, date, author, subject }; }); return c.json(versions); } catch { return c.json({ error: 'Verlauf nicht verfügbar' }, 500); } }); // Eine bestimmte Fassung gerendert (HTML), zum Anzeigen auf der Seite. history.get('/version', async (c) => { let rel; try { rel = safeRel(c.req.query('path')); } catch { return c.json({ error: 'Ungültiger Pfad' }, 400); } const rev = c.req.query('rev') || ''; if (!/^[0-9a-f]{7,40}$/i.test(rev)) return c.json({ error: 'Ungültige Version' }, 400); try { const { stdout } = await git('show', `${rev}:content/${rel}`); const { data, content } = matter(stdout); return c.json({ rev, title: data.title || '', date: data.date ? new Date(data.date).toISOString().slice(0, 10) : null, html: renderMarkdown(content), }); } catch { return c.json({ error: 'Version nicht gefunden' }, 404); } }); // Unified-Diff einer Fassung (was dieser Commit an der Datei geändert hat) — // fürs rot/grün-Diff auf der Seite. Roh-Diff; das Frontend färbt +/- ein. history.get('/diff', async (c) => { let rel; try { rel = safeRel(c.req.query('path')); } catch { return c.json({ error: 'Ungültiger Pfad' }, 400); } const rev = c.req.query('rev') || ''; if (!/^[0-9a-f]{7,40}$/i.test(rev)) return c.json({ error: 'Ungültige Version' }, 400); try { const { stdout } = await git('show', '--format=', '--no-color', rev, '--', `content/${rel}`); return c.json({ rev, diff: stdout }); } catch { return c.json({ error: 'Diff nicht verfügbar' }, 404); } }); // Markdown → HTML. marked kennt Goldmarks Fußnoten ([^id]) nicht — daher // vorab: Definitionen einsammeln, Verweise zu -Nummern, „Quellen" anhängen // (greift dieselbe .footnotes-CSS wie die Live-Seite). function renderMarkdown(md) { const defs = {}; const order = []; md = md.replace(/^\[\^([^\]]+)\]:[ \t]*(.*)$/gm, (_, id, txt) => { defs[id] = txt; return ''; }); md = md.replace(/\[\^([^\]]+)\]/g, (_, id) => { if (!order.includes(id)) order.push(id); return `${order.indexOf(id) + 1}`; }); let html = marked.parse(md); if (order.length) { html += '
    ' + order.map((id) => `
  1. ${marked.parseInline(defs[id] || '')}
  2. `).join('') + '
'; } return html; } export default history;