@tnotesjs/core 0.2.0 → 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/config/ConfigManager.ts +137 -0
- package/config/constants.ts +121 -0
- package/config/index.ts +25 -0
- package/config/templates.ts +49 -0
- package/core/GitManager.ts +513 -0
- package/core/NoteIndexCache.ts +194 -0
- package/core/NoteManager.ts +407 -0
- package/core/ProcessManager.ts +180 -0
- package/core/ReadmeGenerator.ts +215 -0
- package/core/TocGenerator.ts +212 -0
- package/core/index.ts +11 -0
- package/dist/chunk-5QN44ES5.js +6050 -0
- package/dist/chunk-5Y5IYS6C.js +200 -0
- package/dist/cli/index.js +986 -0
- package/dist/index.js +8 -0
- package/dist/vitepress/config/index.js +1686 -0
- package/package.json +6 -2
- package/services/file-watcher/configChangeHandler.ts +64 -0
- package/services/file-watcher/eventScheduler.ts +179 -0
- package/services/file-watcher/folderChangeHandler.ts +325 -0
- package/services/file-watcher/fsWatcherAdapter.ts +128 -0
- package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
- package/services/file-watcher/index.ts +7 -0
- package/services/file-watcher/internal.ts +79 -0
- package/services/file-watcher/readmeChangeHandler.ts +28 -0
- package/services/file-watcher/renameDetector.ts +120 -0
- package/services/file-watcher/service.ts +352 -0
- package/services/file-watcher/watchState.ts +194 -0
- package/services/git/index.ts +7 -0
- package/services/git/service.ts +114 -0
- package/services/index.ts +15 -0
- package/services/init-sub-repo/index.ts +2 -0
- package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
- package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
- package/services/init-sub-repo/service.ts +101 -0
- package/services/note/index.ts +7 -0
- package/services/note/service.ts +362 -0
- package/services/readme/index.ts +7 -0
- package/services/readme/service.ts +761 -0
- package/services/timestamp/index.ts +7 -0
- package/services/timestamp/service.ts +465 -0
- package/services/toc/index.ts +5 -0
- package/services/toc/moveTocInside.test.ts +73 -0
- package/services/toc/service.ts +759 -0
- package/services/vitepress/index.ts +7 -0
- package/services/vitepress/service.ts +339 -0
- package/utils/errorHandler.ts +174 -0
- package/utils/file.ts +17 -0
- package/utils/genHierarchicalSidebar.ts +69 -0
- package/utils/generateAnchor.ts +24 -0
- package/utils/getChangedIds.ts +35 -0
- package/utils/index.ts +71 -0
- package/utils/logger.ts +231 -0
- package/utils/markdown.ts +75 -0
- package/utils/migrateReadmeToToc.test.ts +111 -0
- package/utils/migrateReadmeToToc.ts +135 -0
- package/utils/parseArgs.ts +90 -0
- package/utils/parseReadmeCompletedNotes.test.ts +90 -0
- package/utils/parseReadmeCompletedNotes.ts +108 -0
- package/utils/portUtils.ts +113 -0
- package/utils/readmeHelpers.ts +190 -0
- package/utils/runCommand.ts +29 -0
- package/utils/tocHelpers.test.ts +278 -0
- package/utils/tocHelpers.ts +855 -0
- package/utils/tocNodeId.test.ts +60 -0
- package/utils/tocNodeId.ts +97 -0
- package/utils/validators.ts +102 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/tocHelpers.test.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
parseTocLine,
|
|
9
|
+
parseTocToTree,
|
|
10
|
+
serializeTocTree,
|
|
11
|
+
getSubtreeLineRange,
|
|
12
|
+
getFolderSubtreeRange,
|
|
13
|
+
getTocEntrySubtreeRange,
|
|
14
|
+
adjustTocLineIndexAfterSubtreeRemoval,
|
|
15
|
+
collectNoteIndexesInSubtree,
|
|
16
|
+
renameFolderLine,
|
|
17
|
+
getPreviousNoteIndexOutsideSubtree,
|
|
18
|
+
isNoteIndexInSubtree,
|
|
19
|
+
findFolderLineIndex,
|
|
20
|
+
parseTocCompletedNotes,
|
|
21
|
+
buildSidebarFromTocTree,
|
|
22
|
+
getPreviousTocNoteIndex,
|
|
23
|
+
} from './tocHelpers'
|
|
24
|
+
|
|
25
|
+
import type { NoteInfo } from '../types'
|
|
26
|
+
|
|
27
|
+
function makeNote(index: string, dirName: string, done = false): NoteInfo {
|
|
28
|
+
return {
|
|
29
|
+
index,
|
|
30
|
+
dirName,
|
|
31
|
+
path: `/notes/${dirName}`,
|
|
32
|
+
readmePath: `/notes/${dirName}/README.md`,
|
|
33
|
+
configPath: `/notes/${dirName}/.tnotes.json`,
|
|
34
|
+
config: {
|
|
35
|
+
id: `id-${index}`,
|
|
36
|
+
bilibili: [],
|
|
37
|
+
tnotes: [],
|
|
38
|
+
yuque: [],
|
|
39
|
+
done,
|
|
40
|
+
enableDiscussions: false,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const notes: NoteInfo[] = [
|
|
46
|
+
makeNote('0001', '0001. Alpha', true),
|
|
47
|
+
makeNote('0002', '0002. Beta', false),
|
|
48
|
+
makeNote('0003', '0003. Giscus', false),
|
|
49
|
+
makeNote('0019', '0019. Author', true),
|
|
50
|
+
makeNote('0027', '0027. emoji', true),
|
|
51
|
+
makeNote('0036', '0036. Workflow', false),
|
|
52
|
+
makeNote('0037', '0037. Writing guide', false),
|
|
53
|
+
makeNote('0039', '0039. new', false),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
describe('parseTocLine', () => {
|
|
57
|
+
it('parses canonical note with title', () => {
|
|
58
|
+
const result = parseTocLine(' - [x] 0002. Beta')
|
|
59
|
+
expect(result.kind).toBe('note')
|
|
60
|
+
expect(result.noteIndex).toBe('0002')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('parses folder line', () => {
|
|
64
|
+
const result = parseTocLine('- TNotes 组件')
|
|
65
|
+
expect(result.kind).toBe('folder')
|
|
66
|
+
expect(result.folderTitle).toBe('TNotes 组件')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('parseTocToTree / serializeTocTree', () => {
|
|
71
|
+
it('assigns tocLineIndex from source lines', () => {
|
|
72
|
+
const lines = [
|
|
73
|
+
'- Group',
|
|
74
|
+
' - [x] 0001. Alpha',
|
|
75
|
+
'- [ ] 0003. Gamma',
|
|
76
|
+
]
|
|
77
|
+
const tree = parseTocToTree(lines, notes)
|
|
78
|
+
expect(tree[0].kind).toBe('folder')
|
|
79
|
+
if (tree[0].kind === 'folder') {
|
|
80
|
+
expect(tree[0].tocLineIndex).toBe(0)
|
|
81
|
+
expect(tree[0].children[0]).toMatchObject({
|
|
82
|
+
kind: 'note',
|
|
83
|
+
noteIndex: '0001',
|
|
84
|
+
tocLineIndex: 1,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
expect(tree[1]).toMatchObject({ kind: 'note', tocLineIndex: 2 })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('parses note -> folder -> note nesting (introduction 0027 case)', () => {
|
|
91
|
+
const lines = [
|
|
92
|
+
'- Writing',
|
|
93
|
+
' - [ ] 0037. Writing guide',
|
|
94
|
+
' - [x] 0027. emoji',
|
|
95
|
+
' - Giscus',
|
|
96
|
+
' - [ ] 0003. Giscus',
|
|
97
|
+
]
|
|
98
|
+
const tree = parseTocToTree(lines, notes)
|
|
99
|
+
const writing = tree[0]
|
|
100
|
+
expect(writing.kind).toBe('folder')
|
|
101
|
+
if (writing.kind !== 'folder') return
|
|
102
|
+
|
|
103
|
+
const emoji = writing.children[1]
|
|
104
|
+
expect(emoji.kind).toBe('note')
|
|
105
|
+
if (emoji.kind !== 'note') return
|
|
106
|
+
|
|
107
|
+
expect(emoji.tocLineIndex).toBe(2)
|
|
108
|
+
expect(emoji.children[0].kind).toBe('folder')
|
|
109
|
+
if (emoji.children[0].kind === 'folder') {
|
|
110
|
+
expect(emoji.children[0].title).toBe('Giscus')
|
|
111
|
+
expect(emoji.children[0].tocLineIndex).toBe(3)
|
|
112
|
+
expect(emoji.children[0].children[0]).toMatchObject({
|
|
113
|
+
kind: 'note',
|
|
114
|
+
noteIndex: '0003',
|
|
115
|
+
tocLineIndex: 4,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('round-trips mixed nesting', () => {
|
|
121
|
+
const lines = [
|
|
122
|
+
'- [x] 0019. Author',
|
|
123
|
+
' - Appendix',
|
|
124
|
+
' - [ ] 0036. Workflow',
|
|
125
|
+
]
|
|
126
|
+
const tree = parseTocToTree(lines, notes)
|
|
127
|
+
expect(serializeTocTree(tree, notes)).toEqual(lines)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('distinguishes duplicate folder titles by tocLineIndex', () => {
|
|
131
|
+
const lines = [
|
|
132
|
+
'- [x] 0019. Author',
|
|
133
|
+
' - Appendix',
|
|
134
|
+
' - [ ] 0036. Workflow',
|
|
135
|
+
'- [ ] 0039. new',
|
|
136
|
+
' - Appendix',
|
|
137
|
+
' - [ ] 0002. Beta',
|
|
138
|
+
]
|
|
139
|
+
const tree = parseTocToTree(lines, notes)
|
|
140
|
+
const folders: number[] = []
|
|
141
|
+
function walk(nodes: typeof tree) {
|
|
142
|
+
for (const node of nodes) {
|
|
143
|
+
if (node.kind === 'folder') {
|
|
144
|
+
folders.push(node.tocLineIndex)
|
|
145
|
+
walk(node.children)
|
|
146
|
+
} else {
|
|
147
|
+
walk(node.children)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
walk(tree)
|
|
152
|
+
expect(folders).toEqual([1, 4])
|
|
153
|
+
expect(findFolderLineIndex(lines, ['Appendix'])).toBe(1)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('getTocEntrySubtreeRange', () => {
|
|
158
|
+
it('covers note and nested folder children', () => {
|
|
159
|
+
const lines = [
|
|
160
|
+
' - [x] 0027. emoji',
|
|
161
|
+
' - Giscus',
|
|
162
|
+
' - [ ] 0003. Giscus',
|
|
163
|
+
'- [ ] 0039. new',
|
|
164
|
+
]
|
|
165
|
+
expect(getTocEntrySubtreeRange(lines, 0)).toEqual({ start: 0, end: 3 })
|
|
166
|
+
expect(getTocEntrySubtreeRange(lines, 1)).toEqual({ start: 1, end: 3 })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('matches getSubtreeLineRange and getFolderSubtreeRange', () => {
|
|
170
|
+
const lines = ['- Group', ' - [x] 0001. Alpha', '- [ ] 0003. Gamma']
|
|
171
|
+
expect(getTocEntrySubtreeRange(lines, 0)).toEqual(
|
|
172
|
+
getFolderSubtreeRange(lines, 0),
|
|
173
|
+
)
|
|
174
|
+
expect(getTocEntrySubtreeRange(lines, 1)).toEqual(
|
|
175
|
+
getSubtreeLineRange(lines, 1),
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('buildSidebarFromTocTree', () => {
|
|
181
|
+
it('includes tocLineIndex on folder and note items', () => {
|
|
182
|
+
const tree = parseTocToTree(
|
|
183
|
+
['- Components', ' - [ ] 0002. Beta', '- [x] 0019. Author', ' - [ ] 0036. Workflow'],
|
|
184
|
+
notes,
|
|
185
|
+
)
|
|
186
|
+
const sidebar = buildSidebarFromTocTree(tree, notes, {
|
|
187
|
+
sidebarShowNoteId: true,
|
|
188
|
+
})
|
|
189
|
+
expect(sidebar[0].tocLineIndex).toBe(0)
|
|
190
|
+
expect(sidebar[0].items![0].tocLineIndex).toBe(1)
|
|
191
|
+
expect(sidebar[1].tocLineIndex).toBe(2)
|
|
192
|
+
expect(sidebar[1].items![0].tocLineIndex).toBe(3)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('folder under note renders with link parent and folder child', () => {
|
|
196
|
+
const tree = parseTocToTree(
|
|
197
|
+
['- [x] 0027. emoji', ' - Giscus', ' - [ ] 0003. Giscus'],
|
|
198
|
+
notes,
|
|
199
|
+
)
|
|
200
|
+
const sidebar = buildSidebarFromTocTree(tree, notes, {
|
|
201
|
+
sidebarShowNoteId: true,
|
|
202
|
+
})
|
|
203
|
+
expect(sidebar[0].link).toBeDefined()
|
|
204
|
+
expect(sidebar[0].items![0].link).toBeUndefined()
|
|
205
|
+
expect(sidebar[0].items![0].tocLineIndex).toBe(1)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('getPreviousTocNoteIndex', () => {
|
|
210
|
+
it('returns previous note in document order', () => {
|
|
211
|
+
const lines = ['- [x] 0001. Alpha', '- [ ] 0003. Gamma']
|
|
212
|
+
expect(getPreviousTocNoteIndex(lines, '0003')).toBe('0001')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('parseTocCompletedNotes', () => {
|
|
217
|
+
it('counts completed notes', () => {
|
|
218
|
+
const content = ['- [x] 0001. Alpha', '- [ ] 0002. Beta'].join('\n')
|
|
219
|
+
const result = parseTocCompletedNotes(content)
|
|
220
|
+
expect(result.completedCount).toBe(1)
|
|
221
|
+
expect(result.totalCount).toBe(2)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('folder subtree helpers', () => {
|
|
226
|
+
const introLines = [
|
|
227
|
+
'- TNotes 笔记书写规范',
|
|
228
|
+
' - [ ] 0037. Writing guide',
|
|
229
|
+
' - [x] 0027. emoji',
|
|
230
|
+
' - 评论功能的技术实现(Giscus)',
|
|
231
|
+
' - [ ] 0003. Giscus',
|
|
232
|
+
' - [ ] 0023. git log',
|
|
233
|
+
' - [ ] 0009. search',
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
it('collectNoteIndexesInSubtree on 0027 includes nested notes', () => {
|
|
237
|
+
const indexes = collectNoteIndexesInSubtree(introLines, 2)
|
|
238
|
+
expect(indexes).toEqual(['0027', '0003', '0023', '0009'])
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('collectNoteIndexesInSubtree on giscus folder excludes 0027', () => {
|
|
242
|
+
const indexes = collectNoteIndexesInSubtree(introLines, 3)
|
|
243
|
+
expect(indexes).toEqual(['0003', '0023', '0009'])
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('renameFolderLine updates folder title only', () => {
|
|
247
|
+
const updated = renameFolderLine(introLines, 3, 'Giscus Renamed')
|
|
248
|
+
expect(updated[3]).toBe(' - Giscus Renamed')
|
|
249
|
+
expect(updated[2]).toBe(introLines[2])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('getPreviousNoteIndexOutsideSubtree for giscus folder', () => {
|
|
253
|
+
expect(getPreviousNoteIndexOutsideSubtree(introLines, 3)).toBe('0027')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('isNoteIndexInSubtree detects membership', () => {
|
|
257
|
+
expect(isNoteIndexInSubtree(introLines, 2, '0003')).toBe(true)
|
|
258
|
+
expect(isNoteIndexInSubtree(introLines, 3, '0027')).toBe(false)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('adjustTocLineIndexAfterSubtreeRemoval', () => {
|
|
263
|
+
it('shifts target line index when source subtree is removed above it', () => {
|
|
264
|
+
// introduction:0019 子树占 37–43,0039 原在行 45
|
|
265
|
+
expect(adjustTocLineIndexAfterSubtreeRemoval(45, 37, 43)).toBe(39)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('keeps target line index when target is above removed subtree', () => {
|
|
269
|
+
expect(adjustTocLineIndexAfterSubtreeRemoval(30, 37, 43)).toBe(30)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('parseTocLine safety', () => {
|
|
274
|
+
it('does not throw on undefined line', () => {
|
|
275
|
+
expect(parseTocLine(undefined).isMatch).toBe(false)
|
|
276
|
+
expect(parseTocLine(null).isMatch).toBe(false)
|
|
277
|
+
})
|
|
278
|
+
})
|