@tnotesjs/core 0.1.28 → 0.2.1

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 (71) hide show
  1. package/README.md +2 -1
  2. package/dist/{chunk-4KG6RXFS.js → chunk-5QN44ES5.js} +2946 -1533
  3. package/dist/{chunk-AGSL3VZE.js → chunk-5Y5IYS6C.js} +7 -0
  4. package/dist/cli/index.js +343 -40
  5. package/dist/index.js +1 -1
  6. package/dist/vitepress/config/index.js +573 -65
  7. package/package.json +4 -1
  8. package/templates/sub-repo/.gitattributes +30 -0
  9. package/templates/sub-repo/.github/workflows/deploy.yml +87 -0
  10. package/templates/sub-repo/.vitepress/config.mts +12 -0
  11. package/templates/sub-repo/.vitepress/env.d.ts +10 -0
  12. package/templates/sub-repo/.vitepress/theme/index.ts +12 -0
  13. package/templates/sub-repo/.vscode/settings.json +330 -0
  14. package/templates/sub-repo/.vscode/tnotes.code-snippets +110 -0
  15. package/templates/sub-repo/public/favicon.ico +0 -0
  16. package/templates/sub-repo/public/logo.png +0 -0
  17. package/templates/sub-repo/tsconfig.json +45 -0
  18. package/vitepress/assets/icons/icon__setting_dark.svg +4 -0
  19. package/vitepress/assets/icons/icon__setting_light.svg +4 -0
  20. package/vitepress/assets/icons/index.ts +2 -2
  21. package/vitepress/client/localSearchIndexBridge.ts +24 -0
  22. package/vitepress/components/Discussions/Discussions.vue +1 -1
  23. package/vitepress/components/Footprints/Footprints.vue +90 -25
  24. package/vitepress/components/Layout/CustomSidebar.vue +489 -117
  25. package/vitepress/components/Layout/Layout.vue +5 -0
  26. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +10 -2
  27. package/vitepress/components/Layout/SidebarItems.vue +517 -342
  28. package/vitepress/components/Layout/SidebarNavBefore.vue +1 -50
  29. package/vitepress/components/Layout/SidebarResizeHandle.vue +6 -23
  30. package/vitepress/components/Layout/composables/docLayoutLogic.ts +59 -0
  31. package/vitepress/components/Layout/composables/useDocLayout.test.ts +69 -0
  32. package/vitepress/components/Layout/composables/useDocLayout.ts +128 -0
  33. package/vitepress/components/Layout/composables/useSidebarDrag.ts +514 -0
  34. package/vitepress/components/Layout/composables/useSidebarLayout.ts +25 -8
  35. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +8 -3
  36. package/vitepress/components/Layout/sidebarCollapsedState.test.ts +183 -0
  37. package/vitepress/components/Layout/sidebarCollapsedState.ts +241 -0
  38. package/vitepress/components/Layout/sidebarDragLogic.test.ts +1146 -0
  39. package/vitepress/components/Layout/sidebarDragLogic.ts +1307 -0
  40. package/vitepress/components/Layout/sidebarHitTest.test.ts +572 -0
  41. package/vitepress/components/Layout/sidebarHitTest.ts +801 -0
  42. package/vitepress/components/Layout/sidebarItemRenderLogic.test.ts +53 -0
  43. package/vitepress/components/Layout/sidebarItemRenderLogic.ts +50 -0
  44. package/vitepress/components/MarkMap/MarkMap.vue +67 -10
  45. package/vitepress/components/Settings/Settings.vue +78 -74
  46. package/vitepress/components/SidebarCard/FolderTreeItems.vue +268 -0
  47. package/vitepress/components/SidebarCard/SidebarCard.vue +125 -274
  48. package/vitepress/components/constants.ts +18 -6
  49. package/vitepress/components/sidebar.data.ts +13 -11
  50. package/vitepress/components/sidebarTreeHelpers.test.ts +142 -0
  51. package/vitepress/components/sidebarTreeHelpers.ts +168 -0
  52. package/vitepress/components/utils/vscodePaths.test.ts +41 -0
  53. package/vitepress/components/utils/vscodePaths.ts +52 -0
  54. package/vitepress/config/index.ts +2 -0
  55. package/vitepress/plugins/localSearchIndexBuilder.ts +209 -0
  56. package/vitepress/plugins/localSearchReindex.test.ts +46 -0
  57. package/vitepress/plugins/localSearchReindexLogic.ts +50 -0
  58. package/vitepress/plugins/localSearchReindexPatch.test.ts +30 -0
  59. package/vitepress/plugins/localSearchReindexPatch.ts +37 -0
  60. package/vitepress/plugins/localSearchReindexPlugin.ts +318 -0
  61. package/vitepress/plugins/sidebarStructurePlugin.ts +209 -63
  62. package/vitepress/theme/index.ts +8 -0
  63. package/vitepress/theme/styles/base.scss +0 -8
  64. package/vitepress/theme/styles/components/markmap.scss +23 -2
  65. package/vitepress/theme/styles/components/swiper.scss +1 -1
  66. package/vitepress/theme/styles/doc-layout.scss +212 -0
  67. package/vitepress/theme/styles/index.scss +3 -0
  68. package/vitepress/theme/styles/layout.scss +60 -10
  69. package/vitepress/assets/icons/icon__mindmap.svg +0 -1
  70. package/vitepress/assets/icons/icon__setting.svg +0 -1
  71. package/vitepress/components/SidebarCard/MindMapView.vue +0 -488
@@ -0,0 +1,50 @@
1
+ /**
2
+ * localSearchReindexLogic.ts
3
+ *
4
+ * 纯函数:笔记 README 路径判定(可单测)
5
+ */
6
+
7
+ import path from 'node:path'
8
+
9
+ function slash(value: string): string {
10
+ return value.replace(/\\/g, '/')
11
+ }
12
+
13
+ const NOTE_README_RE =
14
+ /(?:^|[\\/])notes[\\/](\d{4}\.[^\\/]+)[\\/]README\.md$/i
15
+
16
+ export function isNoteReadmePath(
17
+ filePath: string,
18
+ ignoreDirNames: string[] = [],
19
+ ): boolean {
20
+ const normalized = filePath.replace(/\\/g, '/')
21
+ const match = NOTE_README_RE.exec(normalized)
22
+ if (!match) return false
23
+
24
+ const folderName = match[1]
25
+ if (!folderName) return false
26
+
27
+ return !ignoreDirNames.some((dir) => folderName === dir)
28
+ }
29
+
30
+ export function getDocIdFromFile(input: {
31
+ srcDir: string
32
+ base: string
33
+ cleanUrls: boolean
34
+ rewrites?: Record<string, string>
35
+ absoluteOrRelativeFile: string
36
+ }): string {
37
+ const absoluteFile = path.isAbsolute(input.absoluteOrRelativeFile)
38
+ ? input.absoluteOrRelativeFile
39
+ : path.join(input.srcDir, input.absoluteOrRelativeFile)
40
+
41
+ let relFile = slash(path.relative(input.srcDir, absoluteFile))
42
+ if (input.rewrites?.[relFile]) {
43
+ relFile = input.rewrites[relFile]
44
+ }
45
+
46
+ let id = slash(path.posix.join(input.base, relFile))
47
+ id = id.replace(/(^|\/)index\.md$/, '$1')
48
+ id = id.replace(/\.md$/, input.cleanUrls ? '' : '.html')
49
+ return id
50
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * localSearchReindexPatch.test.ts
3
+ */
4
+
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { describe, expect, it } from 'vitest'
9
+
10
+ import { patchVPNavBarSearch } from './localSearchReindexPatch'
11
+
12
+ describe('patchVPNavBarSearch', () => {
13
+ it('patches vitepress VPNavBarSearch with remount key', () => {
14
+ const vpRoot = path.dirname(
15
+ fileURLToPath(import.meta.resolve('vitepress/package.json')),
16
+ )
17
+ const source = fs.readFileSync(
18
+ path.join(
19
+ vpRoot,
20
+ 'dist/client/theme-default/components/VPNavBarSearch.vue',
21
+ ),
22
+ 'utf8',
23
+ )
24
+
25
+ const patched = patchVPNavBarSearch(source)
26
+ expect(patched).not.toBeNull()
27
+ expect(patched).toContain('localSearchIndexBridge')
28
+ expect(patched).toContain(':key="searchBoxRemountKey"')
29
+ })
30
+ })
@@ -0,0 +1,37 @@
1
+ /**
2
+ * localSearchReindexPatch.ts
3
+ *
4
+ * VitePress Local Search 组件 dev patch(可单测)
5
+ *
6
+ * 只 patch VPNavBarSearch(:key remount),不修改 VPLocalSearchBox 索引逻辑。
7
+ */
8
+
9
+ export function patchVPLocalSearchBox(_code: string): string | null {
10
+ return null
11
+ }
12
+
13
+ export function patchVPNavBarSearch(code: string): string | null {
14
+ if (code.includes('localSearchIndexBridge')) return null
15
+ if (!code.includes('VPLocalSearchBox')) return null
16
+
17
+ let next = code.replace(
18
+ /<script lang="ts" setup>\r?\n/,
19
+ `<script lang="ts" setup>
20
+ import { searchBoxRemountKey } from '@tnotesjs/core/vitepress/client/localSearchIndexBridge'
21
+ `,
22
+ )
23
+
24
+ next = next.replace(
25
+ /<VPLocalSearchBox\r?\n\s+v-if="showSearch"\r?\n\s+@close="showSearch = false"\r?\n\s+\/>/,
26
+ `<VPLocalSearchBox
27
+ v-if="showSearch"
28
+ :key="searchBoxRemountKey"
29
+ @close="showSearch = false"
30
+ />`,
31
+ )
32
+
33
+ return next === code ? null : next
34
+ }
35
+
36
+ /** @internal test alias */
37
+ export const patchVPLocalSearchBoxForTest = patchVPLocalSearchBox
@@ -0,0 +1,318 @@
1
+ /**
2
+ * localSearchReindexPlugin.ts
3
+ *
4
+ * dev 下监听笔记 README 变更,debounced 全量重建 Local Search 索引并 HMR 推送。
5
+ */
6
+
7
+ import path from 'node:path'
8
+
9
+
10
+
11
+ import {
12
+ buildLocalSearchIndexSnapshot,
13
+ createLocalSearchMarkdownRenderer,
14
+ type LocalSearchIndexSnapshot,
15
+ } from './localSearchIndexBuilder'
16
+ import { isNoteReadmePath } from './localSearchReindexLogic'
17
+ import {
18
+ patchVPNavBarSearch,
19
+ } from './localSearchReindexPatch'
20
+ import { ConfigManager } from '../../config/ConfigManager'
21
+
22
+ import type { PluginOption, ViteDevServer } from 'vite'
23
+ import type { SiteConfig } from 'vitepress'
24
+
25
+ const LOCAL_SEARCH_INDEX_REQUEST_PATH = '/@localSearchIndex'
26
+ const LOCAL_SEARCH_INDEX_ID = '@localSearchIndex'
27
+ const SEARCH_REINDEX_HTTP_PATH = '/__tnotes_search_reindex'
28
+ const SEARCH_INDEX_HMR_EVENT = 'tnotes:search-index-updated'
29
+ const REBUILD_DEBOUNCE_MS = 400
30
+
31
+ const VP_NAV_BAR_SEARCH =
32
+ /vitepress[/\\]dist[/\\]client[/\\]theme-default[/\\]components[/\\]VPNavBarSearch\.vue$/
33
+
34
+ type ReindexController = {
35
+ schedule: (reason: string) => void
36
+ }
37
+
38
+ type ViteServerConfigWithVp = ViteDevServer['config'] & {
39
+ vitepress?: SiteConfig
40
+ }
41
+
42
+ let reindexController: ReindexController | null = null
43
+ let indexSnapshot: LocalSearchIndexSnapshot | null = null
44
+ let snapshotRevision = 0
45
+
46
+ const DEBUG = process.env.TNOTES_DEBUG_SEARCH_REINDEX === '1'
47
+
48
+ function debugLog(message: string, detail?: unknown): void {
49
+ if (!DEBUG) return
50
+ if (detail === undefined) {
51
+ console.log(`[tnotes:search-reindex] ${message}`)
52
+ return
53
+ }
54
+ console.log(`[tnotes:search-reindex] ${message}`, detail)
55
+ }
56
+
57
+ function logInfo(message: string): void {
58
+ console.warn(`[tnotes:search-reindex] ${message}`)
59
+ }
60
+
61
+ function normalizeId(id: string): string {
62
+ return id.replace(/\\/g, '/')
63
+ }
64
+
65
+ function getVitePressSiteConfig(server: ViteDevServer): SiteConfig | null {
66
+ return (server.config as ViteServerConfigWithVp).vitepress ?? null
67
+ }
68
+
69
+ function isLocalSearchEnabled(server: ViteDevServer): boolean {
70
+ return server.config.plugins.some(
71
+ (entry) =>
72
+ entry.name === 'vitepress:local-search' && typeof entry.load === 'function',
73
+ )
74
+ }
75
+
76
+ function normalizeLocalSearchModuleId(id: string): string {
77
+ return id.split('?')[0]!
78
+ }
79
+
80
+ function getLocaleFromIndexModuleId(id: string): string | null {
81
+ const normalized = normalizeLocalSearchModuleId(id)
82
+ if (normalized === LOCAL_SEARCH_INDEX_REQUEST_PATH) return null
83
+ if (!normalized.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) return null
84
+ return normalized.slice(LOCAL_SEARCH_INDEX_REQUEST_PATH.length) || null
85
+ }
86
+
87
+ function getSearchIndexModuleIds(snapshot: LocalSearchIndexSnapshot): string[] {
88
+ const ids = [LOCAL_SEARCH_INDEX_REQUEST_PATH]
89
+ for (const locale of snapshot.keys()) {
90
+ ids.push(`${LOCAL_SEARCH_INDEX_REQUEST_PATH}${locale}`)
91
+ }
92
+ return ids
93
+ }
94
+
95
+ function notifySearchIndexUpdated(
96
+ server: ViteDevServer,
97
+ snapshot: LocalSearchIndexSnapshot,
98
+ revision: number,
99
+ ): void {
100
+ const moduleIds = getSearchIndexModuleIds(snapshot)
101
+ const updates: Array<{
102
+ acceptedPath: string
103
+ path: string
104
+ timestamp: number
105
+ type: 'js-update'
106
+ }> = []
107
+
108
+ for (const moduleId of moduleIds) {
109
+ server.moduleGraph.onFileChange(moduleId)
110
+ const mod = server.moduleGraph.getModuleById(moduleId)
111
+ if (mod) {
112
+ server.moduleGraph.invalidateModule(mod)
113
+ updates.push({
114
+ acceptedPath: mod.url,
115
+ path: mod.url,
116
+ timestamp: Date.now(),
117
+ type: 'js-update',
118
+ })
119
+ continue
120
+ }
121
+ debugLog(`HMR module not loaded yet: ${moduleId}`)
122
+ }
123
+
124
+ if (updates.length > 0) {
125
+ server.ws.send({ type: 'update', updates })
126
+ debugLog('HMR pushed', updates.map((item) => item.path))
127
+ } else {
128
+ debugLog('HMR skipped: no loaded search index modules')
129
+ }
130
+
131
+ server.ws.send({
132
+ type: 'custom',
133
+ event: SEARCH_INDEX_HMR_EVENT,
134
+ data: { revision },
135
+ })
136
+ debugLog('custom HMR event sent', { revision })
137
+ }
138
+
139
+ function renderSnapshotLoad(
140
+ id: string,
141
+ snapshot: LocalSearchIndexSnapshot,
142
+ ): string | null {
143
+ const normalizedId = normalizeLocalSearchModuleId(id)
144
+
145
+ if (normalizedId === LOCAL_SEARCH_INDEX_REQUEST_PATH) {
146
+ const records: string[] = []
147
+ for (const locale of snapshot.keys()) {
148
+ records.push(
149
+ `${JSON.stringify(locale)}: () => import('${LOCAL_SEARCH_INDEX_ID}${locale}')`,
150
+ )
151
+ }
152
+ return `export default {${records.join(',')}}`
153
+ }
154
+
155
+ const locale = getLocaleFromIndexModuleId(id)
156
+ if (locale !== null) {
157
+ const payload = snapshot.get(locale) ?? JSON.stringify({})
158
+ return `export default ${payload}`
159
+ }
160
+
161
+ return null
162
+ }
163
+
164
+ function createDebouncer(delayMs: number) {
165
+ let timer: ReturnType<typeof setTimeout> | null = null
166
+ let pendingReason = ''
167
+
168
+ return (reason: string, task: () => Promise<void>) => {
169
+ pendingReason = reason
170
+ if (timer) clearTimeout(timer)
171
+ timer = setTimeout(async () => {
172
+ timer = null
173
+ const reasonToRun = pendingReason
174
+ pendingReason = ''
175
+ try {
176
+ await task()
177
+ } catch (error) {
178
+ console.error(
179
+ `[tnotes:search-reindex] rebuild failed (${reasonToRun})`,
180
+ error,
181
+ )
182
+ }
183
+ }, delayMs)
184
+ }
185
+ }
186
+
187
+ function matchesSearchReindexPath(url: string): boolean {
188
+ return url === SEARCH_REINDEX_HTTP_PATH || url.endsWith(SEARCH_REINDEX_HTTP_PATH)
189
+ }
190
+
191
+ /**
192
+ * 由 sidebar API 等同进程模块调用,调度 debounced 全量索引重建。
193
+ */
194
+ export function scheduleNoteSearchReindex(reason = 'external'): void {
195
+ if (!reindexController) {
196
+ logInfo(`schedule ignored (plugin not ready): ${reason}`)
197
+ return
198
+ }
199
+ reindexController.schedule(reason)
200
+ }
201
+
202
+ export function localSearchReindexPlugin(): PluginOption {
203
+ return {
204
+ name: 'tnotes-local-search-reindex',
205
+ apply: 'serve',
206
+ enforce: 'pre',
207
+
208
+ resolveId(source) {
209
+ if (!source.startsWith(LOCAL_SEARCH_INDEX_ID)) return null
210
+ const clean = source.split('?')[0]!
211
+ if (clean === source) return null
212
+ return `/${clean}`
213
+ },
214
+
215
+ transform(code, id) {
216
+ const normalizedId = normalizeId(id)
217
+ if (VP_NAV_BAR_SEARCH.test(normalizedId)) {
218
+ return patchVPNavBarSearch(code)
219
+ }
220
+ return null
221
+ },
222
+
223
+ load(id) {
224
+ if (!indexSnapshot || !id.startsWith(LOCAL_SEARCH_INDEX_REQUEST_PATH)) {
225
+ return null
226
+ }
227
+ return renderSnapshotLoad(id, indexSnapshot)
228
+ },
229
+
230
+ configureServer(server) {
231
+ server.middlewares.use((req, res, next) => {
232
+ const url = req.url ?? ''
233
+ if (!matchesSearchReindexPath(url) || req.method !== 'POST') {
234
+ return next()
235
+ }
236
+
237
+ let body = ''
238
+ req.on('data', (chunk) => {
239
+ body += chunk.toString()
240
+ })
241
+ req.on('end', () => {
242
+ try {
243
+ const data = JSON.parse(body || '{}') as { reason?: string }
244
+ scheduleNoteSearchReindex(data.reason || 'bridge:http')
245
+ res.statusCode = 204
246
+ res.end()
247
+ } catch (error) {
248
+ res.statusCode = 400
249
+ res.end(
250
+ error instanceof Error ? error.message : 'Bad search reindex payload',
251
+ )
252
+ }
253
+ })
254
+ })
255
+
256
+ return async () => {
257
+ indexSnapshot = null
258
+ snapshotRevision = 0
259
+
260
+ if (!isLocalSearchEnabled(server)) {
261
+ reindexController = null
262
+ logInfo('vitepress:local-search not found, reindex disabled')
263
+ return
264
+ }
265
+
266
+ const siteConfig = getVitePressSiteConfig(server)
267
+ if (!siteConfig) {
268
+ reindexController = null
269
+ logInfo('server.config.vitepress missing, reindex disabled')
270
+ return
271
+ }
272
+
273
+ ConfigManager.init({ rootPath: process.cwd() })
274
+ const ignoreDirNames =
275
+ ConfigManager.getInstance().getAll().ignore_dirs ?? []
276
+
277
+ const md = await createLocalSearchMarkdownRenderer(siteConfig)
278
+ const debouncedRebuild = createDebouncer(REBUILD_DEBOUNCE_MS)
279
+
280
+ async function rebuildFullIndex(reason: string): Promise<void> {
281
+ const startedAt = Date.now()
282
+ snapshotRevision += 1
283
+ debugLog(`rebuild start (${reason}) rev=${snapshotRevision}`)
284
+ const { snapshot, pageCount } = await buildLocalSearchIndexSnapshot(
285
+ siteConfig,
286
+ md,
287
+ )
288
+ indexSnapshot = snapshot
289
+ notifySearchIndexUpdated(server, indexSnapshot, snapshotRevision)
290
+ logInfo(
291
+ `rebuild done (${reason}) rev=${snapshotRevision} pages=${pageCount} locales=${indexSnapshot.size} elapsed=${Date.now() - startedAt}ms`,
292
+ )
293
+ }
294
+
295
+ reindexController = {
296
+ schedule(reason: string) {
297
+ debugLog(`scheduled (${reason})`)
298
+ debouncedRebuild(reason, () => rebuildFullIndex(reason))
299
+ },
300
+ }
301
+
302
+ const onWatcherEvent = (
303
+ absoluteFile: string,
304
+ event: 'add' | 'change' | 'unlink',
305
+ ) => {
306
+ if (!isNoteReadmePath(absoluteFile, ignoreDirNames)) return
307
+ scheduleNoteSearchReindex(
308
+ `watcher:${event}:${path.basename(absoluteFile)}`,
309
+ )
310
+ }
311
+
312
+ server.watcher.on('add', (file) => onWatcherEvent(file, 'add'))
313
+ server.watcher.on('change', (file) => onWatcherEvent(file, 'change'))
314
+ server.watcher.on('unlink', (file) => onWatcherEvent(file, 'unlink'))
315
+ }
316
+ },
317
+ }
318
+ }