@tnotesjs/core 0.2.1 → 0.3.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/README.md +0 -1
- package/config/ConfigManager.ts +137 -0
- package/config/constants.ts +121 -0
- package/config/defaultConfig.ts +33 -4
- package/config/index.ts +31 -0
- package/config/templates.ts +49 -0
- package/core/GitManager.ts +513 -0
- package/core/NoteIndexCache.ts +200 -0
- package/core/NoteManager.ts +395 -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-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
- package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
- package/dist/cli/index.js +26 -78
- package/dist/index.js +1 -1
- package/dist/vitepress/config/index.js +2 -2
- package/package.json +5 -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 +14 -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 +303 -0
- package/services/init-sub-repo/service.ts +101 -0
- package/services/note/index.ts +7 -0
- package/services/note/service.ts +361 -0
- package/services/readme/index.ts +7 -0
- package/services/readme/service.ts +761 -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/types/config.ts +0 -3
- package/types/note.ts +0 -2
- 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
- package/vitepress/assets/icons/icon__cursor.svg +4 -0
- package/vitepress/assets/icons/index.ts +1 -0
- package/vitepress/components/Layout/AboutPanel.vue +0 -24
- package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
- package/vitepress/components/Layout/DocFooter.vue +3 -3
- package/vitepress/components/Layout/Layout.vue +0 -30
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
- package/vitepress/components/Layout/homeReadme.data.ts +0 -48
- package/vitepress/components/MarkMap/MarkMap.vue +6 -13
- package/vitepress/components/Settings/Settings.vue +76 -73
- package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
- package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
- package/vitepress/components/composables/useLocalIde.ts +83 -0
- package/vitepress/components/constants.ts +5 -6
- package/vitepress/components/utils/vscodePaths.test.ts +22 -0
- package/vitepress/components/utils/vscodePaths.ts +24 -2
- package/vitepress/theme/styles/doc-layout.scss +4 -4
package/utils/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/index.ts
|
|
3
|
+
*
|
|
4
|
+
* util 统一入口
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { handleError, createError } from './errorHandler'
|
|
8
|
+
export { ensureDirectory } from './file'
|
|
9
|
+
export { generateAnchor } from './generateAnchor'
|
|
10
|
+
export { genHierarchicalSidebar } from './genHierarchicalSidebar'
|
|
11
|
+
export { getChangedIds } from './getChangedIds'
|
|
12
|
+
export { LogLevel, Logger, logger, createLogger } from './logger'
|
|
13
|
+
export { createAddNumberToTitle, generateToc } from './markdown'
|
|
14
|
+
export { parseArgs } from './parseArgs'
|
|
15
|
+
export { parseReadmeCompletedNotes } from './parseReadmeCompletedNotes'
|
|
16
|
+
export {
|
|
17
|
+
parseTocLine,
|
|
18
|
+
buildTocLine,
|
|
19
|
+
buildFolderTocLine,
|
|
20
|
+
parseTocToTree,
|
|
21
|
+
parseTocToMutableTree,
|
|
22
|
+
migrateLegacyNoteParents,
|
|
23
|
+
serializeTocTree,
|
|
24
|
+
buildTreeFromFlatNodes,
|
|
25
|
+
parseTocLinesToFlatNodes,
|
|
26
|
+
getSubtreeLineRange,
|
|
27
|
+
getFolderSubtreeRange,
|
|
28
|
+
getTocEntrySubtreeRange,
|
|
29
|
+
collectNoteIndexesInSubtree,
|
|
30
|
+
renameFolderLine,
|
|
31
|
+
getPreviousNoteIndexOutsideSubtree,
|
|
32
|
+
isNoteIndexInSubtree,
|
|
33
|
+
findTocLineIndex,
|
|
34
|
+
findFolderLineIndex,
|
|
35
|
+
findTocEntryLineIndex,
|
|
36
|
+
getPreviousTocNoteIndex,
|
|
37
|
+
processTocEmptyLines,
|
|
38
|
+
parseTocCompletedNotes,
|
|
39
|
+
buildSidebarFromTocTree,
|
|
40
|
+
resolveNoteFromIndex,
|
|
41
|
+
getTocLineCompleted,
|
|
42
|
+
folderTitleFromNoteDirName,
|
|
43
|
+
isTocContentLine,
|
|
44
|
+
TOC_INDENT_SPACES,
|
|
45
|
+
} from './tocHelpers'
|
|
46
|
+
export type {
|
|
47
|
+
ParsedTocLine,
|
|
48
|
+
TocNode,
|
|
49
|
+
TocTreeNode,
|
|
50
|
+
TocFolderNode,
|
|
51
|
+
TocNoteNode,
|
|
52
|
+
TocSidebarItem,
|
|
53
|
+
TocLineKind,
|
|
54
|
+
} from './tocHelpers'
|
|
55
|
+
export {
|
|
56
|
+
migrateReadmeToToc,
|
|
57
|
+
extractReadmeBodyAfterToc,
|
|
58
|
+
README_TOC_END_TAG,
|
|
59
|
+
} from './migrateReadmeToToc'
|
|
60
|
+
export type {
|
|
61
|
+
MigratedTocEntry,
|
|
62
|
+
MigrateReadmeToTocResult,
|
|
63
|
+
} from './migrateReadmeToToc'
|
|
64
|
+
export { isPortInUse, killPortProcess, waitForPort } from './portUtils'
|
|
65
|
+
export {
|
|
66
|
+
parseNoteLine,
|
|
67
|
+
buildNoteLineMarkdown,
|
|
68
|
+
processEmptyLines,
|
|
69
|
+
} from './readmeHelpers'
|
|
70
|
+
export { runCommand } from './runCommand'
|
|
71
|
+
export { validateNoteTitle } from './validators'
|
package/utils/logger.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/logger.ts
|
|
3
|
+
*
|
|
4
|
+
* 统一的日志系统
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 日志级别枚举
|
|
9
|
+
*/
|
|
10
|
+
export enum LogLevel {
|
|
11
|
+
DEBUG = 0,
|
|
12
|
+
INFO = 1,
|
|
13
|
+
WARN = 2,
|
|
14
|
+
ERROR = 3,
|
|
15
|
+
SILENT = 4,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 日志配置接口
|
|
20
|
+
*/
|
|
21
|
+
interface LoggerConfig {
|
|
22
|
+
level?: LogLevel
|
|
23
|
+
prefix?: string
|
|
24
|
+
timestamp?: boolean
|
|
25
|
+
colors?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 日志记录器类
|
|
30
|
+
*/
|
|
31
|
+
export class Logger {
|
|
32
|
+
private level: LogLevel
|
|
33
|
+
private prefix: string
|
|
34
|
+
private timestamp: boolean
|
|
35
|
+
private colors: boolean
|
|
36
|
+
|
|
37
|
+
constructor(config: LoggerConfig = {}) {
|
|
38
|
+
this.level = config.level ?? LogLevel.INFO
|
|
39
|
+
this.prefix = config.prefix ?? ''
|
|
40
|
+
this.timestamp = config.timestamp ?? false
|
|
41
|
+
this.colors = config.colors ?? true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 设置日志级别
|
|
46
|
+
*/
|
|
47
|
+
setLevel(level: LogLevel): void {
|
|
48
|
+
this.level = level
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 获取当前时间戳(精确到毫秒)
|
|
53
|
+
*/
|
|
54
|
+
private getTimestamp(): string {
|
|
55
|
+
if (!this.timestamp) return ''
|
|
56
|
+
const now = new Date()
|
|
57
|
+
const hours = String(now.getHours()).padStart(2, '0')
|
|
58
|
+
const minutes = String(now.getMinutes()).padStart(2, '0')
|
|
59
|
+
const seconds = String(now.getSeconds()).padStart(2, '0')
|
|
60
|
+
const milliseconds = String(now.getMilliseconds()).padStart(3, '0')
|
|
61
|
+
return `[${hours}:${minutes}:${seconds}.${milliseconds}] `
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 格式化日志消息
|
|
66
|
+
*/
|
|
67
|
+
private formatMessage(message: string): string {
|
|
68
|
+
const timestamp = this.getTimestamp()
|
|
69
|
+
const prefix = this.prefix ? `[${this.prefix}] ` : ''
|
|
70
|
+
return `${timestamp}${prefix}${message}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* DEBUG 级别日志
|
|
75
|
+
*/
|
|
76
|
+
debug(message: string, ...args: any[]): void {
|
|
77
|
+
if (this.level <= LogLevel.DEBUG) {
|
|
78
|
+
console.log(`🐛 ${this.formatMessage(message)}`, ...args)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* INFO 级别日志
|
|
84
|
+
*/
|
|
85
|
+
info(message: string, ...args: any[]): void {
|
|
86
|
+
if (this.level <= LogLevel.INFO) {
|
|
87
|
+
console.log(`ℹ️ ${this.formatMessage(message)}`, ...args)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 成功日志(特殊的 INFO 级别)
|
|
93
|
+
*/
|
|
94
|
+
success(message: string, ...args: any[]): void {
|
|
95
|
+
if (this.level <= LogLevel.INFO) {
|
|
96
|
+
console.log(`✅ ${this.formatMessage(message)}`, ...args)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 警告日志
|
|
102
|
+
*/
|
|
103
|
+
warn(message: string, ...args: any[]): void {
|
|
104
|
+
if (this.level <= LogLevel.WARN) {
|
|
105
|
+
console.warn(`⚠️ ${this.formatMessage(message)}`, ...args)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 错误日志
|
|
111
|
+
*/
|
|
112
|
+
error(message: string, ...args: any[]): void {
|
|
113
|
+
if (this.level <= LogLevel.ERROR) {
|
|
114
|
+
console.error(`❌ ${this.formatMessage(message)}`, ...args)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 进度日志
|
|
120
|
+
*/
|
|
121
|
+
progress(message: string, ...args: any[]): void {
|
|
122
|
+
if (this.level <= LogLevel.INFO) {
|
|
123
|
+
console.log(`⏳ ${this.formatMessage(message)}`, ...args)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 启动日志
|
|
129
|
+
*/
|
|
130
|
+
start(message: string, ...args: any[]): void {
|
|
131
|
+
if (this.level <= LogLevel.INFO) {
|
|
132
|
+
console.log(`🚀 ${this.formatMessage(message)}`, ...args)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 停止日志
|
|
138
|
+
*/
|
|
139
|
+
stop(message: string, ...args: any[]): void {
|
|
140
|
+
if (this.level <= LogLevel.INFO) {
|
|
141
|
+
console.log(`🛑 ${this.formatMessage(message)}`, ...args)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 完成日志
|
|
147
|
+
*/
|
|
148
|
+
done(message: string, duration?: number, ...args: any[]): void {
|
|
149
|
+
if (this.level <= LogLevel.INFO) {
|
|
150
|
+
const durationStr = duration ? ` (${duration}ms)` : ''
|
|
151
|
+
console.log(`✨ ${this.formatMessage(message)}${durationStr}`, ...args)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 链接日志
|
|
157
|
+
*/
|
|
158
|
+
link(message: string, url: string, ...args: any[]): void {
|
|
159
|
+
if (this.level <= LogLevel.INFO) {
|
|
160
|
+
console.log(`🔗 ${this.formatMessage(message)} ${url}`, ...args)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 文件操作日志
|
|
166
|
+
*/
|
|
167
|
+
file(action: string, path: string, ...args: any[]): void {
|
|
168
|
+
if (this.level <= LogLevel.INFO) {
|
|
169
|
+
console.log(`📄 ${this.formatMessage(`${action}: ${path}`)}`, ...args)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Git 操作日志
|
|
175
|
+
*/
|
|
176
|
+
git(message: string, ...args: any[]): void {
|
|
177
|
+
if (this.level <= LogLevel.INFO) {
|
|
178
|
+
console.log(`📦 ${this.formatMessage(message)}`, ...args)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 创建子 Logger
|
|
184
|
+
*/
|
|
185
|
+
child(prefix: string): Logger {
|
|
186
|
+
return new Logger({
|
|
187
|
+
level: this.level,
|
|
188
|
+
prefix: this.prefix ? `${this.prefix}:${prefix}` : prefix,
|
|
189
|
+
timestamp: this.timestamp,
|
|
190
|
+
colors: this.colors,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 执行函数并记录耗时
|
|
196
|
+
*/
|
|
197
|
+
async time<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
198
|
+
this.progress(`${label}...`)
|
|
199
|
+
const startTime = Date.now()
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const result = await fn()
|
|
203
|
+
const duration = Date.now() - startTime
|
|
204
|
+
this.done(label, duration)
|
|
205
|
+
return result
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const duration = Date.now() - startTime
|
|
208
|
+
this.error(`${label} failed after ${duration}ms`)
|
|
209
|
+
throw error
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 默认 Logger 实例
|
|
216
|
+
*/
|
|
217
|
+
export const logger = new Logger({
|
|
218
|
+
level: process.env.DEBUG ? LogLevel.DEBUG : LogLevel.INFO,
|
|
219
|
+
timestamp: true, // 启用时间戳(精确到毫秒)
|
|
220
|
+
colors: true,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 创建带前缀的 Logger
|
|
225
|
+
*/
|
|
226
|
+
export function createLogger(prefix: string, config?: LoggerConfig): Logger {
|
|
227
|
+
return new Logger({
|
|
228
|
+
...config,
|
|
229
|
+
prefix,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/markdown.ts
|
|
3
|
+
*
|
|
4
|
+
* Markdown 处理工具函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { generateAnchor } from './generateAnchor'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 工厂函数,创建一个带有独立编号状态的标题编号生成器
|
|
11
|
+
* @returns 返回 addNumberToTitle 方法
|
|
12
|
+
*/
|
|
13
|
+
export function createAddNumberToTitle() {
|
|
14
|
+
const titleNumbers = Array(7).fill(0) // 用于存储每个级别的编号
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 为标题添加编号
|
|
18
|
+
* @param title - 原始标题
|
|
19
|
+
* @returns [新标题, 纯文本标题]
|
|
20
|
+
*/
|
|
21
|
+
return function addNumberToTitle(title: string): [string, string] {
|
|
22
|
+
// 正则匹配提取标题信息
|
|
23
|
+
const match = title.match(
|
|
24
|
+
/^(#+)\s*((\d+(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?)\.\s*)?(.*)/,
|
|
25
|
+
)
|
|
26
|
+
const plainTitle = match ? match[9].trim() : title.trim()
|
|
27
|
+
|
|
28
|
+
const level = title.indexOf(' ')
|
|
29
|
+
const baseLevel = 2 // 基础级别为2
|
|
30
|
+
|
|
31
|
+
// 一级标题不编号
|
|
32
|
+
if (level === 1) return [title, plainTitle]
|
|
33
|
+
|
|
34
|
+
// 重置当前级别以上的编号
|
|
35
|
+
for (let i = level + 1; i < titleNumbers.length; i++) titleNumbers[i] = 0
|
|
36
|
+
|
|
37
|
+
// 增加当前级别的编号
|
|
38
|
+
titleNumbers[level] += 1
|
|
39
|
+
|
|
40
|
+
// 生成新的编号
|
|
41
|
+
const newNumber = titleNumbers.slice(baseLevel, level + 1).join('.')
|
|
42
|
+
|
|
43
|
+
// 构建新的标题
|
|
44
|
+
const headerSymbol = title.slice(0, level).trim() // 获取原有的 # 符号
|
|
45
|
+
const newTitle = `${headerSymbol} ${newNumber}. ${plainTitle}`
|
|
46
|
+
|
|
47
|
+
return [newTitle, plainTitle]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 生成 Markdown TOC(目录)
|
|
53
|
+
* @param titles - 标题列表
|
|
54
|
+
* @param baseLevel - 基础层级(默认为 2)
|
|
55
|
+
* @param eol - 换行符(默认为 '\n')
|
|
56
|
+
* @returns 生成的 TOC 字符串
|
|
57
|
+
*/
|
|
58
|
+
export function generateToc(
|
|
59
|
+
titles: string[],
|
|
60
|
+
baseLevel = 2,
|
|
61
|
+
eol = '\n',
|
|
62
|
+
): string {
|
|
63
|
+
const toc = titles
|
|
64
|
+
.map((title) => {
|
|
65
|
+
const level = title.indexOf(' ')
|
|
66
|
+
const text = title.slice(level).trim()
|
|
67
|
+
const anchor = generateAnchor(text)
|
|
68
|
+
const indent = Math.max(0, (level - baseLevel) * 2)
|
|
69
|
+
return ' '.repeat(indent) + `- [${text}](#${anchor})`
|
|
70
|
+
})
|
|
71
|
+
.join(eol)
|
|
72
|
+
|
|
73
|
+
// !在 TOC 区域前后添加换行符 - 适配 prettier 格式化
|
|
74
|
+
return `${eol}${toc}${eol}`
|
|
75
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/migrateReadmeToToc.test.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
extractReadmeBodyAfterToc,
|
|
9
|
+
migrateReadmeToToc,
|
|
10
|
+
README_TOC_END_TAG,
|
|
11
|
+
} from './migrateReadmeToToc'
|
|
12
|
+
|
|
13
|
+
const TOC_REGION = `<!-- region:toc -->
|
|
14
|
+
|
|
15
|
+
- [1. Alpha](#1-alpha)
|
|
16
|
+
|
|
17
|
+
${README_TOC_END_TAG}`
|
|
18
|
+
|
|
19
|
+
describe('extractReadmeBodyAfterToc', () => {
|
|
20
|
+
it('returns lines after endregion:toc', () => {
|
|
21
|
+
const content = `${TOC_REGION}\n\n## 1. Alpha\n- [x] [0001. A](url)`
|
|
22
|
+
const lines = extractReadmeBodyAfterToc(content)
|
|
23
|
+
expect(lines.some((l) => l.includes('## 1. Alpha'))).toBe(true)
|
|
24
|
+
expect(lines.some((l) => l.includes('#1-alpha'))).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('throws when endregion:toc is missing', () => {
|
|
28
|
+
expect(() => extractReadmeBodyAfterToc('# no toc')).toThrow(
|
|
29
|
+
README_TOC_END_TAG,
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('migrateReadmeToToc', () => {
|
|
35
|
+
it('outputs folder + nested notes for heading sections', () => {
|
|
36
|
+
const content = `${TOC_REGION}
|
|
37
|
+
|
|
38
|
+
## 1. One
|
|
39
|
+
- [x] [0001. Alpha](https://github.com/a/b)
|
|
40
|
+
- [ ] [0002. Beta](https://github.com/a/b)
|
|
41
|
+
`
|
|
42
|
+
const { entries } = migrateReadmeToToc(content)
|
|
43
|
+
expect(entries).toEqual([
|
|
44
|
+
{ kind: 'folder', folderTitle: '1. One', indent: 0 },
|
|
45
|
+
{ kind: 'note', noteIndex: '0001', indent: 1, completed: true },
|
|
46
|
+
{ kind: 'note', noteIndex: '0002', indent: 1, completed: false },
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('handles single note section with folder', () => {
|
|
51
|
+
const content = `${TOC_REGION}
|
|
52
|
+
|
|
53
|
+
## 7. Solo
|
|
54
|
+
- [ ] [0036. Only](https://github.com/a/b)
|
|
55
|
+
`
|
|
56
|
+
const { entries } = migrateReadmeToToc(content)
|
|
57
|
+
expect(entries).toEqual([
|
|
58
|
+
{ kind: 'folder', folderTitle: '7. Solo', indent: 0 },
|
|
59
|
+
{ kind: 'note', noteIndex: '0036', indent: 1, completed: false },
|
|
60
|
+
])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('treats ## and ### as separate groups', () => {
|
|
64
|
+
const content = `${TOC_REGION}
|
|
65
|
+
|
|
66
|
+
## 1. A
|
|
67
|
+
- [x] [0001. Alpha](url)
|
|
68
|
+
- [ ] [0002. Beta](url)
|
|
69
|
+
### 1.1 B
|
|
70
|
+
- [x] [0003. Gamma](url)
|
|
71
|
+
- [ ] [0004. Delta](url)
|
|
72
|
+
`
|
|
73
|
+
const { entries } = migrateReadmeToToc(content)
|
|
74
|
+
expect(entries).toEqual([
|
|
75
|
+
{ kind: 'folder', folderTitle: '1. A', indent: 0 },
|
|
76
|
+
{ kind: 'note', noteIndex: '0001', indent: 1, completed: true },
|
|
77
|
+
{ kind: 'note', noteIndex: '0002', indent: 1, completed: false },
|
|
78
|
+
{ kind: 'folder', folderTitle: '1.1 B', indent: 0 },
|
|
79
|
+
{ kind: 'note', noteIndex: '0003', indent: 1, completed: true },
|
|
80
|
+
{ kind: 'note', noteIndex: '0004', indent: 1, completed: false },
|
|
81
|
+
])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('deduplicates note indexes with warnings', () => {
|
|
85
|
+
const content = `${TOC_REGION}
|
|
86
|
+
|
|
87
|
+
## 1. A
|
|
88
|
+
- [x] [0001. Alpha](url)
|
|
89
|
+
## 2. B
|
|
90
|
+
- [ ] [0001. Alpha again](url)
|
|
91
|
+
`
|
|
92
|
+
const { entries, warnings } = migrateReadmeToToc(content)
|
|
93
|
+
expect(entries).toHaveLength(2)
|
|
94
|
+
expect(warnings.some((w) => w.includes('0001'))).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('skips empty sections', () => {
|
|
98
|
+
const content = `${TOC_REGION}
|
|
99
|
+
|
|
100
|
+
## Empty
|
|
101
|
+
|
|
102
|
+
## 2. B
|
|
103
|
+
- [x] [0002. Beta](url)
|
|
104
|
+
`
|
|
105
|
+
const { entries } = migrateReadmeToToc(content)
|
|
106
|
+
expect(entries).toEqual([
|
|
107
|
+
{ kind: 'folder', folderTitle: '2. B', indent: 0 },
|
|
108
|
+
{ kind: 'note', noteIndex: '0002', indent: 1, completed: true },
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/migrateReadmeToToc.ts
|
|
3
|
+
*
|
|
4
|
+
* 从 v0.1.x 根目录 README.md 解析笔记分区,生成 TOC.md 条目(init-toc 过渡命令)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseTocLine } from './tocHelpers'
|
|
8
|
+
|
|
9
|
+
/** 与 TocGenerator 保持一致 */
|
|
10
|
+
export const README_TOC_END_TAG = '<!-- endregion:toc -->'
|
|
11
|
+
|
|
12
|
+
/** 迁移后的 flat TOC 条目 */
|
|
13
|
+
export type MigratedTocEntry =
|
|
14
|
+
| {
|
|
15
|
+
kind: 'folder'
|
|
16
|
+
folderTitle: string
|
|
17
|
+
indent: number
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
kind: 'note'
|
|
21
|
+
noteIndex: string
|
|
22
|
+
indent: number
|
|
23
|
+
completed: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** migrateReadmeToToc 返回结果 */
|
|
27
|
+
export interface MigrateReadmeToTocResult {
|
|
28
|
+
entries: MigratedTocEntry[]
|
|
29
|
+
warnings: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface NoteGroupItem {
|
|
33
|
+
noteIndex: string
|
|
34
|
+
completed: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ReadmeSection {
|
|
38
|
+
heading: string | null
|
|
39
|
+
items: NoteGroupItem[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 取 `<!-- endregion:toc -->` 之后的 README 正文行
|
|
44
|
+
*/
|
|
45
|
+
export function extractReadmeBodyAfterToc(content: string): string[] {
|
|
46
|
+
const endTagIndex = content.indexOf(README_TOC_END_TAG)
|
|
47
|
+
if (endTagIndex === -1) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`README.md 中未找到 ${README_TOC_END_TAG},无法执行 init-toc 迁移`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const afterTag = content.slice(endTagIndex + README_TOC_END_TAG.length)
|
|
54
|
+
return afterTag.split('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 从 v0.1.x README 正文解析 TOC 条目。
|
|
59
|
+
*
|
|
60
|
+
* 规则:
|
|
61
|
+
* - 每个 #{2,} 标题独立成组,输出为 folder 行
|
|
62
|
+
* - 组内笔记统一为 folder 下 indent 1 的 note 行
|
|
63
|
+
* - 无标题组内笔记为根级 note 行
|
|
64
|
+
*/
|
|
65
|
+
export function migrateReadmeToToc(content: string): MigrateReadmeToTocResult {
|
|
66
|
+
const lines = extractReadmeBodyAfterToc(content)
|
|
67
|
+
const warnings: string[] = []
|
|
68
|
+
const seenIndexes = new Set<string>()
|
|
69
|
+
const sections: ReadmeSection[] = []
|
|
70
|
+
let currentHeading: string | null = null
|
|
71
|
+
let currentGroup: NoteGroupItem[] = []
|
|
72
|
+
|
|
73
|
+
const flushGroup = () => {
|
|
74
|
+
if (currentGroup.length > 0) {
|
|
75
|
+
sections.push({ heading: currentHeading, items: currentGroup })
|
|
76
|
+
}
|
|
77
|
+
currentGroup = []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const headingMatch = line.match(/^(#{2,})\s+(.+)$/)
|
|
82
|
+
if (headingMatch) {
|
|
83
|
+
flushGroup()
|
|
84
|
+
currentHeading = headingMatch[2].trim()
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parsed = parseTocLine(line)
|
|
89
|
+
if (!parsed.isMatch || !parsed.noteIndex) continue
|
|
90
|
+
|
|
91
|
+
if (seenIndexes.has(parsed.noteIndex)) {
|
|
92
|
+
warnings.push(`跳过重复笔记索引: ${parsed.noteIndex}`)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
seenIndexes.add(parsed.noteIndex)
|
|
97
|
+
currentGroup.push({
|
|
98
|
+
noteIndex: parsed.noteIndex,
|
|
99
|
+
completed: parsed.completed,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
flushGroup()
|
|
104
|
+
|
|
105
|
+
const entries: MigratedTocEntry[] = []
|
|
106
|
+
for (const section of sections) {
|
|
107
|
+
if (section.heading) {
|
|
108
|
+
entries.push({
|
|
109
|
+
kind: 'folder',
|
|
110
|
+
folderTitle: section.heading,
|
|
111
|
+
indent: 0,
|
|
112
|
+
})
|
|
113
|
+
for (const item of section.items) {
|
|
114
|
+
entries.push({
|
|
115
|
+
kind: 'note',
|
|
116
|
+
noteIndex: item.noteIndex,
|
|
117
|
+
indent: 1,
|
|
118
|
+
completed: item.completed,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const item of section.items) {
|
|
125
|
+
entries.push({
|
|
126
|
+
kind: 'note',
|
|
127
|
+
noteIndex: item.noteIndex,
|
|
128
|
+
indent: 0,
|
|
129
|
+
completed: item.completed,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { entries, warnings }
|
|
135
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/parseArgs.ts
|
|
3
|
+
*
|
|
4
|
+
* 轻量级命令行参数解析器
|
|
5
|
+
* 替代 minimist,仅实现项目需要的功能
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface ParsedArgs {
|
|
9
|
+
/** 非选项参数 */
|
|
10
|
+
_: string[]
|
|
11
|
+
/** 选项参数 */
|
|
12
|
+
[key: string]: boolean | string | string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 解析命令行参数
|
|
17
|
+
*
|
|
18
|
+
* 支持的格式:
|
|
19
|
+
* - `--flag` → { flag: true }
|
|
20
|
+
* - `--key=value` → { key: 'value' }
|
|
21
|
+
* - `--key value` → { key: 'value' }(如果 value 不以 - 开头)
|
|
22
|
+
* - `--no-flag` → { flag: false }
|
|
23
|
+
* - 其他参数 → 添加到 `_` 数组
|
|
24
|
+
*
|
|
25
|
+
* @param args 命令行参数数组(通常是 process.argv.slice(2))
|
|
26
|
+
* @returns 解析后的参数对象
|
|
27
|
+
*/
|
|
28
|
+
export function parseArgs(args: string[]): ParsedArgs {
|
|
29
|
+
const result: ParsedArgs = { _: [] }
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const arg = args[i]
|
|
33
|
+
|
|
34
|
+
// 处理 --key=value 格式
|
|
35
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
36
|
+
const eqIndex = arg.indexOf('=')
|
|
37
|
+
const key = arg.slice(2, eqIndex)
|
|
38
|
+
const value = arg.slice(eqIndex + 1)
|
|
39
|
+
result[key] = value
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 处理 --no-flag 格式(否定形式)
|
|
44
|
+
if (arg.startsWith('--no-')) {
|
|
45
|
+
const key = arg.slice(5)
|
|
46
|
+
result[key] = false
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 处理 --flag 或 --key value 格式
|
|
51
|
+
if (arg.startsWith('--')) {
|
|
52
|
+
const key = arg.slice(2)
|
|
53
|
+
const next = args[i + 1]
|
|
54
|
+
|
|
55
|
+
// 如果下一个参数存在且不是选项,则作为值
|
|
56
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
57
|
+
// 检查是否为布尔字符串
|
|
58
|
+
if (next === 'true') {
|
|
59
|
+
result[key] = true
|
|
60
|
+
i++
|
|
61
|
+
} else if (next === 'false') {
|
|
62
|
+
result[key] = false
|
|
63
|
+
i++
|
|
64
|
+
} else {
|
|
65
|
+
// 对于这个项目,所有选项都是布尔值
|
|
66
|
+
// 所以这里保持 --flag 为 true,不消费下一个参数
|
|
67
|
+
result[key] = true
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
result[key] = true
|
|
71
|
+
}
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 处理短选项 -x(目前项目未使用,但保留基础支持)
|
|
76
|
+
if (arg.startsWith('-') && arg.length > 1 && !arg.startsWith('--')) {
|
|
77
|
+
// 将 -abc 解析为 { a: true, b: true, c: true }
|
|
78
|
+
const flags = arg.slice(1)
|
|
79
|
+
for (const flag of flags) {
|
|
80
|
+
result[flag] = true
|
|
81
|
+
}
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 非选项参数
|
|
86
|
+
result._.push(arg)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
}
|