From 8f070b62cb9e2eccb816a3fb0ae0b9e42c9d4cda Mon Sep 17 00:00:00 2001 From: Oliver-Tobias Ripka Date: Tue, 12 Mar 2024 13:01:34 +0100 Subject: [PATCH 01/18] feat: Handouts wip --- demo/starter/handout-bottom.vue | 20 + demo/starter/handout-cover.vue | 68 +++ demo/starter/slides.md | 2 + packages/client/composables/useNav.ts | 4 +- .../internals/HandoutPrintContainer.vue | 59 ++ .../client/internals/HandoutPrintSlide.vue | 38 ++ packages/client/pages/cover/print.vue | 62 +++ packages/client/pages/handout/print.vue | 61 +++ packages/client/routes.ts | 11 +- packages/slidev/node/cli.ts | 91 ++++ packages/slidev/node/commands/export.ts | 511 +++++++++++++++++- .../slidev/node/virtual/global-components.ts | 16 +- packages/slidev/node/virtual/index.ts | 4 +- packages/types/client.d.ts | 16 + packages/types/src/cli.ts | 15 + 15 files changed, 972 insertions(+), 6 deletions(-) create mode 100644 demo/starter/handout-bottom.vue create mode 100644 demo/starter/handout-cover.vue create mode 100644 packages/client/internals/HandoutPrintContainer.vue create mode 100644 packages/client/internals/HandoutPrintSlide.vue create mode 100644 packages/client/pages/cover/print.vue create mode 100644 packages/client/pages/handout/print.vue diff --git a/demo/starter/handout-bottom.vue b/demo/starter/handout-bottom.vue new file mode 100644 index 0000000000..d8e51c820a --- /dev/null +++ b/demo/starter/handout-bottom.vue @@ -0,0 +1,20 @@ + + \ No newline at end of file diff --git a/demo/starter/handout-cover.vue b/demo/starter/handout-cover.vue new file mode 100644 index 0000000000..6468eef56d --- /dev/null +++ b/demo/starter/handout-cover.vue @@ -0,0 +1,68 @@ + diff --git a/demo/starter/slides.md b/demo/starter/slides.md index 07677b1684..7df8e23d13 100644 --- a/demo/starter/slides.md +++ b/demo/starter/slides.md @@ -599,3 +599,5 @@ class: text-center # Learn More [Documentations](https://sli.dev) · [GitHub](https://github.com/slidevjs/slidev) · [Showcases](https://sli.dev/showcases.html) + + \ No newline at end of file diff --git a/packages/client/composables/useNav.ts b/packages/client/composables/useNav.ts index db4026210c..cc899238db 100644 --- a/packages/client/composables/useNav.ts +++ b/packages/client/composables/useNav.ts @@ -65,6 +65,7 @@ export interface SlidevContextNavState { currentRoute: ComputedRef isPrintMode: ComputedRef isPrintWithClicks: ComputedRef + isHandout: ComputedRef isEmbedded: ComputedRef isPlaying: ComputedRef isPresenter: ComputedRef @@ -237,7 +238,8 @@ const useNavState = createSharedComposable((): SlidevContextNavState => { const router = useRouter() const currentRoute = computed(() => router.currentRoute.value) - const isPrintMode = computed(() => currentRoute.value.query.print !== undefined) + const isPrintMode = computed(() => currentRoute.value.query.print !== undefined || isHandout.value) + const isHandout = computed(() => currentRoute.value.query.handout !== undefined || currentRoute.value.path.startsWith('/handout')) const isPrintWithClicks = computed(() => currentRoute.value.query.print === 'clicks') const isEmbedded = computed(() => currentRoute.value.query.embedded !== undefined) const isPlaying = computed(() => currentRoute.value.name === 'play') diff --git a/packages/client/internals/HandoutPrintContainer.vue b/packages/client/internals/HandoutPrintContainer.vue new file mode 100644 index 0000000000..e0bbf5f20f --- /dev/null +++ b/packages/client/internals/HandoutPrintContainer.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/packages/client/internals/HandoutPrintSlide.vue b/packages/client/internals/HandoutPrintSlide.vue new file mode 100644 index 0000000000..18277b98c0 --- /dev/null +++ b/packages/client/internals/HandoutPrintSlide.vue @@ -0,0 +1,38 @@ + + + + \ No newline at end of file diff --git a/packages/client/pages/cover/print.vue b/packages/client/pages/cover/print.vue new file mode 100644 index 0000000000..f76fa15806 --- /dev/null +++ b/packages/client/pages/cover/print.vue @@ -0,0 +1,62 @@ + + + + + \ No newline at end of file diff --git a/packages/client/pages/handout/print.vue b/packages/client/pages/handout/print.vue new file mode 100644 index 0000000000..888506fc18 --- /dev/null +++ b/packages/client/pages/handout/print.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/client/routes.ts b/packages/client/routes.ts index e178c4fa4d..b8253b1a80 100644 --- a/packages/client/routes.ts +++ b/packages/client/routes.ts @@ -7,7 +7,16 @@ export const routes: RouteRecordRaw[] = [ path: '/print', component: () => import('./pages/print.vue'), }, - + { + name: 'handout', + path: '/handout', + component: () => import('./pages/handout/print.vue'), + }, + { + name: 'cover', + path: '/cover', + component: () => import('./pages/cover/print.vue'), + }, // Redirects { path: '', redirect: { path: '/1' } }, ] diff --git a/packages/slidev/node/cli.ts b/packages/slidev/node/cli.ts index bd6aa3b6f0..06e61807a7 100644 --- a/packages/slidev/node/cli.ts +++ b/packages/slidev/node/cli.ts @@ -533,6 +533,97 @@ cli.command( }, ) +cli.command( + 'export-handout [entry..]', + 'Export handout to PDF', + args => exportOptionsHandout(commonOptions(args)) + .strict() + .help(), + async (args) => { + const { entry, theme } = args + process.env.NODE_ENV = 'production' + const { exportHandout, getExportOptionsHandout } = await import('./commands/export') + const port = await getPort(12445) + + for (const entryFile of entry as unknown as string) { + const options = await resolveOptions({ entry: entryFile, theme }, 'export') + const server = await createServer( + options, + { + server: { port }, + clearScreen: false, + }, + ) + await server.listen(port) + printInfo(options) + + const result = await exportHandout({ + port, + + ...getExportOptionsHandout({ ...args, entry: entryFile }, options), + }) + console.log(`${green(' ✓ ')}${dim('exported to ')}./${result}\n`) + server.close() + } + + process.exit(0) + }, +) + +function exportOptionsHandout(args: Argv) { + return args + .option('output', { + type: 'string', + describe: 'path to the output', + }) + .option('format', { + type: 'string', + choices: ['pdf'], + describe: 'output format', + }) + .option('timeout', { + type: 'number', + describe: 'timeout for rendering the print page', + }) + .option('range', { + type: 'string', + describe: 'page ranges to export, for example "1,4-5,6"', + }) + .option('dark', { + type: 'boolean', + describe: 'export as dark theme', + }) + .option('with-clicks', { + alias: 'c', + type: 'boolean', + describe: 'export pages for every clicks', + }) + .option('executable-path', { + type: 'string', + describe: 'executable to override playwright bundled browser', + }) + .option('per-slide', { + type: 'boolean', + describe: 'slide slides slide by slide. Works better with global components, but will break cross slide links and TOC in PDF', + }) + .option('cover', { + type: 'boolean', + describe: 'prepend cover to handout, needs handout-cover.vue in project', + }) + .option('slide-format', { + choices: ['pdf', 'png', 'jpeg'], + describe: 'intermediate output format of slides', + }) + .option('jpeg-image-quality', { + type: 'number', + describe: 'jpeg quality of intermediate slides', + }) + .option('write-slide-images-to-disk', { + type: 'boolean', + describe: 'write intermediate slide images to disk if --slide-format is png or jpeg', + }) +} + cli .help() .parse() diff --git a/packages/slidev/node/commands/export.ts b/packages/slidev/node/commands/export.ts index bee9e222b5..b454705a76 100644 --- a/packages/slidev/node/commands/export.ts +++ b/packages/slidev/node/commands/export.ts @@ -4,10 +4,10 @@ import fs from 'fs-extra' import { blue, cyan, dim, green, yellow } from 'kolorist' import { Presets, SingleBar } from 'cli-progress' import { parseRangeString } from '@slidev/parser/core' -import type { ExportArgs, ResolvedSlidevOptions, SlideInfo, TocItem } from '@slidev/types' +import type { ExportArgs, ExportArgsHandout, ResolvedSlidevOptions, SlideInfo, TocItem } from '@slidev/types' import { outlinePdfFactory } from '@lillallol/outline-pdf' import * as pdfLib from 'pdf-lib' -import { PDFDocument } from 'pdf-lib' +import { PDFDocument, PageSizes, rgb } from 'pdf-lib' import { resolve } from 'mlly' import { getRoots } from '../resolver' @@ -35,6 +35,33 @@ export interface ExportOptions { scale?: number } +export interface ExportOptionsHandout { + total: number + range?: string + slides: SlideInfo[] + port?: number + base?: string + format?: 'pdf' | 'png' | 'md' + output?: string + slideFormat?: 'jpeg' | 'png' | 'pdf' + cover?: boolean + jpegImageQuality?: number + writeSlideImagesToDisk?: boolean + timeout?: number + dark?: boolean + routerMode?: 'hash' | 'history' + width?: number + height?: number + withClicks?: boolean + executablePath?: string + withToc?: boolean + /** + * Render slides slide by slide. Works better with global components, but will break cross slide links and TOC in PDF. + * @default false + */ + perSlide?: boolean +} + function addToTree(tree: TocItem[], info: SlideInfo, slideIndexes: Record, level = 1) { const titleLevel = info.level if (titleLevel && titleLevel > level && tree.length > 0) { @@ -506,6 +533,486 @@ export function getExportOptions(args: ExportArgs, options: ResolvedSlidevOption } } +export async function exportHandout({ + port = 18724, + total = 0, + range, + format = 'pdf', + output = 'handout', + slideFormat = 'pdf', + jpegImageQuality = 90, + cover = false, + writeSlideImagesToDisk = false, + slides, + base = '/', + timeout = 30000, + dark = false, + routerMode = 'history', + width = 1920, + height = 1080, + withClicks = false, + executablePath = undefined, + perSlide = false, +}: ExportOptionsHandout) { + const pages: number[] = 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: 2, + }) + const page = await context.newPage() + const progress = createSlidevProgress(!perSlide) + + async function go(no: number | string, clicks?: string) { + const path = `${no}?handout${withClicks ? '=clicks' : ''}${clicks ? `&clicks=${clicks}` : ''}${range ? `&range=${range}` : ''}` + const url = routerMode === 'hash' + ? `http://localhost:${port}${base}#${path}` + : `http://localhost:${port}${base}${path}` + + await page.goto(url, { + waitUntil: 'networkidle', + timeout, + }) + await page.waitForLoadState('networkidle') + await page.emulateMedia({ colorScheme: dark ? 'dark' : 'light', media: 'screen' }) + // Wait for slides to be loaded + { + const elements = page.locator('.slidev-slide-loading') + const count = await elements.count() + for (let index = 0; index < count; index++) + await elements.nth(index).waitFor({ state: 'detached' }) + } + // Check for "data-waitfor" attribute and wait for given element to be loaded + { + const elements = page.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() + } + } + // Wait for frames to load + { + const frames = page.frames() + await Promise.all(frames.map(frame => frame.waitForLoadState())) + } + // Wait for Mermaid graphs to be rendered + { + const container = page.locator('#mermaid-rendering-container') + 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') + } + // Hide Monaco aria container + { + const elements = page.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') + } + } + } + + function getClicksFromUrl(url: string) { + return url.match(/clicks=([1-9][0-9]*)/)?.[1] + } + + async function genPageWithClicks( + fn: (i: number, clicks?: string) => Promise, + i: number, + clicks?: string, + ) { + await fn(i, clicks) + if (withClicks) { + await page.keyboard.press('ArrowRight', { delay: 100 }) + const _clicks = getClicksFromUrl(page.url()) + if (_clicks && clicks !== _clicks) + await genPageWithClicks(fn, i, _clicks) + } + } + + async function genPagePdfPerSlide() { + const buffers: Buffer[] = [] + const genPdfBuffer = async (i: number, clicks?: string) => { + 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) + } + + const 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((page) => { + mergedPdf.addPage(page) + }) + } + + const buffer = await mergedPdf.save() + await fs.writeFile(output, buffer) + } + + async function genCoverPdfOnePiece() { + const output_notes = `${output}-handout-cover.pdf` + await go('cover') + await page.pdf({ + path: output_notes, + width, + height, + margin: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + printBackground: true, + preferCSSPageSize: true, + }) + return output_notes + } + + async function genNotesPdfOnePiece() { + const output_notes = `${output}-notes.pdf` + await go('handout') + await page.pdf({ + path: output_notes, + width, + height, + margin: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + printBackground: true, + preferCSSPageSize: true, + }) + return output_notes + } + + async function genSlidePdfOnePiece() { + const output_slides = `${output}-slides.pdf` + + await go('print') + + await page.pdf({ + path: output_slides, + width, + height, + margin: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + printBackground: true, + preferCSSPageSize: true, + }) + + return output_slides + } + + async function mergeSlidesWithNotes(slides: Buffer[] | pdfLib.PDFDocument, pdfNotes: pdfLib.PDFDocument) { + let numSlides + let pdfSlidePages + + if (slideFormat !== 'pdf') { + numSlides = (slides as Buffer[]).length + } + else { + pdfSlidePages = (slides as pdfLib.PDFDocument).getPages() + numSlides = pdfSlidePages.length + } + + const pdf = await PDFDocument.create() + + if (cover) { + const coverPath = await genCoverPdfOnePiece() + const coverData = await fs.readFile(coverPath) + const pdfCover = await PDFDocument.load(coverData) + for (let i = 0; i < pdfCover.getPages().length; i++) { + const coverPage = pdf.addPage(PageSizes.A4) + const coverEmbedded = await pdf.embedPage(pdfCover.getPages()[i]) + const coverEmbeddedDims = coverEmbedded.scale(1) + coverPage.drawPage(coverEmbedded, { + ...coverEmbeddedDims, + x: coverPage.getWidth() / 2 - coverEmbeddedDims.width / 2, + y: 0, + }) + } + } + + const notesPages = pdfNotes.getPages() + + for (let i = 0; i < numSlides; i++) { + let slideEmbedded + let slideEmbeddedDims + + /* add slide */ + if (slideFormat === 'png') { + slideEmbedded = await pdf.embedPng((slides as Buffer[])[i]) + slideEmbeddedDims = slideEmbedded.scale(0.27) + } + else if (slideFormat === 'jpeg') { + slideEmbedded = await pdf.embedJpg((slides as Buffer[])[i]) + slideEmbeddedDims = slideEmbedded.scale(0.27) + } + else if (slideFormat === 'pdf' && pdfSlidePages !== undefined) { + slideEmbedded = await pdf.embedPage(pdfSlidePages[i]) + slideEmbeddedDims = slideEmbedded.scale(0.72) + } + else { + throw new Error(`Unsupported slide exporting format "${slideFormat}"`) + } + + const firstPage = pdf.addPage(PageSizes.A4) + + if (slideFormat !== 'pdf') { + firstPage.drawImage(slideEmbedded as pdfLib.PDFImage, { + x: firstPage.getWidth() / 2 - slideEmbeddedDims.width / 2, + y: firstPage.getHeight() - slideEmbeddedDims.height - 70, + width: slideEmbeddedDims.width, + height: slideEmbeddedDims.height, + }) + + firstPage.drawRectangle({ + x: firstPage.getWidth() / 2 - slideEmbeddedDims.width / 2, + y: firstPage.getHeight() - slideEmbeddedDims.height - 70, + width: slideEmbeddedDims.width, + height: slideEmbeddedDims.height, + borderColor: rgb(0, 0, 0), + borderWidth: 1.5, + }) + } + else if (slideFormat === 'pdf') { + firstPage.drawPage(slideEmbedded as pdfLib.PDFEmbeddedPage, { + ...slideEmbeddedDims, + x: firstPage.getWidth() / 2 - slideEmbeddedDims.width / 2, + y: firstPage.getHeight() - slideEmbeddedDims.height - 30, + width: slideEmbeddedDims.width, + height: slideEmbeddedDims.height, + }) + + firstPage.drawRectangle({ + x: firstPage.getWidth() / 2 - slideEmbeddedDims.width / 2, + y: firstPage.getHeight() - slideEmbeddedDims.height - 30, + width: slideEmbeddedDims.width, + height: slideEmbeddedDims.height, + borderColor: rgb(0, 0, 0), + borderWidth: 1.5, + }) + } + + /* add notes */ + + if (notesPages[i]) { + + const noteEmbedded = await pdf.embedPage(notesPages[i], { + left: 0, + bottom: 0, + right: 600, + top: 530, + }) + + const noteEmbeddedDims = noteEmbedded.scale(0.93) + + firstPage.drawPage(noteEmbedded, { + ...noteEmbeddedDims, + x: firstPage.getWidth() / 2 - noteEmbeddedDims.width / 2, + y: 0, + }) + } else{ + console.error(`Notes page index "${i}" does not exist`) + } + + } + + return pdf + } + + async function genPagePdfOnePiece() { + let slidesData + let pdfSlides + let imageBuffer + let pdf + + if (slideFormat === 'pdf') { + // slidesPath = "handout-slides.pdf" + const slidesPath = await genSlidePdfOnePiece() + slidesData = await fs.readFile(slidesPath) + pdfSlides = await PDFDocument.load(slidesData) + } + else { + imageBuffer = await genPagePngOnePieceToBuffer() + } + + let notesPath = 'handout-notes.pdf' + notesPath = await genNotesPdfOnePiece() + + const notesData = await fs.readFile(notesPath) + const pdfNotes = await PDFDocument.load(notesData) + + if (slideFormat === 'pdf' && pdfSlides) + pdf = await mergeSlidesWithNotes(pdfSlides, pdfNotes) + else if (imageBuffer) + pdf = await mergeSlidesWithNotes(imageBuffer, pdfNotes) + + if (!pdf) + throw new Error('PDF could not be generated') + + 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(',')) + } + + const pdfData = Buffer.from(await pdf.save()) + await fs.writeFile(`${output}.pdf`, pdfData) + } + + async function genPagePngOnePieceToBuffer() { + await go('print') + await fs.emptyDir(output) + const slides = await page.locator('.print-slide-container') + const count = await slides.count() + const pngSlidesBuffer = [] + for (let i = 0; i < count; i++) { + progress.update(i + 1) + let id = (await slides.nth(i).getAttribute('id')) || '' + id = withClicks ? id : id.split('-')[0] + + let buffer + if (slideFormat === 'png') + buffer = await slides.nth(i).screenshot({ scale: 'device', type: 'png' }) + + else + buffer = await slides.nth(i).screenshot({ scale: 'device', quality: jpegImageQuality, type: 'jpeg' }) + + pngSlidesBuffer.push(buffer) + + if (writeSlideImagesToDisk) { + if (slideFormat === 'png') + await fs.writeFile(path.join(output, `${id}.png`), buffer) + else + await fs.writeFile(path.join(output, `${id}.jpeg`), buffer) + } + } + return pngSlidesBuffer + } + + function genPagePdf() { + // if (!output.endsWith('.pdf')) + // output = `${output}.pdf` + return perSlide + ? genPagePdfPerSlide() + : genPagePdfOnePiece() + } + + progress.start(pages.length) + + if (format === 'pdf') + await genPagePdf() + + else + throw new Error(`Unsupported exporting format "${format}"`) + + progress.stop() + browser.close() + return output +} + +export function getExportOptionsHandout(args: ExportArgsHandout, options: ResolvedSlidevOptions, outDir?: string, outFilename?: string): Omit { + const config = { + ...options.data.config.export, + ...args, + withClicks: args['with-clicks'], + executablePath: args['executable-path'], + perSlide: args['per-slide'], + slideFormat: args['slide-format'], + jpegImageQuality: args['jpeg-image-quality'], + writeSlideImagesToDisk: args['write-slide-images-to-disk'], + } + const { + entry, + output, + format, + timeout, + range, + dark, + withClicks, + executablePath, + cover, + withToc, + perSlide, + slideFormat, + jpegImageQuality, + writeSlideImagesToDisk, + } = config + outFilename = output || options.data.config.exportFilename || outFilename || `${path.basename(entry, '.md')}-export` + if (outDir) + outFilename = path.join(outDir, outFilename) + return { + slideFormat: slideFormat as 'pdf' | 'png' | 'jpeg', + jpegImageQuality, + writeSlideImagesToDisk: writeSlideImagesToDisk || false, + output: outFilename, + slides: options.data.slides, + total: options.data.slides.length, + range, + cover: cover || false, + format: (format || 'pdf') as 'pdf', + timeout: timeout ?? 30000, + 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 || false, + executablePath, + withToc: withToc || false, + perSlide: perSlide || false, + } +} + async function importPlaywright(): Promise { const { userRoot, userWorkspaceRoot } = await getRoots() diff --git a/packages/slidev/node/virtual/global-components.ts b/packages/slidev/node/virtual/global-components.ts index c44a07700f..cb617e7e69 100644 --- a/packages/slidev/node/virtual/global-components.ts +++ b/packages/slidev/node/virtual/global-components.ts @@ -3,7 +3,7 @@ import { join } from 'node:path' import { toAtFS } from '../resolver' import type { VirtualModuleTemplate } from './types' -function createGlobalComponentTemplate(layer: 'top' | 'bottom'): VirtualModuleTemplate { +function createGlobalComponentTemplate(layer: 'top' | 'bottom' | 'handout-bottom' | 'handout-cover'): VirtualModuleTemplate { return { id: `/@slidev/global-components/${layer}`, getContent({ roots }) { @@ -16,6 +16,18 @@ function createGlobalComponentTemplate(layer: 'top' | 'bottom'): VirtualModuleTe join(root, 'GlobalTop.vue'), ] } + else if (layer === 'handout-bottom') { + return [ + join(root, 'handout-bottom.vue'), + join(root, 'HandoutBottom.vue'), + ] + } + else if (layer === 'handout-cover') { + return [ + join(root, 'handout-cover.vue'), + join(root, 'HandoutCover.vue'), + ] + } else { return [ join(root, 'global-bottom.vue'), @@ -70,3 +82,5 @@ render() { export const templateGlobalTop = createGlobalComponentTemplate('top') export const templateGlobalBottom = createGlobalComponentTemplate('bottom') +export const templateGlobalHandoutBottom = createGlobalComponentTemplate('handout-bottom') +export const templateGlobalHandoutCover = createGlobalComponentTemplate('handout-cover') diff --git a/packages/slidev/node/virtual/index.ts b/packages/slidev/node/virtual/index.ts index 080be10f2d..261b46de71 100644 --- a/packages/slidev/node/virtual/index.ts +++ b/packages/slidev/node/virtual/index.ts @@ -1,6 +1,6 @@ import { templateConfigs } from './configs' import { templateLegacyRoutes, templateLegacyTitles } from './deprecated' -import { templateGlobalBottom, templateGlobalTop, templateNavControls } from './global-components' +import { templateGlobalBottom, templateGlobalTop, templateNavControls, templateGlobalHandoutBottom, templateGlobalHandoutCover } from './global-components' import { templateLayouts } from './layouts' import { templateMonacoTypes } from './monaco-types' import { templateSetups } from './setups' @@ -16,6 +16,8 @@ export const templates = [ templateStyle, templateGlobalBottom, templateGlobalTop, + templateGlobalHandoutBottom, + templateGlobalHandoutCover, templateNavControls, templateSlides, templateLayouts, diff --git a/packages/types/client.d.ts b/packages/types/client.d.ts index 39a864d563..011e86e46b 100644 --- a/packages/types/client.d.ts +++ b/packages/types/client.d.ts @@ -22,6 +22,22 @@ declare module '#slidev/global-components/bottom' { export default component } + +declare module '#slidev/global-components/handout-bottom' { + import type { ComponentOptions } from 'vue' + + const component: ComponentOptions + export default component +} + +declare module '#slidev/global-components/handout-cover' { + import type { ComponentOptions } from 'vue' + + const component: ComponentOptions + export default component +} + + declare module '#slidev/slides' { import type { ShallowRef } from 'vue' import type { SlideRoute } from '@slidev/types' diff --git a/packages/types/src/cli.ts b/packages/types/src/cli.ts index 86860ee263..3a7a990b5f 100644 --- a/packages/types/src/cli.ts +++ b/packages/types/src/cli.ts @@ -3,6 +3,21 @@ export interface CommonArgs { theme?: string } +export interface ExportArgsHandout extends CommonArgs { + output?: string + format?: string + timeout?: number + range?: string + dark?: boolean + cover?: boolean + 'with-clicks'?: boolean + 'executable-path'?: string + 'per-slide'?: boolean + 'slide-format'?: string + 'jpeg-image-quality'?: number + 'write-slide-images-to-disk'?: boolean +} + export interface ExportArgs extends CommonArgs { 'output'?: string 'format'?: string From d37e7594ae39e70e4bc0d5490b76c352e25aaea5 Mon Sep 17 00:00:00 2001 From: Oliver-Tobias Ripka Date: Tue, 12 Mar 2024 13:23:58 +0100 Subject: [PATCH 02/18] fix: margins and remove debugging bg colors --- demo/starter/handout-bottom.vue | 2 +- packages/client/internals/HandoutPrintSlide.vue | 6 +++--- packages/slidev/node/commands/export.ts | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/demo/starter/handout-bottom.vue b/demo/starter/handout-bottom.vue index d8e51c820a..a3b9d038e2 100644 --- a/demo/starter/handout-bottom.vue +++ b/demo/starter/handout-bottom.vue @@ -1,5 +1,5 @@