528 lines
16 KiB
JavaScript
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
|
|
};
|