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.
Files changed (41) hide show
  1. package/index.js +1 -0
  2. package/package.json +1 -1
  3. package/src/build-system.js +1 -0
  4. package/src/client/virtual-module/assets.js +7 -6
  5. package/src/config.js +33 -3
  6. package/src/dev-server.js +68 -26
  7. package/src/error-page.jsx +49 -0
  8. package/src/mdx.js +235 -8
  9. package/src/pages-index.js +42 -0
  10. package/src/pages.js +1 -0
  11. package/src/reframe.js +1 -1
  12. package/src/state.js +8 -0
  13. package/src/text-utils.js +60 -0
  14. package/src/vite-plugins.js +12 -27
  15. package/src/workers/build-pool.js +1 -0
  16. package/themes/blog/README.md +26 -0
  17. package/themes/blog/components/CategoryView.client.jsx +164 -0
  18. package/themes/blog/components/CategoryView.static.jsx +35 -0
  19. package/themes/blog/components/CollectionView.client.jsx +151 -0
  20. package/themes/blog/components/CollectionView.static.jsx +37 -0
  21. package/themes/blog/components/PostList.client.jsx +92 -0
  22. package/themes/blog/components/PostList.static.jsx +36 -0
  23. package/themes/blog/components/ThemeSearchBox.client.jsx +427 -0
  24. package/themes/blog/components/ThemeSearchBox.static.jsx +40 -0
  25. package/themes/blog/index.js +40 -0
  26. package/themes/blog/pages/404.mdx +12 -0
  27. package/themes/blog/pages/about.mdx +14 -0
  28. package/themes/blog/pages/categories.mdx +6 -0
  29. package/themes/blog/pages/collections.mdx +6 -0
  30. package/themes/blog/pages/index.mdx +16 -0
  31. package/themes/blog/pages/offline.mdx +11 -0
  32. package/themes/blog/sources/style.css +579 -0
  33. package/themes/blog/src/date-utils.js +28 -0
  34. package/themes/blog/src/heading.jsx +37 -0
  35. package/themes/blog/src/layout-categories.jsx +65 -0
  36. package/themes/blog/src/layout-collections.jsx +64 -0
  37. package/themes/blog/src/layout-home.jsx +65 -0
  38. package/themes/blog/src/layout-post.jsx +40 -0
  39. package/themes/blog/src/page.jsx +152 -0
  40. package/themes/blog/src/post-utils.js +83 -0
  41. package/themes/default/src/page.jsx +2 -2
package/index.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { HTMLRenderer } from './src/renderer.js'
22
+ export { extractExcerpt } from './src/text-utils.js'
22
23
 
23
24
  export { env } from './src/reframe.js'
24
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "methanol",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Static site generator powered by rEFui and MDX",
5
5
  "main": "./index.js",
6
6
  "type": "module",
@@ -147,6 +147,7 @@ export const buildHtmlEntries = async () => {
147
147
  }
148
148
  }
149
149
  pagesContext.refreshPagesTree?.()
150
+ state.PAGES_CONTEXT = pagesContext
150
151
 
151
152
  const titleSnapshot = pages.map((page) => page.title)
152
153
  await runWorkerStage({
@@ -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 readStatic = (filePath) => cached(() => readFileSync(resolve(__dirname, filePath), 'utf-8'))
29
+ export const virtualModuleDir = __dirname
30
+ export const createStaticResource = (filePath) => cached(() => readFileSync(resolve(__dirname, filePath), 'utf-8'))
30
31
 
31
- export const INJECT_SCRIPT = readStatic('./inject.js')
32
- export const LOADER_SCRIPT = readStatic('./loader.js')
33
- export const PAGEFIND_LOADER_SCRIPT = readStatic('./pagefind-loader.js')
34
- export const PWA_INJECT_SCRIPT = readStatic('./pwa-inject.js')
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
- state.USER_THEME = config.theme || await defaultTheme()
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
- return next()
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.statusCode = 500
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.statusCode = 500
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.statusCode = 500
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
- invalidateRewindInject()
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
- invalidateRewindInject()
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
- invalidateRewindInject()
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
- invalidateRewindInject()
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
- invalidateRewindInject()
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 override = normalizeStarryNightConfig(frontmatter.starryNight)
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
- const plugin = starryNightConfig.options ? [rehypeStarryNight, starryNightConfig.options] : [rehypeStarryNight]
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) }