78 lines
3.3 KiB
JavaScript
78 lines
3.3 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 { DIST, 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() {
|
||
|
|
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 }
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function capture() {
|
||
|
|
if (!existsSync(join(DIST, 'index.html'))) {
|
||
|
|
throw new Error(`no hay build en ${DIST} — ejecuta 'pnpm build' en outreach/presentation primero`)
|
||
|
|
}
|
||
|
|
const { server, port } = await serveDist()
|
||
|
|
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()
|
||
|
|
}
|
||
|
|
}
|