ontoref-outreach/scripts/decks/lib/capture.mjs
2026-07-17 01:00:32 +01:00

80 lines
3.5 KiB
JavaScript

// Capture a built Slidev deck: its rendered DOM and the CSS actually in effect.
//
// The deck is NOT re-parsed. Slidev's markdown is not standard markdown (::right::,
// per-slide frontmatter, v-clicks) and its meaning does not live in the markdown at all
// — it lives in a compiled bundle of UnoCSS utilities, theme rules and <style scoped>
// blocks. Re-implementing that is re-implementing a renderer. So we let Slidev render,
// and take what it produced.
//
// The /print route lays every slide out at once with all clicks revealed — it is what
// `slidev export` drives internally.
//
// The CSS is read from document.styleSheets, not from disk: the build emits 17
// stylesheets, index.html links 2, and Vite loads the rest as route chunks. Picking
// files by hand is guesswork; the browser knows exactly what it applied, in cascade
// order. (Cross-origin sheets — Google Fonts — cannot be read this way and are reported,
// never silently dropped; fetch-fonts.mjs self-hosts them.)
import { createServer } from 'node:http'
import { readFile } from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { extname, join, normalize } from 'node:path'
import { chromium } from './paths.mjs'
const MIME = {
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json',
'.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
'.webp': 'image/webp', '.gif': 'image/gif', '.woff': 'font/woff', '.woff2': 'font/woff2',
'.ttf': 'font/ttf',
}
async function serveDist(DIST) {
const server = createServer(async (req, res) => {
const p = join(DIST, normalize(new URL(req.url, 'http://x').pathname))
const file = existsSync(p) && extname(p) ? p : join(DIST, 'index.html')
res.setHeader('content-type', MIME[extname(file)] ?? 'application/octet-stream')
res.end(await readFile(file))
})
await new Promise(r => server.listen(0, r))
return { server, port: server.address().port }
}
// One dist PER DECK. `slidev build` builds ONE entry: a shared dist/ means the second deck
// silently captures the first one's slides. The deck's own build dir is the only thing that
// makes this a pipeline rather than a one-off.
export async function capture(DIST) {
if (!existsSync(join(DIST, 'index.html'))) {
throw new Error(`no hay build en ${DIST} — ejecuta: just deck-build <id>`)
}
const { server, port } = await serveDist(DIST)
const browser = await chromium.launch()
try {
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
await page.goto(`http://localhost:${port}/print`, { waitUntil: 'networkidle' })
await page.waitForTimeout(4000) // Shiki + fonts + click resolution
const out = await page.evaluate(() => {
const parts = [], external = []
for (const s of document.styleSheets) {
try {
parts.push([...s.cssRules].map(r => r.cssText).join('\n'))
} catch {
external.push(s.href) // cross-origin: reported, not dropped
}
}
const content = document.querySelector('#print-content')
const slides = content ? content.querySelectorAll('.print-slide-container') : []
return {
html: content?.innerHTML ?? '',
css: parts.join('\n'),
slideCount: slides.length,
externalSheets: external,
}
})
if (!out.slideCount) throw new Error('la ruta /print no produjo ninguna slide — ¿cambió Slidev?')
return out
} finally {
await browser.close()
server.close()
}
}