@life-and-dev/mdsite 0.0.3
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/README.md +228 -0
- package/bin/mdsite.js +3 -0
- package/dist/commands/commands.test.d.ts +1 -0
- package/dist/commands/commands.test.js +302 -0
- package/dist/commands/commands.test.js.map +1 -0
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.js +20 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/prepare.d.ts +1 -0
- package/dist/commands/prepare.js +111 -0
- package/dist/commands/prepare.js.map +1 -0
- package/dist/commands/prepare.test.d.ts +1 -0
- package/dist/commands/prepare.test.js +131 -0
- package/dist/commands/prepare.test.js.map +1 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +62 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/start.d.ts +5 -0
- package/dist/commands/start.js +50 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +26 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/workflows.test.d.ts +1 -0
- package/dist/commands/workflows.test.js +183 -0
- package/dist/commands/workflows.test.js.map +1 -0
- package/dist/config/default-mdsite-config.d.ts +2 -0
- package/dist/config/default-mdsite-config.js +82 -0
- package/dist/config/default-mdsite-config.js.map +1 -0
- package/dist/config/default-mdsite-config.test.d.ts +1 -0
- package/dist/config/default-mdsite-config.test.js +42 -0
- package/dist/config/default-mdsite-config.test.js.map +1 -0
- package/dist/config/mdsite-config.d.ts +42 -0
- package/dist/config/mdsite-config.js +74 -0
- package/dist/config/mdsite-config.js.map +1 -0
- package/dist/config/mdsite-config.test.d.ts +1 -0
- package/dist/config/mdsite-config.test.js +115 -0
- package/dist/config/mdsite-config.test.js.map +1 -0
- package/dist/config/menu.d.ts +3 -0
- package/dist/config/menu.js +58 -0
- package/dist/config/menu.js.map +1 -0
- package/dist/config/menu.test.d.ts +1 -0
- package/dist/config/menu.test.js +53 -0
- package/dist/config/menu.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +133 -0
- package/dist/index.test.js.map +1 -0
- package/dist/process/child-process.d.ts +9 -0
- package/dist/process/child-process.js +142 -0
- package/dist/process/child-process.js.map +1 -0
- package/dist/process/child-process.test.d.ts +1 -0
- package/dist/process/child-process.test.js +131 -0
- package/dist/process/child-process.test.js.map +1 -0
- package/dist/process/runtime-state.d.ts +16 -0
- package/dist/process/runtime-state.js +42 -0
- package/dist/process/runtime-state.js.map +1 -0
- package/dist/process/runtime-state.test.d.ts +1 -0
- package/dist/process/runtime-state.test.js +57 -0
- package/dist/process/runtime-state.test.js.map +1 -0
- package/dist/renderer/mdsite-nuxt.d.ts +22 -0
- package/dist/renderer/mdsite-nuxt.js +194 -0
- package/dist/renderer/mdsite-nuxt.js.map +1 -0
- package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
- package/dist/renderer/mdsite-nuxt.test.js +226 -0
- package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
- package/mdsite-nuxt/app/app.vue +7 -0
- package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
- package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
- package/mdsite-nuxt/app/assets/css/print.css +133 -0
- package/mdsite-nuxt/app/components/AppBar.vue +138 -0
- package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
- package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
- package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
- package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
- package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
- package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
- package/mdsite-nuxt/app/components/TocItem.vue +75 -0
- package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
- package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
- package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
- package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
- package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
- package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
- package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
- package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
- package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
- package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
- package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
- package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
- package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
- package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
- package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
- package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
- package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
- package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
- package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
- package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
- package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
- package/mdsite-nuxt/app/config/themes.ts +17 -0
- package/mdsite-nuxt/app/layouts/default.vue +376 -0
- package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
- package/mdsite-nuxt/app/pages/index.vue +54 -0
- package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
- package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
- package/mdsite-nuxt/app/types/content.d.ts +10 -0
- package/mdsite-nuxt/app/types/table.ts +57 -0
- package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
- package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
- package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
- package/mdsite-nuxt/content.config.ts +18 -0
- package/mdsite-nuxt/example.config.yml +67 -0
- package/mdsite-nuxt/nuxt.config.ts +247 -0
- package/mdsite-nuxt/package-lock.json +15922 -0
- package/mdsite-nuxt/package.json +50 -0
- package/mdsite-nuxt/playwright.config.js +47 -0
- package/mdsite-nuxt/public/robots.txt +2 -0
- package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
- package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
- package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
- package/mdsite-nuxt/scripts/start.test.ts +164 -0
- package/mdsite-nuxt/scripts/start.ts +55 -0
- package/mdsite-nuxt/scripts/sync-content.ts +504 -0
- package/mdsite-nuxt/tsconfig.json +18 -0
- package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
- package/mdsite-nuxt/vitest.config.ts +7 -0
- package/package.json +36 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { parse as parseYaml } from 'yaml'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = path.dirname(__filename)
|
|
10
|
+
|
|
11
|
+
// ----------------------------------------------------------------------------
|
|
12
|
+
// SHARED TYPES & HELPERS
|
|
13
|
+
// ----------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get content domain from environment variable
|
|
17
|
+
*/
|
|
18
|
+
function getContentDomain(): string {
|
|
19
|
+
return process.env.CONTENT || 'cms'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get source directory for content domain
|
|
24
|
+
*/
|
|
25
|
+
function getSourceDir(): string {
|
|
26
|
+
if (process.env.CONTENT_DIR) return process.env.CONTENT_DIR
|
|
27
|
+
return path.resolve(__dirname, '..', '../../md-content', getContentDomain())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get target public directory
|
|
32
|
+
*/
|
|
33
|
+
function getTargetDir(): string {
|
|
34
|
+
return path.resolve(__dirname, '..', 'public')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract H1 title from markdown content
|
|
39
|
+
*/
|
|
40
|
+
function extractH1Title(content: string): string | null {
|
|
41
|
+
const h1Match = content.match(/^#\s+(.+)$/m)
|
|
42
|
+
return h1Match?.[1]?.trim() ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract frontmatter metadata from markdown content
|
|
47
|
+
* Unified to support both navigation (description) and search (keywords)
|
|
48
|
+
*/
|
|
49
|
+
function extractMarkdownMetadata(content: string): {
|
|
50
|
+
title: string | null
|
|
51
|
+
description?: string
|
|
52
|
+
keywords?: string[]
|
|
53
|
+
} {
|
|
54
|
+
const title = extractH1Title(content)
|
|
55
|
+
|
|
56
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
57
|
+
const frontmatter = frontmatterMatch?.[1]
|
|
58
|
+
|
|
59
|
+
if (!frontmatter) {
|
|
60
|
+
return { title }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract description
|
|
64
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m)
|
|
65
|
+
const description = descMatch?.[1]?.trim()
|
|
66
|
+
|
|
67
|
+
// Extract keywords (YAML array format)
|
|
68
|
+
const keywordsMatch = frontmatter.match(/^keywords:\s*\[(.*)\]$/m)
|
|
69
|
+
const keywordsStr = keywordsMatch?.[1]
|
|
70
|
+
let keywords: string[] | undefined
|
|
71
|
+
if (keywordsStr) {
|
|
72
|
+
keywords = keywordsStr
|
|
73
|
+
.split(',')
|
|
74
|
+
.map(k => k.trim().replace(/['"]/g, ''))
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { title, description, keywords }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get markdown file path from relative path
|
|
83
|
+
*/
|
|
84
|
+
function getMarkdownPath(relativePath: string): string {
|
|
85
|
+
const sourceDir = getSourceDir()
|
|
86
|
+
// If it already has an extension, don't append .md
|
|
87
|
+
if (relativePath.endsWith('.md')) {
|
|
88
|
+
return path.join(sourceDir, relativePath)
|
|
89
|
+
}
|
|
90
|
+
return path.join(sourceDir, `${relativePath}.md`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ----------------------------------------------------------------------------
|
|
94
|
+
// NAVIGATION LOGIC
|
|
95
|
+
// ----------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Minimal navigation tree node for client consumption
|
|
99
|
+
*/
|
|
100
|
+
export interface MinimalTreeNode {
|
|
101
|
+
id: string
|
|
102
|
+
title: string
|
|
103
|
+
path: string
|
|
104
|
+
type: 'link' | 'separator' | 'header' | 'external'
|
|
105
|
+
children?: MinimalTreeNode[]
|
|
106
|
+
description?: string
|
|
107
|
+
isPrimary?: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type MenuItemType = string | { [key: string]: string | null | MenuItemType[] } | null
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve relative/absolute paths in menu
|
|
114
|
+
*/
|
|
115
|
+
function resolvePath(menuPath: string, contextPath: string): string {
|
|
116
|
+
// Absolute path
|
|
117
|
+
if (menuPath.startsWith('/')) {
|
|
118
|
+
return menuPath
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Parent directory (..)
|
|
122
|
+
if (menuPath.startsWith('../')) {
|
|
123
|
+
const contextSegments = contextPath.split('/').filter(Boolean)
|
|
124
|
+
contextSegments.pop() // Remove last segment
|
|
125
|
+
const relativePart = menuPath.substring(3) // Remove '../'
|
|
126
|
+
return contextSegments.length > 0
|
|
127
|
+
? `/${contextSegments.join('/')}/${relativePart}`
|
|
128
|
+
: `/${relativePart}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Current directory (./)
|
|
132
|
+
if (menuPath.startsWith('./')) {
|
|
133
|
+
const relativePart = menuPath.substring(2)
|
|
134
|
+
return contextPath === '/' ? `/${relativePart}` : `${contextPath}/${relativePart}`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Relative to context (no prefix)
|
|
138
|
+
return contextPath === '/' ? `/${menuPath}` : `${contextPath}/${menuPath}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Process menu items recursively and build minimal tree structure
|
|
143
|
+
*/
|
|
144
|
+
async function processMenuItems(
|
|
145
|
+
items: MenuItemType[],
|
|
146
|
+
contextPath: string,
|
|
147
|
+
order: number = 0
|
|
148
|
+
): Promise<{ nodes: MinimalTreeNode[], nextOrder: number }> {
|
|
149
|
+
const nodes: MinimalTreeNode[] = []
|
|
150
|
+
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
// Handle null/blank (separator) - legacy support
|
|
153
|
+
if (item === null) {
|
|
154
|
+
nodes.push({
|
|
155
|
+
id: `separator-${order}`,
|
|
156
|
+
title: '---',
|
|
157
|
+
path: `${contextPath}/__separator-${order}`,
|
|
158
|
+
type: 'separator'
|
|
159
|
+
})
|
|
160
|
+
order++
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Handle string items
|
|
165
|
+
if (typeof item === 'string') {
|
|
166
|
+
// Check for separator marker
|
|
167
|
+
if (item === '===') {
|
|
168
|
+
nodes.push({
|
|
169
|
+
id: `separator-${order}`,
|
|
170
|
+
title: '---',
|
|
171
|
+
path: `${contextPath}/__separator-${order}`,
|
|
172
|
+
type: 'separator'
|
|
173
|
+
})
|
|
174
|
+
order++
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// String → lookup H1 and description from markdown file
|
|
179
|
+
const resolvedPath = resolvePath(item, contextPath)
|
|
180
|
+
const markdownPath = getMarkdownPath(resolvedPath)
|
|
181
|
+
|
|
182
|
+
let title: string | null = null
|
|
183
|
+
let description: string | undefined = undefined
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
if (await fs.pathExists(markdownPath)) {
|
|
187
|
+
const content = await fs.readFile(markdownPath, 'utf-8')
|
|
188
|
+
const metadata = extractMarkdownMetadata(content)
|
|
189
|
+
title = metadata.title
|
|
190
|
+
description = metadata.description
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// Ignore missing files
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
nodes.push({
|
|
197
|
+
id: `${resolvedPath.split('/').filter(Boolean).pop() || 'home'}-${order}`,
|
|
198
|
+
title: title || item,
|
|
199
|
+
path: resolvedPath,
|
|
200
|
+
type: 'link',
|
|
201
|
+
description,
|
|
202
|
+
isPrimary: true
|
|
203
|
+
})
|
|
204
|
+
order++
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Handle object (custom title, external link, header, or submenu)
|
|
209
|
+
if (typeof item === 'object') {
|
|
210
|
+
for (const [key, value] of Object.entries(item)) {
|
|
211
|
+
// Handle header marker (value === '===')
|
|
212
|
+
if (value === '===') {
|
|
213
|
+
nodes.push({
|
|
214
|
+
id: `header-${order}`,
|
|
215
|
+
title: key,
|
|
216
|
+
path: `${contextPath}/__header-${order}`,
|
|
217
|
+
type: 'header'
|
|
218
|
+
})
|
|
219
|
+
order++
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle null/empty value (legacy header support)
|
|
224
|
+
if (value === null || value === '') {
|
|
225
|
+
nodes.push({
|
|
226
|
+
id: `header-${order}`,
|
|
227
|
+
title: key,
|
|
228
|
+
path: `${contextPath}/__header-${order}`,
|
|
229
|
+
type: 'header'
|
|
230
|
+
})
|
|
231
|
+
order++
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Handle external URL
|
|
236
|
+
if (typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'))) {
|
|
237
|
+
nodes.push({
|
|
238
|
+
id: `external-${order}`,
|
|
239
|
+
title: key,
|
|
240
|
+
path: value,
|
|
241
|
+
type: 'external'
|
|
242
|
+
})
|
|
243
|
+
order++
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Handle submenu (array value) - key is markdown filename
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
const submenuPath = resolvePath(key, contextPath)
|
|
250
|
+
const markdownPath = getMarkdownPath(submenuPath)
|
|
251
|
+
|
|
252
|
+
let title: string | null = null
|
|
253
|
+
let description: string | undefined = undefined
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
if (await fs.pathExists(markdownPath)) {
|
|
257
|
+
const content = await fs.readFile(markdownPath, 'utf-8')
|
|
258
|
+
const metadata = extractMarkdownMetadata(content)
|
|
259
|
+
title = metadata.title
|
|
260
|
+
description = metadata.description
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// Ignore missing files
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Process children recursively
|
|
267
|
+
const { nodes: childNodes } = await processMenuItems(value, submenuPath, 0)
|
|
268
|
+
|
|
269
|
+
nodes.push({
|
|
270
|
+
id: `${submenuPath.split('/').filter(Boolean).pop() || 'home'}-${order}`,
|
|
271
|
+
title: title || key,
|
|
272
|
+
path: submenuPath,
|
|
273
|
+
type: 'link',
|
|
274
|
+
description,
|
|
275
|
+
isPrimary: true,
|
|
276
|
+
children: childNodes
|
|
277
|
+
})
|
|
278
|
+
order++
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle custom title with path (alias link)
|
|
283
|
+
if (typeof value === 'string') {
|
|
284
|
+
const resolvedPath = resolvePath(value, contextPath)
|
|
285
|
+
const markdownPath = getMarkdownPath(resolvedPath)
|
|
286
|
+
let description: string | undefined = undefined
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
if (await fs.pathExists(markdownPath)) {
|
|
290
|
+
const content = await fs.readFile(markdownPath, 'utf-8')
|
|
291
|
+
const metadata = extractMarkdownMetadata(content)
|
|
292
|
+
description = metadata.description
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
// Ignore missing files
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
nodes.push({
|
|
299
|
+
id: `link-${key.replace(/\s+/g, '-').toLowerCase()}-${order}`,
|
|
300
|
+
title: key,
|
|
301
|
+
path: resolvedPath,
|
|
302
|
+
type: 'link',
|
|
303
|
+
description,
|
|
304
|
+
isPrimary: false // Aliases are NOT primary
|
|
305
|
+
})
|
|
306
|
+
order++
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { nodes, nextOrder: order }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Count total nodes in tree
|
|
317
|
+
*/
|
|
318
|
+
function countNodes(nodes: MinimalTreeNode[]): number {
|
|
319
|
+
let count = nodes.length
|
|
320
|
+
for (const node of nodes) {
|
|
321
|
+
if (node.children) {
|
|
322
|
+
count += countNodes(node.children)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return count
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Generate navigation JSON file
|
|
330
|
+
*/
|
|
331
|
+
export async function generateNavigationJson() {
|
|
332
|
+
const domain = getContentDomain()
|
|
333
|
+
console.log(`📋 Building navigation tree for: ${domain}`)
|
|
334
|
+
|
|
335
|
+
const sourceDir = getSourceDir()
|
|
336
|
+
let menuPath = path.join(sourceDir, '_menu.yml')
|
|
337
|
+
|
|
338
|
+
if (!await fs.pathExists(menuPath)) {
|
|
339
|
+
menuPath = path.join(sourceDir, '_menu.yaml')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let tree: MinimalTreeNode[] = []
|
|
343
|
+
try {
|
|
344
|
+
if (await fs.pathExists(menuPath)) {
|
|
345
|
+
const menuContent = await fs.readFile(menuPath, 'utf-8')
|
|
346
|
+
const menuItems = parseYaml(menuContent) as MenuItemType[]
|
|
347
|
+
const result = await processMenuItems(menuItems, '/')
|
|
348
|
+
tree = result.nodes
|
|
349
|
+
} else {
|
|
350
|
+
console.warn('⚠️ No _menu.yml or _menu.yaml found at:', menuPath)
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
console.error('Error building navigation tree:', error)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const targetDir = getTargetDir()
|
|
357
|
+
const outputPath = path.join(targetDir, '_navigation.json')
|
|
358
|
+
|
|
359
|
+
await fs.ensureDir(targetDir)
|
|
360
|
+
await fs.writeJson(outputPath, tree, { spaces: 2 })
|
|
361
|
+
|
|
362
|
+
const fileSize = (await fs.stat(outputPath)).size
|
|
363
|
+
const fileSizeKB = (fileSize / 1024).toFixed(2)
|
|
364
|
+
|
|
365
|
+
console.log(`✓ Navigation tree generated: ${outputPath} (${fileSizeKB} KB)`)
|
|
366
|
+
console.log(`✓ Total menu items: ${countNodes(tree)}\n`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
// ----------------------------------------------------------------------------
|
|
371
|
+
// SEARCH INDEX LOGIC
|
|
372
|
+
// ----------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
export interface SearchIndexEntry {
|
|
375
|
+
path: string
|
|
376
|
+
title: string
|
|
377
|
+
description?: string
|
|
378
|
+
keywords?: string[]
|
|
379
|
+
excerpt?: string
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Extract excerpt from markdown content (first 150 chars after frontmatter and H1)
|
|
384
|
+
*/
|
|
385
|
+
function extractExcerpt(content: string): string | undefined {
|
|
386
|
+
// Remove frontmatter
|
|
387
|
+
let cleanContent = content.replace(/^---\n[\s\S]*?\n---\n/, '')
|
|
388
|
+
|
|
389
|
+
// Remove H1
|
|
390
|
+
cleanContent = cleanContent.replace(/^#\s+.+$/m, '')
|
|
391
|
+
|
|
392
|
+
// Get first paragraph, trim whitespace
|
|
393
|
+
const firstParagraph = cleanContent.trim().split('\n\n')[0]
|
|
394
|
+
if (!firstParagraph) return undefined
|
|
395
|
+
|
|
396
|
+
// Trim to 150 chars
|
|
397
|
+
const excerpt = firstParagraph.trim().substring(0, 150)
|
|
398
|
+
return excerpt.length > 0 ? excerpt : undefined
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Convert file path to URL path
|
|
403
|
+
*/
|
|
404
|
+
function filePathToUrlPath(filePath: string, sourceDir: string): string {
|
|
405
|
+
const relativePath = path.relative(sourceDir, filePath)
|
|
406
|
+
const urlPath = '/' + relativePath
|
|
407
|
+
.replace(/\.md$/, '')
|
|
408
|
+
.replace(/index$/, '')
|
|
409
|
+
.replace(/\\/g, '/')
|
|
410
|
+
.replace(/\/+$/, '')
|
|
411
|
+
|
|
412
|
+
return urlPath === '' ? '/' : urlPath
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get all markdown files recursively
|
|
417
|
+
*/
|
|
418
|
+
async function getAllMarkdownFiles(dir: string): Promise<string[]> {
|
|
419
|
+
const files: string[] = []
|
|
420
|
+
|
|
421
|
+
if (!await fs.pathExists(dir)) {
|
|
422
|
+
return files
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const items = await fs.readdir(dir)
|
|
426
|
+
|
|
427
|
+
for (const item of items) {
|
|
428
|
+
const itemPath = path.join(dir, item)
|
|
429
|
+
const stat = await fs.stat(itemPath)
|
|
430
|
+
|
|
431
|
+
if (stat.isDirectory()) {
|
|
432
|
+
const subFiles = await getAllMarkdownFiles(itemPath)
|
|
433
|
+
files.push(...subFiles)
|
|
434
|
+
} else if (item.endsWith('.md') && !item.endsWith('.draft.md')) {
|
|
435
|
+
// Only include published markdown files
|
|
436
|
+
files.push(itemPath)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return files
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Generate search index JSON file
|
|
445
|
+
*/
|
|
446
|
+
export async function generateSearchIndexJson() {
|
|
447
|
+
const domain = getContentDomain()
|
|
448
|
+
console.log(`🔍 Building search index for: ${domain}`)
|
|
449
|
+
|
|
450
|
+
const sourceDir = getSourceDir()
|
|
451
|
+
const markdownFiles = await getAllMarkdownFiles(sourceDir)
|
|
452
|
+
|
|
453
|
+
const index: SearchIndexEntry[] = []
|
|
454
|
+
|
|
455
|
+
for (const filePath of markdownFiles) {
|
|
456
|
+
try {
|
|
457
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
458
|
+
const metadata = extractMarkdownMetadata(content)
|
|
459
|
+
const excerpt = extractExcerpt(content)
|
|
460
|
+
const urlPath = filePathToUrlPath(filePath, sourceDir)
|
|
461
|
+
|
|
462
|
+
// Skip files without H1 (malformed)
|
|
463
|
+
if (!metadata.title) {
|
|
464
|
+
console.warn(`⚠️ No H1 found in: ${path.relative(sourceDir, filePath)}`)
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
index.push({
|
|
469
|
+
path: urlPath,
|
|
470
|
+
title: metadata.title,
|
|
471
|
+
description: metadata.description,
|
|
472
|
+
keywords: metadata.keywords,
|
|
473
|
+
excerpt
|
|
474
|
+
})
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error(`❌ Failed to process ${filePath}:`, error)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const targetDir = getTargetDir()
|
|
481
|
+
const outputPath = path.join(targetDir, '_search-index.json')
|
|
482
|
+
|
|
483
|
+
await fs.ensureDir(targetDir)
|
|
484
|
+
await fs.writeJson(outputPath, index, { spaces: 2 })
|
|
485
|
+
|
|
486
|
+
const fileSize = (await fs.stat(outputPath)).size
|
|
487
|
+
const fileSizeKB = (fileSize / 1024).toFixed(2)
|
|
488
|
+
|
|
489
|
+
console.log(`✓ Search index generated: ${outputPath} (${fileSizeKB} KB)`)
|
|
490
|
+
console.log(`✓ Total indexed pages: ${index.length}\n`)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ----------------------------------------------------------------------------
|
|
494
|
+
// MAIN EXECUTION
|
|
495
|
+
// ----------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
export async function buildContentData() {
|
|
498
|
+
await generateNavigationJson()
|
|
499
|
+
await generateSearchIndexJson()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Run if called directly
|
|
503
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
504
|
+
buildContentData().catch((error) => {
|
|
505
|
+
console.error(error)
|
|
506
|
+
process.exit(1)
|
|
507
|
+
})
|
|
508
|
+
}
|