返回 slidev
utils.ts
根目录 / packages / slidev / node / utils.ts
1 import type { ResolvedFontOptions, SourceSlideInfo } from '@slidev/types'
2 import type MarkdownExit from 'markdown-exit'
3 import type { Connect, GeneralImportGlobOptions } from 'vite'
4 import { createHash } from 'node:crypto'
5 import { mkdirSync, writeFileSync } from 'node:fs'
6 import { fileURLToPath } from 'node:url'
7 import { slash } from '@antfu/utils'
8 import { createJiti } from 'jiti'
9 import { dirname, join, relative, win32 } from 'pathe'
10 import YAML from 'yaml'
11 import { toAtFS } from './resolver'
12 import { isAllowedFile } from './vite/importGuard'
13
14 /**
15 * Whether `filePath` resolves inside any of `roots` (no `..` escape). Shared
16 * containment predicate reused by the snippet (`<<<`) and `src:` deck-file
17 * reads, and by the Vite slide-import guard (`isAllowedFile`).
18 */
19 export function isPathInsideRoots(filePath: string, roots: string[]): boolean {
20 return isAllowedFile(filePath, roots)
21 }
22
23 const RE_WHITESPACE_ONLY = /^\s*$/
24 const RE_QUOTED_STRING = /^(['"])(.*)\1$/
25 const RE_WHITESPACE = /\s+/g
26 const RE_WINDOWS_DRIVE = /^[A-Z]:\//i
27
28 type Token = ReturnType<MarkdownExit['parseInline']>[number]
29
30 type Jiti = ReturnType<typeof createJiti>
31 let jiti: Jiti | undefined
32 export function loadModule<T = unknown>(absolutePath: string): Promise<T> {
33 jiti ??= createJiti(fileURLToPath(import.meta.url), {
34 // Allows changes to take effect
35 moduleCache: false,
36 })
37 return jiti.import(absolutePath) as Promise<T>
38 }
39
40 export function stringifyMarkdownTokens(tokens: Token[]) {
41 return tokens.map(token => token.children
42 ?.filter(t => ['text', 'code_inline'].includes(t.type) && !t.content.match(RE_WHITESPACE_ONLY))
43 .map(t => t.content.trim())
44 .join(' '))
45 .filter(Boolean)
46 .join(' ')
47 }
48
49 export function generateFontParams(options: ResolvedFontOptions) {
50 const weights = options.weights
51 .flatMap(i => options.italic ? [`0,${i}`, `1,${i}`] : [`${i}`])
52 .sort()
53 .join(';')
54 const fontParams = options.webfonts
55 .map(i => `family=${i.replace(RE_QUOTED_STRING, '$1').replace(RE_WHITESPACE, '+')}:${options.italic ? 'ital,' : ''}wght@${weights}`)
56 .join('&')
57 return fontParams
58 }
59
60 export function generateGoogleFontsUrl(options: ResolvedFontOptions) {
61 return `https://fonts.googleapis.com/css2?${generateFontParams(options)}&display=swap`
62 }
63
64 export function generateCoollabsFontsUrl(options: ResolvedFontOptions) {
65 return `https://api.fonts.coollabs.io/fonts?${generateFontParams(options)}&display=swap`
66 }
67
68 /**
69 * Update frontmatter patch and preserve the comments
70 */
71 export function updateFrontmatterPatch(source: SourceSlideInfo, frontmatter: Record<string, any>) {
72 let doc = source.frontmatterDoc
73 if (!doc) {
74 source.frontmatterStyle = 'frontmatter'
75 source.frontmatterDoc = doc = new YAML.Document({})
76 }
77 for (const [key, value] of Object.entries(frontmatter)) {
78 source.frontmatter[key] = value
79 if (value == null) {
80 doc.delete(key)
81 }
82 else {
83 const valueNode = doc.createNode(value)
84 let found = false
85 YAML.visit(doc.contents, {
86 Pair(_key, node, path) {
87 if (path.length === 1 && YAML.isScalar(node.key) && node.key.value === key) {
88 node.value = valueNode
89 found = true
90 return YAML.visit.BREAK
91 }
92 },
93 })
94 if (!found) {
95 if (!YAML.isMap(doc.contents))
96 doc.contents = doc.createNode({})
97 doc.contents.add(
98 doc.createPair(key, valueNode),
99 )
100 }
101 }
102 }
103 }
104
105 export function getBodyJson(req: Connect.IncomingMessage) {
106 return new Promise<any>((resolve, reject) => {
107 let body = ''
108 req.on('data', chunk => body += chunk)
109 req.on('error', reject)
110 req.on('end', () => {
111 try {
112 resolve(JSON.parse(body) || {})
113 }
114 catch (e) {
115 reject(e)
116 }
117 })
118 })
119 }
120
121 function getImportGlobRelativePath(from: string, to: string) {
122 const normalizedFrom = slash(from)
123 const normalizedTo = slash(to)
124 return slash(
125 RE_WINDOWS_DRIVE.test(normalizedFrom) || RE_WINDOWS_DRIVE.test(normalizedTo)
126 ? win32.relative(normalizedFrom, normalizedTo)
127 : relative(normalizedFrom, normalizedTo),
128 )
129 }
130
131 function resolveImportGlobProxyModule(proxyBase: string, content: string) {
132 const hash = createHash('sha256').update(content).digest('hex').slice(0, 10)
133 return `${proxyBase}.${hash}.ts`
134 }
135
136 export function createMakeAbsoluteImportGlob(baseRoot: string) {
137 const proxyModules = new Map<string, string>()
138 const proxyBasename = 'node_modules/.slidev/virtual/import-glob'
139 const proxyBase = slash(join(baseRoot, proxyBasename))
140
141 return function makeAbsoluteImportGlob(
142 globs: string[],
143 options: Partial<GeneralImportGlobOptions> = {},
144 ) {
145 // Vite does not treat /@slidev/* as a real filesystem importer. Emit
146 // import.meta.glob from a proxy file so Vite resolves imports from disk.
147 const content = `export default ${makeAbsoluteImportGlobExpression(dirname(proxyBase), globs, options)}\n`
148 const proxyModule = resolveImportGlobProxyModule(proxyBase, content)
149 if (proxyModules.get(proxyModule) !== content) {
150 mkdirSync(dirname(proxyModule), { recursive: true })
151 writeFileSync(proxyModule, content, 'utf-8')
152 proxyModules.set(proxyModule, content)
153 }
154 return toAtFS(proxyModule)
155 }
156 }
157
158 export type MakeAbsoluteImportGlob = ReturnType<typeof createMakeAbsoluteImportGlob>
159
160 function makeAbsoluteImportGlobExpression(
161 self: string,
162 globs: string[],
163 options: Partial<GeneralImportGlobOptions> = {},
164 ) {
165 const relativeGlobs = globs.map((glob) => {
166 const relativeGlob = getImportGlobRelativePath(self, glob)
167 return !relativeGlob.startsWith('.') && !RE_WINDOWS_DRIVE.test(relativeGlob)
168 ? `./${relativeGlob}`
169 : relativeGlob
170 })
171 const opts: GeneralImportGlobOptions = {
172 eager: true,
173 exhaustive: true,
174 ...options,
175 }
176 return `import.meta.glob(${JSON.stringify(relativeGlobs)}, ${JSON.stringify(opts)})`
177 }
178
178 lines TYPESCRIPT