tools: preserve HTML deck exporter
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# HTML deck to PPTX
|
||||
|
||||
This tool renders a ThothII HTML deck as one full-bleed PNG per slide and assembles
|
||||
those images into a 16:9 PowerPoint file with native speaker notes and document
|
||||
metadata. The HTML and notes JSON under `presentations/` are the editable sources;
|
||||
the PPTX is a rendered release artifact.
|
||||
|
||||
Using images preserves the web typography and fine CSS details across machines. The
|
||||
tradeoff is that slide text is not editable in PowerPoint. Speaker notes remain native
|
||||
and editable.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18 or newer and npm.
|
||||
- Playwright's Chromium build.
|
||||
- Network access while rendering the current deck, because its Fraunces and Manrope
|
||||
fonts are loaded from Google Fonts. The render is therefore not fully hermetic.
|
||||
|
||||
Install the locked JavaScript dependencies and Chromium from the repository root:
|
||||
|
||||
```bash
|
||||
npm ci --prefix tools/html-to-pptx
|
||||
npx --prefix tools/html-to-pptx playwright install chromium
|
||||
```
|
||||
|
||||
Dependencies are local to this tool. The scripts intentionally do not use globally
|
||||
installed npm packages.
|
||||
|
||||
## Render slides
|
||||
|
||||
Use a dedicated render directory. The renderer removes stale files matching
|
||||
`NN.png`, writes fresh slide images, and creates a raw `notes.json` containing the
|
||||
title extracted from each slide.
|
||||
|
||||
```bash
|
||||
npm --prefix tools/html-to-pptx run render -- \
|
||||
presentations/<deck>.html <render-dir> \
|
||||
--hide ".hud button,.hint"
|
||||
```
|
||||
|
||||
The HTML deck must expose `.slide` elements activated by `#N` hash routes. Optional
|
||||
rendering flags are `--scale`, `--width`, and `--height`; their defaults are `2`,
|
||||
`1600`, and `900`.
|
||||
|
||||
## Build the PowerPoint file
|
||||
|
||||
Keep refined speaker notes in `presentations/<deck>.notes.json`, using the shape
|
||||
`{"1": "...", "2": "..."}`. Pass that file instead of the raw titles generated
|
||||
during rendering:
|
||||
|
||||
```bash
|
||||
npm --prefix tools/html-to-pptx run build -- \
|
||||
<render-dir> presentations/<deck>.pptx \
|
||||
--notes presentations/<deck>.notes.json \
|
||||
--title "..." --author "..." --subject "..."
|
||||
```
|
||||
|
||||
The output uses the wide PowerPoint layout (13.333 x 7.5 inches), with one image per
|
||||
slide. Edit the HTML and rebuild whenever slide content changes.
|
||||
|
||||
## Validation
|
||||
|
||||
Run the syntax checks with:
|
||||
|
||||
```bash
|
||||
npm --prefix tools/html-to-pptx test
|
||||
```
|
||||
|
||||
Before publishing a regenerated deck, inspect at least one PNG at full resolution and
|
||||
use `python-pptx` to verify the slide count, the 12192000 x 6858000 EMU page size, and
|
||||
one picture per slide. `python-pptx` is an optional, currently unpinned QA dependency;
|
||||
it is not needed to render or build the deck.
|
||||
|
||||
The locked direct dependencies are Playwright 1.58.0 (Apache-2.0) and PptxGenJS
|
||||
3.12.0 (MIT). Their transitive versions and integrity hashes are recorded in
|
||||
`package-lock.json`.
|
||||
|
||||
### Temporary dependency exception
|
||||
|
||||
PptxGenJS 3.12.0 declares `image-size` 1.2.1, which npm flags for the availability-only
|
||||
[GHSA-w3rx-r6r6-pgpr](https://github.com/advisories/GHSA-w3rx-r6r6-pgpr) and
|
||||
[GHSA-5p2g-fcmc-qvqq](https://github.com/advisories/GHSA-5p2g-fcmc-qvqq) advisories. No patched
|
||||
`image-size` release exists as of 2026-08-31, and current PptxGenJS releases retain the dependency.
|
||||
The affected parser is not imported by PptxGenJS's published builder path used here; this tool also
|
||||
accepts only locally generated PNG slides with explicit dimensions. Do not use it with untrusted
|
||||
image inputs. Reassess this exception by 2026-11-29, or sooner when a patched dependency is released.
|
||||
Generated
+217
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"name": "thothii-html-to-pptx",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "thothii-html-to-pptx",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.0",
|
||||
"pptxgenjs": "3.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.130",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
|
||||
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
|
||||
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
|
||||
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pptxgenjs": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.12.0.tgz",
|
||||
"integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.7.3",
|
||||
"https": "^1.0.0",
|
||||
"image-size": "^1.0.0",
|
||||
"jszip": "^3.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "thothii-html-to-pptx",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
"check": "node --check scripts/render.mjs && node --check scripts/build.mjs",
|
||||
"render": "node scripts/render.mjs",
|
||||
"test": "npm run check"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "1.58.0",
|
||||
"pptxgenjs": "3.12.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
// Assemble rendered slide PNGs into a 16:9 PPTX: one full-bleed image per slide,
|
||||
// native speaker notes, document metadata.
|
||||
//
|
||||
// Usage:
|
||||
// node build.mjs <pngdir> <out.pptx> [--notes notes.json] [--title T] [--author A] [--subject S]
|
||||
import { createRequire } from 'node:module';
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const PptxGenJS = require('pptxgenjs');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (!args[0] || !args[1]) {
|
||||
console.error(
|
||||
'usage: node build.mjs <pngdir> <out.pptx> [--notes notes.json] [--title T] [--author A] [--subject S]',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const invocationDir = process.env.INIT_CWD || process.cwd();
|
||||
const dir = path.resolve(invocationDir, args[0]);
|
||||
const out = path.resolve(invocationDir, args[1]);
|
||||
const opt = (flag, fallback) => {
|
||||
const index = args.indexOf(flag);
|
||||
return index >= 0 ? args[index + 1] : fallback;
|
||||
};
|
||||
|
||||
const presentation = new PptxGenJS();
|
||||
presentation.layout = 'LAYOUT_WIDE'; // 13.333 x 7.5 in
|
||||
presentation.title = opt('--title', path.basename(out, '.pptx'));
|
||||
presentation.author = opt('--author', 'ThothII');
|
||||
presentation.subject = opt('--subject', '');
|
||||
|
||||
const notesOption = opt('--notes', null);
|
||||
const notesPath = notesOption
|
||||
? path.resolve(invocationDir, notesOption)
|
||||
: path.join(dir, 'notes.json');
|
||||
const notes = existsSync(notesPath)
|
||||
? JSON.parse(readFileSync(notesPath, 'utf8'))
|
||||
: {};
|
||||
|
||||
const pngs = readdirSync(dir)
|
||||
.filter(file => /^\d+\.png$/.test(file))
|
||||
.sort((left, right) => Number.parseInt(left, 10) - Number.parseInt(right, 10));
|
||||
if (pngs.length === 0) {
|
||||
console.error('no NN.png files in ' + dir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const file of pngs) {
|
||||
const slideNumber = Number.parseInt(file, 10);
|
||||
const slide = presentation.addSlide();
|
||||
slide.background = { color: 'FCFAFA' };
|
||||
slide.addImage({
|
||||
path: path.join(dir, file),
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 13.333,
|
||||
h: 7.5,
|
||||
});
|
||||
if (notes[slideNumber]) {
|
||||
slide.addNotes(notes[slideNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
await presentation.writeFile({ fileName: out });
|
||||
console.log('built', out, '-', pngs.length, 'slides');
|
||||
@@ -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