rspace-online/shared/markwhen/html-render.ts

63 lines
2.2 KiB
TypeScript

/**
* 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 `<!doctype html><meta charset="utf-8"><title>empty</title>
<body style="display:grid;place-items:center;height:100vh;margin:0;font:13px system-ui;color:#94a3b8;background:#0b1221">
<div>No dated events to show yet.</div>
</body>`;
}
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 {}
}
}