| 1 | import type { ResolvedSlidevOptions } from '@slidev/types' |
| 2 | import { existsSync } from 'node:fs' |
| 3 | import fs from 'node:fs/promises' |
| 4 | import fg from 'fast-glob' |
| 5 | import { basename, dirname, isAbsolute, join, relative, resolve } from 'pathe' |
| 6 | |
| 7 | /** |
| 8 | * Whether `key` is a safe bare slide id that can be joined onto `dir` without |
| 9 | * escaping it. `loadDrawings` only ever produces numeric keys, so writes |
| 10 | * should only ever accept numeric keys too. |
| 11 | */ |
| 12 | export function isSafeDrawingKey(dir: string, key: string): boolean { |
| 13 | if (!/^\d+$/.test(key)) |
| 14 | return false |
| 15 | const target = join(dir, `${key}.svg`) |
| 16 | const rel = relative(dir, target) |
| 17 | return !rel.startsWith('..') && !isAbsolute(rel) |
| 18 | } |
| 19 | |
| 20 | function resolveDrawingsDir(options: ResolvedSlidevOptions): string | undefined { |
| 21 | return options.data.config.drawings.persist |
| 22 | ? resolve( |
| 23 | dirname(options.entry), |
| 24 | options.data.config.drawings.persist, |
| 25 | ) |
| 26 | : undefined |
| 27 | } |
| 28 | |
| 29 | export async function loadDrawings(options: ResolvedSlidevOptions) { |
| 30 | const dir = resolveDrawingsDir(options) |
| 31 | if (!dir || !existsSync(dir)) |
| 32 | return {} |
| 33 | |
| 34 | const files = await fg('*.svg', { |
| 35 | onlyFiles: true, |
| 36 | cwd: dir, |
| 37 | absolute: true, |
| 38 | suppressErrors: true, |
| 39 | }) |
| 40 | |
| 41 | const obj: Record<string, string> = {} |
| 42 | await Promise.all(files.map(async (path) => { |
| 43 | const num = +basename(path, '.svg') |
| 44 | if (Number.isNaN(num)) |
| 45 | return |
| 46 | const content = await fs.readFile(path, 'utf8') |
| 47 | const lines = content.split(/\n/g) |
| 48 | obj[num.toString()] = lines.slice(1, -1).join('\n') |
| 49 | })) |
| 50 | |
| 51 | return obj |
| 52 | } |
| 53 | |
| 54 | export async function writeDrawings(options: ResolvedSlidevOptions, drawing: Record<string, string>) { |
| 55 | const dir = resolveDrawingsDir(options) |
| 56 | if (!dir) |
| 57 | return |
| 58 | |
| 59 | const width = options.data.config.canvasWidth |
| 60 | const height = Math.round(width / options.data.config.aspectRatio) |
| 61 | const SVG_HEAD = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">` |
| 62 | |
| 63 | await fs.mkdir(dir, { recursive: true }) |
| 64 | |
| 65 | return Promise.all( |
| 66 | Object.entries(drawing).map(async ([key, value]) => { |
| 67 | if (!value) |
| 68 | return |
| 69 | |
| 70 | if (!isSafeDrawingKey(dir, key)) { |
| 71 | console.warn(`[slidev] Ignoring drawing with unsafe key: ${key}`) |
| 72 | return |
| 73 | } |
| 74 | |
| 75 | const svg = `${SVG_HEAD}\n${value}\n</svg>` |
| 76 | await fs.writeFile(join(dir, `${key}.svg`), svg, 'utf-8') |
| 77 | }), |
| 78 | ) |
| 79 | } |
| 80 |