@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 128 128">
|
|
2
|
+
<path d="M0 0h128v128H0z" fill="none" />
|
|
3
|
+
<path fill="#656cff" fill-rule="evenodd" d="M117.9 30.289L66.664.713a5.32 5.32 0 0 0-5.323 0L10.09 30.29a4.48 4.48 0 0 0-2.234 3.872v59.663c0 1.6.853 3.077 2.24 3.878l51.24 29.586a5.33 5.33 0 0 0 5.324 0l51.246-29.586a4.48 4.48 0 0 0 2.24-3.878V34.166a4.48 4.48 0 0 0-2.24-3.872zm-3.216 6.272l-49.47 85.681c-.337.576-1.217.341-1.217-.325V65.81a3.15 3.15 0 0 0-1.573-2.72l-48.59-28.055c-.571-.331-.336-1.216.33-1.216h98.94c1.409 0 2.284 1.525 1.58 2.741" />
|
|
4
|
+
</svg>
|
|
@@ -14,6 +14,7 @@ export { default as icon__close } from './icon__close.svg'
|
|
|
14
14
|
export { default as icon__collapse } from './icon__collapse.svg'
|
|
15
15
|
export { default as icon__confirm } from './icon__confirm.svg'
|
|
16
16
|
export { default as icon__copy } from './icon__copy.svg'
|
|
17
|
+
export { default as icon__cursor } from './icon__cursor.svg'
|
|
17
18
|
export { default as icon__focus } from './icon__focus.svg'
|
|
18
19
|
export { default as icon__fold } from './icon__fold.svg'
|
|
19
20
|
export { default as icon__folder } from './icon__folder.svg'
|
|
@@ -107,22 +107,6 @@ vitepress/components/Layout/AboutPanel.vue
|
|
|
107
107
|
</div>
|
|
108
108
|
</div>
|
|
109
109
|
|
|
110
|
-
<!-- 首次提交时间 -->
|
|
111
|
-
<div class="timeLine" title="首次提交时间">
|
|
112
|
-
<div class="timeLabel"><strong>⌛️ 首次提交</strong></div>
|
|
113
|
-
<div class="timeValue">
|
|
114
|
-
{{ formatOptionalDate(modalCreatedAt) }}
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<!-- 最近提交时间 -->
|
|
119
|
-
<div class="timeLine" title="最近提交时间">
|
|
120
|
-
<div class="timeLabel"><strong>⌛️ 最近提交</strong></div>
|
|
121
|
-
<div class="timeValue">
|
|
122
|
-
{{ formatOptionalDate(modalUpdatedAt) }}
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
110
|
<!-- GitHub -->
|
|
127
111
|
<div
|
|
128
112
|
class="timeLine"
|
|
@@ -196,8 +180,6 @@ vitepress/components/Layout/AboutPanel.vue
|
|
|
196
180
|
<script setup lang="ts">
|
|
197
181
|
import { computed } from 'vue'
|
|
198
182
|
|
|
199
|
-
import { formatDate } from '../utils'
|
|
200
|
-
|
|
201
183
|
const props = defineProps<{
|
|
202
184
|
isHomeReadme: boolean
|
|
203
185
|
currentNoteId: string | null
|
|
@@ -207,8 +189,6 @@ const props = defineProps<{
|
|
|
207
189
|
editableNoteStatus: boolean
|
|
208
190
|
editableDiscussionsEnabled: boolean
|
|
209
191
|
titleError: string
|
|
210
|
-
modalCreatedAt: number | undefined
|
|
211
|
-
modalUpdatedAt: number | undefined
|
|
212
192
|
modalGithubUrl: string
|
|
213
193
|
modalGithubPageUrl: string
|
|
214
194
|
completionPercentage: number | null
|
|
@@ -263,10 +243,6 @@ function onDescriptionInput() {
|
|
|
263
243
|
function onConfigChange() {
|
|
264
244
|
emit('configChange')
|
|
265
245
|
}
|
|
266
|
-
|
|
267
|
-
function formatOptionalDate(timestamp: number | undefined) {
|
|
268
|
-
return timestamp === undefined ? '-' : formatDate(timestamp)
|
|
269
|
-
}
|
|
270
246
|
</script>
|
|
271
247
|
|
|
272
248
|
<style scoped lang="scss">
|
|
@@ -14,10 +14,10 @@ vitepress/components/Layout/DocBeforeControls.vue
|
|
|
14
14
|
<div class="vscodeBox pcOnly" v-show="vscodeNotesDir">
|
|
15
15
|
<a
|
|
16
16
|
:href="vscodeNotesDir"
|
|
17
|
-
title="
|
|
17
|
+
:title="openCurrentTitle"
|
|
18
18
|
target="_blank"
|
|
19
19
|
>
|
|
20
|
-
<img :src="
|
|
20
|
+
<img :src="localIdeIcon" :alt="openCurrentTitle" />
|
|
21
21
|
</a>
|
|
22
22
|
</div>
|
|
23
23
|
<div class="contentToggleBox pcOnly">
|
|
@@ -65,13 +65,7 @@ vitepress/components/Layout/DocBeforeControls.vue
|
|
|
65
65
|
</button>
|
|
66
66
|
</div>
|
|
67
67
|
<!-- 单个图标,点击打开 modal,只在有笔记数据的页面显示 -->
|
|
68
|
-
<div
|
|
69
|
-
class="aboutBtn"
|
|
70
|
-
v-show="
|
|
71
|
-
(currentNoteId && created_at && updated_at) ||
|
|
72
|
-
(isHomeReadme && homeReadmeCreatedAt && homeReadmeUpdatedAt)
|
|
73
|
-
"
|
|
74
|
-
>
|
|
68
|
+
<div class="aboutBtn" v-show="currentNoteId || isHomeReadme">
|
|
75
69
|
<button
|
|
76
70
|
class="aboutIconButton"
|
|
77
71
|
@click="$emit('open-time-modal')"
|
|
@@ -93,24 +87,22 @@ import ToggleFullContent from './ToggleFullContent.vue'
|
|
|
93
87
|
import ToggleSidebar from './ToggleSidebar.vue'
|
|
94
88
|
import {
|
|
95
89
|
icon__github,
|
|
96
|
-
icon__vscode,
|
|
97
90
|
icon__fold,
|
|
98
91
|
icon__clipboard,
|
|
99
92
|
} from '../../assets/icons'
|
|
93
|
+
import { useLocalIde } from '../composables/useLocalIde'
|
|
100
94
|
|
|
101
95
|
const props = defineProps<{
|
|
102
96
|
isFullContentMode: boolean
|
|
103
97
|
vscodeNotesDir: string
|
|
104
98
|
isHomeReadme: boolean
|
|
105
99
|
currentNoteId: string | null
|
|
106
|
-
created_at: string | undefined
|
|
107
|
-
updated_at: string | undefined
|
|
108
|
-
homeReadmeCreatedAt: string | undefined
|
|
109
|
-
homeReadmeUpdatedAt: string | undefined
|
|
110
100
|
timeModalOpen: boolean
|
|
111
101
|
allCollapsed: boolean
|
|
112
102
|
}>()
|
|
113
103
|
|
|
104
|
+
const { icon: localIdeIcon, openCurrentTitle } = useLocalIde()
|
|
105
|
+
|
|
114
106
|
const showCopyToast = ref(false)
|
|
115
107
|
let copyToastTimer: ReturnType<typeof setTimeout> | null = null
|
|
116
108
|
|
|
@@ -4,7 +4,7 @@ vitepress/components/Layout/DocFooter.vue
|
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
6
|
<nav class="custom-doc-footer" v-if="prev || next">
|
|
7
|
-
<div class="container">
|
|
7
|
+
<div class="pager-container">
|
|
8
8
|
<div class="prev">
|
|
9
9
|
<a v-if="prev" :href="prev.link" class="pager-link prev-link">
|
|
10
10
|
<span class="desc">上一篇</span>
|
|
@@ -147,7 +147,7 @@ const next = computed(() => {
|
|
|
147
147
|
padding-top: 32px;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
.container {
|
|
150
|
+
.pager-container {
|
|
151
151
|
display: flex;
|
|
152
152
|
gap: 16px;
|
|
153
153
|
}
|
|
@@ -219,7 +219,7 @@ const next = computed(() => {
|
|
|
219
219
|
|
|
220
220
|
/* 响应式调整 */
|
|
221
221
|
@media (max-width: 768px) {
|
|
222
|
-
.container {
|
|
222
|
+
.pager-container {
|
|
223
223
|
flex-direction: column;
|
|
224
224
|
gap: 12px;
|
|
225
225
|
}
|
|
@@ -32,10 +32,6 @@ vitepress/components/Layout/Layout.vue
|
|
|
32
32
|
:vscode-notes-dir="vscodeNotesDir"
|
|
33
33
|
:is-home-readme="isHomeReadme"
|
|
34
34
|
:current-note-id="currentNoteId"
|
|
35
|
-
:created_at="created_at"
|
|
36
|
-
:updated_at="updated_at"
|
|
37
|
-
:home-readme-created-at="homeReadmeCreatedAt"
|
|
38
|
-
:home-readme-updated-at="homeReadmeUpdatedAt"
|
|
39
35
|
:time-modal-open="timeModalOpen"
|
|
40
36
|
:all-collapsed="allCollapsed"
|
|
41
37
|
@open-time-modal="openTimeModal"
|
|
@@ -65,8 +61,6 @@ vitepress/components/Layout/Layout.vue
|
|
|
65
61
|
v-model:editable-note-status="editableNoteStatus"
|
|
66
62
|
v-model:editable-discussions-enabled="editableDiscussionsEnabled"
|
|
67
63
|
v-model:title-error="titleError"
|
|
68
|
-
:modal-created-at="modalCreatedAt"
|
|
69
|
-
:modal-updated-at="modalUpdatedAt"
|
|
70
64
|
:modal-github-url="modalGithubUrl"
|
|
71
65
|
:modal-github-page-url="modalGithubPageUrl"
|
|
72
66
|
:completion-percentage="completionPercentage"
|
|
@@ -116,10 +110,6 @@ vitepress/components/Layout/Layout.vue
|
|
|
116
110
|
</AboutModal>
|
|
117
111
|
</template>
|
|
118
112
|
<template #doc-footer-before>
|
|
119
|
-
<!-- <div class="footer-time-info">
|
|
120
|
-
<p title="首次提交时间">首次提交时间:{{ formatDate(created_at) }}</p>
|
|
121
|
-
<p title="最近提交时间">最近提交时间:{{ formatDate(updated_at) }}</p>
|
|
122
|
-
</div> -->
|
|
123
113
|
</template>
|
|
124
114
|
<template #doc-after>
|
|
125
115
|
<!-- 自定义 DocFooter -->
|
|
@@ -335,8 +325,6 @@ function getEmptyNoteConfig() {
|
|
|
335
325
|
const isDiscussionsVisible = computed(
|
|
336
326
|
() => currentNoteConfig.value.enableDiscussions,
|
|
337
327
|
);
|
|
338
|
-
const updated_at = computed(() => currentNoteConfig.value.updated_at);
|
|
339
|
-
const created_at = computed(() => currentNoteConfig.value.created_at);
|
|
340
328
|
|
|
341
329
|
// 判断是否为首页 README.md
|
|
342
330
|
const isHomeReadme = computed(() => vpData.page.value.filePath === "README.md");
|
|
@@ -349,10 +337,6 @@ const completionPercentage = computed(() => {
|
|
|
349
337
|
return Math.round((doneNotesLen.value / totalNotesLen.value) * 100);
|
|
350
338
|
});
|
|
351
339
|
|
|
352
|
-
// 首页 README.md 的时间戳
|
|
353
|
-
const homeReadmeCreatedAt = computed(() => readmeData?.created_at);
|
|
354
|
-
const homeReadmeUpdatedAt = computed(() => readmeData?.updated_at);
|
|
355
|
-
|
|
356
340
|
// #region - Composables
|
|
357
341
|
// 404 重定向
|
|
358
342
|
const { showNotFound, decodedCurrentPath, initRedirectCheck } =
|
|
@@ -509,20 +493,6 @@ const modalGithubPageUrl = computed(() => {
|
|
|
509
493
|
return "";
|
|
510
494
|
});
|
|
511
495
|
|
|
512
|
-
// modal 中显示的创建时间
|
|
513
|
-
const modalCreatedAt = computed(() => {
|
|
514
|
-
return modalIsHomeReadme.value
|
|
515
|
-
? homeReadmeCreatedAt.value
|
|
516
|
-
: modalNoteConfig.value.created_at;
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// modal 中显示的更新时间
|
|
520
|
-
const modalUpdatedAt = computed(() => {
|
|
521
|
-
return modalIsHomeReadme.value
|
|
522
|
-
? homeReadmeUpdatedAt.value
|
|
523
|
-
: modalNoteConfig.value.updated_at;
|
|
524
|
-
});
|
|
525
|
-
|
|
526
496
|
function openTimeModal() {
|
|
527
497
|
sidebarAboutNoteId.value = null;
|
|
528
498
|
timeModalOpen.value = true;
|
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vitepress/components/Layout/composables/useVSCodeIntegration.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 本地 IDE 集成和 GitHub 链接拦截
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useData } from 'vitepress'
|
|
8
|
-
import { ref } from 'vue'
|
|
8
|
+
import { computed, ref, watch } from 'vue'
|
|
9
9
|
|
|
10
|
+
import { useLocalIde } from '../../composables/useLocalIde'
|
|
10
11
|
import { NOTES_DIR_KEY } from '../../constants'
|
|
11
|
-
import {
|
|
12
|
-
resolveNoteReadmePath,
|
|
13
|
-
toVscodeFileUrl,
|
|
14
|
-
} from '../../utils/vscodePaths'
|
|
12
|
+
import { resolveNoteReadmePath, toIdeFileUrl } from '../../utils/vscodePaths'
|
|
15
13
|
|
|
16
14
|
import type { Router } from 'vitepress'
|
|
17
15
|
import type { Ref, ComputedRef } from 'vue'
|
|
18
16
|
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
18
|
+
* 本地 IDE 集成和 GitHub 链接拦截
|
|
22
19
|
*/
|
|
23
20
|
export function useVSCodeIntegration() {
|
|
24
21
|
const vpData = useData()
|
|
25
|
-
const
|
|
22
|
+
const { ide } = useLocalIde()
|
|
23
|
+
const noteLocalPath = ref('')
|
|
24
|
+
|
|
25
|
+
const vscodeNotesDir = computed(() => {
|
|
26
|
+
if (!noteLocalPath.value) return ''
|
|
27
|
+
// 显式依赖 ide,确保切换 IDE 后 href 更新
|
|
28
|
+
return toIdeFileUrl(noteLocalPath.value, ide.value)
|
|
29
|
+
})
|
|
26
30
|
|
|
27
|
-
//
|
|
31
|
+
// 更新当前页面对应的本地笔记路径
|
|
28
32
|
const updateVscodeNoteDir = () => {
|
|
29
33
|
if (typeof window !== 'undefined') {
|
|
30
34
|
const notesDir = localStorage.getItem(NOTES_DIR_KEY)
|
|
31
35
|
const notePath = notesDir
|
|
32
36
|
? resolveNoteReadmePath(notesDir, vpData.page.value.relativePath)
|
|
33
37
|
: null
|
|
34
|
-
|
|
38
|
+
noteLocalPath.value = notePath || ''
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
watch(ide, () => {
|
|
43
|
+
// IDE 切换后重新解析路径(无需刷新页面)
|
|
44
|
+
updateVscodeNoteDir()
|
|
45
|
+
})
|
|
46
|
+
|
|
38
47
|
// 拦截 home README 中的笔记链接,将 GitHub 链接转换为站点内跳转
|
|
39
48
|
const interceptHomeReadmeLinks = (
|
|
40
49
|
isHomeReadme: Ref<boolean> | ComputedRef<boolean>,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vitepress/components/Layout/homeReadme.data.ts
|
|
3
3
|
*/
|
|
4
|
-
import { execSync } from 'node:child_process'
|
|
5
4
|
import fs from 'node:fs'
|
|
6
5
|
import path from 'node:path'
|
|
7
6
|
|
|
@@ -12,8 +11,6 @@ interface ReadmeData {
|
|
|
12
11
|
doneNotesID: string[]
|
|
13
12
|
doneNotesLen: number
|
|
14
13
|
totalNotesLen: number
|
|
15
|
-
created_at?: number
|
|
16
|
-
updated_at?: number
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export default {
|
|
@@ -28,15 +25,10 @@ export default {
|
|
|
28
25
|
|
|
29
26
|
watchedFiles.forEach((file) => {
|
|
30
27
|
if (file.endsWith('README.md')) {
|
|
31
|
-
// console.log('file:', file) // => file: README.md
|
|
32
|
-
|
|
33
28
|
const fileContent = fs.readFileSync(file, 'utf-8')
|
|
34
29
|
const doneNotesID = getDoneNotesID(fileContent)
|
|
35
30
|
const doneNotesLen = doneNotesID.length
|
|
36
31
|
|
|
37
|
-
// 获取 git 仓库的时间戳
|
|
38
|
-
const timestamps = getGitTimestamps()
|
|
39
|
-
|
|
40
32
|
// 计算总笔记数
|
|
41
33
|
const notesDir = path.join(path.dirname(file), 'notes')
|
|
42
34
|
const totalNotesLen = getTotalNotesCount(notesDir)
|
|
@@ -46,7 +38,6 @@ export default {
|
|
|
46
38
|
doneNotesID,
|
|
47
39
|
doneNotesLen,
|
|
48
40
|
totalNotesLen,
|
|
49
|
-
...timestamps,
|
|
50
41
|
}
|
|
51
42
|
}
|
|
52
43
|
})
|
|
@@ -74,45 +65,6 @@ function getTotalNotesCount(notesDir: string): number {
|
|
|
74
65
|
}
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
/**
|
|
78
|
-
* 获取 git 仓库的时间戳
|
|
79
|
-
* @returns 包含 created_at 和 updated_at 的对象
|
|
80
|
-
*/
|
|
81
|
-
function getGitTimestamps(): {
|
|
82
|
-
created_at?: number
|
|
83
|
-
updated_at?: number
|
|
84
|
-
} {
|
|
85
|
-
const now = Date.now()
|
|
86
|
-
let created_at = now
|
|
87
|
-
let updated_at = now
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// 仓库的首次提交时间(最早的提交)
|
|
91
|
-
// 先获取所有提交,然后取第一行
|
|
92
|
-
const createdStdout = execSync(`git log --reverse --format=%ct`, {
|
|
93
|
-
encoding: 'utf-8',
|
|
94
|
-
})
|
|
95
|
-
const createdTs = createdStdout.toString().trim().split('\n')[0]
|
|
96
|
-
if (createdTs) {
|
|
97
|
-
created_at = parseInt(createdTs, 10) * 1000
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 仓库的最近一次提交时间
|
|
101
|
-
const updatedStdout = execSync(`git log -1 --format=%ct`, {
|
|
102
|
-
encoding: 'utf-8',
|
|
103
|
-
})
|
|
104
|
-
const updatedTs = updatedStdout.toString().trim()
|
|
105
|
-
if (updatedTs) {
|
|
106
|
-
updated_at = parseInt(updatedTs, 10) * 1000
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { created_at, updated_at }
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error(`获取 git 仓库时间戳失败:`, error)
|
|
112
|
-
return { created_at, updated_at }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
68
|
/**
|
|
117
69
|
* 返回已完成的笔记的 ID 列表
|
|
118
70
|
* @param fileContent 文件内容
|
|
@@ -12,17 +12,19 @@ import { onContentUpdated } from 'vitepress'
|
|
|
12
12
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
13
13
|
|
|
14
14
|
import { icon__fullscreen, icon__fullscreen_exit, icon__confirm } from '../../assets/icons'
|
|
15
|
-
import { MARKMAP_THEME_KEY
|
|
15
|
+
import { MARKMAP_THEME_KEY } from '../constants'
|
|
16
16
|
|
|
17
17
|
// doc: https://github.com/markmap/markmap/blob/205367a24603dc187f67da1658940c6cade20dce/packages/markmap-view/src/constants.ts#L15
|
|
18
18
|
|
|
19
|
+
const DEFAULT_EXPAND_LEVEL = 3
|
|
20
|
+
|
|
19
21
|
const props = defineProps({
|
|
20
22
|
content: { type: String, default: '' },
|
|
21
23
|
duration: { type: Number, default: 100 },
|
|
22
24
|
spacingVertical: { type: Number, default: 10 },
|
|
23
25
|
spacingHorizontal: { type: Number, default: 20 },
|
|
24
26
|
nodeMinHeight: { type: Number, default: 24 },
|
|
25
|
-
initialExpandLevel: { type: Number, default:
|
|
27
|
+
initialExpandLevel: { type: Number, default: DEFAULT_EXPAND_LEVEL },
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
const svgRef = ref<SVGSVGElement | null>(null)
|
|
@@ -36,15 +38,6 @@ let darkClassObserver: MutationObserver | null = null
|
|
|
36
38
|
|
|
37
39
|
const isMarkmapDark = ref(false)
|
|
38
40
|
|
|
39
|
-
// 从 localStorage 读取配置,如果没有则使用 props 或默认值
|
|
40
|
-
const getInitialExpandLevel = () => {
|
|
41
|
-
if (typeof window !== 'undefined') {
|
|
42
|
-
const saved = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
|
|
43
|
-
if (saved) return parseInt(saved)
|
|
44
|
-
}
|
|
45
|
-
return props.initialExpandLevel
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
const getThemeColorFn = () => {
|
|
49
42
|
if (typeof window !== 'undefined') {
|
|
50
43
|
if (document.documentElement.classList.contains('markmap-dark')) {
|
|
@@ -63,8 +56,8 @@ const getThemeColorFn = () => {
|
|
|
63
56
|
return scaleOrdinal(schemePastel2)
|
|
64
57
|
}
|
|
65
58
|
|
|
66
|
-
//
|
|
67
|
-
const expandLevel = ref(
|
|
59
|
+
// 可由 props 初始化,也可以通过工具栏改动
|
|
60
|
+
const expandLevel = ref(props.initialExpandLevel)
|
|
68
61
|
const transformer = new Transformer()
|
|
69
62
|
const isFullscreen = ref(false)
|
|
70
63
|
|