-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvite.config.ts
More file actions
229 lines (214 loc) · 10 KB
/
Copy pathvite.config.ts
File metadata and controls
229 lines (214 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import { resolve } from 'path'
import { readFileSync, existsSync } from 'fs'
const VAR_MAP: Record<string, Record<string, string>> = {
'red-bar': { y: '--ed-red-y', x: '--ed-red-x', w: '--ed-red-w', h: '--ed-red-h' },
logo: { y: '--ed-logo-y', x: '--ed-logo-rx', w: '--ed-logo-w', h: '--ed-logo-h' },
title: { y: '--ed-title-y', x: '--ed-title-x', w: '--ed-title-w', h: '--ed-title-h' },
content: { y: '--ed-content-y', x: '--ed-content-x', w: '--ed-content-w', h: '--ed-content-h' },
image: { y: '--ed-image-y', x: '--ed-image-x', w: '--ed-image-w', h: '--ed-image-h' },
}
const customSideEditorPath = resolve(import.meta.dirname, '_override/SideEditor.vue')
const useEditorAbsPath = `/@fs${resolve(import.meta.dirname, 'composables/useEditor.ts')}`
const IMAGE_MIME_EXT: Record<string, string> = {
'image/png': 'png',
'image/jpeg': 'jpg',
'image/webp': 'webp',
'image/gif': 'gif',
}
export default {
resolve: {
// Slidev's markdown-image-to-import transform rejects absolute "/images/..."
// specifiers via its slidev:slide-import-guard (it resolves them as literal
// filesystem-root paths rather than through publicDir, even for
// long-existing public files). Aliasing this one prefix to the real
// public/images directory lets pasted-image markdown references resolve
// correctly without loosening Vite's dev-server fs access more broadly.
alias: [
{ find: /^\/images\//, replacement: `${resolve(import.meta.dirname, 'public/images')}/` },
],
},
plugins: [
{
name: 'slidev-side-editor-override',
enforce: 'pre',
transform(code, id) {
if (!id.includes('SideEditor.vue') || id.includes('?vue')) return null
let content = readFileSync(customSideEditorPath, 'utf-8')
content = content.replace('__USE_EDITOR_PATH__', useEditorAbsPath)
return content
},
},
{
name: 'slidev-layout-saver',
configureServer(server) {
server.middlewares.use('/api/save-layout', async (req, res) => {
if (req.method !== 'POST') {
res.statusCode = 405
res.end()
return
}
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk)
const body = JSON.parse(Buffer.concat(chunks).toString())
const { readFileSync, writeFileSync, realpathSync } = await import('fs')
const { resolve: resolvePath } = await import('path')
const layoutDir = resolvePath(import.meta.dirname, 'layouts')
const currentLayoutName = (body.currentLayout && String(body.currentLayout).trim()) || 'default'
const layoutPath = resolvePath(layoutDir, `${currentLayoutName}.vue`)
let content = readFileSync(layoutPath, 'utf-8')
// Build inline style attribute value with CSS variable overrides
// Exclude position variables for hidden elements
const hidden = body.hidden || {}
const styleParts: string[] = []
for (const [name, map] of Object.entries(VAR_MAP)) {
if (hidden[name]) continue
const pos = body.positions[name]
if (!pos) continue
for (const [prop, cssVar] of Object.entries(map)) {
const val = pos[prop]
if (val !== undefined) {
styleParts.push(`${cssVar}: ${val}px`)
}
}
}
if (hidden.title) {
styleParts.push('--ed-title-d: none')
}
if (hidden.content) {
styleParts.push('--ed-content-d: none')
}
if (hidden.image) {
styleParts.push('--ed-image-d: none')
}
if (styleParts.length > 0) {
const newStyle = styleParts.join('; ')
// Replace data-styles (used by onMounted to restore positions)
content = content.replace(/data-styles="[^"]*"/, `data-styles="${newStyle}"`)
// Replace the existing style="..." attribute on the root div with updated values
content = content.replace(/style="[^"]*"/, `style="${newStyle}"`)
}
// Persist hidden state as data-hidden attribute on root div.
// `image` is excluded: unlike the other four elements (which
// default to shown, so only need recording when explicitly
// hidden), `image` defaults to hidden -- recording it here would
// pollute every ordinary save with a spurious "image" entry even
// when the slide has nothing to do with images. Its shown/hidden
// state is instead derived at runtime from whether content
// actually has a trackable image (see layouts/default.vue).
const hiddenNames = Object.entries(hidden)
.filter(([name, v]) => v && name !== 'image')
.map(([k]) => k)
content = content.replace(/\s*data-hidden="[^"]*"/, '')
if (hiddenNames.length > 0) {
content = content.replace(
/(class="slidev-layout default[^"]*"\s*)/,
`$1data-hidden="${hiddenNames.join(',')}" `
)
}
// Persist aspect-lock state as data-aspect-locked, storing only the
// locked exceptions since every element is unlocked by default
const aspectLocked = body.aspectLocked || {}
const lockedNames = Object.keys(aspectLocked).filter(name => aspectLocked[name] === true)
content = content.replace(/\s*data-aspect-locked="[^"]*"/, '')
if (lockedNames.length > 0) {
content = content.replace(
/(class="slidev-layout default[^"]*"\s*)/,
`$1data-aspect-locked="${lockedNames.join(',')}" `
)
}
// Deleted elements are stripped from the template entirely (not just
// hidden), so they can't be restored after a reload -- only Undo
// during the same editing session can bring them back.
for (const name of ['red-bar', 'logo']) {
if (!hidden[name]) continue
const markerRe = new RegExp(`\\n?\\s*<!-- ed:${name}:start -->[\\s\\S]*?<!-- ed:${name}:end -->\\n?`)
content = content.replace(markerRe, '\n')
}
let layoutName = currentLayoutName
const saveAs = body.saveAs !== false
let writtenPath: string | null = null
if (saveAs) {
let name: string
if (body.layoutName && body.layoutName.trim()) {
name = body.layoutName.trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
if (!name) name = `layout-${Date.now()}`
} else {
name = `layout-${Date.now()}`
}
if (existsSync(resolvePath(layoutDir, `${name}.vue`))) {
name = `${name}-${Date.now()}`
}
writtenPath = resolvePath(layoutDir, `${name}.vue`)
writeFileSync(writtenPath, content, 'utf-8')
layoutName = name
} else {
writtenPath = layoutPath
writeFileSync(layoutPath, content, 'utf-8')
}
// Invalidate the layout module so Vite re-reads it from disk
if (writtenPath && server) {
const realPath = realpathSync(resolvePath(writtenPath))
const mods = server.moduleGraph.getModulesByFile(realPath)
if (mods) {
for (const mod of mods) {
server.moduleGraph.invalidateModule(mod)
}
}
// A brand-new layout file needs an 'add' event, not 'change' --
// Slidev's layouts virtual module (the list of known layout
// names) only refreshes in response to its own fs watcher
// noticing a real add, and a 'change' event on a path it has
// never seen doesn't trigger that. Without this, saveAs's newly
// written file can 404 as "Unknown layout" the moment frontmatter
// references it, since the layouts list is otherwise cached
// until invalidated.
server.watcher.emit(saveAs ? 'add' : 'change', resolvePath(realPath))
}
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ layoutName }))
})
},
},
{
name: 'slidev-image-saver',
configureServer(server) {
server.middlewares.use('/api/save-image', async (req, res) => {
if (req.method !== 'POST') {
res.statusCode = 405
res.end()
return
}
const mime = (req.headers['content-type'] || '').split(';')[0].trim()
const ext = IMAGE_MIME_EXT[mime]
if (!ext) {
res.statusCode = 400
res.end()
return
}
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk)
const buffer = Buffer.concat(chunks)
const { writeFileSync, mkdirSync, existsSync } = await import('fs')
const { resolve: resolvePath } = await import('path')
const imagesDir = resolvePath(import.meta.dirname, 'public/images')
if (!existsSync(imagesDir)) mkdirSync(imagesDir, { recursive: true })
const filename = `paste-${Date.now()}.${ext}`
const writtenPath = resolvePath(imagesDir, filename)
writeFileSync(writtenPath, buffer)
// Vite caches its known "public files" set at startup and only
// updates it reactively from its own fs watcher's add/unlink
// events. Without this, the slide's next re-render can race ahead
// of that watcher and reject the fresh image via
// slidev:slide-import-guard. Emitting synchronously (mirroring how
// /api/save-layout notifies the watcher of layout changes) avoids
// depending on real filesystem-event timing.
server.watcher.emit('add', writtenPath)
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ filename, path: `/images/${filename}` }))
})
},
},
],
}