@lincyaw/web 1.0.0-rc.0 → 1.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/package.json +21 -19
- package/src/app/base.css +117 -0
- package/src/app/block-playback.ts +81 -0
- package/src/app/block-report.ts +104 -0
- package/src/app/document-modules.ts +30 -0
- package/src/app/env.d.ts +12 -0
- package/src/app/main.ts +199 -0
- package/src/app/page-app.ts +166 -0
- package/src/app/router.ts +109 -0
- package/src/app/server.ts +32 -0
- package/src/app/site/home.tsx +117 -0
- package/src/app/site/layout.tsx +93 -0
- package/src/app/site/search.tsx +22 -0
- package/src/app/site.css +1450 -0
- package/src/client/document.css +172 -0
- package/src/client/host.ts +22 -94
- package/src/client/preview-frame.ts +152 -0
- package/src/client/runtime.ts +160 -0
- package/src/domain/app-config.ts +91 -0
- package/src/domain/config.ts +16 -28
- package/src/domain/dev-updates.ts +12 -0
- package/src/domain/index.ts +17 -7
- package/src/domain/landing.ts +83 -0
- package/src/domain/links.ts +23 -18
- package/src/domain/messages.ts +76 -63
- package/src/domain/pages.ts +23 -41
- package/src/domain/themes.ts +74 -0
- package/src/domain/workspace-home.ts +12 -0
- package/src/node/client-modules.ts +92 -0
- package/src/node/document-bundle.ts +227 -0
- package/src/node/landing-template.ts +34 -0
- package/src/node/landing.ts +91 -0
- package/src/node/offline.ts +34 -11
- package/src/node/publish.ts +83 -15
- package/src/node/renderer.ts +77 -117
- package/src/node/site-assets.ts +19 -0
- package/src/node/site-build.ts +332 -0
- package/src/node/site-files.ts +87 -0
- package/src/node/site-model.ts +337 -0
- package/src/node/site-page.ts +109 -0
- package/src/node/site-server.ts +825 -0
- package/src/node/site-theme.ts +190 -0
- package/src/node/slides.ts +135 -0
- package/src/node/viewer-app.ts +504 -0
- package/src/node/viewer-build.ts +38 -0
- package/custom-nav-controls.vue +0 -17
- package/src/client/env.d.ts +0 -1
- package/src/client/home.ts +0 -182
- package/src/client/index.ts +0 -4
- package/src/client/page.ts +0 -38
- package/src/client/preview.ts +0 -71
- package/src/client/style.css +0 -427
- package/src/client/theme.ts +0 -322
- package/src/node/addons.ts +0 -76
- package/src/node/site.ts +0 -349
- package/src/node/sites.ts +0 -531
- package/src/node/vite.ts +0 -166
- package/src/node/vitepress.ts +0 -72
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
4
|
+
import { deckLocale, isRecord } from '@lincyaw/kernel'
|
|
5
|
+
import type { RenderContext, SiteSpec } from '@lincyaw/kernel'
|
|
6
|
+
import { findInstalledPackage } from '@lincyaw/kernel/node'
|
|
7
|
+
import { isThemeReference, SITE_THEME_IDS, WEB_MESSAGES } from '../domain/index.ts'
|
|
8
|
+
import type { SiteThemeId } from '../domain/index.ts'
|
|
9
|
+
import { isSingle, siteLanguage } from './site-model.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A site theme of the workspace's own (`site.theme: ./theme`, or an installed package): a directory with `theme.json`.
|
|
13
|
+
* It extends a built-in theme (its layouts and defaults), sets the theme's parameters, overrides design tokens for the
|
|
14
|
+
* light and the dark appearance, and adds web fonts and stylesheets. Its files are published below `_theme/<name>/`;
|
|
15
|
+
* every page of the site links them after the app's styles and carries `data-pm-site-theme-name`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const THEME_FILE = 'theme.json'
|
|
19
|
+
export const THEME_DIR = '_theme'
|
|
20
|
+
const TOKENS_FILE = 'presmium-tokens.css'
|
|
21
|
+
|
|
22
|
+
export interface ThemeManifest {
|
|
23
|
+
name: string
|
|
24
|
+
extends: SiteThemeId
|
|
25
|
+
params: Record<string, unknown>
|
|
26
|
+
/** Token name (without `--pm-`) to value; those the dark appearance overrides (colors, shadows) are the light ones. */
|
|
27
|
+
tokens: Record<string, string>
|
|
28
|
+
/** Values for the dark appearance, of the tokens it overrides (colors, shadows). */
|
|
29
|
+
dark: Record<string, string>
|
|
30
|
+
/** Stylesheets of web fonts (`https:` URLs). */
|
|
31
|
+
fonts: string[]
|
|
32
|
+
/** The theme's own stylesheets, relative to its directory. */
|
|
33
|
+
styles: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoadedTheme {
|
|
37
|
+
dir: string
|
|
38
|
+
file: string
|
|
39
|
+
manifest: ThemeManifest
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let designTokens: { all: Set<string>, mode: Set<string> } | undefined
|
|
43
|
+
/** The design system's token names, without `--pm-`: what a theme may set, and those the dark appearance overrides. */
|
|
44
|
+
function tokenNames(): { all: Set<string>, mode: Set<string> } {
|
|
45
|
+
if (designTokens) return designTokens
|
|
46
|
+
const css = readFileSync(createRequire(import.meta.url).resolve('@lincyaw/design/tokens.css'), 'utf8')
|
|
47
|
+
const names = (text: string) => [...text.matchAll(/--pm-([a-z0-9-]+)\s*:/g)].map(match => match[1]!)
|
|
48
|
+
const dark = [...css.matchAll(/:root\.dark\s*\{([^}]*)\}/g)].flatMap(match => names(match[1]!))
|
|
49
|
+
designTokens = { all: new Set(names(css)), mode: new Set(dark) }
|
|
50
|
+
return designTokens
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const NAME = /^[a-z0-9][a-z0-9-]{0,63}$/
|
|
54
|
+
// A value stays inside its declaration: no other declaration, rule or markup.
|
|
55
|
+
const VALUE = /^[^;{}<>\\]+$/
|
|
56
|
+
|
|
57
|
+
/** The directory `ref` names, from `from`: a path (`./theme`), else an installed package with `theme.json`. */
|
|
58
|
+
function themeDirectory(ref: string, from: string, lang: string): string {
|
|
59
|
+
const t = WEB_MESSAGES[deckLocale(lang)]
|
|
60
|
+
let dir: string | undefined
|
|
61
|
+
if (ref.startsWith('.') || isAbsolute(ref)) dir = resolve(from, ref)
|
|
62
|
+
else {
|
|
63
|
+
const pkg = findInstalledPackage(ref, from)
|
|
64
|
+
if (pkg) {
|
|
65
|
+
const own = JSON.parse(readFileSync(join(pkg, 'package.json'), 'utf8')) as { presmium?: { theme?: unknown } }
|
|
66
|
+
dir = typeof own.presmium?.theme === 'string' ? resolve(pkg, own.presmium.theme) : pkg
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!dir || !existsSync(join(dir, THEME_FILE))) throw new Error(t.unknownTheme(ref))
|
|
70
|
+
return dir
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const loaded = new Map<string, { at: number, theme: LoadedTheme }>()
|
|
74
|
+
|
|
75
|
+
export function loadTheme(ref: string, from: string, lang: string): LoadedTheme {
|
|
76
|
+
const dir = themeDirectory(ref, from, lang)
|
|
77
|
+
const file = join(dir, THEME_FILE)
|
|
78
|
+
const at = statSync(file).mtimeMs
|
|
79
|
+
const known = loaded.get(file)
|
|
80
|
+
if (known?.at === at) return known.theme
|
|
81
|
+
const t = WEB_MESSAGES[deckLocale(lang)]
|
|
82
|
+
const fail = (field: string): never => { throw new Error(t.invalidTheme(file, field)) }
|
|
83
|
+
let input: unknown
|
|
84
|
+
try { input = JSON.parse(readFileSync(file, 'utf8')) } catch { fail('JSON') }
|
|
85
|
+
if (!isRecord(input)) return fail('JSON')
|
|
86
|
+
if (input.schemaVersion !== undefined && input.schemaVersion !== 1) fail('schemaVersion')
|
|
87
|
+
if (typeof input.name !== 'string' || !NAME.test(input.name)) fail('name')
|
|
88
|
+
const base = input.extends ?? 'docs'
|
|
89
|
+
if (!SITE_THEME_IDS.includes(base as SiteThemeId)) fail('extends')
|
|
90
|
+
if (input.params !== undefined && !isRecord(input.params)) fail('params')
|
|
91
|
+
const tokens = (key: 'tokens' | 'dark'): Record<string, string> => {
|
|
92
|
+
const value = input[key]
|
|
93
|
+
if (value === undefined) return {}
|
|
94
|
+
if (!isRecord(value)) return fail(key)
|
|
95
|
+
for (const [name, token] of Object.entries(value)) {
|
|
96
|
+
if (!tokenNames().all.has(name) || key === 'dark' && !tokenNames().mode.has(name)) fail(`${key}.${name}`)
|
|
97
|
+
if (typeof token !== 'string' || !VALUE.test(token.trim())) fail(`${key}.${name}`)
|
|
98
|
+
}
|
|
99
|
+
return Object.fromEntries(Object.entries(value).map(([name, token]) => [name, (token as string).trim()]))
|
|
100
|
+
}
|
|
101
|
+
const list = (key: 'fonts' | 'styles', valid: (item: string) => boolean): string[] => {
|
|
102
|
+
const value = input[key]
|
|
103
|
+
if (value === undefined) return []
|
|
104
|
+
if (!Array.isArray(value)) return fail(key)
|
|
105
|
+
value.forEach((item, i) => { if (typeof item !== 'string' || !valid(item)) fail(`${key}[${i}]`) })
|
|
106
|
+
return value as string[]
|
|
107
|
+
}
|
|
108
|
+
const manifest: ThemeManifest = {
|
|
109
|
+
name: input.name as string,
|
|
110
|
+
extends: base as SiteThemeId,
|
|
111
|
+
params: isRecord(input.params) ? input.params : {},
|
|
112
|
+
tokens: tokens('tokens'),
|
|
113
|
+
dark: tokens('dark'),
|
|
114
|
+
fonts: list('fonts', item => /^https:\/\/[^\s"'<>]+$/.test(item)),
|
|
115
|
+
styles: list('styles', (item) => {
|
|
116
|
+
const path = resolve(dir, item)
|
|
117
|
+
return item.endsWith('.css') && !isAbsolute(item) && !relative(dir, path).startsWith('..') && existsSync(path)
|
|
118
|
+
}),
|
|
119
|
+
}
|
|
120
|
+
const theme = { dir, file, manifest }
|
|
121
|
+
loaded.set(file, { at, theme })
|
|
122
|
+
return theme
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The directory the site's configuration is written in: a theme's path is relative to it. */
|
|
126
|
+
function configDirectory(site: SiteSpec, ctx: RenderContext): string {
|
|
127
|
+
return isSingle(site) ? ctx.deckDir : ctx.workspaceRoot
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The site with its workspace theme resolved: `theme` is the built-in theme it extends, `params` the theme's under the
|
|
132
|
+
* site's own, and `themePackage` names it. Every path that reads the site's shell goes through this first.
|
|
133
|
+
*/
|
|
134
|
+
export function themedSite(site: SiteSpec, ctx: RenderContext): SiteSpec {
|
|
135
|
+
const ref = site.config.theme
|
|
136
|
+
if (!isThemeReference(ref) || isRecord(site.config.themePackage)) return site
|
|
137
|
+
const { dir, manifest } = loadTheme(ref.trim(), configDirectory(site, ctx), siteLanguage(site))
|
|
138
|
+
const own = site.config.params
|
|
139
|
+
const params = own === undefined ? manifest.params : isRecord(own) ? { ...manifest.params, ...own } : own
|
|
140
|
+
return { ...site, config: { ...site.config, theme: manifest.extends, params, themePackage: { name: manifest.name, dir } } }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The design tokens of the theme as CSS: the light appearance's colors and shadows, the dark one's, and the rest. */
|
|
144
|
+
export function themeTokensCss(manifest: ThemeManifest): string {
|
|
145
|
+
const own = `[data-pm-site-theme-name='${manifest.name}']`
|
|
146
|
+
const rule = (selectors: string[], entries: [string, string][]) => entries.length ? `${selectors.join(',\n')} {\n${entries.map(([name, value]) => ` --pm-${name}: ${value};`).join('\n')}\n}\n` : ''
|
|
147
|
+
const tokens = Object.entries(manifest.tokens)
|
|
148
|
+
return [
|
|
149
|
+
`/* The tokens of site theme ${manifest.name}, generated from its ${THEME_FILE}. */\n`,
|
|
150
|
+
rule([`:root:has(${own})`, own], tokens.filter(([name]) => !tokenNames().mode.has(name))),
|
|
151
|
+
rule([`:root:not(.dark):has(${own})`, `:root:not(.dark) ${own}`], tokens.filter(([name]) => tokenNames().mode.has(name))),
|
|
152
|
+
rule([`:root.dark:has(${own})`, `:root.dark ${own}`], Object.entries(manifest.dark)),
|
|
153
|
+
].filter(Boolean).join('\n')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The files of a theme directory with their modification times, to copy it again only when one changed. */
|
|
157
|
+
function signature(dir: string): string {
|
|
158
|
+
const files: string[] = []
|
|
159
|
+
const walk = (at: string) => {
|
|
160
|
+
for (const entry of readdirSync(at, { withFileTypes: true })) {
|
|
161
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
162
|
+
const path = join(at, entry.name)
|
|
163
|
+
if (entry.isDirectory()) walk(path)
|
|
164
|
+
else files.push(`${relative(dir, path)}:${statSync(path).mtimeMs}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
walk(dir)
|
|
168
|
+
return files.sort().join('\n')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const copied = new Map<string, string>()
|
|
172
|
+
|
|
173
|
+
/** Publishes the theme's files below `outDir/_theme/<name>/` and gives the site the stylesheets its pages link. */
|
|
174
|
+
export function publishedTheme(site: SiteSpec, outDir: string, base: string): SiteSpec {
|
|
175
|
+
const own = site.config.themePackage
|
|
176
|
+
if (!isRecord(own) || typeof own.dir !== 'string' || typeof own.name !== 'string') return site
|
|
177
|
+
const { dir, manifest } = loadTheme(own.dir, own.dir, siteLanguage(site))
|
|
178
|
+
const target = join(outDir, THEME_DIR, manifest.name)
|
|
179
|
+
const files = signature(dir) + manifest.name
|
|
180
|
+
if (copied.get(target) !== files || !existsSync(join(target, TOKENS_FILE))) {
|
|
181
|
+
rmSync(target, { recursive: true, force: true })
|
|
182
|
+
mkdirSync(target, { recursive: true })
|
|
183
|
+
cpSync(dir, target, { recursive: true, filter: source => !source.slice(dir.length).split(sep).some(part => part === 'node_modules' || part.startsWith('.')) })
|
|
184
|
+
writeFileSync(join(target, TOKENS_FILE), themeTokensCss(manifest))
|
|
185
|
+
copied.set(target, files)
|
|
186
|
+
}
|
|
187
|
+
const url = (path: string) => `${base}${THEME_DIR}/${manifest.name}/${relative(dir, resolve(dir, path)).split(sep).join('/')}`
|
|
188
|
+
const stylesheets = [...manifest.fonts, url(TOKENS_FILE), ...manifest.styles.map(url)]
|
|
189
|
+
return { ...site, config: { ...site.config, themePackage: { name: manifest.name, stylesheets } } }
|
|
190
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
import type { AddressInfo } from 'node:net'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { PREVIEW_APPEARANCE_PARAM, slideShape } from '@lincyaw/kernel'
|
|
6
|
+
import type { IRDocument, Renderer, SlideShape } from '@lincyaw/kernel'
|
|
7
|
+
import send from 'send'
|
|
8
|
+
import { PRINTED_ATTRIBUTE, printQuery } from '../domain/index.ts'
|
|
9
|
+
import type { SlidesPrintQuery } from '../domain/index.ts'
|
|
10
|
+
import { devDocument } from './renderer.ts'
|
|
11
|
+
import { buildSlidesSite } from './site-build.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The slides form: the viewer app's slides page (its presentation, one slide at a time on a canvas of a fixed size), built
|
|
15
|
+
* as the web form is, and served by the same dev server. The exports print it: the page lays every slide out at the
|
|
16
|
+
* canvas's size (`?print`), and Chromium prints it to a PDF or takes a picture of each slide.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const BUILD_DIR = '.presmium/slides/build'
|
|
20
|
+
const PRINT_TIMEOUT = 120_000
|
|
21
|
+
// The pictures are taken at twice the canvas's size, sharp on a projector.
|
|
22
|
+
const PICTURE_SCALE = 2
|
|
23
|
+
|
|
24
|
+
export const slidesRenderer: Renderer = {
|
|
25
|
+
id: 'slides',
|
|
26
|
+
site: true,
|
|
27
|
+
support: () => 'full',
|
|
28
|
+
async build(doc, ctx) {
|
|
29
|
+
await buildSlidesSite(doc, { ...ctx, workDir: join(ctx.deckDir, BUILD_DIR) }, ctx.nodeVitePlugins ?? [])
|
|
30
|
+
return { outDir: ctx.outDir, skipped: [] }
|
|
31
|
+
},
|
|
32
|
+
dev: (doc, ctx) => devDocument(doc, ctx, 'slides'),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PrintingPage {
|
|
36
|
+
goto(url: string, options: { waitUntil: 'load', timeout: number }): Promise<unknown>
|
|
37
|
+
waitForSelector(selector: string, options: { timeout: number }): Promise<unknown>
|
|
38
|
+
pdf(options: { path: string, width: string, height: string, printBackground: boolean }): Promise<unknown>
|
|
39
|
+
$$(selector: string): Promise<{ screenshot(options: { path: string }): Promise<unknown> }[]>
|
|
40
|
+
evaluate<T>(run: () => T): Promise<T>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What printing uses of Playwright's Chromium (`playwright-chromium`), which the caller loads. */
|
|
44
|
+
export interface PrintingChromium {
|
|
45
|
+
launch(): Promise<{
|
|
46
|
+
newPage(options: { viewport: SlideShape, deviceScaleFactor: number, reducedMotion: 'reduce', serviceWorkers: 'block' }): Promise<PrintingPage>
|
|
47
|
+
close(): Promise<void>
|
|
48
|
+
}>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PrintOptions extends SlidesPrintQuery {
|
|
52
|
+
/** The slides as `slidesRenderer` built them, for the root path. */
|
|
53
|
+
dir: string
|
|
54
|
+
chromium: PrintingChromium
|
|
55
|
+
/** One PDF file, or a picture of each page in a directory. */
|
|
56
|
+
format: 'pdf' | 'png'
|
|
57
|
+
/** The PDF file, or the directory of the pictures. */
|
|
58
|
+
output: string
|
|
59
|
+
dark?: boolean
|
|
60
|
+
/** How long the page may take to be ready, in milliseconds. */
|
|
61
|
+
timeout?: number
|
|
62
|
+
signal?: AbortSignal
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A printed page: a slide (by number from 1) at a step, and its picture (`<slide>.png`, `<slide>-<step + 1>.png` with `steps`). */
|
|
66
|
+
export interface PrintedPage {
|
|
67
|
+
section: string
|
|
68
|
+
slide: number
|
|
69
|
+
step: number
|
|
70
|
+
file?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Printed {
|
|
74
|
+
shape: SlideShape
|
|
75
|
+
pages: PrintedPage[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Serves the built slides on a loopback port, through `send` (Express's): types and validators as HTTP defines them. */
|
|
79
|
+
async function serveBuilt(dir: string): Promise<{ url: string, close(): Promise<void> }> {
|
|
80
|
+
const server = createServer((req, res) => {
|
|
81
|
+
send(req, new URL(req.url ?? '/', 'http://localhost').pathname, { root: dir, dotfiles: 'allow' })
|
|
82
|
+
.on('error', (error: { status?: number }) => res.writeHead(error.status ?? 500).end())
|
|
83
|
+
.pipe(res)
|
|
84
|
+
})
|
|
85
|
+
await new Promise<void>(done => server.listen(0, '127.0.0.1', done))
|
|
86
|
+
return {
|
|
87
|
+
url: `http://127.0.0.1:${(server.address() as AddressInfo).port}`,
|
|
88
|
+
close: () => new Promise((done) => {
|
|
89
|
+
server.close(() => done())
|
|
90
|
+
server.closeAllConnections()
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Prints the built slides: every slide (or `slides`) at its last step, or each of its steps with `steps`. */
|
|
96
|
+
export async function printSlides(options: PrintOptions): Promise<Printed> {
|
|
97
|
+
const shape = slideShape(JSON.parse(await readFile(join(options.dir, 'ir.json'), 'utf8')) as IRDocument)
|
|
98
|
+
const timeout = options.timeout ?? PRINT_TIMEOUT
|
|
99
|
+
const server = await serveBuilt(options.dir)
|
|
100
|
+
try {
|
|
101
|
+
const browser = await options.chromium.launch()
|
|
102
|
+
const stop = () => void browser.close()
|
|
103
|
+
options.signal?.addEventListener('abort', stop, { once: true })
|
|
104
|
+
try {
|
|
105
|
+
options.signal?.throwIfAborted()
|
|
106
|
+
const page = await browser.newPage({ viewport: shape, deviceScaleFactor: options.format === 'png' ? PICTURE_SCALE : 1, reducedMotion: 'reduce', serviceWorkers: 'block' })
|
|
107
|
+
const query = `${printQuery(options)}&${PREVIEW_APPEARANCE_PARAM}=${options.dark ? 'dark' : 'light'}`
|
|
108
|
+
await page.goto(`${server.url}/${query}`, { waitUntil: 'load', timeout })
|
|
109
|
+
await page.waitForSelector(`html[${PRINTED_ATTRIBUTE}]`, { timeout })
|
|
110
|
+
options.signal?.throwIfAborted()
|
|
111
|
+
const pages: PrintedPage[] = await page.evaluate(() => [...document.querySelectorAll<HTMLElement>('.pm-slides-print__page > .pm-slide')]
|
|
112
|
+
.map(slide => ({ section: slide.dataset.section!, slide: Number(slide.dataset.slide), step: Number(slide.dataset.step) })))
|
|
113
|
+
if (options.format === 'pdf') {
|
|
114
|
+
await page.pdf({ path: options.output, width: `${shape.width}px`, height: `${shape.height}px`, printBackground: true })
|
|
115
|
+
return { shape, pages }
|
|
116
|
+
}
|
|
117
|
+
const elements = await page.$$('.pm-slides-print__page')
|
|
118
|
+
for (const [i, element] of elements.entries()) {
|
|
119
|
+
options.signal?.throwIfAborted()
|
|
120
|
+
const printed = pages[i]!
|
|
121
|
+
printed.file = `${printed.slide}${options.steps ? `-${printed.step + 1}` : ''}.png`
|
|
122
|
+
await element.screenshot({ path: join(options.output, printed.file) })
|
|
123
|
+
}
|
|
124
|
+
return { shape, pages }
|
|
125
|
+
} catch (error) {
|
|
126
|
+
options.signal?.throwIfAborted()
|
|
127
|
+
throw error
|
|
128
|
+
} finally {
|
|
129
|
+
options.signal?.removeEventListener('abort', stop)
|
|
130
|
+
await browser.close()
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
await server.close()
|
|
134
|
+
}
|
|
135
|
+
}
|