@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,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/tocNodeId.test.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
computeSidebarNodeId,
|
|
9
|
+
enrichSidebarNodeIds,
|
|
10
|
+
nodeIdForNote,
|
|
11
|
+
parseNodeId,
|
|
12
|
+
} from './tocNodeId'
|
|
13
|
+
|
|
14
|
+
import type { SidebarNodeLike } from './tocNodeId'
|
|
15
|
+
|
|
16
|
+
describe('tocNodeId', () => {
|
|
17
|
+
it('computes note nodeId', () => {
|
|
18
|
+
expect(
|
|
19
|
+
computeSidebarNodeId({
|
|
20
|
+
text: '✅ 0019. Author',
|
|
21
|
+
link: '/notes/0019. Author/README',
|
|
22
|
+
tocLineIndex: 0,
|
|
23
|
+
}),
|
|
24
|
+
).toBe(nodeIdForNote('0019'))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('computes folder nodeId from tocLineIndex', () => {
|
|
28
|
+
expect(
|
|
29
|
+
computeSidebarNodeId({
|
|
30
|
+
text: 'build',
|
|
31
|
+
folderPath: ['build'],
|
|
32
|
+
tocLineIndex: 12,
|
|
33
|
+
}),
|
|
34
|
+
).toBe('line:12')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('enriches tree recursively', () => {
|
|
38
|
+
const tree = enrichSidebarNodeIds<SidebarNodeLike>([
|
|
39
|
+
{
|
|
40
|
+
text: 'Parent',
|
|
41
|
+
link: '/notes/0019. Author/README',
|
|
42
|
+
tocLineIndex: 0,
|
|
43
|
+
items: [{ text: 'Child', link: '/notes/0039. new/README', tocLineIndex: 1 }],
|
|
44
|
+
},
|
|
45
|
+
])
|
|
46
|
+
expect(tree[0].nodeId).toBe('note:0019')
|
|
47
|
+
expect(tree[0].items?.[0].nodeId).toBe('note:0039')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('parses nodeId kinds', () => {
|
|
51
|
+
expect(parseNodeId('note:0019')).toEqual({
|
|
52
|
+
kind: 'note',
|
|
53
|
+
noteIndex: '0019',
|
|
54
|
+
})
|
|
55
|
+
expect(parseNodeId('line:34')).toEqual({
|
|
56
|
+
kind: 'line',
|
|
57
|
+
tocLineIndex: 34,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/tocNodeId.ts
|
|
3
|
+
*
|
|
4
|
+
* 语雀式拖拽 nodeId:dev 确定性 id,不写 TOC.md。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type TocMoveAction = 'moveAfter' | 'prependChild'
|
|
8
|
+
|
|
9
|
+
export interface SidebarNodeLike {
|
|
10
|
+
text: string
|
|
11
|
+
link?: string
|
|
12
|
+
folderPath?: string[]
|
|
13
|
+
tocLineIndex?: number
|
|
14
|
+
nodeId?: string
|
|
15
|
+
items?: SidebarNodeLike[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractNoteIndexFromLink(link?: string): string | null {
|
|
19
|
+
if (!link) return null
|
|
20
|
+
const match = link.match(/\/notes\/(\d{4})\./)
|
|
21
|
+
return match ? match[1] : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SidebarNodeLike {
|
|
25
|
+
text: string
|
|
26
|
+
link?: string
|
|
27
|
+
folderPath?: string[]
|
|
28
|
+
tocLineIndex?: number
|
|
29
|
+
nodeId?: string
|
|
30
|
+
items?: SidebarNodeLike[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function nodeIdForNote(noteIndex: string): string {
|
|
34
|
+
return `note:${noteIndex}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function nodeIdForFolder(folderPath: string[]): string {
|
|
38
|
+
return `folder:${folderPath.join('/')}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function nodeIdForLine(tocLineIndex: number): string {
|
|
42
|
+
return `line:${tocLineIndex}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function computeSidebarNodeId(item: SidebarNodeLike): string {
|
|
46
|
+
if (item.nodeId) return item.nodeId
|
|
47
|
+
|
|
48
|
+
const noteIndex = extractNoteIndexFromLink(item.link)
|
|
49
|
+
if (noteIndex) return nodeIdForNote(noteIndex)
|
|
50
|
+
|
|
51
|
+
if (item.tocLineIndex !== undefined && !item.link) {
|
|
52
|
+
return nodeIdForLine(item.tocLineIndex)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (item.folderPath?.length) {
|
|
56
|
+
return nodeIdForFolder(item.folderPath)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (item.tocLineIndex !== undefined) {
|
|
60
|
+
return nodeIdForLine(item.tocLineIndex)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `text:${item.text}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function enrichSidebarNodeIds<T extends SidebarNodeLike>(items: T[]): T[] {
|
|
67
|
+
return items.map((item) => {
|
|
68
|
+
const nodeId = computeSidebarNodeId(item)
|
|
69
|
+
const next = { ...item, nodeId } as T
|
|
70
|
+
if (item.items?.length) {
|
|
71
|
+
next.items = enrichSidebarNodeIds(item.items) as T['items']
|
|
72
|
+
}
|
|
73
|
+
return next
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseNodeId(nodeId: string): {
|
|
78
|
+
kind: 'note' | 'folder' | 'line' | 'text'
|
|
79
|
+
noteIndex?: string
|
|
80
|
+
folderPath?: string[]
|
|
81
|
+
tocLineIndex?: number
|
|
82
|
+
} {
|
|
83
|
+
if (nodeId.startsWith('note:')) {
|
|
84
|
+
return { kind: 'note', noteIndex: nodeId.slice(5) }
|
|
85
|
+
}
|
|
86
|
+
if (nodeId.startsWith('folder:')) {
|
|
87
|
+
const path = nodeId.slice(7)
|
|
88
|
+
return {
|
|
89
|
+
kind: 'folder',
|
|
90
|
+
folderPath: path ? path.split('/') : [],
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (nodeId.startsWith('line:')) {
|
|
94
|
+
return { kind: 'line', tocLineIndex: Number(nodeId.slice(5)) }
|
|
95
|
+
}
|
|
96
|
+
return { kind: 'text' }
|
|
97
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/validators.ts
|
|
3
|
+
*
|
|
4
|
+
* 验证工具函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Windows 和 macOS 都不允许的文件名字符
|
|
9
|
+
* Windows: < > : " / \ | ? *
|
|
10
|
+
* macOS: : (冒号会被转换为斜杠)
|
|
11
|
+
*
|
|
12
|
+
* 为了兼容性,我们禁止以下字符:
|
|
13
|
+
* - < > : " / \ | ? *
|
|
14
|
+
* - 控制字符 (0x00-0x1F)
|
|
15
|
+
*/
|
|
16
|
+
const INVALID_FILENAME_CHARS = /[<>:"/\\|?*\x00-\x1F]/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Windows 保留文件名
|
|
20
|
+
*/
|
|
21
|
+
const WINDOWS_RESERVED_NAMES = new Set([
|
|
22
|
+
'CON',
|
|
23
|
+
'PRN',
|
|
24
|
+
'AUX',
|
|
25
|
+
'NUL',
|
|
26
|
+
'COM1',
|
|
27
|
+
'COM2',
|
|
28
|
+
'COM3',
|
|
29
|
+
'COM4',
|
|
30
|
+
'COM5',
|
|
31
|
+
'COM6',
|
|
32
|
+
'COM7',
|
|
33
|
+
'COM8',
|
|
34
|
+
'COM9',
|
|
35
|
+
'LPT1',
|
|
36
|
+
'LPT2',
|
|
37
|
+
'LPT3',
|
|
38
|
+
'LPT4',
|
|
39
|
+
'LPT5',
|
|
40
|
+
'LPT6',
|
|
41
|
+
'LPT7',
|
|
42
|
+
'LPT8',
|
|
43
|
+
'LPT9',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 验证标题是否为合法的文件夹名称
|
|
48
|
+
* @param title - 笔记标题
|
|
49
|
+
* @returns 验证结果 { valid: boolean, error?: string }
|
|
50
|
+
*/
|
|
51
|
+
export function validateNoteTitle(title: string): {
|
|
52
|
+
valid: boolean
|
|
53
|
+
error?: string
|
|
54
|
+
} {
|
|
55
|
+
// 检查是否为空
|
|
56
|
+
if (!title || title.trim().length === 0) {
|
|
57
|
+
return { valid: false, error: '标题不能为空' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const trimmedTitle = title.trim()
|
|
61
|
+
|
|
62
|
+
// 检查长度 (Windows 路径限制 260 字符,留足够空间)
|
|
63
|
+
if (trimmedTitle.length > 200) {
|
|
64
|
+
return { valid: false, error: '标题过长(最多200个字符)' }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 检查是否包含非法字符
|
|
68
|
+
if (INVALID_FILENAME_CHARS.test(trimmedTitle)) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
error: '标题包含非法字符(不允许: < > : " / \\ | ? *)',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 检查是否以点或空格开头/结尾 (Windows 限制)
|
|
76
|
+
if (/^[.\s]|[.\s]$/.test(trimmedTitle)) {
|
|
77
|
+
return {
|
|
78
|
+
valid: false,
|
|
79
|
+
error: '标题不能以点或空格开头/结尾',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 检查 Windows 保留名称
|
|
84
|
+
const upperTitle = trimmedTitle.toUpperCase()
|
|
85
|
+
if (WINDOWS_RESERVED_NAMES.has(upperTitle)) {
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
error: `"${trimmedTitle}" 是 Windows 系统保留名称`,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 检查是否为 Windows 保留名称加扩展名 (例如 CON.txt)
|
|
93
|
+
const baseName = trimmedTitle.split('.')[0].toUpperCase()
|
|
94
|
+
if (WINDOWS_RESERVED_NAMES.has(baseName)) {
|
|
95
|
+
return {
|
|
96
|
+
valid: false,
|
|
97
|
+
error: `"${trimmedTitle}" 包含 Windows 系统保留名称`,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { valid: true }
|
|
102
|
+
}
|