@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.
Files changed (62) hide show
  1. package/config/ConfigManager.ts +137 -0
  2. package/config/constants.ts +121 -0
  3. package/config/index.ts +25 -0
  4. package/config/templates.ts +49 -0
  5. package/core/GitManager.ts +513 -0
  6. package/core/NoteIndexCache.ts +194 -0
  7. package/core/NoteManager.ts +407 -0
  8. package/core/ProcessManager.ts +180 -0
  9. package/core/ReadmeGenerator.ts +215 -0
  10. package/core/TocGenerator.ts +212 -0
  11. package/core/index.ts +11 -0
  12. package/package.json +5 -2
  13. package/services/file-watcher/configChangeHandler.ts +64 -0
  14. package/services/file-watcher/eventScheduler.ts +179 -0
  15. package/services/file-watcher/folderChangeHandler.ts +325 -0
  16. package/services/file-watcher/fsWatcherAdapter.ts +128 -0
  17. package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
  18. package/services/file-watcher/index.ts +7 -0
  19. package/services/file-watcher/internal.ts +79 -0
  20. package/services/file-watcher/readmeChangeHandler.ts +28 -0
  21. package/services/file-watcher/renameDetector.ts +120 -0
  22. package/services/file-watcher/service.ts +352 -0
  23. package/services/file-watcher/watchState.ts +194 -0
  24. package/services/git/index.ts +7 -0
  25. package/services/git/service.ts +114 -0
  26. package/services/index.ts +15 -0
  27. package/services/init-sub-repo/index.ts +2 -0
  28. package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
  29. package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
  30. package/services/init-sub-repo/service.ts +101 -0
  31. package/services/note/index.ts +7 -0
  32. package/services/note/service.ts +362 -0
  33. package/services/readme/index.ts +7 -0
  34. package/services/readme/service.ts +761 -0
  35. package/services/timestamp/index.ts +7 -0
  36. package/services/timestamp/service.ts +465 -0
  37. package/services/toc/index.ts +5 -0
  38. package/services/toc/moveTocInside.test.ts +73 -0
  39. package/services/toc/service.ts +759 -0
  40. package/services/vitepress/index.ts +7 -0
  41. package/services/vitepress/service.ts +339 -0
  42. package/utils/errorHandler.ts +174 -0
  43. package/utils/file.ts +17 -0
  44. package/utils/genHierarchicalSidebar.ts +69 -0
  45. package/utils/generateAnchor.ts +24 -0
  46. package/utils/getChangedIds.ts +35 -0
  47. package/utils/index.ts +71 -0
  48. package/utils/logger.ts +231 -0
  49. package/utils/markdown.ts +75 -0
  50. package/utils/migrateReadmeToToc.test.ts +111 -0
  51. package/utils/migrateReadmeToToc.ts +135 -0
  52. package/utils/parseArgs.ts +90 -0
  53. package/utils/parseReadmeCompletedNotes.test.ts +90 -0
  54. package/utils/parseReadmeCompletedNotes.ts +108 -0
  55. package/utils/portUtils.ts +113 -0
  56. package/utils/readmeHelpers.ts +190 -0
  57. package/utils/runCommand.ts +29 -0
  58. package/utils/tocHelpers.test.ts +278 -0
  59. package/utils/tocHelpers.ts +855 -0
  60. package/utils/tocNodeId.test.ts +60 -0
  61. package/utils/tocNodeId.ts +97 -0
  62. 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
+ }