@tnotesjs/core 0.2.1 → 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/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 +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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/parseReadmeCompletedNotes.test.ts
|
|
3
|
+
*
|
|
4
|
+
* 测试 parseReadmeCompletedNotes 函数,确保正确解析 README 中的完成状态
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { parseReadmeCompletedNotes } from './parseReadmeCompletedNotes'
|
|
10
|
+
|
|
11
|
+
describe('parseReadmeCompletedNotes', () => {
|
|
12
|
+
const REMOTE = 'https://github.com/owner/repo/notes'
|
|
13
|
+
|
|
14
|
+
it('should count completed and total notes correctly', () => {
|
|
15
|
+
const content = [
|
|
16
|
+
`- [x] [0001. First note](${REMOTE}/0001.first-note/README) ✅`,
|
|
17
|
+
`- [ ] [0002. Second note](${REMOTE}/0002.second-note/README) ❌`,
|
|
18
|
+
`- [x] [0003. Third note](${REMOTE}/0003.third-note/README)`,
|
|
19
|
+
`- [ ] [0004. Fourth note](${REMOTE}/0004.fourth-note/README)`,
|
|
20
|
+
].join('\n')
|
|
21
|
+
|
|
22
|
+
const result = parseReadmeCompletedNotes(content)
|
|
23
|
+
|
|
24
|
+
expect(result.totalCount).toBe(4)
|
|
25
|
+
expect(result.completedCount).toBe(2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should prioritize emoji over checkbox markers', () => {
|
|
29
|
+
const content = [
|
|
30
|
+
`- [ ] [0001. Note with x](${REMOTE}/0001.x/README) ❌`,
|
|
31
|
+
`- [x] [0002. Note with check](${REMOTE}/0002.check/README) ✅`,
|
|
32
|
+
].join('\n')
|
|
33
|
+
|
|
34
|
+
const result = parseReadmeCompletedNotes(content)
|
|
35
|
+
|
|
36
|
+
expect(result.notes[0].completed).toBe(false) // ❌ takes priority
|
|
37
|
+
expect(result.notes[1].completed).toBe(true) // ✅ takes priority
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should deduplicate identical note indexes with same status', () => {
|
|
41
|
+
const content = [
|
|
42
|
+
`- [x] [0001. First note](${REMOTE}/0001.first/README) ✅`,
|
|
43
|
+
`- [x] [0001. First note dup](${REMOTE}/0001.first/README)`,
|
|
44
|
+
].join('\n')
|
|
45
|
+
|
|
46
|
+
const result = parseReadmeCompletedNotes(content)
|
|
47
|
+
|
|
48
|
+
expect(result.totalCount).toBe(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should throw when same note index has conflicting statuses', () => {
|
|
52
|
+
const content = [
|
|
53
|
+
`- [x] [0001. First note](${REMOTE}/0001.first/README) ✅`,
|
|
54
|
+
`- [ ] [0001. First note undone](${REMOTE}/0001.first/README)`,
|
|
55
|
+
].join('\n')
|
|
56
|
+
|
|
57
|
+
expect(() => parseReadmeCompletedNotes(content)).toThrow(
|
|
58
|
+
/相同编号.*不同的完成状态/,
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should ignore lines without note index', () => {
|
|
63
|
+
const content = [
|
|
64
|
+
'# Header',
|
|
65
|
+
'',
|
|
66
|
+
`- [x] [0001. First note](${REMOTE}/0001.first/README)`,
|
|
67
|
+
'some random text without index',
|
|
68
|
+
`- [ ] [0002. Second note](${REMOTE}/0002.second/README)`,
|
|
69
|
+
].join('\n')
|
|
70
|
+
|
|
71
|
+
const result = parseReadmeCompletedNotes(content)
|
|
72
|
+
|
|
73
|
+
expect(result.totalCount).toBe(2)
|
|
74
|
+
expect(result.completedCount).toBe(1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should handle empty content', () => {
|
|
78
|
+
const result = parseReadmeCompletedNotes('')
|
|
79
|
+
|
|
80
|
+
expect(result.totalCount).toBe(0)
|
|
81
|
+
expect(result.completedCount).toBe(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should handle content with no matching lines', () => {
|
|
85
|
+
const result = parseReadmeCompletedNotes('# Just a header\n\nSome text')
|
|
86
|
+
|
|
87
|
+
expect(result.totalCount).toBe(0)
|
|
88
|
+
expect(result.completedCount).toBe(0)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/parseReadmeCompletedNotes.ts
|
|
3
|
+
*
|
|
4
|
+
* 解析 README.md 中的完成笔记数量
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 笔记状态
|
|
9
|
+
*/
|
|
10
|
+
interface NoteStatus {
|
|
11
|
+
noteIndex: string // 笔记编号(如 "0001")
|
|
12
|
+
completed: boolean // 是否完成
|
|
13
|
+
line: string // 原始行内容
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 解析结果
|
|
18
|
+
*/
|
|
19
|
+
interface ParseResult {
|
|
20
|
+
completedCount: number // 完成的笔记数量
|
|
21
|
+
totalCount: number // 总笔记数量
|
|
22
|
+
notes: NoteStatus[] // 所有笔记的状态
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 从 README.md 内容中解析完成笔记数量
|
|
27
|
+
*
|
|
28
|
+
* 优先级判断逻辑(从上到下,匹配一个即停止):
|
|
29
|
+
* 1. 如果有 ❌ → 未完成
|
|
30
|
+
* 2. 如果有 ⏰ → 未完成
|
|
31
|
+
* 3. 如果有 ✅ → 完成
|
|
32
|
+
* 4. 如果以 - [ ] 开头 → 未完成
|
|
33
|
+
* 5. 如果以 - [x] 开头 → 完成
|
|
34
|
+
*
|
|
35
|
+
* @param content - README.md 的内容
|
|
36
|
+
* @returns 解析结果
|
|
37
|
+
* @throws 如果发现相同编号的笔记有不同的完成状态
|
|
38
|
+
*/
|
|
39
|
+
export function parseReadmeCompletedNotes(content: string): ParseResult {
|
|
40
|
+
const lines = content.split('\n')
|
|
41
|
+
const noteMap = new Map<string, NoteStatus>()
|
|
42
|
+
|
|
43
|
+
// 笔记编号正则:匹配 4 位数字(如 0001, 0002)
|
|
44
|
+
const noteIndexRegex = /\[(\d{4})\./
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
// 提取笔记编号
|
|
48
|
+
const match = line.match(noteIndexRegex)
|
|
49
|
+
if (!match) continue
|
|
50
|
+
|
|
51
|
+
const noteIndex = match[1]
|
|
52
|
+
|
|
53
|
+
// 判断完成状态(按优先级)
|
|
54
|
+
let completed: boolean
|
|
55
|
+
|
|
56
|
+
if (line.includes('❌')) {
|
|
57
|
+
// 优先级 1: ❌ 表示未完成
|
|
58
|
+
completed = false
|
|
59
|
+
} else if (line.includes('⏰')) {
|
|
60
|
+
// 优先级 2: ⏰ 表示未完成
|
|
61
|
+
completed = false
|
|
62
|
+
} else if (line.includes('✅')) {
|
|
63
|
+
// 优先级 3: ✅ 表示完成
|
|
64
|
+
completed = true
|
|
65
|
+
} else if (line.trim().startsWith('- [ ]')) {
|
|
66
|
+
// 优先级 4: - [ ] 表示未完成
|
|
67
|
+
completed = false
|
|
68
|
+
} else if (line.trim().startsWith('- [x]')) {
|
|
69
|
+
// 优先级 5: - [x] 表示完成
|
|
70
|
+
completed = true
|
|
71
|
+
} else {
|
|
72
|
+
// 未匹配任何规则,跳过
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 检查是否已存在该编号的笔记
|
|
77
|
+
if (noteMap.has(noteIndex)) {
|
|
78
|
+
const existing = noteMap.get(noteIndex)!
|
|
79
|
+
if (existing.completed !== completed) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`发现相同编号 ${noteIndex} 的笔记有不同的完成状态:\n` +
|
|
82
|
+
` 第一次出现: ${existing.line}\n` +
|
|
83
|
+
` 第二次出现: ${line}`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
// 状态相同,跳过(去重)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 记录笔记状态
|
|
91
|
+
noteMap.set(noteIndex, {
|
|
92
|
+
noteIndex,
|
|
93
|
+
completed,
|
|
94
|
+
line: line.trim(),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 统计结果
|
|
99
|
+
const notes = Array.from(noteMap.values())
|
|
100
|
+
const completedCount = notes.filter((note) => note.completed).length
|
|
101
|
+
const totalCount = notes.length
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
completedCount,
|
|
105
|
+
totalCount,
|
|
106
|
+
notes,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/portUtils.ts
|
|
3
|
+
*
|
|
4
|
+
* 端口管理工具函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process'
|
|
8
|
+
|
|
9
|
+
import { logger } from './logger'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 检查端口是否被占用
|
|
13
|
+
*/
|
|
14
|
+
export function isPortInUse(port: number): boolean {
|
|
15
|
+
try {
|
|
16
|
+
// Windows 系统使用 netstat 命令,只检测 LISTENING 状态(忽略 TIME_WAIT)
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
const output = execSync(
|
|
19
|
+
`netstat -ano | findstr :${port} | findstr LISTENING`,
|
|
20
|
+
{ encoding: 'utf-8', stdio: 'pipe' },
|
|
21
|
+
)
|
|
22
|
+
return output.trim().length > 0
|
|
23
|
+
}
|
|
24
|
+
// Unix-like 系统使用 lsof 命令
|
|
25
|
+
else {
|
|
26
|
+
const output = execSync(`lsof -i :${port}`, {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
stdio: 'pipe',
|
|
29
|
+
})
|
|
30
|
+
return output.trim().length > 0
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// 如果命令执行失败(没有找到占用),返回 false
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 获取占用端口的进程 PID
|
|
40
|
+
*/
|
|
41
|
+
function getPortPid(port: number): number | null {
|
|
42
|
+
try {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
const output = execSync(`netstat -ano | findstr :${port}`, {
|
|
45
|
+
encoding: 'utf-8',
|
|
46
|
+
stdio: 'pipe',
|
|
47
|
+
})
|
|
48
|
+
const lines = output.trim().split('\n')
|
|
49
|
+
if (lines.length > 0) {
|
|
50
|
+
const match = lines[0].match(/\s+(\d+)\s*$/)
|
|
51
|
+
if (match) {
|
|
52
|
+
return parseInt(match[1])
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
const output = execSync(`lsof -t -i :${port}`, {
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
stdio: 'pipe',
|
|
59
|
+
})
|
|
60
|
+
const pid = parseInt(output.trim())
|
|
61
|
+
if (!isNaN(pid)) {
|
|
62
|
+
return pid
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// 命令执行失败
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 终止占用端口的进程
|
|
73
|
+
*/
|
|
74
|
+
export function killPortProcess(port: number): boolean {
|
|
75
|
+
const pid = getPortPid(port)
|
|
76
|
+
if (!pid) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' })
|
|
83
|
+
} else {
|
|
84
|
+
execSync(`kill -9 ${pid}`, { stdio: 'pipe' })
|
|
85
|
+
}
|
|
86
|
+
logger.info(`已终止占用端口 ${port} 的进程 (PID: ${pid})`)
|
|
87
|
+
return true
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error(
|
|
90
|
+
`终止进程失败 (PID: ${pid}): ${
|
|
91
|
+
error instanceof Error ? error.message : String(error)
|
|
92
|
+
}`,
|
|
93
|
+
)
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 等待端口可用
|
|
100
|
+
*/
|
|
101
|
+
export async function waitForPort(
|
|
102
|
+
port: number,
|
|
103
|
+
timeout: number = 5000,
|
|
104
|
+
): Promise<boolean> {
|
|
105
|
+
const startTime = Date.now()
|
|
106
|
+
while (Date.now() - startTime < timeout) {
|
|
107
|
+
if (!isPortInUse(port)) {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/readmeHelpers.ts
|
|
3
|
+
*
|
|
4
|
+
* README 更新的公共辅助函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { NoteManager } from '../core/NoteManager'
|
|
8
|
+
|
|
9
|
+
import type { NoteInfo } from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 笔记行匹配正则表达式
|
|
13
|
+
*
|
|
14
|
+
* 格式:
|
|
15
|
+
* - [x] [0001. 笔记标题]
|
|
16
|
+
* 或
|
|
17
|
+
* - [x] [0001. 笔记标题] (支持缩进)
|
|
18
|
+
*
|
|
19
|
+
* 要求: 笔记名称必须是 4 个数字开头,后面紧跟着一个小数点和一个空格,随后跟着任意标题内容
|
|
20
|
+
*/
|
|
21
|
+
const NOTE_LINE_REGEX = /^( *)- \[.\] \[(\d{4}\. .+?)\]/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 笔记链接解析结果
|
|
25
|
+
*/
|
|
26
|
+
interface ParsedNoteLine {
|
|
27
|
+
isMatch: boolean // 是否匹配到笔记行
|
|
28
|
+
noteIndex: string | null // 笔记索引 (如 "0001")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 解析 home readme 笔记链接行,提取笔记 ID
|
|
33
|
+
* @param line - 要解析的行
|
|
34
|
+
* @returns 解析结果 { isMatch, noteIndex }
|
|
35
|
+
*/
|
|
36
|
+
export function parseNoteLine(line: string): ParsedNoteLine {
|
|
37
|
+
// 匹配笔记链接格式: - [x] [0001. xxx](...)
|
|
38
|
+
// 或简单格式: - [ ] [0001. xxx]
|
|
39
|
+
// 支持缩进: " - [x] [0001. xxx]"
|
|
40
|
+
const noteMatch = line.match(NOTE_LINE_REGEX)
|
|
41
|
+
|
|
42
|
+
if (!noteMatch) {
|
|
43
|
+
return {
|
|
44
|
+
isMatch: false,
|
|
45
|
+
noteIndex: null,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [, , text] = noteMatch // 第一个捕获组是缩进,第二个是文本
|
|
50
|
+
|
|
51
|
+
// 提取笔记 ID
|
|
52
|
+
const noteIndex = NoteManager.extractNoteIndex(text)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
isMatch: true,
|
|
56
|
+
noteIndex,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 构建 home readme 笔记链接
|
|
62
|
+
* @param note - 笔记信息
|
|
63
|
+
* @param repoOwner - 仓库所有者
|
|
64
|
+
* @param repoName - 仓库名称
|
|
65
|
+
* @returns 完整的 GitHub URL
|
|
66
|
+
*/
|
|
67
|
+
function buildNoteLink(
|
|
68
|
+
note: NoteInfo,
|
|
69
|
+
repoOwner: string,
|
|
70
|
+
repoName: string,
|
|
71
|
+
): string {
|
|
72
|
+
const encodedDirName = encodeURIComponent(note.dirName)
|
|
73
|
+
return `https://github.com/${repoOwner}/${repoName}/tree/main/notes/${encodedDirName}/README.md`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 根据笔记配置更新状态
|
|
78
|
+
* @param note - 笔记信息
|
|
79
|
+
* @returns { status, deprecatedMark } 状态字符和弃用标记
|
|
80
|
+
*/
|
|
81
|
+
function updateNoteStatus(note: NoteInfo): {
|
|
82
|
+
status: string
|
|
83
|
+
deprecatedMark: string
|
|
84
|
+
} {
|
|
85
|
+
let status = ' ' // 默认未完成
|
|
86
|
+
const deprecatedMark = '' // 弃用标记(已废弃,保留返回值结构以免破坏 API)
|
|
87
|
+
|
|
88
|
+
if (note.config) {
|
|
89
|
+
if (note.config.done) {
|
|
90
|
+
status = 'x' // 完成的笔记,勾选复选框
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { status, deprecatedMark }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 构建完整的笔记行
|
|
99
|
+
* @param note - 笔记信息
|
|
100
|
+
* @param repoOwner - 仓库所有者
|
|
101
|
+
* @param repoName - 仓库名称
|
|
102
|
+
* @returns 完整的 Markdown 行
|
|
103
|
+
*/
|
|
104
|
+
export function buildNoteLineMarkdown(
|
|
105
|
+
note: NoteInfo,
|
|
106
|
+
repoOwner: string,
|
|
107
|
+
repoName: string,
|
|
108
|
+
): string {
|
|
109
|
+
const url = buildNoteLink(note, repoOwner, repoName)
|
|
110
|
+
const { status, deprecatedMark } = updateNoteStatus(note)
|
|
111
|
+
return `- [${status}] [${note.dirName}](${url})${deprecatedMark}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 检查是否是笔记列表行
|
|
116
|
+
* @param line - 要检查的行
|
|
117
|
+
* @returns 是否是笔记行
|
|
118
|
+
*/
|
|
119
|
+
function isNoteLine(line: string): boolean {
|
|
120
|
+
return NOTE_LINE_REGEX.test(line)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 合并 home readme 连续空行
|
|
125
|
+
* @param lines - 原始行数组
|
|
126
|
+
* @returns 合并后的行数组
|
|
127
|
+
*/
|
|
128
|
+
function mergeConsecutiveEmptyLines(lines: string[]): string[] {
|
|
129
|
+
const result: string[] = []
|
|
130
|
+
let previousLineIsEmpty = false
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
const isCurrentLineEmpty = line === ''
|
|
134
|
+
|
|
135
|
+
if (isCurrentLineEmpty) {
|
|
136
|
+
// 只有前一行不是空行时才保留当前空行
|
|
137
|
+
if (!previousLineIsEmpty) {
|
|
138
|
+
result.push(line)
|
|
139
|
+
previousLineIsEmpty = true
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
result.push(line)
|
|
143
|
+
previousLineIsEmpty = false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 移除相邻 home readme 笔记之间的空行
|
|
152
|
+
* @param lines - 行数组
|
|
153
|
+
* @returns 处理后的行数组
|
|
154
|
+
*/
|
|
155
|
+
function removeEmptyLinesBetweenNotes(lines: string[]): string[] {
|
|
156
|
+
const result: string[] = []
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < lines.length; i++) {
|
|
159
|
+
const currentLine = lines[i]
|
|
160
|
+
const prevLine = i > 0 ? lines[i - 1] : null
|
|
161
|
+
const nextLine = i < lines.length - 1 ? lines[i + 1] : null
|
|
162
|
+
|
|
163
|
+
// 如果当前行是空行,且前后都是笔记,则跳过这个空行
|
|
164
|
+
if (currentLine === '' && prevLine && nextLine) {
|
|
165
|
+
const isPrevLineNote = isNoteLine(prevLine)
|
|
166
|
+
const isNextLineNote = isNoteLine(nextLine)
|
|
167
|
+
|
|
168
|
+
if (isPrevLineNote && isNextLineNote) {
|
|
169
|
+
continue // 跳过笔记之间的空行
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result.push(currentLine)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 处理 README 中的空行
|
|
181
|
+
* 1. 合并连续空行
|
|
182
|
+
* 2. 移除相邻笔记之间的空行
|
|
183
|
+
* @param lines - 原始行数组
|
|
184
|
+
* @returns 处理后的行数组
|
|
185
|
+
*/
|
|
186
|
+
export function processEmptyLines(lines: string[]): string[] {
|
|
187
|
+
const stepOne = mergeConsecutiveEmptyLines(lines)
|
|
188
|
+
const stepTwo = removeEmptyLinesBetweenNotes(stepOne)
|
|
189
|
+
return stepTwo
|
|
190
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/runCommand.ts
|
|
3
|
+
*
|
|
4
|
+
* 运行命令的工具函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { exec } from 'child_process'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 使用 exec 执行命令
|
|
11
|
+
* @param command - 要执行的命令
|
|
12
|
+
* @param dir - 执行目录
|
|
13
|
+
* @returns Promise<string> 命令输出
|
|
14
|
+
*/
|
|
15
|
+
export async function runCommand(
|
|
16
|
+
command: string,
|
|
17
|
+
dir: string
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
exec(command, { cwd: dir }, (error, stdout, stderr) => {
|
|
21
|
+
if (error) {
|
|
22
|
+
console.error(`处理 ${dir} 时出错:${stderr}`)
|
|
23
|
+
reject(error)
|
|
24
|
+
} else {
|
|
25
|
+
resolve(stdout.trim())
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
}
|