cocowiki 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mueo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # CoCoWiki
2
+
3
+ CoCoWiki 是一个基于 VitePress 深度封装的、搜索优先的轻量静态 Wiki 框架,支持 Markdown、Vue 专属页面、自定义 Markdown 语法、构建期搜索和纯静态部署。
4
+
5
+ ```bash
6
+ pnpm create cocowiki my-wiki
7
+ cd my-wiki
8
+ pnpm install
9
+ pnpm dev
10
+ ```
11
+
12
+ 项目主页与完整说明位于 [CoCoWiki 源码仓库](https://github.com/coconi-dev/cocowiki)。
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ import fg from 'fast-glob'
3
+ import { existsSync } from 'node:fs'
4
+ import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
5
+ import { basename, dirname, relative, resolve, sep } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { loadConfigFromFile } from 'vite'
8
+ import { build, createServer, serve } from 'vitepress'
9
+
10
+ const VERSION = JSON.parse(await readFile(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version
11
+ const COLOR = { reset: '\x1b[0m', dim: '\x1b[2m', cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', violet: '\x1b[38;5;141m' }
12
+ const supportsColor = Boolean(process.stdout.isTTY && !process.env.NO_COLOR)
13
+ const paint = (color, text) => supportsColor ? `${color}${text}${COLOR.reset}` : text
14
+ const brand = paint(COLOR.violet, '[cocowiki]')
15
+ const command = process.argv[2] || 'dev'
16
+ const projectRoot = process.cwd()
17
+ const configFile = resolve(projectRoot, 'cocowiki.config.ts')
18
+ const THEME_COMPONENT_NAMES = new Set([
19
+ 'Header', 'Home', 'Search', 'Archive', 'Contributors', 'PageMeta',
20
+ 'PageOutline', 'ContentSidebar', 'PageNavigation', 'PageFooter',
21
+ 'SearchOverlay', 'Loading', 'NotFound'
22
+ ])
23
+
24
+ const log = {
25
+ info(message) { console.log(`${brand} ${paint(COLOR.cyan, message)}`) },
26
+ success(message) { console.log(`${brand} ${paint(COLOR.green, `✓ ${message}`)}`) },
27
+ warn(message) { console.warn(`${brand} ${paint(COLOR.yellow, `⚠ ${message}`)}`) },
28
+ error(message) { console.error(`${brand} ${paint(COLOR.red, `✗ ${message}`)}`) }
29
+ }
30
+
31
+ function fail(message) {
32
+ log.error(message)
33
+ process.exit(1)
34
+ }
35
+
36
+ function pathForImport(fromDirectory, target) {
37
+ const path = relative(fromDirectory, target).split(sep).join('/')
38
+ return path.startsWith('.') ? path : `./${path}`
39
+ }
40
+
41
+ function isInside(parent, filename) {
42
+ const path = relative(parent, filename)
43
+ return path !== '' && !path.startsWith('..') && !path.startsWith(`..${sep}`)
44
+ }
45
+
46
+ function componentName(filename) {
47
+ const stem = basename(filename, '.vue')
48
+ return stem.split(/[-_.\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
49
+ }
50
+
51
+ async function writeIfChanged(filename, content) {
52
+ try {
53
+ if (await readFile(filename, 'utf8') === content) return false
54
+ } catch {}
55
+ await mkdir(dirname(filename), { recursive: true })
56
+ await writeFile(filename, content)
57
+ return true
58
+ }
59
+
60
+ function parseOptions(args) {
61
+ const options = {}
62
+ for (let index = 0; index < args.length; index += 1) {
63
+ const argument = args[index]
64
+ if (argument === '--host') {
65
+ const value = args[index + 1]
66
+ options.host = value && !value.startsWith('-') ? (index += 1, value) : true
67
+ }
68
+ if (argument === '--port') options.port = Number(args[index += 1])
69
+ }
70
+ return options
71
+ }
72
+
73
+ function previewUrl(serverUrl, requestedBase = '/') {
74
+ const url = new URL(serverUrl)
75
+ const path = requestedBase.replace(/^\/+|\/+$/g, '')
76
+ url.pathname = path ? `/${path}/` : '/'
77
+ return url.href
78
+ }
79
+
80
+ async function readProjectConfig() {
81
+ if (!existsSync(configFile)) fail('未找到 cocowiki.config.ts。')
82
+ const loaded = await loadConfigFromFile({ command: command === 'build' ? 'build' : 'serve', mode: command === 'build' ? 'production' : 'development' }, configFile, projectRoot)
83
+ if (!loaded?.config?.title) fail('配置文件必须提供 title。')
84
+ return loaded.config
85
+ }
86
+
87
+ function resolvePaths(config) {
88
+ const directories = config.directories || {}
89
+ return {
90
+ runtimeRoot: resolve(projectRoot, '.cocowiki/site'),
91
+ contentDir: resolve(projectRoot, directories.content || 'content'),
92
+ componentsDir: resolve(projectRoot, directories.components || 'components')
93
+ }
94
+ }
95
+
96
+ async function writeVuePage(source, routeFile) {
97
+ const metadataFile = source.replace(/\.page\.vue$/, '.page.json')
98
+ const metadata = existsSync(metadataFile) ? JSON.parse(await readFile(metadataFile, 'utf8')) : {}
99
+ const title = metadata.title || basename(source, '.page.vue')
100
+ const componentImport = pathForImport(dirname(routeFile), source)
101
+ const frontmatter = { ...metadata, layout: 'special', title }
102
+ const frontmatterLines = Object.entries(frontmatter).map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
103
+ await writeIfChanged(routeFile, `---\n${frontmatterLines.join('\n')}\n---\n\n<script setup lang="ts">\nimport CocoPage from ${JSON.stringify(componentImport)}\n</script>\n\n<CocoPage />\n`)
104
+ }
105
+
106
+ async function syncContentFile(paths, changedFile) {
107
+ const path = relative(paths.contentDir, changedFile)
108
+ if (!path || path.startsWith('..')) return
109
+
110
+ if (path.endsWith('.page.json')) {
111
+ const source = changedFile.replace(/\.page\.json$/, '.page.vue')
112
+ const routeFile = resolve(paths.runtimeRoot, path.replace(/\.page\.json$/, '.md'))
113
+ if (existsSync(source)) await writeVuePage(source, routeFile)
114
+ return
115
+ }
116
+
117
+ if (path.endsWith('.page.vue')) {
118
+ const routeFile = resolve(paths.runtimeRoot, path.replace(/\.page\.vue$/, '.md'))
119
+ if (existsSync(changedFile)) await writeVuePage(changedFile, routeFile)
120
+ else await rm(routeFile, { force: true })
121
+ return
122
+ }
123
+
124
+ const destination = resolve(paths.runtimeRoot, path)
125
+ if (existsSync(changedFile)) {
126
+ await mkdir(dirname(destination), { recursive: true })
127
+ await copyFile(changedFile, destination)
128
+ } else {
129
+ await rm(destination, { force: true })
130
+ }
131
+ }
132
+
133
+ async function prepareRuntime(config, { clean = true, syncContent = true } = {}) {
134
+ const paths = resolvePaths(config)
135
+ if (!existsSync(paths.contentDir)) fail(`内容目录不存在:${paths.contentDir}`)
136
+
137
+ if (clean) await rm(paths.runtimeRoot, { recursive: true, force: true })
138
+ const configDirectory = resolve(paths.runtimeRoot, '.vitepress')
139
+ const themeDirectory = resolve(configDirectory, 'theme')
140
+ await mkdir(themeDirectory, { recursive: true })
141
+
142
+ if (syncContent) {
143
+ const files = await fg('**/*', { cwd: paths.contentDir, onlyFiles: true, dot: true, ignore: ['**/*.page.json'] })
144
+ for (const file of files) {
145
+ const source = resolve(paths.contentDir, file)
146
+ if (file.endsWith('.page.vue')) await writeVuePage(source, resolve(paths.runtimeRoot, file.replace(/\.page\.vue$/, '.md')))
147
+ else {
148
+ const destination = resolve(paths.runtimeRoot, file)
149
+ await mkdir(dirname(destination), { recursive: true })
150
+ await copyFile(source, destination)
151
+ }
152
+ }
153
+ }
154
+
155
+ const configImport = pathForImport(configDirectory, configFile)
156
+ await writeIfChanged(resolve(configDirectory, 'config.mts'), `import userConfig from ${JSON.stringify(configImport)}\nimport { createVitePressConfig } from 'cocowiki/runtime'\nexport default createVitePressConfig(userConfig, ${JSON.stringify(projectRoot)})\n`)
157
+
158
+ const selectedTheme = config.theme?.entry
159
+ ? pathForImport(themeDirectory, resolve(projectRoot, config.theme.entry))
160
+ : 'cocowiki/theme'
161
+ const configuredStyles = config.theme?.styles
162
+ const themeStyles = (Array.isArray(configuredStyles) ? configuredStyles : configuredStyles ? [configuredStyles] : [])
163
+ .map((style) => `import ${JSON.stringify(pathForImport(themeDirectory, resolve(projectRoot, style)))}`)
164
+ const componentEntries = Object.entries(config.theme?.components || {})
165
+ const invalidComponents = componentEntries.filter(([name]) => !THEME_COMPONENT_NAMES.has(name))
166
+ if (invalidComponents.length) {
167
+ fail(`未知主题组件:${invalidComponents.map(([name]) => name).join('、')}。`)
168
+ }
169
+ const globalComponentFiles = existsSync(paths.componentsDir)
170
+ ? await fg('**/*.vue', { cwd: paths.componentsDir, onlyFiles: true })
171
+ : []
172
+ const componentNames = new Map()
173
+ for (const file of globalComponentFiles) {
174
+ const name = componentName(file)
175
+ if (!name) fail(`无法从组件文件名生成全局名称:${file}`)
176
+ const duplicate = componentNames.get(name)
177
+ if (duplicate) fail(`全局组件名称冲突:${duplicate} 与 ${file} 都会注册为 ${name}。`)
178
+ componentNames.set(name, file)
179
+ }
180
+ const overrideDeclarations = componentEntries.map(([name, entry], index) => {
181
+ const source = pathForImport(themeDirectory, resolve(projectRoot, entry))
182
+ return `const ThemeComponent${index} = defineAsyncComponent(() => import(${JSON.stringify(source)}))`
183
+ })
184
+ const overrides = componentEntries.map(([name], index) => `${JSON.stringify(name)}: ThemeComponent${index}`).join(', ')
185
+ const componentsPattern = `${pathForImport(themeDirectory, paths.componentsDir)}/**/*.vue`
186
+ await writeIfChanged(resolve(themeDirectory, 'index.ts'), `import { defineAsyncComponent } from 'vue'\nimport baseTheme from ${JSON.stringify(selectedTheme)}\nimport { cocoWikiThemeComponentsKey } from 'cocowiki/theme/customization'\n${themeStyles.join('\n')}\nconst modules = import.meta.glob(${JSON.stringify(componentsPattern)})\n${overrideDeclarations.join('\n')}\nconst themeComponentOverrides = { ${overrides} }\nfunction componentName(path) {\n const stem = path.split('/').pop()?.replace(/\\.vue$/, '') || ''\n return stem.split(/[-_.\\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')\n}\nexport default {\n ...baseTheme,\n enhanceApp(context) {\n baseTheme.enhanceApp?.(context)\n context.app.provide(cocoWikiThemeComponentsKey, themeComponentOverrides)\n for (const [path, loader] of Object.entries(modules)) {\n const name = componentName(path)\n if (name) context.app.component(name, defineAsyncComponent(loader))\n }\n }\n}\n`)
187
+
188
+ return paths.runtimeRoot
189
+ }
190
+
191
+ if (!new Set(['dev', 'build', 'preview']).has(command)) {
192
+ fail(`未知命令“${command}”,可用命令为 dev、build、preview。`)
193
+ }
194
+
195
+ try {
196
+ let config = await readProjectConfig()
197
+ const runtimeRoot = await prepareRuntime(config)
198
+ const options = parseOptions(process.argv.slice(3))
199
+
200
+ if (command === 'build') {
201
+ console.log('')
202
+ log.info(`CoCoWiki v${VERSION} · 正在生成静态站点`)
203
+ await build(runtimeRoot)
204
+ log.success(`构建完成:${resolve(projectRoot, config.directories?.out || 'dist')}`)
205
+ console.log('')
206
+ } else if (command === 'preview') {
207
+ console.log('')
208
+ log.info(`CoCoWiki v${VERSION} · 静态预览`)
209
+ await serve({ root: runtimeRoot, port: options.port, host: options.host })
210
+ } else {
211
+ const server = await createServer(runtimeRoot, options)
212
+ await server.listen()
213
+ let refreshTimer
214
+ let refreshing = false
215
+ let refreshQueued = false
216
+ const changedFiles = new Set()
217
+ let configChanged = false
218
+ let componentsChanged = false
219
+
220
+ const watchProjectPaths = () => {
221
+ const paths = resolvePaths(config)
222
+ server.watcher.add([paths.contentDir, paths.componentsDir, configFile])
223
+ return paths
224
+ }
225
+ let watchedPaths = watchProjectPaths()
226
+
227
+ const refreshRuntime = async () => {
228
+ if (refreshing) {
229
+ refreshQueued = true
230
+ return
231
+ }
232
+ refreshing = true
233
+ try {
234
+ if (configChanged) {
235
+ configChanged = false
236
+ const nextConfig = await readProjectConfig()
237
+ const nextPaths = resolvePaths(nextConfig)
238
+ const directoriesChanged = nextPaths.contentDir !== watchedPaths.contentDir || nextPaths.componentsDir !== watchedPaths.componentsDir
239
+ config = nextConfig
240
+ await prepareRuntime(config, { clean: directoriesChanged, syncContent: directoriesChanged })
241
+ watchedPaths = watchProjectPaths()
242
+ if (directoriesChanged) changedFiles.clear()
243
+ log.info('配置已更新')
244
+ }
245
+ if (componentsChanged) {
246
+ componentsChanged = false
247
+ await prepareRuntime(config, { clean: false, syncContent: false })
248
+ }
249
+ const files = [...changedFiles]
250
+ changedFiles.clear()
251
+ await Promise.all(files.map((file) => syncContentFile(watchedPaths, file)))
252
+ } catch (error) {
253
+ log.error(error instanceof Error ? error.message : String(error))
254
+ } finally {
255
+ refreshing = false
256
+ if (refreshQueued) {
257
+ refreshQueued = false
258
+ void refreshRuntime()
259
+ }
260
+ }
261
+ }
262
+
263
+ server.watcher.on('all', (event, file) => {
264
+ if (file === configFile) configChanged = true
265
+ else if (isInside(watchedPaths.componentsDir, file)) {
266
+ // Existing components are already part of Vite's module graph. Only a
267
+ // changed file list requires regenerating the global registration entry.
268
+ if (event === 'add' || event === 'unlink') componentsChanged = true
269
+ else return
270
+ } else if (isInside(watchedPaths.contentDir, file)) {
271
+ if (event === 'add' || event === 'change' || event === 'unlink') changedFiles.add(file)
272
+ else return
273
+ }
274
+ else return
275
+ clearTimeout(refreshTimer)
276
+ refreshTimer = setTimeout(() => void refreshRuntime(), 80)
277
+ })
278
+ const serverUrl = server.resolvedUrls?.local[0] || `http://localhost:${options.port || 5173}/`
279
+ const url = previewUrl(serverUrl, config.base)
280
+ console.log('')
281
+ log.info(`CoCoWiki v${VERSION}`)
282
+ log.success(`本地预览:${url}`)
283
+ console.log('')
284
+ const close = async () => { await server.close(); process.exit(0) }
285
+ process.on('SIGINT', close)
286
+ process.on('SIGTERM', close)
287
+ }
288
+ } catch (error) {
289
+ console.log('')
290
+ log.error(error instanceof Error ? error.message : String(error))
291
+ console.log('')
292
+ process.exit(1)
293
+ }
package/dist/config.js ADDED
@@ -0,0 +1,7 @@
1
+ // src/config.ts
2
+ function defineCocoWikiConfig(config) {
3
+ return config;
4
+ }
5
+ export {
6
+ defineCocoWikiConfig
7
+ };
@@ -0,0 +1,321 @@
1
+ // src/node/vitepress.ts
2
+ import { existsSync } from "node:fs";
3
+ import { readFile as readFile2 } from "node:fs/promises";
4
+ import { relative as relative2, resolve as resolve2 } from "node:path";
5
+ import { defineConfig } from "vitepress";
6
+ import { createLogger } from "vite";
7
+
8
+ // src/node/content.ts
9
+ import fg from "fast-glob";
10
+ import matter from "gray-matter";
11
+ import { readFile } from "node:fs/promises";
12
+ import { relative, sep } from "node:path";
13
+ function plainText(markdown) {
14
+ return markdown.replace(/```[\s\S]*?```/g, " ").replace(/<[^>]+>/g, " ").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/\[\[([^\]]+)\]\]/g, "$1").replace(/[#>*_`~|:-]/g, " ").replace(/\s+/g, " ").trim();
15
+ }
16
+ function routeFromFile(contentRoot, filename) {
17
+ const path = relative(contentRoot, filename).split(sep).join("/").replace(/(?:\.page\.json|\.md)$/, "");
18
+ return path === "index" ? "/" : `/${path.replace(/\/index$/, "")}`;
19
+ }
20
+ function recordFromMeta(filename, contentRoot, meta, content = "") {
21
+ const route = routeFromFile(contentRoot, filename);
22
+ return {
23
+ id: route,
24
+ route,
25
+ title: meta.title || route.split("/").pop() || "Untitled",
26
+ layout: meta.layout,
27
+ description: meta.description || "",
28
+ type: meta.type || "",
29
+ category: meta.category || "",
30
+ tags: meta.tags || [],
31
+ aliases: meta.aliases || [],
32
+ contributors: meta.contributors || [],
33
+ image: meta.image,
34
+ updated: meta.updated,
35
+ search: meta.search,
36
+ archive: meta.archive,
37
+ content,
38
+ excerpt: meta.description || content.slice(0, 120)
39
+ };
40
+ }
41
+ async function scanContent(contentRoot, options = {}) {
42
+ const filenames = await fg(options.include || ["**/*.md"], {
43
+ cwd: contentRoot,
44
+ absolute: true,
45
+ ignore: options.exclude || []
46
+ });
47
+ const pageMetadata = await fg("**/*.page.json", {
48
+ cwd: contentRoot,
49
+ absolute: true,
50
+ ignore: options.exclude || []
51
+ });
52
+ const records = await Promise.all(filenames.map(async (filename) => {
53
+ const source = await readFile(filename, "utf8");
54
+ const parsed = matter(source);
55
+ const content = plainText(parsed.content);
56
+ const firstHeading = parsed.content.match(/^#\s+(.+)$/m)?.[1]?.trim();
57
+ const meta = parsed.data;
58
+ return recordFromMeta(filename, contentRoot, { ...meta, title: meta.title || firstHeading }, content);
59
+ }));
60
+ const vuePageRecords = await Promise.all(pageMetadata.map(async (filename) => {
61
+ const meta = JSON.parse(await readFile(filename, "utf8"));
62
+ return recordFromMeta(filename, contentRoot, meta, meta.description || "");
63
+ }));
64
+ return [...records, ...vuePageRecords].sort((a, b) => a.title.localeCompare(b.title, "zh-CN"));
65
+ }
66
+
67
+ // src/node/markdown.ts
68
+ import container from "markdown-it-container";
69
+ function addSpoilerRule(md) {
70
+ md.core.ruler.after("inline", "cocowiki-spoiler", (state) => {
71
+ for (const blockToken of state.tokens) {
72
+ if (blockToken.type !== "inline" || !blockToken.children) continue;
73
+ const children = [];
74
+ for (const token of blockToken.children) {
75
+ if (token.type !== "text" || !token.content.includes("||")) {
76
+ children.push(token);
77
+ continue;
78
+ }
79
+ const pattern = /\|\|([^|]+?)\|\|/g;
80
+ let cursor = 0;
81
+ let match;
82
+ while (match = pattern.exec(token.content)) {
83
+ if (match.index > cursor) {
84
+ const text = new state.Token("text", "", 0);
85
+ text.content = token.content.slice(cursor, match.index);
86
+ children.push(text);
87
+ }
88
+ const spoiler = new state.Token("html_inline", "", 0);
89
+ spoiler.content = `<button class="cw-spoiler" type="button" aria-expanded="false" aria-label="\u5267\u900F\u5185\u5BB9\uFF0C\u70B9\u51FB\u663E\u793A"><span>${md.utils.escapeHtml(match[1])}</span></button>`;
90
+ children.push(spoiler);
91
+ cursor = pattern.lastIndex;
92
+ }
93
+ if (cursor === 0) {
94
+ children.push(token);
95
+ } else if (cursor < token.content.length) {
96
+ const text = new state.Token("text", "", 0);
97
+ text.content = token.content.slice(cursor);
98
+ children.push(text);
99
+ }
100
+ }
101
+ blockToken.children = children;
102
+ }
103
+ });
104
+ }
105
+ function addWikiLinkRule(md, records, base) {
106
+ const prefix = base === "/" ? "" : base.replace(/\/$/, "");
107
+ const routes = /* @__PURE__ */ new Map();
108
+ for (const record of records) {
109
+ routes.set(record.title, record.route);
110
+ for (const alias of record.aliases || []) routes.set(alias, record.route);
111
+ }
112
+ md.inline.ruler.before("link", "cocowiki-wikilink", (state, silent) => {
113
+ const start = state.pos;
114
+ if (state.src.slice(start, start + 2) !== "[[") return false;
115
+ const end = state.src.indexOf("]]", start + 2);
116
+ if (end < 0) return false;
117
+ const name = state.src.slice(start + 2, end).trim();
118
+ if (!name) return false;
119
+ if (!silent) {
120
+ const route = routes.get(name) || `/search?q=${encodeURIComponent(name)}`;
121
+ const token = state.push("html_inline", "", 0);
122
+ token.content = `<a class="cw-wikilink" href="${prefix}${route}">${md.utils.escapeHtml(name)}</a>`;
123
+ }
124
+ state.pos = end + 2;
125
+ return true;
126
+ });
127
+ }
128
+ function addTableWrapper(md) {
129
+ const open = md.renderer.rules.table_open || ((tokens, index, options, _env, renderer) => renderer.renderToken(tokens, index, options));
130
+ const close = md.renderer.rules.table_close || ((tokens, index, options, _env, renderer) => renderer.renderToken(tokens, index, options));
131
+ md.renderer.rules.table_open = (...args) => `<div class="cw-table-scroll">${open(...args)}`;
132
+ md.renderer.rules.table_close = (...args) => `${close(...args)}</div>`;
133
+ }
134
+ var calloutPresets = {
135
+ info: { title: "\u4FE1\u606F", icon: "i" },
136
+ tip: { title: "\u63D0\u793A", icon: "\u2713" },
137
+ warning: { title: "\u6CE8\u610F", icon: "!" },
138
+ danger: { title: "\u8B66\u544A", icon: "!" }
139
+ };
140
+ function addCalloutContainers(md) {
141
+ for (const [name, preset] of Object.entries(calloutPresets)) {
142
+ md.use(container, name, {
143
+ render(tokens, index) {
144
+ if (tokens[index].nesting !== 1) return "</aside>\n";
145
+ const customTitle = tokens[index].info.trim().slice(name.length).trim();
146
+ const title = md.utils.escapeHtml(customTitle || preset.title);
147
+ return `<aside class="cw-callout cw-callout--${name}"><span class="cw-callout__icon" aria-hidden="true">${preset.icon}</span><p class="cw-callout__title">${title}</p>
148
+ `;
149
+ }
150
+ });
151
+ }
152
+ md.use(container, "details", {
153
+ render(tokens, index) {
154
+ if (tokens[index].nesting !== 1) return "</details>\n";
155
+ const customTitle = tokens[index].info.trim().slice("details".length).trim();
156
+ const title = md.utils.escapeHtml(customTitle || "\u8BE6\u7EC6\u4FE1\u606F");
157
+ return `<details class="cw-callout cw-callout--details"><summary>${title}</summary>
158
+ `;
159
+ }
160
+ });
161
+ }
162
+ function applyCocoWikiMarkdown(md, records, options) {
163
+ if (options.spoiler !== false) addSpoilerRule(md);
164
+ if (options.wikiLink !== false) addWikiLinkRule(md, records, options.base || "/");
165
+ addTableWrapper(md);
166
+ addCalloutContainers(md);
167
+ }
168
+
169
+ // src/node/project.ts
170
+ import { resolve } from "node:path";
171
+ function resolveProjectPaths(projectRoot, config) {
172
+ const directories = config.directories || {};
173
+ return {
174
+ projectRoot,
175
+ runtimeRoot: resolve(projectRoot, ".cocowiki/site"),
176
+ contentDir: resolve(projectRoot, directories.content || "content"),
177
+ componentsDir: resolve(projectRoot, directories.components || "components"),
178
+ dataDir: resolve(projectRoot, directories.data || "data"),
179
+ publicDir: resolve(projectRoot, directories.public || "public"),
180
+ outDir: resolve(projectRoot, directories.out || "dist")
181
+ };
182
+ }
183
+
184
+ // src/node/vitepress.ts
185
+ var CONFIG_ID = "virtual:cocowiki-config";
186
+ var CONTRIBUTORS_ID = "virtual:cocowiki-contributors";
187
+ var ANSI = { reset: "\x1B[0m", dim: "\x1B[2m", cyan: "\x1B[36m", yellow: "\x1B[33m", red: "\x1B[31m" };
188
+ function createCocoWikiLogger() {
189
+ const logger = createLogger();
190
+ const label = "[cocowiki]";
191
+ const supportsColor = Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
192
+ const paint = (text, color) => supportsColor ? `${color}${text}${ANSI.reset}` : text;
193
+ const branded = (message) => {
194
+ const normalized = message.replace(/\[(?:vitepress|vite)\]/gi, label);
195
+ return normalized.includes(label) ? normalized : `${label} ${normalized}`;
196
+ };
197
+ const optionsWithoutViteLabel = (options) => options ? { ...options, timestamp: false } : options;
198
+ const timestamp = (options) => options?.timestamp ? paint(`${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })} `, ANSI.dim) : "";
199
+ const format = (message, color, options) => `${timestamp(options)}${paint(branded(message), color)}`;
200
+ const warned = /* @__PURE__ */ new Set();
201
+ return {
202
+ ...logger,
203
+ info: (message, options) => logger.info(format(message, ANSI.cyan, options), optionsWithoutViteLabel(options)),
204
+ warn: (message, options) => logger.warn(format(message, ANSI.yellow, options), optionsWithoutViteLabel(options)),
205
+ warnOnce: (message, options) => {
206
+ if (warned.has(message)) return;
207
+ warned.add(message);
208
+ logger.warn(format(message, ANSI.yellow, options), optionsWithoutViteLabel(options));
209
+ },
210
+ error: (message, options) => logger.error(format(message, ANSI.red, options), optionsWithoutViteLabel(options))
211
+ };
212
+ }
213
+ function contentPlugin(config, contentDir, dataDir, initialRecords) {
214
+ const records = initialRecords;
215
+ const resolvedConfigId = `\0${CONFIG_ID}`;
216
+ const resolvedContributorsId = `\0${CONTRIBUTORS_ID}`;
217
+ const contributorsFile = resolve2(dataDir, "contributors.json");
218
+ async function refresh() {
219
+ const nextRecords = await scanContent(contentDir, config.search);
220
+ records.splice(0, records.length, ...nextRecords);
221
+ }
222
+ return {
223
+ name: "cocowiki-content-index",
224
+ resolveId(id) {
225
+ if (id === CONFIG_ID) return resolvedConfigId;
226
+ if (id === CONTRIBUTORS_ID) return resolvedContributorsId;
227
+ },
228
+ async load(id) {
229
+ if (id === resolvedConfigId) return `export default ${JSON.stringify(config)}`;
230
+ if (id === resolvedContributorsId) {
231
+ const contributors = existsSync(contributorsFile) ? JSON.parse(await readFile2(contributorsFile, "utf8")) : {};
232
+ return `export default ${JSON.stringify(contributors)}`;
233
+ }
234
+ },
235
+ configureServer(server) {
236
+ server.middlewares.use((request, response, next) => {
237
+ if (!request.url?.split("?")[0]?.endsWith("/search-index.json")) return next();
238
+ response.statusCode = 200;
239
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
240
+ response.setHeader("Cache-Control", "no-store");
241
+ response.end(JSON.stringify(records));
242
+ });
243
+ server.watcher.add([contentDir, contributorsFile]);
244
+ let refreshTimer;
245
+ const refreshContent = (file) => {
246
+ const path = relative2(contentDir, file);
247
+ if (!path || path.startsWith("..") || !path.endsWith(".md") && !path.endsWith(".page.json")) return;
248
+ clearTimeout(refreshTimer);
249
+ refreshTimer = setTimeout(async () => {
250
+ try {
251
+ await refresh();
252
+ server.ws.send({ type: "custom", event: "cocowiki:content-updated" });
253
+ } catch (error) {
254
+ server.config.logger.error(`\u5237\u65B0\u5185\u5BB9\u7D22\u5F15\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
255
+ }
256
+ }, 60);
257
+ };
258
+ const refreshContributors = (file) => {
259
+ if (file !== contributorsFile) return;
260
+ const module = server.moduleGraph.getModuleById(resolvedContributorsId);
261
+ if (module) server.moduleGraph.invalidateModule(module);
262
+ server.ws.send({ type: "full-reload" });
263
+ };
264
+ server.watcher.on("add", refreshContent);
265
+ server.watcher.on("change", refreshContent);
266
+ server.watcher.on("unlink", refreshContent);
267
+ server.watcher.on("add", refreshContributors);
268
+ server.watcher.on("change", refreshContributors);
269
+ server.watcher.on("unlink", refreshContributors);
270
+ },
271
+ generateBundle() {
272
+ this.emitFile({ type: "asset", fileName: "search-index.json", source: JSON.stringify(records) });
273
+ }
274
+ };
275
+ }
276
+ async function createVitePressConfig(config, projectRoot) {
277
+ const paths = resolveProjectPaths(projectRoot, config);
278
+ const records = await scanContent(paths.contentDir, config.search);
279
+ const requestedBase = config.base || "/";
280
+ const base = `/${requestedBase.replace(/^\/+|\/+$/g, "")}${requestedBase === "/" ? "" : "/"}`;
281
+ const favicon = config.favicon || config.logo || "/favicon.svg";
282
+ const faviconType = favicon.endsWith(".svg") ? "image/svg+xml" : favicon.endsWith(".ico") ? "image/x-icon" : "image/png";
283
+ const customLogger = createCocoWikiLogger();
284
+ return defineConfig({
285
+ title: config.title,
286
+ description: config.description,
287
+ lang: config.lang || "zh-CN",
288
+ base,
289
+ srcDir: paths.runtimeRoot,
290
+ cleanUrls: true,
291
+ outDir: paths.outDir,
292
+ cacheDir: resolve2(projectRoot, ".cocowiki/cache"),
293
+ lastUpdated: true,
294
+ head: [
295
+ ["link", { rel: "icon", type: faviconType, href: `${base}${favicon.replace(/^\//, "")}` }],
296
+ ["meta", { name: "theme-color", content: "#ffffff", media: "(prefers-color-scheme: light)" }],
297
+ ["meta", { name: "theme-color", content: "#0f1013", media: "(prefers-color-scheme: dark)" }]
298
+ ],
299
+ vite: {
300
+ customLogger,
301
+ publicDir: paths.publicDir,
302
+ plugins: [contentPlugin(config, paths.contentDir, paths.dataDir, records)],
303
+ server: { fs: { allow: [projectRoot, resolve2(import.meta.dirname, "../..")] } },
304
+ resolve: {
305
+ alias: {
306
+ "@components": paths.componentsDir,
307
+ "@data": paths.dataDir
308
+ }
309
+ }
310
+ },
311
+ markdown: {
312
+ lineNumbers: false,
313
+ config(md) {
314
+ applyCocoWikiMarkdown(md, records, { ...config.markdown, base });
315
+ }
316
+ }
317
+ });
318
+ }
319
+ export {
320
+ createVitePressConfig
321
+ };