2026-05-31 13:54:31 +02:00

528 lines
16 KiB
JavaScript

import {
getRoots
} from "./chunk-UNQ5DBLZ.js";
// node/commands/export.ts
import { Buffer } from "node:buffer";
import path from "node:path";
import process from "node:process";
import { clearUndefined, slash } from "@antfu/utils";
import { outlinePdfFactory } from "@lillallol/outline-pdf";
import { parseRangeString } from "@slidev/parser/core";
import { Presets, SingleBar } from "cli-progress";
import fs from "fs-extra";
import { blue, cyan, dim, green, yellow } from "kolorist";
import { resolve } from "mlly";
import * as pdfLib from "pdf-lib";
import { PDFDocument } from "pdf-lib";
function addToTree(tree, info, slideIndexes, level = 1) {
const titleLevel = info.level;
if (titleLevel && titleLevel > level && tree.length > 0 && tree[tree.length - 1].titleLevel < titleLevel) {
addToTree(tree[tree.length - 1].children, info, slideIndexes, level + 1);
} else {
tree.push({
no: info.index,
children: [],
level,
titleLevel: titleLevel ?? level,
path: String(slideIndexes[info.index + 1]),
hideInToc: Boolean(info.frontmatter?.hideInToc),
title: info.title
});
}
}
function makeOutline(tree) {
return tree.map(({ title, path: path2, level, children }) => {
const rootOutline = title ? `${path2}|${"-".repeat(level - 1)}|${title}` : null;
const childrenOutline = makeOutline(children);
return childrenOutline.length > 0 ? `${rootOutline}
${childrenOutline}` : rootOutline;
}).filter((outline) => !!outline).join("\n");
}
function createSlidevProgress(indeterminate = false) {
function getSpinner(n = 0) {
return [cyan("\u25CF"), green("\u25C6"), blue("\u25A0"), yellow("\u25B2")][n % 4];
}
let current = 0;
let spinner = 0;
let timer;
const progress = new SingleBar({
clearOnComplete: true,
hideCursor: true,
format: ` {spin} ${yellow("rendering")}${indeterminate ? dim(yellow("...")) : " {bar} {value}/{total}"}`,
linewrap: false,
barsize: 30
}, Presets.shades_grey);
return {
bar: progress,
start(total) {
progress.start(total, 0, { spin: getSpinner(spinner) });
timer = setInterval(() => {
spinner += 1;
progress.update(current, { spin: getSpinner(spinner) });
}, 200);
},
update(v) {
current = v;
progress.update(v, { spin: getSpinner(spinner) });
},
stop() {
clearInterval(timer);
progress.stop();
}
};
}
async function exportNotes({
port = 18724,
base = "/",
output = "notes",
timeout = 3e4,
wait = 0
}) {
const { chromium } = await importPlaywright();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const progress = createSlidevProgress(true);
progress.start(1);
if (!output.endsWith(".pdf"))
output = `${output}.pdf`;
await page.goto(`http://localhost:${port}${base}presenter/print`, { waitUntil: "networkidle", timeout });
await page.waitForLoadState("networkidle");
await page.emulateMedia({ media: "screen" });
if (wait)
await page.waitForTimeout(wait);
await page.pdf({
path: output,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
printBackground: true,
preferCSSPageSize: true
});
progress.stop();
browser.close();
return output;
}
async function exportSlides({
port = 18724,
total = 0,
range,
format = "pdf",
output = "slides",
slides,
base = "/",
timeout = 3e4,
wait = 0,
dark = false,
routerMode = "history",
width = 1920,
height = 1080,
withClicks = false,
executablePath = void 0,
withToc = false,
perSlide = false,
scale = 1,
waitUntil,
omitBackground = false
}) {
const pages = parseRangeString(total, range);
const { chromium } = await importPlaywright();
const browser = await chromium.launch({
executablePath
});
const context = await browser.newContext({
viewport: {
width,
// Calculate height for every slides to be in the viewport to trigger the rendering of iframes (twitter, youtube...)
height: perSlide ? height : height * pages.length
},
deviceScaleFactor: scale
});
const page = await context.newPage();
const progress = createSlidevProgress(!perSlide);
async function go(no, clicks) {
const query = new URLSearchParams();
if (withClicks)
query.set("print", "clicks");
else
query.set("print", "true");
if (range)
query.set("range", range);
if (clicks)
query.set("clicks", clicks);
const url = routerMode === "hash" ? `http://localhost:${port}${base}?${query}#${no}` : `http://localhost:${port}${base}${no}?${query}`;
await page.goto(url, {
waitUntil,
timeout
});
if (waitUntil)
await page.waitForLoadState(waitUntil);
await page.emulateMedia({ colorScheme: dark ? "dark" : "light", media: "screen" });
const slide = no === "print" ? page.locator("body") : page.locator(`[data-slidev-no="${no}"]`);
await slide.waitFor();
{
const elements = slide.locator(".slidev-slide-loading");
const count = await elements.count();
for (let index = 0; index < count; index++)
await elements.nth(index).waitFor({ state: "detached" });
}
{
const elements = slide.locator("[data-waitfor]");
const count = await elements.count();
for (let index = 0; index < count; index++) {
const element = elements.nth(index);
const attribute = await element.getAttribute("data-waitfor");
if (attribute) {
await element.locator(attribute).waitFor({ state: "visible" }).catch((e) => {
console.error(e);
process.exitCode = 1;
});
}
}
}
{
const frames = page.frames();
await Promise.all(frames.map((frame) => frame.waitForLoadState()));
}
{
const container = slide.locator("#mermaid-rendering-container");
const count = await container.count();
if (count > 0) {
while (true) {
const element = container.locator("div").first();
if (await element.count() === 0)
break;
await element.waitFor({ state: "detached" });
}
await container.evaluate((node) => node.style.display = "none");
}
}
{
const elements = slide.locator(".monaco-aria-container");
const count = await elements.count();
for (let index = 0; index < count; index++) {
const element = elements.nth(index);
await element.evaluate((node) => node.style.display = "none");
}
}
if (wait)
await page.waitForTimeout(wait);
}
async function getSlidesIndex() {
const clicksBySlide = {};
const slides2 = page.locator(".print-slide-container");
const count = await slides2.count();
for (let i = 0; i < count; i++) {
const id = await slides2.nth(i).getAttribute("id") || "";
const path2 = Number(id.split("-")[0]);
clicksBySlide[path2] = (clicksBySlide[path2] || 0) + 1;
}
const slideIndexes = Object.fromEntries(Object.entries(clicksBySlide).reduce((acc, [path2, clicks], i) => {
acc.push([path2, clicks + (acc[i - 1]?.[1] ?? 0)]);
return acc;
}, []));
return slideIndexes;
}
function getClicksFromUrl(url) {
return url.match(/clicks=([1-9]\d*)/)?.[1];
}
async function genPageWithClicks(fn, no, clicks) {
await fn(no, clicks);
if (withClicks) {
await page.keyboard.press("ArrowRight", { delay: 100 });
const _clicks = getClicksFromUrl(page.url());
if (_clicks && clicks !== _clicks)
await genPageWithClicks(fn, no, _clicks);
}
}
async function genPagePdfPerSlide() {
const buffers = [];
const genPdfBuffer = async (i, clicks) => {
await go(i, clicks);
const pdf = await page.pdf({
width,
height,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
pageRanges: "1",
printBackground: true,
preferCSSPageSize: true
});
buffers.push(pdf);
};
let idx = 0;
for (const i of pages) {
await genPageWithClicks(genPdfBuffer, i);
progress.update(++idx);
}
let mergedPdf = await PDFDocument.create({});
for (const pdfBytes of buffers) {
const pdf = await PDFDocument.load(pdfBytes);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page2) => {
mergedPdf.addPage(page2);
});
}
addPdfMetadata(mergedPdf);
if (withToc)
mergedPdf = await addTocToPdf(mergedPdf);
const buffer = await mergedPdf.save();
await fs.writeFile(output, buffer);
}
async function genPagePdfOnePiece() {
await go("print");
await page.pdf({
path: output,
width,
height,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
printBackground: true,
preferCSSPageSize: true
});
let pdfData = await fs.readFile(output);
let pdf = await PDFDocument.load(pdfData);
addPdfMetadata(pdf);
if (withToc)
pdf = await addTocToPdf(pdf);
pdfData = Buffer.from(await pdf.save());
await fs.writeFile(output, pdfData);
}
async function genPagePngOnePiece(writeToDisk) {
const result = [];
await go("print");
await fs.emptyDir(output);
const slideContainers = page.locator(".print-slide-container");
const count = await slideContainers.count();
for (let i = 0; i < count; i++) {
progress.update(i + 1);
const id = await slideContainers.nth(i).getAttribute("id") || "";
const slideNo = +id.split("-")[0];
const buffer = await slideContainers.nth(i).screenshot({
omitBackground
});
result.push({ slideIndex: slideNo - 1, buffer });
if (writeToDisk)
await fs.writeFile(path.join(output, `${withClicks ? id : slideNo}.png`), buffer);
}
return result;
}
async function genPagePngPerSlide(writeToDisk) {
const result = [];
const genScreenshot = async (no, clicks) => {
await go(no, clicks);
const buffer = await page.screenshot({
omitBackground
});
result.push({ slideIndex: no - 1, buffer });
if (writeToDisk) {
await fs.writeFile(
path.join(output, `${no.toString().padStart(2, "0")}${clicks ? `-${clicks}` : ""}.png`),
buffer
);
}
};
for (const no of pages)
await genPageWithClicks(genScreenshot, no);
return result;
}
function genPagePdf() {
if (!output.endsWith(".pdf"))
output = `${output}.pdf`;
return perSlide ? genPagePdfPerSlide() : genPagePdfOnePiece();
}
function genPagePng(writeToDisk = true) {
return perSlide ? genPagePngPerSlide(writeToDisk) : genPagePngOnePiece(writeToDisk);
}
async function genPageMd() {
const files = await fs.readdir(output);
const mds = files.map((file, i, files2) => {
const slideIndex = getSlideIndex(file);
const mdImg = `![${slides[slideIndex]?.title}](./${slash(path.join(output, file))})
`;
if ((i + 1 === files2.length || getSlideIndex(files2[i + 1]) !== slideIndex) && slides[slideIndex]?.note)
return `${mdImg}${slides[slideIndex]?.note}
`;
return mdImg;
});
if (!output.endsWith(".md"))
output = `${output}.md`;
await fs.writeFile(output, mds.join(""));
}
async function genPagePptx(pngs) {
const { default: PptxGenJS } = await import("pptxgenjs");
const pptx = new PptxGenJS();
const layoutName = `${width}x${height}`;
pptx.defineLayout({
name: layoutName,
width: width / 96,
height: height / 96
});
pptx.layout = layoutName;
const titleSlide = slides[0];
pptx.author = titleSlide?.frontmatter?.author;
pptx.company = "Created using Slidev";
if (titleSlide?.title)
pptx.title = titleSlide?.title;
if (titleSlide?.frontmatter?.info)
pptx.subject = titleSlide?.frontmatter?.info;
pngs.forEach(({ slideIndex, buffer: buffer2 }) => {
const slide = pptx.addSlide();
slide.background = {
data: `data:image/png;base64,${buffer2.toString("base64")}`
};
const note = slides[slideIndex].note;
if (note)
slide.addNotes(note);
});
const buffer = await pptx.write({
outputType: "nodebuffer"
});
if (!output.endsWith(".pptx"))
output = `${output}.pptx`;
await fs.writeFile(output, buffer);
}
function getSlideIndex(file) {
const slideId = file.substring(0, file.indexOf(".")).split("-")[0];
return Number(slideId) - 1;
}
function addPdfMetadata(pdf) {
const titleSlide = slides[0];
if (titleSlide?.title)
pdf.setTitle(titleSlide.title);
if (titleSlide?.frontmatter?.info)
pdf.setSubject(titleSlide.frontmatter.info);
if (titleSlide?.frontmatter?.author)
pdf.setAuthor(titleSlide.frontmatter.author);
if (titleSlide?.frontmatter?.keywords) {
if (Array.isArray(titleSlide?.frontmatter?.keywords))
pdf.setKeywords(titleSlide?.frontmatter?.keywords);
else
pdf.setKeywords(titleSlide?.frontmatter?.keywords.split(","));
}
}
async function addTocToPdf(pdf) {
const outlinePdf = outlinePdfFactory(pdfLib);
const slideIndexes = await getSlidesIndex();
const tocTree = slides.filter((slide) => slide.title).reduce((acc, slide) => {
addToTree(acc, slide, slideIndexes);
return acc;
}, []);
const outline = makeOutline(tocTree);
return await outlinePdf({ outline, pdf });
}
progress.start(pages.length);
if (format === "pdf") {
await genPagePdf();
} else if (format === "png") {
await genPagePng();
} else if (format === "md") {
await genPagePng();
await genPageMd();
} else if (format === "pptx") {
const buffers = await genPagePng(false);
await genPagePptx(buffers);
} else {
throw new Error(`Unsupported exporting format "${format}"`);
}
progress.stop();
browser.close();
return output;
}
function getExportOptions(args, options, outDir, outFilename) {
const config = {
...options.data.config.export,
...args,
...clearUndefined({
waitUntil: args["wait-until"],
withClicks: args["with-clicks"],
executablePath: args["executable-path"],
withToc: args["with-toc"],
perSlide: args["per-slide"],
omitBackground: args["omit-background"]
})
};
const {
entry,
output,
format,
timeout,
wait,
waitUntil,
range,
dark,
withClicks,
executablePath,
withToc,
perSlide,
scale,
omitBackground
} = config;
outFilename = output || options.data.config.exportFilename || outFilename || `${path.basename(entry, ".md")}-export`;
if (outDir)
outFilename = path.join(outDir, outFilename);
return {
output: outFilename,
slides: options.data.slides,
total: options.data.slides.length,
range,
format: format || "pdf",
timeout: timeout ?? 3e4,
wait: wait ?? 0,
waitUntil: waitUntil === "none" ? void 0 : waitUntil ?? "networkidle",
dark: dark || options.data.config.colorSchema === "dark",
routerMode: options.data.config.routerMode,
width: options.data.config.canvasWidth,
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
withClicks: withClicks ?? format === "pptx",
executablePath,
withToc: withToc || false,
perSlide: perSlide || false,
scale: scale || 2,
omitBackground: omitBackground ?? false
};
}
async function importPlaywright() {
const { userRoot, userWorkspaceRoot } = await getRoots();
try {
return await import(await resolve("playwright-chromium", { url: userRoot }));
} catch {
}
if (userWorkspaceRoot !== userRoot) {
try {
return await import(await resolve("playwright-chromium", { url: userWorkspaceRoot }));
} catch {
}
}
const { resolveGlobal } = await import("resolve-global");
try {
const imported = await import(resolveGlobal("playwright-chromium"));
return imported.default ?? imported;
} catch {
}
try {
return await import("playwright-chromium");
} catch {
}
throw new Error("The exporting for Slidev is powered by Playwright, please install it via `npm i -D playwright-chromium`");
}
export {
exportNotes,
exportSlides,
getExportOptions
};