methanol 0.0.0 → 0.0.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 +203 -0
- package/README.md +58 -0
- package/banner.txt +6 -0
- package/bin/methanol.js +24 -0
- package/index.js +22 -0
- package/package.json +51 -9
- package/src/assets.js +30 -0
- package/src/build-system.js +200 -0
- package/src/components.js +145 -0
- package/src/config.js +396 -0
- package/src/dev-server.js +632 -0
- package/src/main.js +133 -0
- package/src/mdx.js +406 -0
- package/src/node-loader.js +88 -0
- package/src/pagefind.js +107 -0
- package/src/pages.js +771 -0
- package/src/preview-server.js +58 -0
- package/src/public-assets.js +73 -0
- package/src/register-loader.js +29 -0
- package/src/rehype-plugins/link-resolve.js +116 -0
- package/src/rehype-plugins/methanol-ctx.js +89 -0
- package/src/renderer.js +25 -0
- package/src/rewind.js +117 -0
- package/src/stage-logger.js +59 -0
- package/src/state.js +179 -0
- package/src/virtual-module/inject.js +30 -0
- package/src/virtual-module/loader.js +116 -0
- package/src/virtual-module/pagefind.js +108 -0
- package/src/vite-plugins.js +173 -0
- package/themes/default/components/ThemeAccentSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeAccentSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeSearchBox.client.jsx +324 -0
- package/themes/default/components/ThemeSearchBox.static.jsx +40 -0
- package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
- package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
- package/themes/default/components/pre.client.jsx +84 -0
- package/themes/default/components/pre.static.jsx +27 -0
- package/themes/default/heading.jsx +35 -0
- package/themes/default/index.js +41 -0
- package/themes/default/page.jsx +303 -0
- package/themes/default/pages/404.mdx +8 -0
- package/themes/default/pages/index.mdx +31 -0
- package/themes/default/public/favicon.png +0 -0
- package/themes/default/public/logo.png +0 -0
- package/themes/default/sources/prefetch.js +49 -0
- package/themes/default/sources/style.css +1660 -0
package/src/pages.js
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
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 matter from 'gray-matter'
|
|
22
|
+
import { readdir, readFile, stat } from 'fs/promises'
|
|
23
|
+
import { existsSync } from 'fs'
|
|
24
|
+
import { resolve, join, relative } from 'path'
|
|
25
|
+
import { state, cli } from './state.js'
|
|
26
|
+
import { compilePageMdx } from './mdx.js'
|
|
27
|
+
import { createStageLogger } from './stage-logger.js'
|
|
28
|
+
|
|
29
|
+
const isPageFile = (name) => name.endsWith('.mdx') || name.endsWith('.md')
|
|
30
|
+
const isInternalPage = (name) => name.startsWith('_') || name.startsWith('.')
|
|
31
|
+
const isIgnoredEntry = (name) => name.startsWith('.') || name.startsWith('_')
|
|
32
|
+
|
|
33
|
+
const pageMetadataCache = new Map()
|
|
34
|
+
const pageDerivedCache = new Map()
|
|
35
|
+
|
|
36
|
+
const collectLanguagesFromPages = (pages = []) => {
|
|
37
|
+
const languages = new Map()
|
|
38
|
+
for (const page of pages) {
|
|
39
|
+
if (!page?.isIndex) continue
|
|
40
|
+
const label = page?.frontmatter?.lang
|
|
41
|
+
if (label == null || label === '') continue
|
|
42
|
+
const routePath = page.routePath || '/'
|
|
43
|
+
const href = page.routeHref || routePath || '/'
|
|
44
|
+
const frontmatterCode = page?.frontmatter?.langCode
|
|
45
|
+
const code =
|
|
46
|
+
typeof frontmatterCode === 'string' && frontmatterCode.trim()
|
|
47
|
+
? frontmatterCode.trim()
|
|
48
|
+
: routePath === '/'
|
|
49
|
+
? null
|
|
50
|
+
: routePath.replace(/^\/+/, '')
|
|
51
|
+
languages.set(routePath, {
|
|
52
|
+
routePath,
|
|
53
|
+
href,
|
|
54
|
+
label: String(label),
|
|
55
|
+
code: code || null
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return Array.from(languages.values()).sort((a, b) => a.href.localeCompare(b.href))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const normalizeRoutePath = (value) => {
|
|
62
|
+
if (!value || value === '/') return '/'
|
|
63
|
+
return value.replace(/\/+$/, '')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resolveLanguageForRoute = (languages = [], routePath = '/') => {
|
|
67
|
+
if (!languages.length) return null
|
|
68
|
+
const normalizedRoute = normalizeRoutePath(routePath)
|
|
69
|
+
let best = null
|
|
70
|
+
let bestLength = -1
|
|
71
|
+
let rootLanguage = null
|
|
72
|
+
for (const lang of languages) {
|
|
73
|
+
const base = normalizeRoutePath(lang?.routePath || lang?.href)
|
|
74
|
+
if (!base) continue
|
|
75
|
+
if (base === '/') {
|
|
76
|
+
rootLanguage = lang
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (normalizedRoute === base || normalizedRoute.startsWith(`${base}/`)) {
|
|
80
|
+
if (base.length > bestLength) {
|
|
81
|
+
best = lang
|
|
82
|
+
bestLength = base.length
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return best || rootLanguage
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const routePathFromFile = (filePath, pagesDir = state.PAGES_DIR) => {
|
|
90
|
+
if (!filePath.endsWith('.mdx') && !filePath.endsWith('.md')) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
const relPath = relative(pagesDir, filePath)
|
|
94
|
+
if (relPath.startsWith('..')) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
const name = relPath.replace(/\.(mdx|md)$/, '')
|
|
98
|
+
const baseName = name.split(/[\\/]/).pop()
|
|
99
|
+
if (isInternalPage(baseName)) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
const normalized = name.replace(/\\/g, '/')
|
|
103
|
+
if (normalized === 'index') {
|
|
104
|
+
return '/'
|
|
105
|
+
}
|
|
106
|
+
if (normalized.endsWith('/index')) {
|
|
107
|
+
return `/${normalized.slice(0, -'/index'.length)}`
|
|
108
|
+
}
|
|
109
|
+
return `/${normalized}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const parseFrontmatter = (raw) => {
|
|
113
|
+
const parsed = matter(raw)
|
|
114
|
+
const data = parsed.data || {}
|
|
115
|
+
const content = parsed.content ?? ''
|
|
116
|
+
return {
|
|
117
|
+
data,
|
|
118
|
+
content,
|
|
119
|
+
matter: parsed.matter ?? null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parsePageMetadata = async (filePath) => {
|
|
124
|
+
const raw = await readFile(filePath, 'utf-8')
|
|
125
|
+
const { data: frontmatter, content, matter } = parseFrontmatter(raw)
|
|
126
|
+
let title = frontmatter.title
|
|
127
|
+
return {
|
|
128
|
+
raw,
|
|
129
|
+
content,
|
|
130
|
+
frontmatter,
|
|
131
|
+
matter,
|
|
132
|
+
title
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parseWeight = (value) => {
|
|
137
|
+
if (value == null || value === '') return null
|
|
138
|
+
const parsed = Number(value)
|
|
139
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const parseDate = (value) => {
|
|
143
|
+
if (!value) return null
|
|
144
|
+
const date = new Date(value)
|
|
145
|
+
return Number.isNaN(date.valueOf()) ? null : date.toISOString()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const stripRootPrefix = (value, rootDir) => {
|
|
149
|
+
if (!rootDir || !value) return value
|
|
150
|
+
if (value === rootDir) return ''
|
|
151
|
+
if (value.startsWith(`${rootDir}/`)) {
|
|
152
|
+
return value.slice(rootDir.length + 1)
|
|
153
|
+
}
|
|
154
|
+
return value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const buildPagesTree = (pages, options = {}) => {
|
|
158
|
+
const rootPath = normalizeRoutePath(options.rootPath || '/')
|
|
159
|
+
const rootDir = rootPath === '/' ? '' : rootPath.replace(/^\/+/, '')
|
|
160
|
+
const includeHiddenRoot = Boolean(options.includeHiddenRoot)
|
|
161
|
+
const currentRoutePath = normalizeRoutePath(options.currentRoutePath || '/')
|
|
162
|
+
const rootSegments = rootDir ? rootDir.split('/') : []
|
|
163
|
+
const resolveRouteWithinRoot = (routePath) => {
|
|
164
|
+
if (!routePath) return '/'
|
|
165
|
+
if (!rootDir) return routePath
|
|
166
|
+
if (routePath === rootPath) return '/'
|
|
167
|
+
if (routePath.startsWith(`${rootPath}/`)) {
|
|
168
|
+
const stripped = routePath.slice(rootPath.length)
|
|
169
|
+
return stripped.startsWith('/') ? stripped : `/${stripped}`
|
|
170
|
+
}
|
|
171
|
+
return routePath
|
|
172
|
+
}
|
|
173
|
+
const currentRouteWithinRoot = resolveRouteWithinRoot(currentRoutePath)
|
|
174
|
+
const isUnderRoot = (page) => {
|
|
175
|
+
if (!rootDir) return true
|
|
176
|
+
return page.routePath === rootPath || page.routePath.startsWith(`${rootPath}/`)
|
|
177
|
+
}
|
|
178
|
+
const treePages = pages
|
|
179
|
+
.filter((page) => !page.isInternal)
|
|
180
|
+
.filter((page) => isUnderRoot(page))
|
|
181
|
+
.map((page) => {
|
|
182
|
+
if (!rootDir) return page
|
|
183
|
+
const relativePath = stripRootPrefix(page.relativePath, rootDir)
|
|
184
|
+
const dir = stripRootPrefix(page.dir, rootDir)
|
|
185
|
+
const segments = page.segments.slice(rootSegments.length)
|
|
186
|
+
const depth = segments.length
|
|
187
|
+
return {
|
|
188
|
+
...page,
|
|
189
|
+
relativePath,
|
|
190
|
+
dir,
|
|
191
|
+
segments,
|
|
192
|
+
depth
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
const root = []
|
|
196
|
+
const dirs = new Map()
|
|
197
|
+
const hiddenDirs = new Set(
|
|
198
|
+
treePages
|
|
199
|
+
.filter((page) => page.isIndex && page.dir && page.hidden && !(includeHiddenRoot && page.routePath === rootPath))
|
|
200
|
+
.map((page) => page.dir)
|
|
201
|
+
)
|
|
202
|
+
const exposedHiddenDirs = new Set()
|
|
203
|
+
if (currentRoutePath && currentRoutePath !== '/' && hiddenDirs.size) {
|
|
204
|
+
for (const hiddenDir of hiddenDirs) {
|
|
205
|
+
const hiddenRoute = `/${hiddenDir}`
|
|
206
|
+
if (
|
|
207
|
+
currentRouteWithinRoot === hiddenRoute ||
|
|
208
|
+
currentRouteWithinRoot.startsWith(`${hiddenRoute}/`)
|
|
209
|
+
) {
|
|
210
|
+
exposedHiddenDirs.add(hiddenDir)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (includeHiddenRoot && rootDir) {
|
|
215
|
+
for (const hiddenDir of Array.from(hiddenDirs)) {
|
|
216
|
+
if (rootDir === hiddenDir || rootDir.startsWith(`${hiddenDir}/`)) {
|
|
217
|
+
hiddenDirs.delete(hiddenDir)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (exposedHiddenDirs.size) {
|
|
222
|
+
for (const hiddenDir of exposedHiddenDirs) {
|
|
223
|
+
hiddenDirs.delete(hiddenDir)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const isUnderHiddenDir = (dir) => {
|
|
227
|
+
if (!dir) return false
|
|
228
|
+
const parts = dir.split('/')
|
|
229
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
230
|
+
const candidate = parts.slice(0, i).join('/')
|
|
231
|
+
if (hiddenDirs.has(candidate)) {
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
const getDirNode = (path, name, depth) => {
|
|
238
|
+
if (dirs.has(path)) return dirs.get(path)
|
|
239
|
+
const dir = {
|
|
240
|
+
type: 'directory',
|
|
241
|
+
name,
|
|
242
|
+
path: `/${path}`,
|
|
243
|
+
children: [],
|
|
244
|
+
depth,
|
|
245
|
+
routePath: null,
|
|
246
|
+
title: null,
|
|
247
|
+
weight: null,
|
|
248
|
+
date: null,
|
|
249
|
+
routeHref: null,
|
|
250
|
+
isRoot: false
|
|
251
|
+
}
|
|
252
|
+
dirs.set(path, dir)
|
|
253
|
+
return dir
|
|
254
|
+
}
|
|
255
|
+
const isUnderExposedHiddenDir = (dir) => {
|
|
256
|
+
if (!dir || !exposedHiddenDirs.size) return false
|
|
257
|
+
for (const hiddenDir of exposedHiddenDirs) {
|
|
258
|
+
if (dir === hiddenDir || dir.startsWith(`${hiddenDir}/`)) {
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
for (const page of treePages) {
|
|
265
|
+
if (page.hidden && !(includeHiddenRoot && page.routePath === rootPath)) {
|
|
266
|
+
const isHidden404 = page.routePath === '/404'
|
|
267
|
+
const shouldExposeHidden =
|
|
268
|
+
!isHidden404 &&
|
|
269
|
+
page.hiddenByFrontmatter === true &&
|
|
270
|
+
(page.routePath === currentRoutePath || isUnderExposedHiddenDir(page.dir))
|
|
271
|
+
if (!shouldExposeHidden) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (isUnderHiddenDir(page.dir)) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
const parts = page.relativePath.split('/')
|
|
279
|
+
parts.pop()
|
|
280
|
+
let cursor = root
|
|
281
|
+
let currentPath = ''
|
|
282
|
+
for (const part of parts) {
|
|
283
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part
|
|
284
|
+
if (hiddenDirs.has(currentPath)) {
|
|
285
|
+
cursor = null
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
const dir = getDirNode(currentPath, part, currentPath.split('/').length)
|
|
289
|
+
if (!cursor.includes(dir)) {
|
|
290
|
+
cursor.push(dir)
|
|
291
|
+
}
|
|
292
|
+
cursor = dir.children
|
|
293
|
+
}
|
|
294
|
+
if (!cursor) {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
if (page.isIndex && page.dir) {
|
|
298
|
+
const dir = getDirNode(page.dir, page.dir.split('/').pop(), page.depth)
|
|
299
|
+
dir.routePath = page.routePath
|
|
300
|
+
dir.routeHref = page.routeHref || page.routePath
|
|
301
|
+
dir.title = page.title
|
|
302
|
+
dir.weight = page.weight ?? null
|
|
303
|
+
dir.date = page.date ?? null
|
|
304
|
+
dir.isRoot = page.isRoot || false
|
|
305
|
+
dir.page = page
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
cursor.push({
|
|
309
|
+
type: 'page',
|
|
310
|
+
...page
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
const compareNodes = (a, b, level = 0) => {
|
|
314
|
+
if (a?.routePath === '/' && b?.routePath !== '/') return -1
|
|
315
|
+
if (b?.routePath === '/' && a?.routePath !== '/') return 1
|
|
316
|
+
if (a?.isIndex && !b?.isIndex) return -1
|
|
317
|
+
if (b?.isIndex && !a?.isIndex) return 1
|
|
318
|
+
if (level === 0 && a?.type !== b?.type) {
|
|
319
|
+
return a.type === 'page' ? -1 : 1
|
|
320
|
+
}
|
|
321
|
+
const weightA = Number.isFinite(a.weight) ? a.weight : null
|
|
322
|
+
const weightB = Number.isFinite(b.weight) ? b.weight : null
|
|
323
|
+
if (weightA != null || weightB != null) {
|
|
324
|
+
if (weightA == null) return 1
|
|
325
|
+
if (weightB == null) return -1
|
|
326
|
+
if (weightA !== weightB) return weightA - weightB
|
|
327
|
+
}
|
|
328
|
+
const dateA = a.date ? new Date(a.date).valueOf() : null
|
|
329
|
+
const dateB = b.date ? new Date(b.date).valueOf() : null
|
|
330
|
+
if (dateA != null || dateB != null) {
|
|
331
|
+
if (dateA == null) return 1
|
|
332
|
+
if (dateB == null) return -1
|
|
333
|
+
if (dateA !== dateB) return dateB - dateA
|
|
334
|
+
}
|
|
335
|
+
const labelA = (a.title || a.name || '').toLowerCase()
|
|
336
|
+
const labelB = (b.title || b.name || '').toLowerCase()
|
|
337
|
+
if (labelA < labelB) return -1
|
|
338
|
+
if (labelA > labelB) return 1
|
|
339
|
+
return 0
|
|
340
|
+
}
|
|
341
|
+
const sortTree = (nodes, level = 0) => {
|
|
342
|
+
nodes.sort((a, b) => compareNodes(a, b, level))
|
|
343
|
+
for (const node of nodes) {
|
|
344
|
+
if (node.type === 'directory') {
|
|
345
|
+
sortTree(node.children, level + 1)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
sortTree(root)
|
|
350
|
+
return root
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const walkPages = async function* (dir, basePath = '') {
|
|
354
|
+
const entries = await readdir(dir)
|
|
355
|
+
const files = []
|
|
356
|
+
const dirs = []
|
|
357
|
+
|
|
358
|
+
for (const entry of entries.sort()) {
|
|
359
|
+
if (isIgnoredEntry(entry)) {
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
const fullPath = join(dir, entry)
|
|
363
|
+
const stats = await stat(fullPath)
|
|
364
|
+
if (stats.isDirectory()) {
|
|
365
|
+
dirs.push({ entry, fullPath })
|
|
366
|
+
} else if (isPageFile(entry)) {
|
|
367
|
+
files.push({ entry, fullPath })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const { entry, fullPath } of files) {
|
|
372
|
+
const name = entry.replace(/\.(mdx|md)$/, '')
|
|
373
|
+
const relativePath = join(basePath, name).replace(/\\/g, '/')
|
|
374
|
+
const isIndex = name === 'index'
|
|
375
|
+
const routePath = isIndex ? (basePath ? `/${basePath}` : '/') : `/${relativePath}`
|
|
376
|
+
const routeHref = isIndex && basePath ? `/${basePath}/` : routePath
|
|
377
|
+
yield { routePath, routeHref, filePath: fullPath, isIndex }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const { entry, fullPath } of dirs) {
|
|
381
|
+
yield* walkPages(fullPath, join(basePath, entry))
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const buildPageEntry = async ({ filePath, pagesDir, source }) => {
|
|
386
|
+
const routePath = routePathFromFile(filePath, pagesDir)
|
|
387
|
+
if (!routePath) return null
|
|
388
|
+
const relPath = relative(pagesDir, filePath).replace(/\\/g, '/')
|
|
389
|
+
const name = relPath.replace(/\.(mdx|md)$/, '')
|
|
390
|
+
const baseName = name.split('/').pop()
|
|
391
|
+
const dir = name.split('/').slice(0, -1).join('/')
|
|
392
|
+
const dirName = dir ? dir.split('/').pop() : ''
|
|
393
|
+
const isIndex = baseName === 'index'
|
|
394
|
+
const routeHref = isIndex && dir ? `/${dir}/` : routePath
|
|
395
|
+
const segments = routePath.split('/').filter(Boolean)
|
|
396
|
+
const stats = await stat(filePath)
|
|
397
|
+
const cached = pageMetadataCache.get(filePath)
|
|
398
|
+
let metadata = null
|
|
399
|
+
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
400
|
+
metadata = cached.metadata
|
|
401
|
+
} else {
|
|
402
|
+
metadata = await parsePageMetadata(filePath)
|
|
403
|
+
pageMetadataCache.set(filePath, { mtimeMs: stats.mtimeMs, metadata })
|
|
404
|
+
}
|
|
405
|
+
const derived = pageDerivedCache.get(filePath)
|
|
406
|
+
const exclude = Boolean(metadata.frontmatter?.exclude)
|
|
407
|
+
const frontmatterHidden = metadata.frontmatter?.hidden
|
|
408
|
+
const hiddenByFrontmatter = frontmatterHidden === true
|
|
409
|
+
const isNotFoundPage = routePath === '/404'
|
|
410
|
+
const hidden = frontmatterHidden === false
|
|
411
|
+
? false
|
|
412
|
+
: frontmatterHidden === true
|
|
413
|
+
? true
|
|
414
|
+
: isNotFoundPage || Boolean(metadata.frontmatter?.isRoot)
|
|
415
|
+
return {
|
|
416
|
+
routePath,
|
|
417
|
+
routeHref,
|
|
418
|
+
filePath,
|
|
419
|
+
source,
|
|
420
|
+
relativePath: relPath,
|
|
421
|
+
name: baseName,
|
|
422
|
+
dir,
|
|
423
|
+
segments,
|
|
424
|
+
depth: segments.length,
|
|
425
|
+
isIndex,
|
|
426
|
+
isInternal: isInternalPage(baseName),
|
|
427
|
+
title: metadata.title || derived?.title || (baseName === 'index' ? (dirName || 'Home') : baseName),
|
|
428
|
+
weight: parseWeight(metadata.frontmatter?.weight),
|
|
429
|
+
date: parseDate(metadata.frontmatter?.date) || parseDate(stats.mtime),
|
|
430
|
+
isRoot: Boolean(metadata.frontmatter?.isRoot),
|
|
431
|
+
hidden,
|
|
432
|
+
hiddenByFrontmatter,
|
|
433
|
+
exclude,
|
|
434
|
+
content: metadata.content,
|
|
435
|
+
frontmatter: metadata.frontmatter,
|
|
436
|
+
toc: derived?.toc || null,
|
|
437
|
+
matter: metadata.matter ?? null,
|
|
438
|
+
stats: {
|
|
439
|
+
size: stats.size,
|
|
440
|
+
createdAt: stats.birthtime?.toISOString?.() || null,
|
|
441
|
+
updatedAt: stats.mtime?.toISOString?.() || null
|
|
442
|
+
},
|
|
443
|
+
createdAt: stats.birthtime?.toISOString?.() || null,
|
|
444
|
+
updatedAt: stats.mtime?.toISOString?.() || null
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const collectPagesFromDir = async (pagesDir, source) => {
|
|
449
|
+
if (!pagesDir || !existsSync(pagesDir)) {
|
|
450
|
+
return []
|
|
451
|
+
}
|
|
452
|
+
const pages = []
|
|
453
|
+
for await (const page of walkPages(pagesDir)) {
|
|
454
|
+
const entry = await buildPageEntry({
|
|
455
|
+
filePath: page.filePath,
|
|
456
|
+
pagesDir,
|
|
457
|
+
source
|
|
458
|
+
})
|
|
459
|
+
if (entry) {
|
|
460
|
+
entry.routeHref = page.routeHref || entry.routeHref
|
|
461
|
+
entry.isIndex = page.isIndex || entry.isIndex
|
|
462
|
+
pages.push(entry)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return pages
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const collectPages = async () => {
|
|
469
|
+
const userPages = await collectPagesFromDir(state.PAGES_DIR, 'user')
|
|
470
|
+
const themePages = state.THEME_PAGES_DIR
|
|
471
|
+
? await collectPagesFromDir(state.THEME_PAGES_DIR, 'theme')
|
|
472
|
+
: []
|
|
473
|
+
const userRoutes = new Set(userPages.map((page) => page.routePath))
|
|
474
|
+
const pages = [...userPages, ...themePages.filter((page) => !userRoutes.has(page.routePath))]
|
|
475
|
+
const excludedDirs = new Set(pages.filter((page) => page.exclude && page.isIndex && page.dir).map((page) => page.dir))
|
|
476
|
+
const isUnderExcludedDir = (dir) => {
|
|
477
|
+
if (!dir) return false
|
|
478
|
+
const parts = dir.split('/')
|
|
479
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
480
|
+
const candidate = parts.slice(0, i).join('/')
|
|
481
|
+
if (excludedDirs.has(candidate)) {
|
|
482
|
+
return true
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return false
|
|
486
|
+
}
|
|
487
|
+
const excludedRoutes = new Set(pages.filter((page) => page.exclude).map((page) => page.routePath))
|
|
488
|
+
const filteredPages = pages.filter((page) => {
|
|
489
|
+
if (page.exclude) return false
|
|
490
|
+
if (isUnderExcludedDir(page.dir)) return false
|
|
491
|
+
return true
|
|
492
|
+
})
|
|
493
|
+
return { pages: filteredPages, excludedRoutes, excludedDirs }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const buildIndexFallback = (pages, siteName) => {
|
|
497
|
+
const visiblePages = pages
|
|
498
|
+
.filter((page) => !page.isInternal && page.routePath !== '/')
|
|
499
|
+
.sort((a, b) => a.routePath.localeCompare(b.routePath))
|
|
500
|
+
|
|
501
|
+
const lines = [
|
|
502
|
+
`# ${siteName || 'Methanol Site'}`,
|
|
503
|
+
'',
|
|
504
|
+
'No `index.md` or `index.mdx` found in your pages directory.',
|
|
505
|
+
'',
|
|
506
|
+
'## Pages'
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
if (!visiblePages.length) {
|
|
510
|
+
lines.push('', 'No pages found yet.')
|
|
511
|
+
return lines.join('\n')
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
lines.push('')
|
|
515
|
+
for (const page of visiblePages) {
|
|
516
|
+
const label = page.title || page.routePath
|
|
517
|
+
lines.push(`- [${label}](${encodeURI(page.routePath)})`)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return lines.join('\n')
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const resolveRootPath = (routePath, pagesByRoute, pagesByRouteIndex = null) => {
|
|
524
|
+
const normalized = normalizeRoutePath(routePath || '/')
|
|
525
|
+
const segments = normalized.split('/').filter(Boolean)
|
|
526
|
+
const lookup = pagesByRouteIndex || pagesByRoute
|
|
527
|
+
for (let i = segments.length; i >= 1; i--) {
|
|
528
|
+
const candidate = `/${segments.slice(0, i).join('/')}`
|
|
529
|
+
const page = lookup.get(candidate)
|
|
530
|
+
if (page?.isIndex && page?.isRoot) {
|
|
531
|
+
return candidate
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return '/'
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const buildNavSequence = (nodes, pagesByRoute) => {
|
|
538
|
+
const result = []
|
|
539
|
+
const seen = new Set()
|
|
540
|
+
const addEntry = (entry) => {
|
|
541
|
+
if (!entry?.routePath) return
|
|
542
|
+
const key = entry.filePath || entry.routePath
|
|
543
|
+
if (seen.has(key)) return
|
|
544
|
+
seen.add(key)
|
|
545
|
+
result.push(entry)
|
|
546
|
+
}
|
|
547
|
+
const walk = (items = []) => {
|
|
548
|
+
for (const node of items) {
|
|
549
|
+
if (node.type === 'directory') {
|
|
550
|
+
if (node.routePath) {
|
|
551
|
+
const page = pagesByRoute.get(node.routePath) || node.page || null
|
|
552
|
+
if (page) addEntry(page)
|
|
553
|
+
}
|
|
554
|
+
if (node.children?.length) {
|
|
555
|
+
walk(node.children)
|
|
556
|
+
}
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
if (node.type === 'page') {
|
|
560
|
+
const page = pagesByRoute.get(node.routePath) || node
|
|
561
|
+
addEntry(page)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
walk(nodes)
|
|
566
|
+
return result
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export const buildPagesContext = async ({ compileAll = true } = {}) => {
|
|
570
|
+
const logEnabled = state.CURRENT_MODE === 'production' && cli.command === 'build'
|
|
571
|
+
const stageLogger = createStageLogger(logEnabled)
|
|
572
|
+
const collectToken = stageLogger.start('Collecting pages')
|
|
573
|
+
const collected = await collectPages()
|
|
574
|
+
stageLogger.end(collectToken)
|
|
575
|
+
let pages = collected.pages
|
|
576
|
+
const excludedRoutes = collected.excludedRoutes
|
|
577
|
+
const excludedDirs = collected.excludedDirs
|
|
578
|
+
const hasIndex = pages.some((page) => page.routePath === '/')
|
|
579
|
+
if (!hasIndex) {
|
|
580
|
+
const content = buildIndexFallback(pages, state.SITE_NAME)
|
|
581
|
+
pages = [
|
|
582
|
+
{
|
|
583
|
+
routePath: '/',
|
|
584
|
+
routeHref: '/',
|
|
585
|
+
filePath: resolve(state.PAGES_DIR, 'index.md'),
|
|
586
|
+
relativePath: 'index.md',
|
|
587
|
+
name: 'index',
|
|
588
|
+
dir: '',
|
|
589
|
+
segments: [],
|
|
590
|
+
depth: 0,
|
|
591
|
+
isIndex: true,
|
|
592
|
+
isInternal: false,
|
|
593
|
+
title: state.SITE_NAME || 'Methanol Site',
|
|
594
|
+
weight: null,
|
|
595
|
+
date: null,
|
|
596
|
+
isRoot: false,
|
|
597
|
+
hidden: false,
|
|
598
|
+
content,
|
|
599
|
+
frontmatter: {},
|
|
600
|
+
matter: null,
|
|
601
|
+
stats: { size: content.length, createdAt: null, updatedAt: null },
|
|
602
|
+
createdAt: null,
|
|
603
|
+
updatedAt: null
|
|
604
|
+
},
|
|
605
|
+
...pages
|
|
606
|
+
]
|
|
607
|
+
if (excludedRoutes?.has('/')) {
|
|
608
|
+
excludedRoutes.delete('/')
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const pagesByRoute = new Map()
|
|
613
|
+
const pagesByRouteIndex = new Map()
|
|
614
|
+
for (const page of pages) {
|
|
615
|
+
if (page.isIndex) {
|
|
616
|
+
pagesByRouteIndex.set(page.routePath, page)
|
|
617
|
+
if (!pagesByRoute.has(page.routePath)) {
|
|
618
|
+
pagesByRoute.set(page.routePath, page)
|
|
619
|
+
}
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
const existing = pagesByRoute.get(page.routePath)
|
|
623
|
+
if (!existing || existing.isIndex) {
|
|
624
|
+
pagesByRoute.set(page.routePath, page)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const getPageByRoute = (routePath, options = {}) => {
|
|
628
|
+
const { filePath, preferIndex } = options || {}
|
|
629
|
+
if (filePath) {
|
|
630
|
+
for (const page of pages) {
|
|
631
|
+
if (page.routePath === routePath && page.filePath === filePath) {
|
|
632
|
+
return page
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (preferIndex === true) {
|
|
637
|
+
return pagesByRouteIndex.get(routePath) || pagesByRoute.get(routePath) || null
|
|
638
|
+
}
|
|
639
|
+
if (preferIndex === false) {
|
|
640
|
+
return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
|
|
641
|
+
}
|
|
642
|
+
return pagesByRoute.get(routePath) || pagesByRouteIndex.get(routePath) || null
|
|
643
|
+
}
|
|
644
|
+
const pagesTreeCache = new Map()
|
|
645
|
+
const navSequenceCache = new Map()
|
|
646
|
+
const getPagesTree = (routePath) => {
|
|
647
|
+
const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
|
|
648
|
+
const normalizedRoute = normalizeRoutePath(routePath || '/')
|
|
649
|
+
const cacheKey = `${rootPath}::${normalizedRoute}`
|
|
650
|
+
if (pagesTreeCache.has(cacheKey)) {
|
|
651
|
+
return pagesTreeCache.get(cacheKey)
|
|
652
|
+
}
|
|
653
|
+
const tree = buildPagesTree(pages, {
|
|
654
|
+
rootPath,
|
|
655
|
+
includeHiddenRoot: rootPath !== '/',
|
|
656
|
+
currentRoutePath: normalizedRoute
|
|
657
|
+
})
|
|
658
|
+
pagesTreeCache.set(cacheKey, tree)
|
|
659
|
+
return tree
|
|
660
|
+
}
|
|
661
|
+
const getNavSequence = (routePath) => {
|
|
662
|
+
const rootPath = resolveRootPath(routePath, pagesByRoute, pagesByRouteIndex)
|
|
663
|
+
const normalizedRoute = normalizeRoutePath(routePath || '/')
|
|
664
|
+
const cacheKey = `${rootPath}::${normalizedRoute}`
|
|
665
|
+
if (navSequenceCache.has(cacheKey)) {
|
|
666
|
+
return navSequenceCache.get(cacheKey)
|
|
667
|
+
}
|
|
668
|
+
const tree = getPagesTree(routePath)
|
|
669
|
+
const sequence = buildNavSequence(tree, pagesByRoute)
|
|
670
|
+
navSequenceCache.set(cacheKey, sequence)
|
|
671
|
+
return sequence
|
|
672
|
+
}
|
|
673
|
+
let pagesTree = getPagesTree('/')
|
|
674
|
+
const notFound = pagesByRoute.get('/404') || null
|
|
675
|
+
const languages = collectLanguagesFromPages(pages)
|
|
676
|
+
const site = {
|
|
677
|
+
name: state.SITE_NAME,
|
|
678
|
+
root: state.ROOT_DIR,
|
|
679
|
+
pagesDir: state.PAGES_DIR,
|
|
680
|
+
componentsDir: state.COMPONENTS_DIR,
|
|
681
|
+
publicDir: state.STATIC_DIR,
|
|
682
|
+
distDir: state.DIST_DIR,
|
|
683
|
+
mode: state.CURRENT_MODE,
|
|
684
|
+
pagefind: {
|
|
685
|
+
enabled: state.PAGEFIND_ENABLED,
|
|
686
|
+
options: state.PAGEFIND_OPTIONS || null,
|
|
687
|
+
build: state.PAGEFIND_BUILD || null
|
|
688
|
+
},
|
|
689
|
+
generatedAt: new Date().toISOString()
|
|
690
|
+
}
|
|
691
|
+
const excludedDirPaths = new Set(Array.from(excludedDirs).map((dir) => `/${dir}`))
|
|
692
|
+
const pagesContext = {
|
|
693
|
+
pages,
|
|
694
|
+
pagesByRoute,
|
|
695
|
+
pagesByRouteIndex,
|
|
696
|
+
getPageByRoute,
|
|
697
|
+
pagesTree,
|
|
698
|
+
getPagesTree,
|
|
699
|
+
derivedTitleCache: pageDerivedCache,
|
|
700
|
+
setDerivedTitle: (filePath, title, toc) => {
|
|
701
|
+
if (!filePath) return
|
|
702
|
+
pageDerivedCache.set(filePath, { title, toc })
|
|
703
|
+
},
|
|
704
|
+
clearDerivedTitle: (filePath) => {
|
|
705
|
+
if (!filePath) return
|
|
706
|
+
pageDerivedCache.delete(filePath)
|
|
707
|
+
},
|
|
708
|
+
refreshPagesTree: () => {
|
|
709
|
+
pagesTreeCache.clear()
|
|
710
|
+
navSequenceCache.clear()
|
|
711
|
+
pagesContext.pagesTree = getPagesTree('/')
|
|
712
|
+
},
|
|
713
|
+
getSiblings: (routePath, filePath = null) => {
|
|
714
|
+
if (!routePath) return { prev: null, next: null }
|
|
715
|
+
const sequence = getNavSequence(routePath)
|
|
716
|
+
if (!sequence.length) return { prev: null, next: null }
|
|
717
|
+
let index = -1
|
|
718
|
+
if (filePath) {
|
|
719
|
+
index = sequence.findIndex((entry) => entry.filePath === filePath)
|
|
720
|
+
}
|
|
721
|
+
if (index < 0) {
|
|
722
|
+
index = sequence.findIndex((entry) => entry.routePath === routePath)
|
|
723
|
+
}
|
|
724
|
+
if (index < 0) return { prev: null, next: null }
|
|
725
|
+
const toNavEntry = (entry) => {
|
|
726
|
+
if (!entry) return null
|
|
727
|
+
return {
|
|
728
|
+
routePath: entry.routePath,
|
|
729
|
+
routeHref: entry.routeHref || entry.routePath,
|
|
730
|
+
title: entry.title || entry.name || entry.routePath,
|
|
731
|
+
filePath: entry.filePath || null
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
prev: toNavEntry(sequence[index - 1] || null),
|
|
736
|
+
next: toNavEntry(sequence[index + 1] || null)
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
refreshLanguages: () => {
|
|
740
|
+
pagesContext.languages = collectLanguagesFromPages(pages)
|
|
741
|
+
pagesContext.getLanguageForRoute = (routePath) =>
|
|
742
|
+
resolveLanguageForRoute(pagesContext.languages, routePath)
|
|
743
|
+
},
|
|
744
|
+
excludedRoutes,
|
|
745
|
+
excludedDirs,
|
|
746
|
+
excludedDirPaths,
|
|
747
|
+
notFound,
|
|
748
|
+
languages,
|
|
749
|
+
getLanguageForRoute: (routePath) => resolveLanguageForRoute(languages, routePath),
|
|
750
|
+
site
|
|
751
|
+
}
|
|
752
|
+
if (compileAll) {
|
|
753
|
+
const compileToken = stageLogger.start('Compiling MDX')
|
|
754
|
+
const totalPages = pages.length
|
|
755
|
+
for (let i = 0; i < pages.length; i++) {
|
|
756
|
+
const page = pages[i]
|
|
757
|
+
if (logEnabled) {
|
|
758
|
+
stageLogger.update(compileToken, `Compiling MDX [${i + 1}/${totalPages}] ${page.filePath}`)
|
|
759
|
+
}
|
|
760
|
+
await compilePageMdx(page, pagesContext, {
|
|
761
|
+
lazyPagesTree: true,
|
|
762
|
+
refreshPagesTree: false
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
stageLogger.end(compileToken)
|
|
766
|
+
pagesTreeCache.clear()
|
|
767
|
+
pagesTree = getPagesTree('/')
|
|
768
|
+
pagesContext.pagesTree = pagesTree
|
|
769
|
+
}
|
|
770
|
+
return pagesContext
|
|
771
|
+
}
|