tools: preserve HTML deck exporter
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
// Render each slide of a ThothII-style HTML deck to a high-res PNG.
|
||||
// Assumes the deck exposes .slide sections navigable via #N hash routes.
|
||||
//
|
||||
// Usage:
|
||||
// node render.mjs <deck.html> <outdir> [--hide "sel1,sel2"] [--scale 2] [--width 1600] [--height 900]
|
||||
//
|
||||
// Output: <outdir>/01.png .. NN.png + <outdir>/notes.json (slide number -> title text)
|
||||
import { createRequire } from 'node:module';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (!args[0] || !args[1]) {
|
||||
console.error(
|
||||
'usage: node render.mjs <deck.html> <outdir> [--hide "sel1,sel2"] [--scale 2] [--width 1600] [--height 900]',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const invocationDir = process.env.INIT_CWD || process.cwd();
|
||||
const html = path.resolve(invocationDir, args[0]);
|
||||
const outdir = path.resolve(invocationDir, args[1]);
|
||||
if (!existsSync(html)) {
|
||||
console.error('HTML deck not found: ' + html);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const opt = (flag, fallback) => {
|
||||
const index = args.indexOf(flag);
|
||||
return index >= 0 ? args[index + 1] : fallback;
|
||||
};
|
||||
const hide = (opt('--hide', '') || '')
|
||||
.split(',')
|
||||
.map(selector => selector.trim())
|
||||
.filter(Boolean);
|
||||
const scale = Number(opt('--scale', '2'));
|
||||
const width = Number(opt('--width', '1600'));
|
||||
const height = Number(opt('--height', '900'));
|
||||
|
||||
mkdirSync(outdir, { recursive: true });
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch();
|
||||
const page = await browser.newPage({
|
||||
viewport: { width, height },
|
||||
deviceScaleFactor: scale,
|
||||
reducedMotion: 'reduce',
|
||||
});
|
||||
await page.goto(pathToFileURL(html).href, { waitUntil: 'networkidle' });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
if (hide.length) {
|
||||
await page.addStyleTag({
|
||||
content: hide
|
||||
.map(selector => `${selector}{display:none!important}`)
|
||||
.join('\n'),
|
||||
});
|
||||
}
|
||||
|
||||
const count = await page.locator('.slide').count();
|
||||
if (count === 0) {
|
||||
throw new Error('no .slide elements found in ' + html);
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(outdir)) {
|
||||
if (/^\d+\.png$/.test(entry)) {
|
||||
unlinkSync(path.join(outdir, entry));
|
||||
}
|
||||
}
|
||||
|
||||
const notes = {};
|
||||
for (let index = 1; index <= count; index++) {
|
||||
await page.evaluate(slideNumber => {
|
||||
location.hash = '#' + slideNumber;
|
||||
}, index);
|
||||
await page.waitForFunction(slideNumber => {
|
||||
const slide = document.querySelectorAll('.slide')[slideNumber - 1];
|
||||
return Boolean(slide) && slide.classList.contains('on');
|
||||
}, index);
|
||||
await page.waitForTimeout(450); // let the crossfade settle
|
||||
|
||||
const file = path.join(outdir, String(index).padStart(2, '0') + '.png');
|
||||
await page.screenshot({ path: file });
|
||||
notes[index] = await page.evaluate(slideNumber => {
|
||||
const slide = document.querySelectorAll('.slide')[slideNumber - 1];
|
||||
const title = slide.querySelector('.title, .cover-mark');
|
||||
const text = title ? title.innerText.trim().replace(/\s+/g, ' ') : '';
|
||||
return text || slide.getAttribute('aria-label') || '';
|
||||
}, index);
|
||||
console.log('rendered', file);
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
path.join(outdir, 'notes.json'),
|
||||
JSON.stringify(notes, null, 2),
|
||||
);
|
||||
console.log('done:', count, 'slides ->', outdir);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
}
|
||||
Reference in New Issue
Block a user