@tnotesjs/core 0.6.1 → 0.7.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.
- package/config/templates.ts +0 -5
- package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
- package/dist/chunk-AGVER5MX.cjs +1746 -0
- package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
- package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
- package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
- package/dist/chunk-ZEQS64PE.js +1746 -0
- package/dist/cli/index.cjs +76 -155
- package/dist/cli/index.js +10 -89
- package/dist/markdown/index.cjs +2 -3
- package/dist/markdown/index.d.cts +1 -1
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/index.js +1 -2
- package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
- package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
- package/dist/vitepress/config/index.cjs +200 -132
- package/dist/vitepress/config/index.js +187 -119
- package/dist/workspace/index.cjs +4 -1202
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.ts +2 -2
- package/dist/workspace/index.js +4 -1202
- package/package.json +1 -1
- package/services/file-watcher/folderChangeHandler.ts +19 -15
- package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
- package/services/file-watcher/service.ts +1 -6
- package/services/index.ts +0 -1
- package/services/note/service.ts +4 -6
- package/services/readme/service.ts +11 -18
- package/services/reconcileToc.ts +20 -0
- package/utils/index.ts +0 -9
- package/{services/toc → utils}/moveTocInside.test.ts +1 -1
- package/vitepress/components/constants.ts +2 -2
- package/vitepress/components/sidebar.data.ts +8 -5
- package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
- package/dist/chunk-3R7QSABB.cjs +0 -502
- package/dist/chunk-CZXRQPFJ.js +0 -11
- package/dist/chunk-HXED7KVT.cjs +0 -11
- package/dist/chunk-U6IBLREL.js +0 -502
- package/services/toc/index.ts +0 -5
- package/services/toc/service.ts +0 -759
- package/utils/migrateReadmeToToc.test.ts +0 -111
- package/utils/migrateReadmeToToc.ts +0 -135
package/services/toc/service.ts
DELETED
|
@@ -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
|
-
}
|