/** * Server-side HTML renderer for markwhen projections. * * Shells out to `@markwhen/mw` CLI (same binary the official tool uses) * rather than importing the parser directly. Why: the view templates' * inlined Vue app was built against `@markwhen/parser@0.10.x`, whose * output shape differs from the npm-current `@markwhen/parser@1.x`. The * CLI ships its own matched parser, so delegating to it avoids any * version-skew silently breaking the render. * * The binary writes HTML to a destination file, so we round-trip through * a per-request temp file pair. ~5ms overhead; cheap vs. the correctness. */ import { writeFileSync, readFileSync, unlinkSync, mkdtempSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { createRequire } from 'node:module'; import { spawnSync } from 'node:child_process'; const require_ = createRequire(import.meta.url); let mwBinPath: string | null = null; function findMwBinary(): string { if (mwBinPath) return mwBinPath; const pkgJson = require_.resolve('@markwhen/mw/package.json'); const pkgDir = pkgJson.slice(0, pkgJson.length - '/package.json'.length); mwBinPath = join(pkgDir, 'lib', 'index.js'); return mwBinPath; } export function renderMarkwhenHtml( mwText: string, view: 'timeline' | 'calendar' = 'timeline', ): string { if (!mwText.trim()) { return `empty
No dated events to show yet.
`; } const dir = mkdtempSync(join(tmpdir(), 'rpast-')); const input = join(dir, 'in.mw'); const output = join(dir, `out.${view === 'calendar' ? 'calendar' : 'timeline'}.html`); writeFileSync(input, mwText, 'utf-8'); try { const result = spawnSync(process.execPath, [findMwBinary(), input, '-o', view, '-d', output], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000, }); if (result.status !== 0) { const stderr = result.stderr?.toString() ?? ''; throw new Error(`mw CLI exited ${result.status}: ${stderr.slice(0, 400)}`); } return readFileSync(output, 'utf-8'); } finally { try { unlinkSync(input); } catch {} try { unlinkSync(output); } catch {} } }