@tnotesjs/core 0.6.1 → 0.8.0

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 (92) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/config/templates.ts +0 -5
  26. package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
  27. package/dist/chunk-AGVER5MX.cjs +1746 -0
  28. package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
  29. package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
  30. package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
  31. package/dist/chunk-ZEQS64PE.js +1746 -0
  32. package/dist/cli/index.cjs +76 -155
  33. package/dist/cli/index.js +10 -89
  34. package/dist/markdown/index.cjs +8 -12
  35. package/dist/markdown/index.d.cts +1 -1
  36. package/dist/markdown/index.d.ts +1 -1
  37. package/dist/markdown/index.js +7 -11
  38. package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
  39. package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
  40. package/dist/vitepress/config/index.cjs +402 -178
  41. package/dist/vitepress/config/index.js +392 -168
  42. package/dist/workspace/index.cjs +4 -1202
  43. package/dist/workspace/index.d.cts +2 -2
  44. package/dist/workspace/index.d.ts +2 -2
  45. package/dist/workspace/index.js +4 -1202
  46. package/markdown/components.ts +86 -0
  47. package/markdown/index.ts +17 -0
  48. package/markdown/noteFormatter.test.ts +44 -0
  49. package/markdown/noteFormatter.ts +237 -0
  50. package/package.json +7 -3
  51. package/services/file-watcher/folderChangeHandler.ts +19 -15
  52. package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
  53. package/services/file-watcher/service.ts +1 -6
  54. package/services/index.ts +0 -1
  55. package/services/note/service.ts +4 -6
  56. package/services/readme/service.ts +11 -18
  57. package/services/reconcileToc.ts +20 -0
  58. package/utils/index.ts +0 -9
  59. package/{services/toc → utils}/moveTocInside.test.ts +1 -1
  60. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  61. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  62. package/vitepress/components/Footprints/Footprints.vue +15 -537
  63. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  64. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  65. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  66. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  67. package/vitepress/components/constants.ts +2 -2
  68. package/vitepress/components/sidebar.data.ts +8 -5
  69. package/vitepress/configs/markdown.config.ts +170 -26
  70. package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
  71. package/vitepress/theme/index.ts +9 -13
  72. package/vitepress/theme/styles/base.scss +15 -0
  73. package/workspace/atomic.ts +113 -0
  74. package/workspace/errors.ts +27 -0
  75. package/workspace/index.ts +40 -0
  76. package/workspace/mutationQueue.ts +28 -0
  77. package/workspace/paths.ts +64 -0
  78. package/workspace/reconcile.test.ts +300 -0
  79. package/workspace/reconcile.ts +95 -0
  80. package/workspace/scanner.ts +292 -0
  81. package/workspace/types.ts +224 -0
  82. package/workspace/workspace.test.ts +333 -0
  83. package/workspace/workspace.ts +1020 -0
  84. package/dist/chunk-3R7QSABB.cjs +0 -502
  85. package/dist/chunk-CZXRQPFJ.js +0 -11
  86. package/dist/chunk-HXED7KVT.cjs +0 -11
  87. package/dist/chunk-U6IBLREL.js +0 -502
  88. package/services/toc/index.ts +0 -5
  89. package/services/toc/service.ts +0 -759
  90. package/utils/migrateReadmeToToc.test.ts +0 -111
  91. package/utils/migrateReadmeToToc.ts +0 -135
  92. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -1,759 +0,0 @@
1
- /**
2
- * services/toc/service.ts
3
- *
4
- * TOC.md 服务 - 知识库目录唯一数据源
5
- */
6
-
7
- import {
8
- existsSync,
9
- readFileSync,
10
- writeFileSync,
11
- promises as fsPromises,
12
- } from 'fs'
13
-
14
- import { ConfigManager } from '../../config/ConfigManager'
15
- import { ROOT_TOC_PATH, VP_SIDEBAR_PATH } from '../../config/constants'
16
- import { NoteIndexCache } from '../../core/NoteIndexCache'
17
- import { NoteManager } from '../../core/NoteManager'
18
- import { logger } from '../../utils'
19
- import {
20
- adjustTocLineIndexAfterSubtreeRemoval,
21
- buildFolderTocLine,
22
- buildSidebarFromTocTree,
23
- buildTocLine,
24
- collectNoteIndexesInSubtree,
25
- findFolderLineIndex,
26
- findTocLineIndex,
27
- getPreviousNoteIndexOutsideSubtree,
28
- getTocEntrySubtreeRange,
29
- getPreviousTocNoteIndex,
30
- getTocLineCompleted,
31
- isNoteIndexInSubtree,
32
- parseTocLine,
33
- parseTocToTree,
34
- processTocEmptyLines,
35
- renameFolderLine,
36
- resolveNoteFromIndex,
37
- serializeTocTree,
38
- TOC_INDENT_SPACES,
39
- } from '../../utils/tocHelpers'
40
- import { parseNodeId } from '../../utils/tocNodeId'
41
- import { NoteService } from '../note/service'
42
-
43
- import type { NoteInfo, NoteConfig } from '../../types'
44
- import type { TocTreeNode } from '../../utils/tocHelpers'
45
-
46
- type NoteInsertPlacement = 'before' | 'after'
47
-
48
- export type MoveNoteTarget =
49
- | {
50
- targetTocLineIndex: number
51
- placement: NoteInsertPlacement | 'inside'
52
- }
53
- | {
54
- targetType: 'note'
55
- targetNoteIndex: string
56
- placement: NoteInsertPlacement | 'inside'
57
- }
58
- | {
59
- targetType: 'folder'
60
- targetFolderPath: string[]
61
- placement: NoteInsertPlacement | 'inside'
62
- }
63
-
64
- /** @alias MoveNoteTarget — 分组/笔记统一移动目标 */
65
- export type MoveTocEntryTarget = MoveNoteTarget
66
-
67
- /**
68
- * TOC 服务类(单例)
69
- */
70
- export class TocService {
71
- private static instance: TocService
72
- private noteManager: NoteManager
73
- private configManager: ConfigManager
74
- private noteIndexCache: NoteIndexCache
75
-
76
- private constructor() {
77
- this.noteManager = NoteManager.getInstance()
78
- this.configManager = ConfigManager.getInstance()
79
- this.noteIndexCache = NoteIndexCache.getInstance()
80
- }
81
-
82
- static getInstance(): TocService {
83
- if (!TocService.instance) {
84
- TocService.instance = new TocService()
85
- }
86
- return TocService.instance
87
- }
88
-
89
- /**
90
- * 确保 TOC.md 存在
91
- */
92
- ensureTocExists(): void {
93
- if (!existsSync(ROOT_TOC_PATH)) {
94
- throw new Error(
95
- `TOC.md 不存在: ${ROOT_TOC_PATH}。请在知识库根目录创建 TOC.md 作为目录数据源。`,
96
- )
97
- }
98
- }
99
-
100
- /**
101
- * update 主流程:规范化 TOC.md
102
- */
103
- async normalizeToc(notes: NoteInfo[]): Promise<void> {
104
- this.ensureTocExists()
105
-
106
- const content = await fsPromises.readFile(ROOT_TOC_PATH, 'utf-8')
107
- const lines = content.split('\n')
108
- const tree = parseTocToTree(lines, notes)
109
-
110
- const existingIndexes = new Set<string>()
111
- const collectNoteIndexes = (nodes: TocTreeNode[]) => {
112
- for (const node of nodes) {
113
- if (node.kind === 'note') {
114
- existingIndexes.add(node.noteIndex)
115
- }
116
- collectNoteIndexes(node.children)
117
- }
118
- }
119
- collectNoteIndexes(tree)
120
-
121
- const missingNotes = notes.filter((n) => !existingIndexes.has(n.index))
122
- if (missingNotes.length > 0) {
123
- logger.info(`TOC.md 追加 ${missingNotes.length} 篇缺失笔记`)
124
- missingNotes.sort((a, b) => a.index.localeCompare(b.index))
125
- for (const note of missingNotes) {
126
- tree.push({
127
- kind: 'note',
128
- noteIndex: note.index,
129
- indent: 0,
130
- tocLineIndex: 0,
131
- children: [],
132
- })
133
- }
134
- }
135
-
136
- const configByIndex = new Map<string, Partial<NoteConfig>>()
137
- for (const note of notes) {
138
- configByIndex.set(note.index, {
139
- done: getTocLineCompleted(note),
140
- })
141
- }
142
-
143
- const normalizedLines = serializeTocTree(tree, notes, configByIndex)
144
- await this.writeTocLines(normalizedLines)
145
- logger.info('已规范化 TOC.md')
146
- }
147
-
148
- /**
149
- * 从 TOC.md 重新生成 sidebar.json
150
- */
151
- async regenerateSidebar(notes?: NoteInfo[]): Promise<void> {
152
- this.ensureTocExists()
153
-
154
- const allNotes =
155
- notes ??
156
- (this.noteIndexCache.isInitialized()
157
- ? this.noteIndexCache.toNoteInfoList()
158
- : this.noteManager.scanNotes())
159
-
160
- const content = readFileSync(ROOT_TOC_PATH, 'utf-8')
161
- const lines = content.split('\n')
162
- const tree = parseTocToTree(lines, allNotes)
163
-
164
- const sidebarShowNoteId = this.configManager.get('sidebarShowNoteId')
165
- const hierarchicalSidebar = buildSidebarFromTocTree(tree, allNotes, {
166
- sidebarShowNoteId,
167
- sidebarIsCollapsed: true,
168
- })
169
-
170
- const sidebarContent = JSON.stringify(hierarchicalSidebar, null, 2)
171
- const existingContent = existsSync(VP_SIDEBAR_PATH)
172
- ? readFileSync(VP_SIDEBAR_PATH, 'utf-8')
173
- : ''
174
-
175
- if (sidebarContent !== existingContent) {
176
- writeFileSync(VP_SIDEBAR_PATH, sidebarContent, 'utf-8')
177
- }
178
-
179
- logger.info('已更新侧边栏配置')
180
- }
181
-
182
- /**
183
- * 增量更新 TOC.md 中某笔记的完成状态与链接
184
- */
185
- async updateNoteInToc(
186
- noteIndex: string,
187
- updates: Partial<NoteConfig>,
188
- ): Promise<void> {
189
- this.ensureTocExists()
190
-
191
- const item = this.noteIndexCache.getByNoteIndex(noteIndex)
192
- if (!item) {
193
- logger.warn(`尝试更新 TOC 中不存在的笔记: ${noteIndex}`)
194
- return
195
- }
196
-
197
- const notes = this.noteIndexCache.isInitialized()
198
- ? this.noteIndexCache.toNoteInfoList()
199
- : this.noteManager.scanNotes()
200
-
201
- const note = resolveNoteFromIndex(noteIndex, notes)
202
- if (!note) return
203
-
204
- const mergedConfig = {
205
- ...note.config,
206
- ...updates,
207
- ...item.noteConfig,
208
- } as NoteConfig
209
-
210
- const tempNote: NoteInfo = {
211
- ...note,
212
- dirName: item.folderName,
213
- config: mergedConfig,
214
- }
215
-
216
- const lines = await this.readTocLines()
217
- let updated = false
218
-
219
- for (let i = 0; i < lines.length; i++) {
220
- const parsed = parseTocLine(lines[i])
221
- if (parsed.noteIndex === noteIndex) {
222
- lines[i] = buildTocLine(
223
- tempNote,
224
- parsed.indentLevel,
225
- getTocLineCompleted(tempNote),
226
- )
227
- updated = true
228
- }
229
- }
230
-
231
- if (updated) {
232
- await this.writeTocLines(lines)
233
- logger.info(`增量更新 TOC.md 中的笔记: ${noteIndex}`)
234
- } else {
235
- logger.warn(`TOC.md 中未找到笔记: ${noteIndex}`)
236
- }
237
- }
238
-
239
- /**
240
- * 在 TOC.md 末尾或父节点下追加笔记
241
- */
242
- async appendNoteToToc(
243
- noteIndex: string,
244
- options?: {
245
- parentTocLineIndex?: number
246
- parentFolderPath?: string[]
247
- parentNoteIndex?: string
248
- },
249
- ): Promise<void> {
250
- const item = this.noteIndexCache.getByNoteIndex(noteIndex)
251
- if (!item) {
252
- logger.warn(`尝试添加不存在的笔记到 TOC: ${noteIndex}`)
253
- return
254
- }
255
-
256
- const notes = this.noteIndexCache.isInitialized()
257
- ? this.noteIndexCache.toNoteInfoList()
258
- : this.noteManager.scanNotes()
259
-
260
- const note = resolveNoteFromIndex(noteIndex, notes)
261
- if (!note) return
262
-
263
- const tempNote: NoteInfo = {
264
- ...note,
265
- dirName: item.folderName,
266
- config: item.noteConfig,
267
- }
268
-
269
- if (!existsSync(ROOT_TOC_PATH)) {
270
- await this.writeTocLines([buildTocLine(tempNote, 0, false)])
271
- logger.info(`创建 TOC.md 并添加笔记: ${noteIndex}`)
272
- return
273
- }
274
-
275
- const lines = await this.readTocLines()
276
-
277
- if (options?.parentTocLineIndex !== undefined) {
278
- await this.insertNotesUnderTocLine(options.parentTocLineIndex, [tempNote])
279
- return
280
- }
281
-
282
- if (options?.parentNoteIndex) {
283
- const parentIndex = findTocLineIndex(lines, options.parentNoteIndex)
284
- await this.insertNotesUnderTocLine(parentIndex, [tempNote])
285
- return
286
- }
287
-
288
- if (options?.parentFolderPath && options.parentFolderPath.length > 0) {
289
- const folderIndex = findFolderLineIndex(lines, options.parentFolderPath)
290
- await this.insertNotesUnderTocLine(folderIndex, [tempNote])
291
- return
292
- }
293
-
294
- lines.push(buildTocLine(tempNote, 0, false))
295
-
296
- await this.writeTocLines(lines)
297
- logger.info(`在 TOC.md 末尾添加笔记: ${noteIndex}`)
298
- }
299
-
300
- /**
301
- * 删除前按 TOC 文档顺序获取上一项笔记索引;首项返回 null
302
- */
303
- async getPreviousNoteIndexBeforeDelete(
304
- noteIndex: string,
305
- ): Promise<string | null> {
306
- if (!existsSync(ROOT_TOC_PATH)) return null
307
-
308
- const lines = await this.readTocLines()
309
- return getPreviousTocNoteIndex(lines, noteIndex)
310
- }
311
-
312
- /**
313
- * 从 TOC.md 删除笔记及其缩进子树
314
- */
315
- async deleteNoteFromToc(noteIndex: string): Promise<void> {
316
- if (!existsSync(ROOT_TOC_PATH)) return
317
-
318
- const lines = await this.readTocLines()
319
- let lineIndex: number
320
- try {
321
- lineIndex = findTocLineIndex(lines, noteIndex)
322
- } catch {
323
- logger.warn(`TOC.md 中未找到笔记: ${noteIndex}`)
324
- return
325
- }
326
-
327
- const { start, end } = getTocEntrySubtreeRange(lines, lineIndex)
328
- lines.splice(start, end - start)
329
-
330
- await this.writeTocLines(lines)
331
- logger.info(`从 TOC.md 删除笔记及子树: ${noteIndex} (${end - start} 行)`)
332
- }
333
-
334
- /**
335
- * 重命名 TOC.md 中的笔记行(保留位置与缩进)
336
- */
337
- async renameNoteInToc(noteIndex: string, newDirName: string): Promise<void> {
338
- if (!existsSync(ROOT_TOC_PATH)) return
339
-
340
- const item = this.noteIndexCache.getByNoteIndex(noteIndex)
341
- const notes = this.noteIndexCache.isInitialized()
342
- ? this.noteIndexCache.toNoteInfoList()
343
- : this.noteManager.scanNotes()
344
-
345
- const note = resolveNoteFromIndex(noteIndex, notes)
346
- if (!note) return
347
-
348
- const tempNote: NoteInfo = {
349
- ...note,
350
- dirName: newDirName,
351
- config: item?.noteConfig ?? note.config,
352
- }
353
-
354
- const lines = await this.readTocLines()
355
- let updated = false
356
-
357
- for (let i = 0; i < lines.length; i++) {
358
- const parsed = parseTocLine(lines[i])
359
- if (parsed.noteIndex === noteIndex) {
360
- lines[i] = buildTocLine(
361
- tempNote,
362
- parsed.indentLevel,
363
- parsed.completed,
364
- )
365
- updated = true
366
- }
367
- }
368
-
369
- if (updated) {
370
- await this.writeTocLines(lines)
371
- logger.info(`TOC.md 重命名笔记: ${noteIndex} -> ${newDirName}`)
372
- }
373
- }
374
-
375
- /**
376
- * 移动笔记(含子树)到目标位置
377
- */
378
- async moveNoteInToc(
379
- noteIndex: string,
380
- target: MoveNoteTarget,
381
- ): Promise<void> {
382
- const lines = await this.readTocLines()
383
- const sourceIndex = findTocLineIndex(lines, noteIndex)
384
- await this.moveTocEntryByLineIndex(sourceIndex, target)
385
- }
386
-
387
- /**
388
- * 移动任意 TOC 行(分组或笔记)及其子树
389
- */
390
- async moveTocEntryByLineIndex(
391
- sourceTocLineIndex: number,
392
- target: MoveTocEntryTarget,
393
- ): Promise<void> {
394
- const lines = await this.readTocLines()
395
- const sourceParsed = parseTocLine(lines[sourceTocLineIndex])
396
- if (!sourceParsed.isMatch) {
397
- throw new Error(`无效的 TOC 行索引: ${sourceTocLineIndex}`)
398
- }
399
-
400
- const { start, end } = getTocEntrySubtreeRange(lines, sourceTocLineIndex)
401
- const resolvedTargetIndex = this.resolveMoveTargetLineIndex(lines, target)
402
-
403
- if (resolvedTargetIndex >= start && resolvedTargetIndex < end) {
404
- throw new Error('不能移动到自身或子树内')
405
- }
406
-
407
- const movingLines = lines.splice(start, end - start)
408
- if (movingLines.length === 0) {
409
- throw new Error(`无法移动空子树: 行 ${sourceTocLineIndex}`)
410
- }
411
-
412
- const targetLineIndex = adjustTocLineIndexAfterSubtreeRemoval(
413
- resolvedTargetIndex,
414
- start,
415
- end,
416
- )
417
- const targetParsed = parseTocLine(lines[targetLineIndex])
418
- if (!targetParsed.isMatch) {
419
- throw new Error(`无效的目标 TOC 行索引: ${targetLineIndex}`)
420
- }
421
-
422
- let insertIndex: number
423
- let newIndent: number
424
-
425
- if (target.placement === 'inside') {
426
- insertIndex = targetLineIndex + 1
427
- newIndent = targetParsed.indentLevel + 1
428
- } else if (target.placement === 'before') {
429
- insertIndex = targetLineIndex
430
- newIndent = targetParsed.indentLevel
431
- } else {
432
- insertIndex = getTocEntrySubtreeRange(lines, targetLineIndex).end
433
- newIndent = targetParsed.indentLevel
434
- }
435
-
436
- const oldBaseIndent = parseTocLine(movingLines[0]).indentLevel
437
- const indentDelta = newIndent - oldBaseIndent
438
- const adjustedLines = this.adjustSubtreeIndent(movingLines, indentDelta)
439
-
440
- const maxDepth = this.getSidebarMaxDepth()
441
- if (maxDepth > 0) {
442
- const maxIndent = this.getMaxSubtreeIndent(adjustedLines)
443
- const maxAllowedIndent = maxDepth - 1
444
- if (maxIndent > maxAllowedIndent) {
445
- throw new Error(`移动后将超出目录最大层级(${maxDepth} 层)`)
446
- }
447
- }
448
-
449
- lines.splice(insertIndex, 0, ...adjustedLines)
450
- await this.writeTocLines(lines)
451
-
452
- logger.info(
453
- `TOC.md 移动行 ${sourceTocLineIndex} -> 行 ${targetLineIndex} (${target.placement})`,
454
- )
455
- }
456
-
457
- /**
458
- * 语雀式移动:成为 target 的下一同级
459
- */
460
- async moveAfterByNodeId(
461
- sourceNodeId: string,
462
- targetNodeId: string,
463
- ): Promise<void> {
464
- const lines = await this.readTocLines()
465
- const sourceIndex = this.resolveNodeIdToTocLineIndex(lines, sourceNodeId)
466
- const targetIndex = this.resolveNodeIdToTocLineIndex(lines, targetNodeId)
467
- await this.moveTocEntryByLineIndex(sourceIndex, {
468
- targetTocLineIndex: targetIndex,
469
- placement: 'after',
470
- })
471
- }
472
-
473
- /**
474
- * 语雀式:prepend 到侧栏列表顶(无 target_uuid,book 即容器)
475
- */
476
- async prependToRootByNodeId(sourceNodeId: string): Promise<void> {
477
- const lines = await this.readTocLines()
478
- const sourceIndex = this.resolveNodeIdToTocLineIndex(lines, sourceNodeId)
479
- let targetLineIndex = 0
480
- while (
481
- targetLineIndex < lines.length &&
482
- !parseTocLine(lines[targetLineIndex]).isMatch
483
- ) {
484
- targetLineIndex++
485
- }
486
- if (targetLineIndex >= lines.length) {
487
- throw new Error('TOC.md 为空,无法移动到顶部')
488
- }
489
- await this.moveTocEntryByLineIndex(sourceIndex, {
490
- targetTocLineIndex: targetLineIndex,
491
- placement: 'before',
492
- })
493
- }
494
-
495
- /**
496
- * 语雀式移动:成为 target 的第一个子项
497
- */
498
- async prependChildByNodeId(
499
- sourceNodeId: string,
500
- targetNodeId: string,
501
- ): Promise<void> {
502
- const lines = await this.readTocLines()
503
- const sourceIndex = this.resolveNodeIdToTocLineIndex(lines, sourceNodeId)
504
- const targetIndex = this.resolveNodeIdToTocLineIndex(lines, targetNodeId)
505
- await this.moveTocEntryByLineIndex(sourceIndex, {
506
- targetTocLineIndex: targetIndex,
507
- placement: 'inside',
508
- })
509
- }
510
-
511
- resolveNodeIdToTocLineIndex(lines: string[], nodeId: string): number {
512
- const parsed = parseNodeId(nodeId)
513
- if (parsed.kind === 'note' && parsed.noteIndex) {
514
- return findTocLineIndex(lines, parsed.noteIndex)
515
- }
516
- if (parsed.kind === 'line' && parsed.tocLineIndex !== undefined) {
517
- return parsed.tocLineIndex
518
- }
519
- if (parsed.kind === 'folder' && parsed.folderPath) {
520
- return findFolderLineIndex(lines, parsed.folderPath)
521
- }
522
- throw new Error(`无法解析 nodeId: ${nodeId}`)
523
- }
524
-
525
- /**
526
- * 重命名 TOC 目录行
527
- */
528
- async renameFolderInToc(
529
- tocLineIndex: number,
530
- newTitle: string,
531
- ): Promise<void> {
532
- const lines = await this.readTocLines()
533
- const updated = renameFolderLine(lines, tocLineIndex, newTitle)
534
- await this.writeTocLines(updated)
535
- logger.info(`TOC.md 重命名目录行 ${tocLineIndex}: ${newTitle.trim()}`)
536
- }
537
-
538
- /**
539
- * 删除子树前获取回退笔记(子树之前的最后一篇笔记)
540
- */
541
- async getPreviousNoteIndexBeforeEntryDelete(
542
- tocLineIndex: number,
543
- ): Promise<string | null> {
544
- const lines = await this.readTocLines()
545
- return getPreviousNoteIndexOutsideSubtree(lines, tocLineIndex)
546
- }
547
-
548
- /**
549
- * 判断 noteIndex 是否在指定 TOC 行子树内
550
- */
551
- async isNoteInTocEntrySubtree(
552
- tocLineIndex: number,
553
- noteIndex: string,
554
- ): Promise<boolean> {
555
- const lines = await this.readTocLines()
556
- return isNoteIndexInSubtree(lines, tocLineIndex, noteIndex)
557
- }
558
-
559
- /**
560
- * 级联删除 TOC 子树,并删除子树内所有笔记磁盘目录
561
- */
562
- async deleteTocEntryCascade(tocLineIndex: number): Promise<string[]> {
563
- if (!existsSync(ROOT_TOC_PATH)) return []
564
-
565
- const lines = await this.readTocLines()
566
- const noteIndexes = collectNoteIndexesInSubtree(lines, tocLineIndex)
567
- const { start, end } = getTocEntrySubtreeRange(lines, tocLineIndex)
568
- lines.splice(start, end - start)
569
- await this.writeTocLines(lines)
570
-
571
- const noteService = NoteService.getInstance()
572
- for (const noteIndex of noteIndexes) {
573
- await noteService.deleteNote(noteIndex)
574
- }
575
-
576
- logger.info(
577
- `级联删除 TOC 行 ${tocLineIndex}(${end - start} 行,${noteIndexes.length} 篇笔记)`,
578
- )
579
- return noteIndexes
580
- }
581
-
582
- /**
583
- * 在指定 TOC 行(folder 或 note)子树末尾插入笔记
584
- */
585
- async insertNotesUnderTocLine(
586
- parentTocLineIndex: number,
587
- notesToInsert: NoteInfo[],
588
- ): Promise<void> {
589
- if (notesToInsert.length === 0) return
590
-
591
- const lines = await this.readTocLines()
592
- const parentParsed = parseTocLine(lines[parentTocLineIndex])
593
- if (!parentParsed.isMatch) {
594
- throw new Error(`无效的 TOC 行索引: ${parentTocLineIndex}`)
595
- }
596
-
597
- const childIndent = parentParsed.indentLevel + 1
598
- const { end } = getTocEntrySubtreeRange(lines, parentTocLineIndex)
599
-
600
- const noteLines = notesToInsert.map((note) =>
601
- buildTocLine(note, childIndent, getTocLineCompleted(note)),
602
- )
603
-
604
- lines.splice(end, 0, ...noteLines)
605
- await this.writeTocLines(lines)
606
- logger.info(
607
- `在 TOC 行 ${parentTocLineIndex} 下插入 ${notesToInsert.length} 篇笔记`,
608
- )
609
- }
610
-
611
- /**
612
- * 在指定 TOC 行(folder 或 note)子树末尾插入子目录
613
- */
614
- async insertFolderUnderParent(
615
- parentTocLineIndex: number,
616
- title: string,
617
- ): Promise<void> {
618
- const trimmed = title.trim()
619
- if (!trimmed) {
620
- throw new Error('目录标题不能为空')
621
- }
622
-
623
- const lines = await this.readTocLines()
624
- const parentParsed = parseTocLine(lines[parentTocLineIndex])
625
- if (!parentParsed.isMatch) {
626
- throw new Error(`无效的 TOC 行索引: ${parentTocLineIndex}`)
627
- }
628
-
629
- const childIndent = parentParsed.indentLevel + 1
630
- const { end } = getTocEntrySubtreeRange(lines, parentTocLineIndex)
631
- lines.splice(end, 0, buildFolderTocLine(trimmed, childIndent))
632
- await this.writeTocLines(lines)
633
- logger.info(`在 TOC 行 ${parentTocLineIndex} 下插入目录: ${trimmed}`)
634
- }
635
-
636
- /**
637
- * 在指定笔记上方或下方插入笔记
638
- */
639
- async insertNotesAroundNote(
640
- targetNoteIndex: string,
641
- notesToInsert: NoteInfo[],
642
- placement: NoteInsertPlacement,
643
- ): Promise<void> {
644
- if (notesToInsert.length === 0) return
645
-
646
- const lines = await this.readTocLines()
647
- const targetIndex = findTocLineIndex(lines, targetNoteIndex)
648
- const targetParsed = parseTocLine(lines[targetIndex])
649
- const indent = targetParsed.indentLevel
650
-
651
- const noteLines = notesToInsert.map((note) =>
652
- buildTocLine(note, indent, getTocLineCompleted(note)),
653
- )
654
-
655
- const insertIndex =
656
- placement === 'before'
657
- ? targetIndex
658
- : getTocEntrySubtreeRange(lines, targetIndex).end
659
- lines.splice(insertIndex, 0, ...noteLines)
660
-
661
- await this.writeTocLines(lines)
662
- logger.info(
663
- `在笔记 ${targetNoteIndex} ${placement === 'before' ? '上方' : '下方'}插入 ${notesToInsert.length} 篇笔记`,
664
- )
665
- }
666
-
667
- /**
668
- * 在目录子树末尾插入笔记
669
- */
670
- async insertNotesUnderFolder(
671
- folderPath: string[],
672
- notesToInsert: NoteInfo[],
673
- ): Promise<void> {
674
- if (notesToInsert.length === 0) return
675
-
676
- const lines = await this.readTocLines()
677
- const folderIndex = findFolderLineIndex(lines, folderPath)
678
- await this.insertNotesUnderTocLine(folderIndex, notesToInsert)
679
- }
680
-
681
- async insertNotesUnderParent(
682
- parentNoteIndex: string,
683
- notesToInsert: NoteInfo[],
684
- ): Promise<void> {
685
- if (notesToInsert.length === 0) return
686
-
687
- const lines = await this.readTocLines()
688
- const parentIndex = findTocLineIndex(lines, parentNoteIndex)
689
- await this.insertNotesUnderTocLine(parentIndex, notesToInsert)
690
- }
691
-
692
- /**
693
- * 规范化 TOC 并刷新 sidebar
694
- */
695
- async refreshTocAndSidebar(notes?: NoteInfo[]): Promise<void> {
696
- const allNotes =
697
- notes ??
698
- (this.noteIndexCache.isInitialized()
699
- ? this.noteIndexCache.toNoteInfoList()
700
- : this.noteManager.scanNotes())
701
-
702
- await this.normalizeToc(allNotes)
703
- await this.regenerateSidebar(allNotes)
704
- }
705
-
706
- private resolveMoveTargetLineIndex(
707
- lines: string[],
708
- target: MoveNoteTarget,
709
- ): number {
710
- if ('targetTocLineIndex' in target) {
711
- return target.targetTocLineIndex
712
- }
713
- if (target.targetType === 'folder') {
714
- return findFolderLineIndex(lines, target.targetFolderPath)
715
- }
716
- return findTocLineIndex(lines, target.targetNoteIndex)
717
- }
718
-
719
- private getSidebarMaxDepth(): number {
720
- const config = this.configManager.getAll() as { sidebarMaxDepth?: number }
721
- const depth = config.sidebarMaxDepth ?? 0
722
- return depth > 0 ? depth : 0
723
- }
724
-
725
- private getMaxSubtreeIndent(lines: string[]): number {
726
- let max = 0
727
- for (const line of lines) {
728
- const parsed = parseTocLine(line)
729
- if (parsed.isMatch) {
730
- max = Math.max(max, parsed.indentLevel)
731
- }
732
- }
733
- return max
734
- }
735
-
736
- private adjustSubtreeIndent(lines: string[], delta: number): string[] {
737
- if (delta === 0) return lines
738
-
739
- return lines.map((line) => {
740
- const parsed = parseTocLine(line)
741
- if (!parsed.isMatch) return line
742
-
743
- const newIndent = Math.max(0, parsed.indentLevel + delta)
744
- const withoutIndent = line.trimStart()
745
- return `${' '.repeat(newIndent * TOC_INDENT_SPACES)}${withoutIndent}`
746
- })
747
- }
748
-
749
- private async readTocLines(): Promise<string[]> {
750
- this.ensureTocExists()
751
- const content = await fsPromises.readFile(ROOT_TOC_PATH, 'utf-8')
752
- return content.split('\n')
753
- }
754
-
755
- private async writeTocLines(lines: string[]): Promise<void> {
756
- const content = processTocEmptyLines(lines).join('\n')
757
- await fsPromises.writeFile(ROOT_TOC_PATH, content, 'utf-8')
758
- }
759
- }