methanol 0.0.14 → 0.0.16
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/index.js +1 -0
- package/package.json +1 -1
- package/src/build-system.js +1 -0
- package/src/client/virtual-module/assets.js +7 -6
- package/src/config.js +33 -3
- package/src/dev-server.js +68 -26
- package/src/error-page.jsx +49 -0
- package/src/mdx.js +235 -8
- package/src/pages-index.js +42 -0
- package/src/pages.js +1 -0
- package/src/reframe.js +1 -1
- package/src/state.js +8 -0
- package/src/text-utils.js +60 -0
- package/src/vite-plugins.js +12 -27
- package/src/workers/build-pool.js +1 -0
- package/themes/blog/README.md +26 -0
- package/themes/blog/components/CategoryView.client.jsx +164 -0
- package/themes/blog/components/CategoryView.static.jsx +35 -0
- package/themes/blog/components/CollectionView.client.jsx +151 -0
- package/themes/blog/components/CollectionView.static.jsx +37 -0
- package/themes/blog/components/PostList.client.jsx +92 -0
- package/themes/blog/components/PostList.static.jsx +36 -0
- package/themes/blog/components/ThemeSearchBox.client.jsx +427 -0
- package/themes/blog/components/ThemeSearchBox.static.jsx +40 -0
- package/themes/blog/index.js +40 -0
- package/themes/blog/pages/404.mdx +12 -0
- package/themes/blog/pages/about.mdx +14 -0
- package/themes/blog/pages/categories.mdx +6 -0
- package/themes/blog/pages/collections.mdx +6 -0
- package/themes/blog/pages/index.mdx +16 -0
- package/themes/blog/pages/offline.mdx +11 -0
- package/themes/blog/sources/style.css +579 -0
- package/themes/blog/src/date-utils.js +28 -0
- package/themes/blog/src/heading.jsx +37 -0
- package/themes/blog/src/layout-categories.jsx +65 -0
- package/themes/blog/src/layout-collections.jsx +64 -0
- package/themes/blog/src/layout-home.jsx +65 -0
- package/themes/blog/src/layout-post.jsx +40 -0
- package/themes/blog/src/page.jsx +152 -0
- package/themes/blog/src/post-utils.js +83 -0
- package/themes/default/src/page.jsx +2 -2
package/index.js
CHANGED
package/package.json
CHANGED
package/src/build-system.js
CHANGED
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
import { readFileSync } from 'fs'
|
|
22
22
|
import { fileURLToPath } from 'url'
|
|
23
23
|
import { dirname, resolve } from 'path'
|
|
24
|
-
import { cached } from '../../utils.js'
|
|
24
|
+
import { cached, cachedStr } from '../../utils.js'
|
|
25
25
|
|
|
26
26
|
const __filename = fileURLToPath(import.meta.url)
|
|
27
27
|
const __dirname = dirname(__filename)
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
export const virtualModuleDir = __dirname
|
|
30
|
+
export const createStaticResource = (filePath) => cached(() => readFileSync(resolve(__dirname, filePath), 'utf-8'))
|
|
30
31
|
|
|
31
|
-
export const INJECT_SCRIPT =
|
|
32
|
-
export const LOADER_SCRIPT =
|
|
33
|
-
export const PAGEFIND_LOADER_SCRIPT =
|
|
34
|
-
export const PWA_INJECT_SCRIPT =
|
|
32
|
+
export const INJECT_SCRIPT = createStaticResource('./inject.js')
|
|
33
|
+
export const LOADER_SCRIPT = createStaticResource('./loader.js')
|
|
34
|
+
export const PAGEFIND_LOADER_SCRIPT = createStaticResource('./pagefind-loader.js')
|
|
35
|
+
export const PWA_INJECT_SCRIPT = createStaticResource('./pwa-inject.js')
|
package/src/config.js
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
import { readFile } from 'fs/promises'
|
|
22
22
|
import { existsSync } from 'fs'
|
|
23
|
-
import { resolve, isAbsolute, extname, basename } from 'path'
|
|
24
|
-
import { pathToFileURL } from 'url'
|
|
23
|
+
import { resolve, isAbsolute, extname, basename, dirname } from 'path'
|
|
24
|
+
import { pathToFileURL, fileURLToPath } from 'url'
|
|
25
25
|
import { mergeConfig } from 'vite'
|
|
26
|
+
import { projectRequire } from './node-loader.js'
|
|
26
27
|
import { cli, state } from './state.js'
|
|
27
28
|
import { logger } from './logger.js'
|
|
28
29
|
import { HTMLRenderer } from './renderer.js'
|
|
@@ -239,6 +240,34 @@ const buildConfigContext = (mode) => ({
|
|
|
239
240
|
HTMLRenderer
|
|
240
241
|
})
|
|
241
242
|
|
|
243
|
+
const resolveTheme = async (themeValue, root) => {
|
|
244
|
+
if (typeof themeValue !== 'string') return themeValue
|
|
245
|
+
|
|
246
|
+
const load = async (p) => {
|
|
247
|
+
const mod = await import(pathToFileURL(p).href)
|
|
248
|
+
const fn = mod.default ?? mod
|
|
249
|
+
return typeof fn === 'function' ? fn() : fn
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 1. Check Methanol themes dir
|
|
253
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
254
|
+
const builtInPath = resolve(__dirname, '../themes', themeValue, 'index.js')
|
|
255
|
+
if (existsSync(builtInPath)) {
|
|
256
|
+
return load(builtInPath)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 2. Resolve methanol-theme-<name> from user dir
|
|
260
|
+
try {
|
|
261
|
+
const pkgName = `methanol-theme-${themeValue}`
|
|
262
|
+
const pkgPath = projectRequire.resolve(pkgName)
|
|
263
|
+
return load(pkgPath)
|
|
264
|
+
} catch (e) {
|
|
265
|
+
// Ignore
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new Error(`Theme not found: ${themeValue}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
242
271
|
export const loadUserConfig = async (mode, configPath = null) => {
|
|
243
272
|
if (configPath) {
|
|
244
273
|
const filePath = resolveConfigPath(configPath)
|
|
@@ -323,7 +352,8 @@ export const applyConfig = async (config, mode) => {
|
|
|
323
352
|
|
|
324
353
|
state.VIRTUAL_HTML_OUTPUT_ROOT = state.PAGES_DIR
|
|
325
354
|
|
|
326
|
-
|
|
355
|
+
const themeValue = cli.CLI_THEME || config.theme
|
|
356
|
+
state.USER_THEME = themeValue ? await resolveTheme(themeValue, root) : await defaultTheme()
|
|
327
357
|
if (!state.USER_THEME?.root && !config.theme?.root) {
|
|
328
358
|
throw new Error('Theme root is required.')
|
|
329
359
|
}
|
package/src/dev-server.js
CHANGED
|
@@ -38,13 +38,16 @@ import {
|
|
|
38
38
|
} from './components.js'
|
|
39
39
|
import { buildPagesContext, buildPageEntry, routePathFromFile } from './pages.js'
|
|
40
40
|
import { compilePageMdx, renderHtml } from './mdx.js'
|
|
41
|
+
import { DevErrorPage } from './error-page.jsx'
|
|
42
|
+
import { HTMLRenderer } from './renderer.js'
|
|
41
43
|
import { methanolResolverPlugin } from './vite-plugins.js'
|
|
42
44
|
import { preparePublicAssets, updateAsset } from './public-assets.js'
|
|
43
45
|
import { createBuildWorkers, runWorkerStage, terminateWorkers } from './workers/build-pool.js'
|
|
46
|
+
import { virtualModuleDir } from './client/virtual-module/assets.js'
|
|
44
47
|
import { style } from './logger.js'
|
|
45
48
|
|
|
46
49
|
export const runViteDev = async () => {
|
|
47
|
-
const baseFsAllow = [state.ROOT_DIR, state.USER_THEME.root].filter(Boolean)
|
|
50
|
+
const baseFsAllow = [virtualModuleDir, state.ROOT_DIR, state.USER_THEME.root].filter(Boolean)
|
|
48
51
|
if (state.MERGED_ASSETS_DIR) {
|
|
49
52
|
baseFsAllow.push(state.MERGED_ASSETS_DIR)
|
|
50
53
|
}
|
|
@@ -143,6 +146,7 @@ export const runViteDev = async () => {
|
|
|
143
146
|
let pagesContextToken = 0
|
|
144
147
|
const setPagesContext = (next) => {
|
|
145
148
|
pagesContext = next
|
|
149
|
+
state.PAGES_CONTEXT = next
|
|
146
150
|
pagesContextToken += 1
|
|
147
151
|
}
|
|
148
152
|
setPagesContext(await buildPagesContext({ compileAll: false }))
|
|
@@ -159,10 +163,54 @@ export const runViteDev = async () => {
|
|
|
159
163
|
console.error(style.red(`\n[methanol] ${phase} error in ${target}`))
|
|
160
164
|
console.error(error?.stack || error)
|
|
161
165
|
}
|
|
166
|
+
const formatDevError = (error) => {
|
|
167
|
+
if (!error) return 'Unknown error'
|
|
168
|
+
if (typeof error === 'string') return error
|
|
169
|
+
if (error?.stack) return error.stack
|
|
170
|
+
if (error?.message) return error.message
|
|
171
|
+
return String(error)
|
|
172
|
+
}
|
|
173
|
+
const sendDevError = async (res, error, url = '/') => {
|
|
174
|
+
const message = formatDevError(error)
|
|
175
|
+
const basePrefix = devBasePrefix || ''
|
|
176
|
+
const rawHtml = HTMLRenderer.serialize(
|
|
177
|
+
DevErrorPage({ message, basePrefix })(HTMLRenderer)
|
|
178
|
+
)
|
|
179
|
+
let html = rawHtml
|
|
180
|
+
try {
|
|
181
|
+
html = await server.transformIndexHtml(url, rawHtml)
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error(err)
|
|
184
|
+
}
|
|
185
|
+
res.statusCode = 500
|
|
186
|
+
res.setHeader('Content-Type', 'text/html')
|
|
187
|
+
res.end(html)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const _invalidate = (id) => {
|
|
191
|
+
const _module = server.moduleGraph.getModuleById(id)
|
|
192
|
+
if (_module) {
|
|
193
|
+
server.moduleGraph.invalidateModule(_module)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const invalidateReframeInject = () => {
|
|
197
|
+
_invalidate('\0methanol:registry')
|
|
198
|
+
_invalidate('\0methanol:inject')
|
|
199
|
+
_invalidate(resolve(virtualModuleDir, 'inject.js'))
|
|
200
|
+
}
|
|
201
|
+
const invalidatePagesIndex = () => {
|
|
202
|
+
_invalidate('\0methanol:pages')
|
|
203
|
+
}
|
|
162
204
|
|
|
163
205
|
const refreshPagesContext = async () => {
|
|
164
206
|
setPagesContext(await buildPagesContext({ compileAll: false }))
|
|
165
207
|
}
|
|
208
|
+
const resolveStaticCandidate = (baseDir, pathname) => {
|
|
209
|
+
if (!baseDir) return null
|
|
210
|
+
const target = resolve(baseDir, pathname.replace(/^\//, ''))
|
|
211
|
+
if (!target.startsWith(baseDir)) return null
|
|
212
|
+
return target
|
|
213
|
+
}
|
|
166
214
|
|
|
167
215
|
const prebuildHtmlCache = async (token) => {
|
|
168
216
|
if (!pagesContext || token !== pagesContextToken) return
|
|
@@ -214,6 +262,7 @@ export const runViteDev = async () => {
|
|
|
214
262
|
}
|
|
215
263
|
}
|
|
216
264
|
pagesContext.refreshPagesTree?.()
|
|
265
|
+
invalidatePagesIndex()
|
|
217
266
|
invalidateHtmlCache()
|
|
218
267
|
const renderEpoch = htmlCacheEpoch
|
|
219
268
|
|
|
@@ -383,7 +432,14 @@ export const runViteDev = async () => {
|
|
|
383
432
|
const requestedPath = routePath
|
|
384
433
|
if (pathname.includes('.') && !pathname.endsWith('.html')) {
|
|
385
434
|
if (!pagesContext?.pagesByRoute?.has(requestedPath)) {
|
|
386
|
-
|
|
435
|
+
const pageCandidate = resolveStaticCandidate(state.PAGES_DIR, pathname)
|
|
436
|
+
if (pageCandidate && existsSync(pageCandidate)) {
|
|
437
|
+
return next()
|
|
438
|
+
}
|
|
439
|
+
const staticCandidate = resolveStaticCandidate(state.STATIC_DIR, pathname)
|
|
440
|
+
if (staticCandidate && existsSync(staticCandidate)) {
|
|
441
|
+
return next()
|
|
442
|
+
}
|
|
387
443
|
}
|
|
388
444
|
}
|
|
389
445
|
const isExcludedPath = () => {
|
|
@@ -429,8 +485,7 @@ export const runViteDev = async () => {
|
|
|
429
485
|
return
|
|
430
486
|
} catch (err) {
|
|
431
487
|
console.error(err)
|
|
432
|
-
res
|
|
433
|
-
res.end('Internal Server Error')
|
|
488
|
+
await sendDevError(res, err, req.url)
|
|
434
489
|
return
|
|
435
490
|
}
|
|
436
491
|
}
|
|
@@ -487,8 +542,7 @@ export const runViteDev = async () => {
|
|
|
487
542
|
})
|
|
488
543
|
} catch (err) {
|
|
489
544
|
logMdxError('MDX render', err, pageMeta || { path, routePath: renderRoutePath })
|
|
490
|
-
res
|
|
491
|
-
res.end('Internal Server Error')
|
|
545
|
+
await sendDevError(res, err, req.url)
|
|
492
546
|
return
|
|
493
547
|
}
|
|
494
548
|
if (renderEpoch === htmlCacheEpoch) {
|
|
@@ -504,8 +558,7 @@ export const runViteDev = async () => {
|
|
|
504
558
|
res.end(html)
|
|
505
559
|
} catch (err) {
|
|
506
560
|
logMdxError('MDX render', err, pageMeta || { path, routePath: renderRoutePath })
|
|
507
|
-
res
|
|
508
|
-
res.end('Internal Server Error')
|
|
561
|
+
await sendDevError(res, err, req.url)
|
|
509
562
|
}
|
|
510
563
|
}
|
|
511
564
|
|
|
@@ -518,19 +571,6 @@ export const runViteDev = async () => {
|
|
|
518
571
|
await server.listen()
|
|
519
572
|
server.printUrls()
|
|
520
573
|
|
|
521
|
-
const _invalidate = (id) => {
|
|
522
|
-
const _module = server.moduleGraph.getModuleById(id)
|
|
523
|
-
if (_module) {
|
|
524
|
-
server.moduleGraph.invalidateModule(_module)
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const invalidateRewindInject = () => {
|
|
528
|
-
_invalidate('\0/.methanol_virtual_module/registry.js')
|
|
529
|
-
_invalidate('\0methanol:registry')
|
|
530
|
-
_invalidate('\0/.methanol_virtual_module/inject.js')
|
|
531
|
-
_invalidate('\0methanol:inject')
|
|
532
|
-
}
|
|
533
|
-
|
|
534
574
|
let queue = Promise.resolve()
|
|
535
575
|
const enqueue = (task) => {
|
|
536
576
|
queue = queue.then(task).catch((err) => {
|
|
@@ -547,6 +587,7 @@ export const runViteDev = async () => {
|
|
|
547
587
|
|
|
548
588
|
const refreshPages = async () => {
|
|
549
589
|
await refreshPagesContext()
|
|
590
|
+
invalidatePagesIndex()
|
|
550
591
|
invalidateHtmlCache()
|
|
551
592
|
reload()
|
|
552
593
|
}
|
|
@@ -700,6 +741,7 @@ export const runViteDev = async () => {
|
|
|
700
741
|
}
|
|
701
742
|
const updated = await updatePageEntry(path, resolved)
|
|
702
743
|
if (updated) {
|
|
744
|
+
invalidatePagesIndex()
|
|
703
745
|
invalidateHtmlCache()
|
|
704
746
|
reload()
|
|
705
747
|
return
|
|
@@ -740,7 +782,7 @@ export const runViteDev = async () => {
|
|
|
740
782
|
enqueue(async () => {
|
|
741
783
|
const { hasClient } = await updateComponentEntry(path)
|
|
742
784
|
if (hasClient) {
|
|
743
|
-
|
|
785
|
+
invalidateReframeInject()
|
|
744
786
|
}
|
|
745
787
|
invalidateHtmlCache()
|
|
746
788
|
reload()
|
|
@@ -751,7 +793,7 @@ export const runViteDev = async () => {
|
|
|
751
793
|
const { hasClient } = await updateComponentEntry(path)
|
|
752
794
|
invalidateHtmlCache()
|
|
753
795
|
if (hasClient) {
|
|
754
|
-
|
|
796
|
+
invalidateReframeInject()
|
|
755
797
|
}
|
|
756
798
|
reload()
|
|
757
799
|
})
|
|
@@ -772,7 +814,7 @@ export const runViteDev = async () => {
|
|
|
772
814
|
const { hasClient } = await updateComponentEntry(path)
|
|
773
815
|
invalidateHtmlCache()
|
|
774
816
|
if (hasClient) {
|
|
775
|
-
|
|
817
|
+
invalidateReframeInject()
|
|
776
818
|
}
|
|
777
819
|
reload()
|
|
778
820
|
})
|
|
@@ -783,7 +825,7 @@ export const runViteDev = async () => {
|
|
|
783
825
|
if (isClientComponent(path)) {
|
|
784
826
|
enqueue(async () => {
|
|
785
827
|
await updateComponentEntry(path, { fallback: true })
|
|
786
|
-
|
|
828
|
+
invalidateReframeInject()
|
|
787
829
|
invalidateHtmlCache()
|
|
788
830
|
reload()
|
|
789
831
|
})
|
|
@@ -800,7 +842,7 @@ export const runViteDev = async () => {
|
|
|
800
842
|
})
|
|
801
843
|
invalidateHtmlCache()
|
|
802
844
|
if (hasClient) {
|
|
803
|
-
|
|
845
|
+
invalidateReframeInject()
|
|
804
846
|
}
|
|
805
847
|
reload()
|
|
806
848
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { HTMLRenderer as R } from './renderer.js'
|
|
22
|
+
|
|
23
|
+
export const DevErrorPage = ({ message = '', basePrefix = ''} = {}) => (
|
|
24
|
+
<>
|
|
25
|
+
{R.rawHTML`<!doctype html>`}
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="UTF-8" />
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
30
|
+
<title>Methanol dev error</title>
|
|
31
|
+
<style>{`
|
|
32
|
+
body { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; background: #0f1115; color: #e9edf1; }
|
|
33
|
+
.main { padding: 24px; max-width: 960px; }
|
|
34
|
+
h1 { margin: 0 0 12px; font-size: 20px; }
|
|
35
|
+
pre { white-space: pre-wrap; background: #151922; padding: 16px; border-radius: 8px; border: 1px solid #2a2f3a; }
|
|
36
|
+
.note { color: #9aa3ad; font-size: 12px; margin-top: 12px; }
|
|
37
|
+
`}</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div class="main">
|
|
41
|
+
<h1>Dev server error</h1>
|
|
42
|
+
<pre>{message}</pre>
|
|
43
|
+
<div class="note">Fix the error and save to reload.</div>
|
|
44
|
+
</div>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
47
|
+
</>
|
|
48
|
+
)
|
|
49
|
+
|
package/src/mdx.js
CHANGED
|
@@ -25,6 +25,7 @@ import rehypeSlug from 'rehype-slug'
|
|
|
25
25
|
import extractToc from '@stefanprobst/rehype-extract-toc'
|
|
26
26
|
import withTocExport from '@stefanprobst/rehype-extract-toc/mdx'
|
|
27
27
|
import rehypeStarryNight from 'rehype-starry-night'
|
|
28
|
+
import { createStarryNight } from '@wooorm/starry-night'
|
|
28
29
|
import remarkGfm from 'remark-gfm'
|
|
29
30
|
import { HTMLRenderer } from './renderer.js'
|
|
30
31
|
import { signal, computed, read, Suspense, nextTick } from 'refui'
|
|
@@ -36,10 +37,12 @@ import { state } from './state.js'
|
|
|
36
37
|
import { resolveUserMdxConfig, withBase } from './config.js'
|
|
37
38
|
import { methanolCtx } from './rehype-plugins/methanol-ctx.js'
|
|
38
39
|
import { linkResolve } from './rehype-plugins/link-resolve.js'
|
|
40
|
+
import { cached } from './utils.js'
|
|
39
41
|
|
|
40
42
|
// Workaround for Vite: it doesn't support resolving module/virtual modules in script src in dev mode
|
|
41
|
-
const resolveRewindInject = () =>
|
|
43
|
+
const resolveRewindInject = cached(() =>
|
|
42
44
|
HTMLRenderer.rawHTML(`<script type="module" src="${withBase('/.methanol_virtual_module/inject.js')}"></script>`)
|
|
45
|
+
)
|
|
43
46
|
const RWND_FALLBACK = HTMLRenderer.rawHTML(
|
|
44
47
|
'<script>if(!window.$$rfrm){var l=[];var r=function(k,i,p){l.push([k,i,p,document.currentScript])};r.$$loaded=l;window.$$rfrm=r}</script>'
|
|
45
48
|
)
|
|
@@ -219,16 +222,228 @@ const normalizeStarryNightConfig = (value) => {
|
|
|
219
222
|
const resolveStarryNightForPage = (frontmatter) => {
|
|
220
223
|
const base = {
|
|
221
224
|
enabled: state.STARRY_NIGHT_ENABLED === true,
|
|
222
|
-
options: state.STARRY_NIGHT_OPTIONS || null
|
|
225
|
+
options: state.STARRY_NIGHT_OPTIONS || null,
|
|
226
|
+
explicit: false
|
|
223
227
|
}
|
|
224
228
|
if (!frontmatter || !Object.prototype.hasOwnProperty.call(frontmatter, 'starryNight')) {
|
|
225
229
|
return base
|
|
226
230
|
}
|
|
227
|
-
const
|
|
231
|
+
const overrideValue = frontmatter.starryNight
|
|
232
|
+
if (typeof overrideValue === 'boolean') {
|
|
233
|
+
if (overrideValue === false) {
|
|
234
|
+
return { enabled: false, options: null, explicit: true }
|
|
235
|
+
}
|
|
236
|
+
return { enabled: true, options: base.options, explicit: false }
|
|
237
|
+
}
|
|
238
|
+
if (!overrideValue || typeof overrideValue !== 'object') {
|
|
239
|
+
return base
|
|
240
|
+
}
|
|
241
|
+
const override = normalizeStarryNightConfig(overrideValue)
|
|
228
242
|
if (!override) return base
|
|
229
|
-
if (override.enabled === false) return { enabled: false, options: null }
|
|
243
|
+
if (override.enabled === false) return { enabled: false, options: null, explicit: true }
|
|
230
244
|
const options = override.options != null ? override.options : base.options
|
|
231
|
-
return { enabled: true, options }
|
|
245
|
+
return { enabled: true, options, explicit: true }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const CODE_FENCE_LANG_PATTERN = /(^|\n)\s*(```|~~~)\s*([^\s{]+)/g
|
|
249
|
+
const extractCodeFenceLanguages = (value) => {
|
|
250
|
+
const text = String(value || '')
|
|
251
|
+
const languages = new Set()
|
|
252
|
+
CODE_FENCE_LANG_PATTERN.lastIndex = 0
|
|
253
|
+
let match
|
|
254
|
+
while ((match = CODE_FENCE_LANG_PATTERN.exec(text))) {
|
|
255
|
+
let lang = match[3] ? String(match[3]).trim() : ''
|
|
256
|
+
if (!lang) continue
|
|
257
|
+
const braceIndex = lang.indexOf('{')
|
|
258
|
+
if (braceIndex >= 0) {
|
|
259
|
+
lang = lang.slice(0, braceIndex).trim()
|
|
260
|
+
}
|
|
261
|
+
if (!lang || !/[A-Za-z]/.test(lang)) continue
|
|
262
|
+
languages.add(lang.toLowerCase())
|
|
263
|
+
}
|
|
264
|
+
return languages
|
|
265
|
+
}
|
|
266
|
+
const cleanStarryOptions = (options) => {
|
|
267
|
+
if (!options || typeof options !== 'object') return undefined
|
|
268
|
+
const next = { ...options }
|
|
269
|
+
delete next.grammars
|
|
270
|
+
return next
|
|
271
|
+
}
|
|
272
|
+
let starryNightFuture = null
|
|
273
|
+
const resolvedGrammarCache = new Map()
|
|
274
|
+
const resolvedCustomGrammarCache = new Map()
|
|
275
|
+
const failedLanguageCache = new Set()
|
|
276
|
+
const failedCustomLanguageCache = new Map()
|
|
277
|
+
const loadStarryNight = async (options) => {
|
|
278
|
+
if (!starryNightFuture) {
|
|
279
|
+
starryNightFuture = import('@wooorm/starry-night').then(async (mod) => {
|
|
280
|
+
const grammars = Array.isArray(mod?.all) ? mod.all : []
|
|
281
|
+
const starryNight = await createStarryNight(grammars, cleanStarryOptions(options))
|
|
282
|
+
return {
|
|
283
|
+
starryNight,
|
|
284
|
+
grammars,
|
|
285
|
+
scopeMap: buildScopeGrammarMap(grammars)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
return starryNightFuture
|
|
290
|
+
}
|
|
291
|
+
const buildScopeGrammarMap = (grammars) => {
|
|
292
|
+
const scopeMap = new Map()
|
|
293
|
+
for (const grammar of grammars || []) {
|
|
294
|
+
const scopeName = grammar?.scopeName
|
|
295
|
+
if (typeof scopeName === 'string' && scopeName) {
|
|
296
|
+
scopeMap.set(scopeName, grammar)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return scopeMap
|
|
300
|
+
}
|
|
301
|
+
const collectExternalScopes = (grammar) => {
|
|
302
|
+
const scopes = new Set()
|
|
303
|
+
const addScope = (value) => {
|
|
304
|
+
if (typeof value !== 'string') return
|
|
305
|
+
if (!value || value.startsWith('#')) return
|
|
306
|
+
scopes.add(value)
|
|
307
|
+
}
|
|
308
|
+
const visit = (node) => {
|
|
309
|
+
if (!node) return
|
|
310
|
+
if (Array.isArray(node)) {
|
|
311
|
+
for (const item of node) {
|
|
312
|
+
visit(item)
|
|
313
|
+
}
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (typeof node !== 'object') return
|
|
317
|
+
if (typeof node.include === 'string') {
|
|
318
|
+
addScope(node.include)
|
|
319
|
+
}
|
|
320
|
+
for (const value of Object.values(node)) {
|
|
321
|
+
visit(value)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(grammar?.dependencies)) {
|
|
325
|
+
for (const dep of grammar.dependencies) {
|
|
326
|
+
addScope(dep)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
visit(grammar?.patterns)
|
|
330
|
+
visit(grammar?.repository)
|
|
331
|
+
return scopes
|
|
332
|
+
}
|
|
333
|
+
const resolveStarryNightGrammars = async (languages, options) => {
|
|
334
|
+
const hasCustomGrammars = Array.isArray(options?.grammars)
|
|
335
|
+
const baseGrammars = hasCustomGrammars ? options.grammars : null
|
|
336
|
+
if (hasCustomGrammars && (!baseGrammars || !baseGrammars.length)) return null
|
|
337
|
+
const getCustomKey = (grammars) =>
|
|
338
|
+
(grammars || [])
|
|
339
|
+
.map((grammar) => grammar?.scopeName)
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.sort()
|
|
342
|
+
.join('|')
|
|
343
|
+
const failedSet = hasCustomGrammars
|
|
344
|
+
? (() => {
|
|
345
|
+
const customKey = getCustomKey(baseGrammars)
|
|
346
|
+
let set = failedCustomLanguageCache.get(customKey)
|
|
347
|
+
if (!set) {
|
|
348
|
+
set = new Set()
|
|
349
|
+
failedCustomLanguageCache.set(customKey, set)
|
|
350
|
+
}
|
|
351
|
+
return set
|
|
352
|
+
})()
|
|
353
|
+
: failedLanguageCache
|
|
354
|
+
const filteredLanguages = Array.from(languages || [])
|
|
355
|
+
.map((lang) => String(lang).toLowerCase())
|
|
356
|
+
.filter((lang) => lang && !failedSet.has(lang))
|
|
357
|
+
const languageKey = filteredLanguages.slice().sort().join('|')
|
|
358
|
+
if (!languageKey) return []
|
|
359
|
+
if (hasCustomGrammars) {
|
|
360
|
+
const customKey = getCustomKey(baseGrammars)
|
|
361
|
+
const customCache = resolvedCustomGrammarCache.get(customKey)
|
|
362
|
+
if (customCache?.has(languageKey)) {
|
|
363
|
+
return customCache.get(languageKey)
|
|
364
|
+
}
|
|
365
|
+
} else if (resolvedGrammarCache.has(languageKey)) {
|
|
366
|
+
return resolvedGrammarCache.get(languageKey)
|
|
367
|
+
}
|
|
368
|
+
const selected = new Set()
|
|
369
|
+
const selectedScopes = new Set()
|
|
370
|
+
let scopeMap = baseGrammars ? buildScopeGrammarMap(baseGrammars) : null
|
|
371
|
+
let loadedStarryNight = null
|
|
372
|
+
const ensureAll = async () => {
|
|
373
|
+
if (!loadedStarryNight && !hasCustomGrammars) {
|
|
374
|
+
loadedStarryNight = await loadStarryNight(options)
|
|
375
|
+
if (!scopeMap) {
|
|
376
|
+
scopeMap = loadedStarryNight?.scopeMap || null
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let flagToScope = null
|
|
381
|
+
if (hasCustomGrammars) {
|
|
382
|
+
const customStarryNight = await createStarryNight(baseGrammars, cleanStarryOptions(options))
|
|
383
|
+
flagToScope = (lang) => customStarryNight.flagToScope(String(lang))
|
|
384
|
+
} else {
|
|
385
|
+
try {
|
|
386
|
+
await ensureAll()
|
|
387
|
+
} catch {
|
|
388
|
+
for (const lang of filteredLanguages) {
|
|
389
|
+
failedSet.add(lang)
|
|
390
|
+
}
|
|
391
|
+
return []
|
|
392
|
+
}
|
|
393
|
+
flagToScope = (lang) => loadedStarryNight?.starryNight.flagToScope(String(lang))
|
|
394
|
+
}
|
|
395
|
+
if (!scopeMap) return null
|
|
396
|
+
const addGrammar = (grammar) => {
|
|
397
|
+
if (!grammar) return false
|
|
398
|
+
const scopeName = grammar.scopeName
|
|
399
|
+
if (!scopeName || selectedScopes.has(scopeName)) return false
|
|
400
|
+
selectedScopes.add(scopeName)
|
|
401
|
+
selected.add(grammar)
|
|
402
|
+
return true
|
|
403
|
+
}
|
|
404
|
+
for (const lang of filteredLanguages) {
|
|
405
|
+
let scope
|
|
406
|
+
try {
|
|
407
|
+
scope = flagToScope ? flagToScope(String(lang)) : undefined
|
|
408
|
+
} catch {
|
|
409
|
+
scope = undefined
|
|
410
|
+
}
|
|
411
|
+
const grammar = scope && scopeMap ? scopeMap.get(scope) : null
|
|
412
|
+
if (!grammar) {
|
|
413
|
+
failedSet.add(lang)
|
|
414
|
+
}
|
|
415
|
+
addGrammar(grammar)
|
|
416
|
+
}
|
|
417
|
+
if (!selected.size) return []
|
|
418
|
+
const queue = Array.from(selected)
|
|
419
|
+
while (queue.length) {
|
|
420
|
+
const grammar = queue.pop()
|
|
421
|
+
const scopes = collectExternalScopes(grammar)
|
|
422
|
+
for (const scope of scopes) {
|
|
423
|
+
if (selectedScopes.has(scope)) continue
|
|
424
|
+
let depGrammar = scopeMap ? scopeMap.get(scope) : null
|
|
425
|
+
if (!depGrammar && !hasCustomGrammars) {
|
|
426
|
+
await ensureAll()
|
|
427
|
+
depGrammar = scopeMap ? scopeMap.get(scope) : null
|
|
428
|
+
}
|
|
429
|
+
if (addGrammar(depGrammar)) {
|
|
430
|
+
queue.push(depGrammar)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const result = selected.size ? Array.from(selected) : []
|
|
435
|
+
if (hasCustomGrammars) {
|
|
436
|
+
const customKey = getCustomKey(baseGrammars)
|
|
437
|
+
let customCache = resolvedCustomGrammarCache.get(customKey)
|
|
438
|
+
if (!customCache) {
|
|
439
|
+
customCache = new Map()
|
|
440
|
+
resolvedCustomGrammarCache.set(customKey, customCache)
|
|
441
|
+
}
|
|
442
|
+
customCache.set(languageKey, result)
|
|
443
|
+
} else {
|
|
444
|
+
resolvedGrammarCache.set(languageKey, result)
|
|
445
|
+
}
|
|
446
|
+
return result
|
|
232
447
|
}
|
|
233
448
|
|
|
234
449
|
const resolveBaseMdxConfig = async () => {
|
|
@@ -262,7 +477,7 @@ const resolveBaseMdxConfig = async () => {
|
|
|
262
477
|
return (cachedMdxConfig = mdxConfig)
|
|
263
478
|
}
|
|
264
479
|
|
|
265
|
-
const resolveMdxConfigForPage = async (frontmatter) => {
|
|
480
|
+
const resolveMdxConfigForPage = async (frontmatter, content = '') => {
|
|
266
481
|
const baseConfig = await resolveBaseMdxConfig()
|
|
267
482
|
const mdxConfig = {
|
|
268
483
|
...baseConfig,
|
|
@@ -270,7 +485,19 @@ const resolveMdxConfigForPage = async (frontmatter) => {
|
|
|
270
485
|
}
|
|
271
486
|
const starryNightConfig = resolveStarryNightForPage(frontmatter)
|
|
272
487
|
if (!starryNightConfig.enabled) return mdxConfig
|
|
273
|
-
|
|
488
|
+
let options = starryNightConfig.options
|
|
489
|
+
if (!starryNightConfig.explicit) {
|
|
490
|
+
const languages = extractCodeFenceLanguages(content)
|
|
491
|
+
if (!languages.size) {
|
|
492
|
+
return mdxConfig
|
|
493
|
+
}
|
|
494
|
+
const grammars = await resolveStarryNightGrammars(languages, options)
|
|
495
|
+
if (!grammars || !grammars.length) {
|
|
496
|
+
return mdxConfig
|
|
497
|
+
}
|
|
498
|
+
options = { ...(options || {}), grammars }
|
|
499
|
+
}
|
|
500
|
+
const plugin = options ? [rehypeStarryNight, options] : [rehypeStarryNight]
|
|
274
501
|
const insertIndex = mdxConfig.rehypePlugins.indexOf(linkResolve)
|
|
275
502
|
if (insertIndex >= 0) {
|
|
276
503
|
mdxConfig.rehypePlugins.splice(insertIndex, 0, plugin)
|
|
@@ -281,7 +508,7 @@ const resolveMdxConfigForPage = async (frontmatter) => {
|
|
|
281
508
|
}
|
|
282
509
|
|
|
283
510
|
export const compileMdxSource = async ({ content, path, frontmatter }) => {
|
|
284
|
-
const mdxConfig = await resolveMdxConfigForPage(frontmatter)
|
|
511
|
+
const mdxConfig = await resolveMdxConfigForPage(frontmatter, content)
|
|
285
512
|
const compiled = await compile({ value: content, path: path }, mdxConfig)
|
|
286
513
|
const code = String(compiled.value ?? compiled)
|
|
287
514
|
return { code, development: Boolean(mdxConfig.development) }
|