slidev-theme-practicum 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +72 -10
- package/components/Slide.vue +81 -26
- package/components/Slot.vue +33 -48
- package/components/Text.vue +68 -2
- package/components/Timeline.vue +2 -23
- package/composables/deck-slot-markup.cjs +249 -0
- package/composables/decor-catalog.mjs +169 -47
- package/composables/layout-authoring.ts +6 -2
- package/composables/layout-markdown.mjs +33 -0
- package/composables/layout-recipes.ts +51 -5
- package/composables/layout-shorthands.ts +251 -81
- package/composables/markdown-to-vnodes.cjs +1 -0
- package/composables/markdown-to-vnodes.mjs +237 -0
- package/composables/slot-placement.ts +1 -1
- package/composables/slot-spacing.ts +66 -0
- package/composables/text-fit-runtime.ts +458 -76
- package/composables/text-fit-scope.ts +13 -0
- package/composables/text-slot-markdown.mjs +193 -0
- package/composables/text-typography.mjs +2 -0
- package/composables/theme-assets.mjs +60 -0
- package/composables/timeline-grid.ts +27 -0
- package/composables/typescript-require.cjs +29 -0
- package/composables/validate-deck-layouts.cjs +277 -0
- package/decor-library.md +2 -0
- package/example.md +132 -10
- package/package.json +25 -11
- package/public/photos/photo-1.webp +0 -0
- package/public/photos/photo-10.webp +0 -0
- package/public/photos/photo-11.webp +0 -0
- package/public/photos/photo-12.webp +0 -0
- package/public/photos/photo-13.webp +0 -0
- package/public/photos/photo-14.webp +0 -0
- package/public/photos/photo-15.webp +0 -0
- package/public/photos/photo-16.webp +0 -0
- package/public/photos/photo-17.webp +0 -0
- package/public/photos/photo-18.webp +0 -0
- package/public/photos/photo-19.webp +0 -0
- package/public/photos/photo-2.webp +0 -0
- package/public/photos/photo-20.webp +0 -0
- package/public/photos/photo-21.webp +0 -0
- package/public/photos/photo-22.webp +0 -0
- package/public/photos/photo-23.webp +0 -0
- package/public/photos/photo-24.webp +0 -0
- package/public/photos/photo-25.webp +0 -0
- package/public/photos/photo-26.webp +0 -0
- package/public/photos/photo-27.webp +0 -0
- package/public/photos/photo-28.webp +0 -0
- package/public/photos/photo-29.webp +0 -0
- package/public/photos/photo-3.webp +0 -0
- package/public/photos/photo-30.webp +0 -0
- package/public/photos/photo-4.webp +0 -0
- package/public/photos/photo-5.webp +0 -0
- package/public/photos/photo-6.webp +0 -0
- package/public/photos/photo-7.webp +0 -0
- package/public/photos/photo-8.webp +0 -0
- package/public/photos/photo-9.webp +0 -0
- package/scripts/browser-smoke-runtime.mjs +428 -0
- package/scripts/browser-smoke.mjs +424 -0
- package/scripts/build-artifact-assets.mjs +55 -0
- package/scripts/check-build-artifact.mjs +243 -0
- package/scripts/check-package.mjs +132 -0
- package/scripts/check-test-architecture.mjs +176 -0
- package/scripts/npm-spawn.mjs +31 -0
- package/scripts/test-architecture-policy.mjs +245 -0
- package/scripts/theme-asset-inventory.mjs +83 -0
- package/scripts/validate-deck.cjs +18 -0
- package/setup/decor-save-middleware.ts +244 -0
- package/setup/vite-plugins.ts +14 -84
- package/skills/slidev-practicum/SKILL.md +25 -0
- package/skills/slidev-practicum/agents/openai.yaml +4 -0
- package/styles/index.css +10 -1
- package/composables/layout-registry.ts +0 -47
- package/env.d.ts +0 -6
- package/public/photos/photo-1.png +0 -0
- package/public/photos/photo-10.png +0 -0
- package/public/photos/photo-2.png +0 -0
- package/public/photos/photo-3.png +0 -0
- package/public/photos/photo-4.png +0 -0
- package/public/photos/photo-5.png +0 -0
- package/public/photos/photo-6.png +0 -0
- package/public/photos/photo-7.png +0 -0
- package/public/photos/photo-8.png +0 -0
- package/public/photos/photo-9.png +0 -0
- package/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +0 -102
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
import {
|
|
4
|
+
extname,
|
|
5
|
+
isAbsolute,
|
|
6
|
+
join,
|
|
7
|
+
relative,
|
|
8
|
+
resolve,
|
|
9
|
+
sep,
|
|
10
|
+
} from 'node:path'
|
|
11
|
+
|
|
12
|
+
const CONTENT_TYPES = new Map([
|
|
13
|
+
['.css', 'text/css; charset=utf-8'],
|
|
14
|
+
['.html', 'text/html; charset=utf-8'],
|
|
15
|
+
['.js', 'text/javascript; charset=utf-8'],
|
|
16
|
+
['.json', 'application/json; charset=utf-8'],
|
|
17
|
+
['.mjs', 'text/javascript; charset=utf-8'],
|
|
18
|
+
['.png', 'image/png'],
|
|
19
|
+
['.svg', 'image/svg+xml'],
|
|
20
|
+
['.txt', 'text/plain; charset=utf-8'],
|
|
21
|
+
['.webp', 'image/webp'],
|
|
22
|
+
['.woff', 'font/woff'],
|
|
23
|
+
['.woff2', 'font/woff2'],
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const STATIC_PREFIXES = ['/assets/', '/theme/']
|
|
27
|
+
const MAX_DECODE_PASSES = 10
|
|
28
|
+
const ALLOWED_LOCAL_STORAGE_WARNING = '`--localstorage-file` was provided without a valid path'
|
|
29
|
+
|
|
30
|
+
export function classifyBrowserConsoleMessage(type, text) {
|
|
31
|
+
if (type === 'warning' && text === ALLOWED_LOCAL_STORAGE_WARNING)
|
|
32
|
+
return 'allowed'
|
|
33
|
+
|
|
34
|
+
if (type === 'error')
|
|
35
|
+
return 'rejected'
|
|
36
|
+
|
|
37
|
+
return 'ignored'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isSameOriginUrl(origin, candidateUrl) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(candidateUrl).origin === new URL(origin).origin
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createInFlightRequestTracker({
|
|
50
|
+
clearTimer = clearTimeout,
|
|
51
|
+
idleMs = 150,
|
|
52
|
+
origin,
|
|
53
|
+
setTimer = setTimeout,
|
|
54
|
+
timeoutMs = 15_000,
|
|
55
|
+
}) {
|
|
56
|
+
const expectedOrigin = new URL(origin).origin
|
|
57
|
+
const inFlight = new Set()
|
|
58
|
+
const waiters = new Set()
|
|
59
|
+
let disposed = false
|
|
60
|
+
|
|
61
|
+
function isSameOrigin(request) {
|
|
62
|
+
try {
|
|
63
|
+
const requestUrl = typeof request === 'string' ? request : request.url()
|
|
64
|
+
return isSameOriginUrl(expectedOrigin, requestUrl)
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function clearIdleTimer(waiter) {
|
|
72
|
+
if (waiter.idleTimer === undefined)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
clearTimer(waiter.idleTimer)
|
|
76
|
+
waiter.idleTimer = undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeWaiter(waiter) {
|
|
80
|
+
if (!waiters.delete(waiter))
|
|
81
|
+
return false
|
|
82
|
+
|
|
83
|
+
clearIdleTimer(waiter)
|
|
84
|
+
if (waiter.timeoutTimer !== undefined) {
|
|
85
|
+
clearTimer(waiter.timeoutTimer)
|
|
86
|
+
waiter.timeoutTimer = undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveWaiter(waiter) {
|
|
93
|
+
if (!removeWaiter(waiter))
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
waiter.resolve()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rejectWaiter(waiter, error) {
|
|
100
|
+
if (!removeWaiter(waiter))
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
waiter.reject(error)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function armIdleTimer(waiter) {
|
|
107
|
+
clearIdleTimer(waiter)
|
|
108
|
+
if (inFlight.size > 0)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
waiter.idleTimer = setTimer(() => resolveWaiter(waiter), idleMs)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
dispose() {
|
|
116
|
+
if (disposed)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
disposed = true
|
|
120
|
+
inFlight.clear()
|
|
121
|
+
for (const waiter of [...waiters])
|
|
122
|
+
rejectWaiter(waiter, new Error('in-flight tracker остановлен'))
|
|
123
|
+
},
|
|
124
|
+
requestSettled(request) {
|
|
125
|
+
if (!inFlight.delete(request) || inFlight.size > 0)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for (const waiter of waiters)
|
|
129
|
+
armIdleTimer(waiter)
|
|
130
|
+
},
|
|
131
|
+
requestStarted(request) {
|
|
132
|
+
if (disposed || !isSameOrigin(request))
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
inFlight.add(request)
|
|
136
|
+
for (const waiter of waiters)
|
|
137
|
+
clearIdleTimer(waiter)
|
|
138
|
+
},
|
|
139
|
+
get size() {
|
|
140
|
+
return inFlight.size
|
|
141
|
+
},
|
|
142
|
+
waitForIdle() {
|
|
143
|
+
if (disposed)
|
|
144
|
+
return Promise.reject(new Error('in-flight tracker остановлен'))
|
|
145
|
+
|
|
146
|
+
return new Promise((resolveIdle, rejectIdle) => {
|
|
147
|
+
const waiter = {
|
|
148
|
+
idleTimer: undefined,
|
|
149
|
+
reject: rejectIdle,
|
|
150
|
+
resolve: resolveIdle,
|
|
151
|
+
timeoutTimer: undefined,
|
|
152
|
+
}
|
|
153
|
+
waiters.add(waiter)
|
|
154
|
+
waiter.timeoutTimer = setTimer(() => {
|
|
155
|
+
rejectWaiter(
|
|
156
|
+
waiter,
|
|
157
|
+
new Error(`не дождался завершения same-origin requests за ${timeoutMs} ms`),
|
|
158
|
+
)
|
|
159
|
+
}, timeoutMs)
|
|
160
|
+
armIdleTimer(waiter)
|
|
161
|
+
})
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function withoutQueryOrHash(value) {
|
|
167
|
+
const queryIndex = value.indexOf('?')
|
|
168
|
+
const hashIndex = value.indexOf('#')
|
|
169
|
+
const indexes = [queryIndex, hashIndex].filter(index => index >= 0)
|
|
170
|
+
|
|
171
|
+
return indexes.length === 0 ? value : value.slice(0, Math.min(...indexes))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function decodePathname(pathname) {
|
|
175
|
+
if (typeof pathname !== 'string')
|
|
176
|
+
return null
|
|
177
|
+
|
|
178
|
+
let decoded = withoutQueryOrHash(pathname)
|
|
179
|
+
if (!decoded.startsWith('/'))
|
|
180
|
+
return null
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
for (let pass = 0; pass < MAX_DECODE_PASSES; pass++) {
|
|
184
|
+
const next = decodeURIComponent(decoded)
|
|
185
|
+
if (next === decoded)
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
decoded = next
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
decoded = decoded.replaceAll('\\', '/')
|
|
196
|
+
if (decoded.includes('\0'))
|
|
197
|
+
return null
|
|
198
|
+
|
|
199
|
+
const segments = decoded.split('/')
|
|
200
|
+
if (segments.includes('..'))
|
|
201
|
+
return null
|
|
202
|
+
|
|
203
|
+
return decoded
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isContained(root, candidate) {
|
|
207
|
+
const relativePath = relative(root, candidate)
|
|
208
|
+
|
|
209
|
+
return relativePath === ''
|
|
210
|
+
|| (!isAbsolute(relativePath)
|
|
211
|
+
&& relativePath !== '..'
|
|
212
|
+
&& !relativePath.startsWith(`..${sep}`))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isSafeFile(root, candidate) {
|
|
216
|
+
if (!isContained(root, candidate))
|
|
217
|
+
return false
|
|
218
|
+
|
|
219
|
+
const relativePath = relative(root, candidate)
|
|
220
|
+
let current = root
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
for (const segment of relativePath.split(sep).filter(Boolean)) {
|
|
224
|
+
current = join(current, segment)
|
|
225
|
+
const stat = lstatSync(current)
|
|
226
|
+
if (stat.isSymbolicLink())
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const stat = lstatSync(candidate)
|
|
231
|
+
if (!stat.isFile())
|
|
232
|
+
return false
|
|
233
|
+
|
|
234
|
+
return isContained(realpathSync(root), realpathSync(candidate))
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isSpaRoute(pathname) {
|
|
242
|
+
if (STATIC_PREFIXES.some(prefix => pathname.startsWith(prefix)))
|
|
243
|
+
return false
|
|
244
|
+
|
|
245
|
+
const lastSegment = pathname.split('/').filter(Boolean).at(-1) ?? ''
|
|
246
|
+
return extname(lastSegment) === ''
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Безопасно сопоставляет URL path с обычным файлом внутри готового dist.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} distDir
|
|
253
|
+
* @param {string} pathname
|
|
254
|
+
* @returns {{ path: string, relativePath: string } | null}
|
|
255
|
+
*/
|
|
256
|
+
export function resolveDistRequest(distDir, pathname) {
|
|
257
|
+
const decodedPathname = decodePathname(pathname)
|
|
258
|
+
if (decodedPathname === null)
|
|
259
|
+
return null
|
|
260
|
+
|
|
261
|
+
const root = resolve(distDir)
|
|
262
|
+
try {
|
|
263
|
+
const rootStat = lstatSync(root)
|
|
264
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const requestRelativePath = decodedPathname.replace(/^\/+/, '')
|
|
272
|
+
const requestedPath = resolve(root, requestRelativePath)
|
|
273
|
+
|
|
274
|
+
if (isSafeFile(root, requestedPath)) {
|
|
275
|
+
return {
|
|
276
|
+
path: requestedPath,
|
|
277
|
+
relativePath: relative(root, requestedPath).split(sep).join('/'),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
if (lstatSync(requestedPath).isDirectory() && requestedPath !== root)
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Отсутствующий route ниже может получить SPA fallback.
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!isSpaRoute(decodedPathname))
|
|
290
|
+
return null
|
|
291
|
+
|
|
292
|
+
const indexPath = join(root, 'index.html')
|
|
293
|
+
if (!isSafeFile(root, indexPath))
|
|
294
|
+
return null
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
path: indexPath,
|
|
298
|
+
relativePath: 'index.html',
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function contentTypeFor(path) {
|
|
303
|
+
return CONTENT_TYPES.get(extname(path).toLowerCase()) ?? 'application/octet-stream'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sendStatus(response, status, message, headers = {}, omitBody = false) {
|
|
307
|
+
const body = `${message}\n`
|
|
308
|
+
response.writeHead(status, {
|
|
309
|
+
'cache-control': 'no-store',
|
|
310
|
+
'content-length': Buffer.byteLength(body),
|
|
311
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
312
|
+
'x-content-type-options': 'nosniff',
|
|
313
|
+
...headers,
|
|
314
|
+
})
|
|
315
|
+
response.end(omitBody ? undefined : body)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function readStaticFile(path, readFile = readFileSync) {
|
|
319
|
+
try {
|
|
320
|
+
return readFile(path)
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR')
|
|
324
|
+
return null
|
|
325
|
+
|
|
326
|
+
throw error
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function handleRequest(distDir, request, response, readFile) {
|
|
331
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
332
|
+
sendStatus(response, 405, 'Method Not Allowed', { allow: 'GET, HEAD' })
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const requestUrl = request.url ?? ''
|
|
337
|
+
if (!requestUrl.startsWith('/')) {
|
|
338
|
+
sendStatus(response, 400, 'Bad Request', {}, request.method === 'HEAD')
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const resolved = resolveDistRequest(distDir, requestUrl)
|
|
343
|
+
if (resolved === null) {
|
|
344
|
+
sendStatus(response, 404, 'Not Found', {}, request.method === 'HEAD')
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const body = readStaticFile(resolved.path, readFile)
|
|
349
|
+
if (body === null) {
|
|
350
|
+
sendStatus(response, 404, 'Not Found', {}, request.method === 'HEAD')
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
response.writeHead(200, {
|
|
355
|
+
'cache-control': 'no-store',
|
|
356
|
+
'content-length': body.byteLength,
|
|
357
|
+
'content-type': contentTypeFor(resolved.path),
|
|
358
|
+
'x-content-type-options': 'nosniff',
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
if (request.method === 'HEAD')
|
|
362
|
+
response.end()
|
|
363
|
+
else
|
|
364
|
+
response.end(body)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Поднимает локальный static server на loopback и случайном свободном порту.
|
|
369
|
+
*
|
|
370
|
+
* @param {string} distDir
|
|
371
|
+
* @param {{ readFile?: (path: string) => Buffer }} options
|
|
372
|
+
* @returns {Promise<{ origin: string, close: () => Promise<void> }>}
|
|
373
|
+
*/
|
|
374
|
+
export async function createStaticDistServer(distDir, { readFile = readFileSync } = {}) {
|
|
375
|
+
const root = resolve(distDir)
|
|
376
|
+
const rootStat = lstatSync(root)
|
|
377
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
378
|
+
throw new Error(`dist должен быть обычной директорией: ${root}`)
|
|
379
|
+
|
|
380
|
+
const server = createServer((request, response) => {
|
|
381
|
+
handleRequest(root, request, response, readFile).catch(() => {
|
|
382
|
+
if (!response.headersSent)
|
|
383
|
+
sendStatus(response, 500, 'Internal Server Error', {}, request.method === 'HEAD')
|
|
384
|
+
else
|
|
385
|
+
response.destroy()
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
390
|
+
const onError = (error) => {
|
|
391
|
+
server.off('listening', onListening)
|
|
392
|
+
rejectListen(error)
|
|
393
|
+
}
|
|
394
|
+
const onListening = () => {
|
|
395
|
+
server.off('error', onError)
|
|
396
|
+
resolveListen()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
server.once('error', onError)
|
|
400
|
+
server.once('listening', onListening)
|
|
401
|
+
server.listen(0, '127.0.0.1')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const address = server.address()
|
|
405
|
+
if (address === null || typeof address === 'string') {
|
|
406
|
+
server.close()
|
|
407
|
+
throw new Error('static server не сообщил TCP port')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let closePromise
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
origin: `http://127.0.0.1:${address.port}`,
|
|
414
|
+
close() {
|
|
415
|
+
closePromise ??= new Promise((resolveClose, rejectClose) => {
|
|
416
|
+
server.close((error) => {
|
|
417
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING')
|
|
418
|
+
rejectClose(error)
|
|
419
|
+
else
|
|
420
|
+
resolveClose()
|
|
421
|
+
})
|
|
422
|
+
server.closeAllConnections?.()
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
return closePromise
|
|
426
|
+
},
|
|
427
|
+
}
|
|
428
|
+
}
|