@tnotesjs/core 0.6.1 → 0.8.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/commands/BaseCommand.ts +58 -0
- package/commands/build/BuildCommand.ts +25 -0
- package/commands/build/PreviewCommand.ts +29 -0
- package/commands/build/index.ts +8 -0
- package/commands/dev/DevCommand.ts +75 -0
- package/commands/dev/index.ts +7 -0
- package/commands/git/PullCommand.ts +25 -0
- package/commands/git/PushCommand.ts +64 -0
- package/commands/git/index.ts +8 -0
- package/commands/index.ts +11 -0
- package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
- package/commands/init-sub-repo/index.ts +1 -0
- package/commands/misc/HelpCommand.ts +104 -0
- package/commands/misc/index.ts +7 -0
- package/commands/models.ts +87 -0
- package/commands/note/CreateNoteCommand.ts +160 -0
- package/commands/note/RenameNoteCommand.ts +147 -0
- package/commands/note/UpdateNoteConfigCommand.ts +78 -0
- package/commands/note/index.ts +9 -0
- package/commands/registry.ts +47 -0
- package/commands/update/UpdateCommand.ts +219 -0
- package/commands/update/index.ts +7 -0
- package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
- package/commands/update-completed-count/index.ts +5 -0
- package/config/templates.ts +0 -5
- package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
- package/dist/chunk-AGVER5MX.cjs +1746 -0
- package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
- package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
- package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
- package/dist/chunk-ZEQS64PE.js +1746 -0
- package/dist/cli/index.cjs +76 -155
- package/dist/cli/index.js +10 -89
- package/dist/markdown/index.cjs +8 -12
- package/dist/markdown/index.d.cts +1 -1
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/index.js +7 -11
- package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
- package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
- package/dist/vitepress/config/index.cjs +402 -178
- package/dist/vitepress/config/index.js +392 -168
- package/dist/workspace/index.cjs +4 -1202
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.ts +2 -2
- package/dist/workspace/index.js +4 -1202
- package/markdown/components.ts +86 -0
- package/markdown/index.ts +17 -0
- package/markdown/noteFormatter.test.ts +44 -0
- package/markdown/noteFormatter.ts +237 -0
- package/package.json +7 -3
- package/services/file-watcher/folderChangeHandler.ts +19 -15
- package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
- package/services/file-watcher/service.ts +1 -6
- package/services/index.ts +0 -1
- package/services/note/service.ts +4 -6
- package/services/readme/service.ts +11 -18
- package/services/reconcileToc.ts +20 -0
- package/utils/index.ts +0 -9
- package/{services/toc → utils}/moveTocInside.test.ts +1 -1
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
- package/vitepress/components/EnWordList/EnWordList.vue +16 -662
- package/vitepress/components/Footprints/Footprints.vue +15 -537
- package/vitepress/components/Mermaid/Mermaid.vue +13 -588
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
- package/vitepress/components/MindmapPreview/markdown.ts +1 -1
- package/vitepress/components/NotesTable/NotesTable.vue +11 -130
- package/vitepress/components/constants.ts +2 -2
- package/vitepress/components/sidebar.data.ts +8 -5
- package/vitepress/configs/markdown.config.ts +170 -26
- package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
- package/vitepress/theme/index.ts +9 -13
- package/vitepress/theme/styles/base.scss +15 -0
- package/workspace/atomic.ts +113 -0
- package/workspace/errors.ts +27 -0
- package/workspace/index.ts +40 -0
- package/workspace/mutationQueue.ts +28 -0
- package/workspace/paths.ts +64 -0
- package/workspace/reconcile.test.ts +300 -0
- package/workspace/reconcile.ts +95 -0
- package/workspace/scanner.ts +292 -0
- package/workspace/types.ts +224 -0
- package/workspace/workspace.test.ts +333 -0
- package/workspace/workspace.ts +1020 -0
- package/dist/chunk-3R7QSABB.cjs +0 -502
- package/dist/chunk-CZXRQPFJ.js +0 -11
- package/dist/chunk-HXED7KVT.cjs +0 -11
- package/dist/chunk-U6IBLREL.js +0 -502
- package/services/toc/index.ts +0 -5
- package/services/toc/service.ts +0 -759
- package/utils/migrateReadmeToToc.test.ts +0 -111
- package/utils/migrateReadmeToToc.ts +0 -135
- package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type TNotesComponentKind =
|
|
2
|
+
| 'block-component'
|
|
3
|
+
| 'inline-component'
|
|
4
|
+
| 'container'
|
|
5
|
+
| 'fenced-language'
|
|
6
|
+
|
|
7
|
+
export interface TNotesComponentDescriptor {
|
|
8
|
+
name: string
|
|
9
|
+
aliases?: string[]
|
|
10
|
+
kind: TNotesComponentKind
|
|
11
|
+
editable: 'visual' | 'source-only' | 'placeholder'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runtime-neutral registry shared by Desk and other TNotes clients. It does not
|
|
16
|
+
* import Vue or VitePress and is therefore safe in Node/Electron workers.
|
|
17
|
+
*/
|
|
18
|
+
export const TNOTES_COMPONENTS: readonly TNotesComponentDescriptor[] = [
|
|
19
|
+
{
|
|
20
|
+
name: 'BilibiliVideo',
|
|
21
|
+
kind: 'block-component',
|
|
22
|
+
editable: 'visual',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'WordList',
|
|
26
|
+
kind: 'block-component',
|
|
27
|
+
editable: 'visual',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Footprints',
|
|
31
|
+
kind: 'block-component',
|
|
32
|
+
editable: 'visual',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'NotesTable',
|
|
36
|
+
kind: 'block-component',
|
|
37
|
+
editable: 'visual',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Tooltip',
|
|
41
|
+
kind: 'inline-component',
|
|
42
|
+
editable: 'visual',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Mindmap',
|
|
46
|
+
aliases: ['MindmapPreview'],
|
|
47
|
+
kind: 'block-component',
|
|
48
|
+
editable: 'visual',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'Mermaid',
|
|
52
|
+
kind: 'block-component',
|
|
53
|
+
editable: 'visual',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Discussions',
|
|
57
|
+
kind: 'block-component',
|
|
58
|
+
editable: 'placeholder',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'SidebarCard',
|
|
62
|
+
kind: 'block-component',
|
|
63
|
+
editable: 'placeholder',
|
|
64
|
+
},
|
|
65
|
+
{ name: 'swiper', kind: 'container', editable: 'visual' },
|
|
66
|
+
{ name: 'code-group', kind: 'container', editable: 'visual' },
|
|
67
|
+
{ name: 'footprints', kind: 'container', editable: 'visual' },
|
|
68
|
+
{ name: 'details', kind: 'container', editable: 'visual' },
|
|
69
|
+
{ name: 'info', kind: 'container', editable: 'visual' },
|
|
70
|
+
{ name: 'tip', kind: 'container', editable: 'visual' },
|
|
71
|
+
{ name: 'warning', kind: 'container', editable: 'visual' },
|
|
72
|
+
{ name: 'danger', kind: 'container', editable: 'visual' },
|
|
73
|
+
{ name: 'mermaid', kind: 'fenced-language', editable: 'visual' },
|
|
74
|
+
{ name: 'mindmap', kind: 'fenced-language', editable: 'visual' },
|
|
75
|
+
] as const
|
|
76
|
+
|
|
77
|
+
export function findTNotesComponent(
|
|
78
|
+
name: string,
|
|
79
|
+
): TNotesComponentDescriptor | undefined {
|
|
80
|
+
const normalized = name.toLowerCase()
|
|
81
|
+
return TNOTES_COMPONENTS.find(
|
|
82
|
+
(component) =>
|
|
83
|
+
component.name.toLowerCase() === normalized ||
|
|
84
|
+
component.aliases?.some((alias) => alias.toLowerCase() === normalized),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
NOTE_TOC_END_TAG,
|
|
3
|
+
NOTE_TOC_START_TAG,
|
|
4
|
+
formatTNotesNote,
|
|
5
|
+
} from './noteFormatter'
|
|
6
|
+
export type {
|
|
7
|
+
FormatTNotesNoteInput,
|
|
8
|
+
FormatTNotesNoteResult,
|
|
9
|
+
} from './noteFormatter'
|
|
10
|
+
export {
|
|
11
|
+
TNOTES_COMPONENTS,
|
|
12
|
+
findTNotesComponent,
|
|
13
|
+
} from './components'
|
|
14
|
+
export type {
|
|
15
|
+
TNotesComponentDescriptor,
|
|
16
|
+
TNotesComponentKind,
|
|
17
|
+
} from './components'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findTNotesComponent,
|
|
5
|
+
formatTNotesNote,
|
|
6
|
+
TNOTES_COMPONENTS,
|
|
7
|
+
} from './index'
|
|
8
|
+
|
|
9
|
+
describe('TNotes Markdown contract', () => {
|
|
10
|
+
it('exposes runtime-neutral component descriptors', () => {
|
|
11
|
+
expect(findTNotesComponent('NotesTable')?.name).toBe('NotesTable')
|
|
12
|
+
expect(findTNotesComponent('WordList')?.name).toBe('WordList')
|
|
13
|
+
expect(findTNotesComponent('BilibiliVideo')?.name).toBe('BilibiliVideo')
|
|
14
|
+
expect(findTNotesComponent('N')).toBeUndefined()
|
|
15
|
+
expect(findTNotesComponent('SWIPER')?.kind).toBe('container')
|
|
16
|
+
expect(findTNotesComponent('Mermaid')?.editable).toBe('visual')
|
|
17
|
+
expect(TNOTES_COMPONENTS.some((item) => item.name === 'markmap')).toBe(false)
|
|
18
|
+
expect(TNOTES_COMPONENTS.some((item) => item.name === 'mindmap')).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('restores missing generated markers without interpreting fenced headings', async () => {
|
|
22
|
+
const result = await formatTNotesNote({
|
|
23
|
+
content: '## Heading\n\n~~~md\n### source\n~~~\n',
|
|
24
|
+
noteIndex: '0042',
|
|
25
|
+
title: 'Answer',
|
|
26
|
+
repoOwner: 'tnotesjs',
|
|
27
|
+
repoName: 'TNotes.fixture',
|
|
28
|
+
noteConfig: {
|
|
29
|
+
id: 'answer',
|
|
30
|
+
bilibili: [],
|
|
31
|
+
tnotes: [],
|
|
32
|
+
yuque: [],
|
|
33
|
+
done: false,
|
|
34
|
+
enableDiscussions: false,
|
|
35
|
+
},
|
|
36
|
+
prettier: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(result.content).toContain('<!-- region:toc -->')
|
|
40
|
+
expect(result.content).toContain('<!-- endregion:toc -->')
|
|
41
|
+
expect(result.generatedToc).toEqual(['- [1. Heading](#1-heading)'])
|
|
42
|
+
expect(result.content).toContain('~~~md\n### source\n~~~')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { generateAnchor } from '../utils/generateAnchor'
|
|
2
|
+
|
|
3
|
+
import type { WorkspaceNoteConfig } from '../workspace/types'
|
|
4
|
+
|
|
5
|
+
export const NOTE_TOC_START_TAG = '<!-- region:toc -->'
|
|
6
|
+
export const NOTE_TOC_END_TAG = '<!-- endregion:toc -->'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lazy-loads prettier so consumers that never format (e.g. the read-only Nav
|
|
10
|
+
* VS Code extension) don't pay the top-level import cost / don't break their
|
|
11
|
+
* bundle (prettier's ESM entry crashes inside a CJS bundle at load time).
|
|
12
|
+
*/
|
|
13
|
+
async function prettierFormat(
|
|
14
|
+
content: string,
|
|
15
|
+
options: { parser: string; proseWrap: string; endOfLine: string },
|
|
16
|
+
): Promise<string> {
|
|
17
|
+
const module = await import('prettier')
|
|
18
|
+
const prettier = (module as { default?: typeof import('prettier') }).default ?? module
|
|
19
|
+
return prettier.format(content, options as Parameters<typeof prettier.format>[1])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FormatTNotesNoteInput {
|
|
23
|
+
content: string
|
|
24
|
+
noteIndex: string
|
|
25
|
+
title: string
|
|
26
|
+
repoOwner: string
|
|
27
|
+
repoName: string
|
|
28
|
+
noteConfig: WorkspaceNoteConfig
|
|
29
|
+
prettier?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FormatTNotesNoteResult {
|
|
33
|
+
content: string
|
|
34
|
+
generatedTitle: string
|
|
35
|
+
generatedToc: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function generatedNoteTitle(input: FormatTNotesNoteInput): string {
|
|
39
|
+
const dirName = `${input.noteIndex}. ${input.title}`
|
|
40
|
+
const encodedDirName = encodeURIComponent(dirName)
|
|
41
|
+
const repositoryUrl = `https://github.com/${input.repoOwner}/${input.repoName}/tree/main/notes`
|
|
42
|
+
return `# [${dirName}](${repositoryUrl}/${encodedDirName})`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureGeneratedTitle(lines: string[], generatedTitle: string): void {
|
|
46
|
+
if (lines.length === 0) {
|
|
47
|
+
lines.push(generatedTitle)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines[0] = lines[0].replace(/^\uFEFF/, '')
|
|
52
|
+
if (lines[0].trimStart().startsWith('# ')) {
|
|
53
|
+
lines[0] = generatedTitle
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines.unshift(generatedTitle, '')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ensureTocRegion(lines: string[]): { start: number; end: number } {
|
|
61
|
+
let start = lines.findIndex((line) => line.trim() === NOTE_TOC_START_TAG)
|
|
62
|
+
let end = lines.findIndex(
|
|
63
|
+
(line, index) => index > start && line.trim() === NOTE_TOC_END_TAG,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (start >= 0 && end > start) return { start, end }
|
|
67
|
+
|
|
68
|
+
if (start >= 0) lines.splice(start, 1)
|
|
69
|
+
if (end >= 0) lines.splice(end > start ? end - 1 : end, 1)
|
|
70
|
+
|
|
71
|
+
let insertAt = 1
|
|
72
|
+
while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++
|
|
73
|
+
lines.splice(
|
|
74
|
+
insertAt,
|
|
75
|
+
0,
|
|
76
|
+
'',
|
|
77
|
+
NOTE_TOC_START_TAG,
|
|
78
|
+
'',
|
|
79
|
+
NOTE_TOC_END_TAG,
|
|
80
|
+
'',
|
|
81
|
+
)
|
|
82
|
+
start = insertAt + 1
|
|
83
|
+
end = insertAt + 3
|
|
84
|
+
return { start, end }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Heading {
|
|
88
|
+
level: number
|
|
89
|
+
text: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeHeadings(lines: string[]): Heading[] {
|
|
93
|
+
const headings: Heading[] = []
|
|
94
|
+
const counters = { h2: 0, h3: 0 }
|
|
95
|
+
let fence: { marker: '`' | '~'; length: number } | null = null
|
|
96
|
+
let inHtmlComment = false
|
|
97
|
+
|
|
98
|
+
for (let index = 0; index < lines.length; index++) {
|
|
99
|
+
const line = lines[index]
|
|
100
|
+
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})/)
|
|
101
|
+
if (fenceMatch) {
|
|
102
|
+
const marker = fenceMatch[1][0] as '`' | '~'
|
|
103
|
+
if (!fence) {
|
|
104
|
+
fence = { marker, length: fenceMatch[1].length }
|
|
105
|
+
} else if (fence.marker === marker && fenceMatch[1].length >= fence.length) {
|
|
106
|
+
fence = null
|
|
107
|
+
}
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if (fence) continue
|
|
111
|
+
|
|
112
|
+
if (inHtmlComment) {
|
|
113
|
+
if (line.includes('-->')) inHtmlComment = false
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
const commentStart = line.indexOf('<!--')
|
|
117
|
+
if (commentStart >= 0) {
|
|
118
|
+
const commentEnd = line.indexOf('-->', commentStart + 4)
|
|
119
|
+
if (commentEnd < 0) inHtmlComment = true
|
|
120
|
+
if (line.trimStart().startsWith('<!--')) continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const match = line.match(/^(#{2,6})\s+(.+?)\s*#*\s*$/)
|
|
124
|
+
if (!match) continue
|
|
125
|
+
|
|
126
|
+
const level = match[1].length
|
|
127
|
+
const plainText = match[2]
|
|
128
|
+
.replace(/^\d+(?:\.\d+)+\.?\s+|^\d+\.\s+/, '')
|
|
129
|
+
.trim()
|
|
130
|
+
let text = plainText
|
|
131
|
+
|
|
132
|
+
if (level === 2) {
|
|
133
|
+
counters.h2 += 1
|
|
134
|
+
counters.h3 = 0
|
|
135
|
+
text = `${counters.h2}. ${plainText}`
|
|
136
|
+
} else if (level === 3) {
|
|
137
|
+
counters.h3 += 1
|
|
138
|
+
text = `${counters.h2}.${counters.h3}. ${plainText}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines[index] = `${'#'.repeat(level)} ${text}`
|
|
142
|
+
headings.push({ level, text })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return headings
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildHeadingToc(headings: Heading[]): string[] {
|
|
149
|
+
return headings.map((heading) => {
|
|
150
|
+
const indent = ' '.repeat(Math.max(0, heading.level - 2) * 2)
|
|
151
|
+
return `${indent}- [${heading.text}](#${generateAnchor(heading.text)})`
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stringArray(value: unknown): string[] {
|
|
156
|
+
return Array.isArray(value)
|
|
157
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
158
|
+
: []
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildResourceToc(input: FormatTNotesNoteInput): string[] {
|
|
162
|
+
const bilibili = stringArray(input.noteConfig.bilibili)
|
|
163
|
+
const relatedTNotes = stringArray(input.noteConfig.tnotes)
|
|
164
|
+
const yuque = stringArray(input.noteConfig.yuque)
|
|
165
|
+
if (bilibili.length + relatedTNotes.length + yuque.length === 0) return []
|
|
166
|
+
|
|
167
|
+
const lines: string[] = ['::: details 📚 相关资源', '']
|
|
168
|
+
if (bilibili.length > 0) {
|
|
169
|
+
lines.push(
|
|
170
|
+
'- [📺 bilibili(笔记视频资源)](https://space.bilibili.com/407241004)',
|
|
171
|
+
...bilibili.map(
|
|
172
|
+
(bvid, index) =>
|
|
173
|
+
` - [bilibili.${input.repoName}.${input.noteIndex}.${index + 1}](https://www.bilibili.com/video/${bvid})`,
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
if (relatedTNotes.length > 0) {
|
|
178
|
+
lines.push(
|
|
179
|
+
'- [📒 TNotes(相关知识库)](https://tnotesjs.github.io/TNotes/)',
|
|
180
|
+
...relatedTNotes.map(
|
|
181
|
+
(repoName) =>
|
|
182
|
+
` - [TNotes.${repoName}](https://tnotesjs.github.io/TNotes.${repoName}/)`,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
if (yuque.length > 0) {
|
|
187
|
+
const base = 'https://www.yuque.com/tdahuyou/tnotes.yuque/'
|
|
188
|
+
lines.push(
|
|
189
|
+
`- [📂 TNotes.yuque(笔记附件资源)](${base})`,
|
|
190
|
+
...yuque.map(
|
|
191
|
+
(slug) =>
|
|
192
|
+
` - [TNotes.yuque.${input.repoName.replace('TNotes.', '')}.${input.noteIndex}](${base}${slug})`,
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
lines.push('', ':::', '')
|
|
197
|
+
return lines
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function formatTNotesNote(
|
|
201
|
+
input: FormatTNotesNoteInput,
|
|
202
|
+
): Promise<FormatTNotesNoteResult> {
|
|
203
|
+
let content = input.content.replace(/\r\n?/g, '\n')
|
|
204
|
+
if (input.prettier !== false) {
|
|
205
|
+
content = await prettierFormat(content, {
|
|
206
|
+
parser: 'markdown',
|
|
207
|
+
proseWrap: 'never',
|
|
208
|
+
endOfLine: 'lf',
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const lines = content.replace(/\n$/, '').split('\n')
|
|
213
|
+
const title = generatedNoteTitle(input)
|
|
214
|
+
ensureGeneratedTitle(lines, title)
|
|
215
|
+
let region = ensureTocRegion(lines)
|
|
216
|
+
const headings = normalizeHeadings(lines)
|
|
217
|
+
|
|
218
|
+
// Heading normalization does not add/remove lines, but resolving the region
|
|
219
|
+
// again makes the invariant explicit and protects future format extensions.
|
|
220
|
+
region = ensureTocRegion(lines)
|
|
221
|
+
const toc = buildHeadingToc(headings)
|
|
222
|
+
const resources = buildResourceToc(input)
|
|
223
|
+
lines.splice(
|
|
224
|
+
region.start + 1,
|
|
225
|
+
region.end - region.start - 1,
|
|
226
|
+
'',
|
|
227
|
+
...resources,
|
|
228
|
+
...toc,
|
|
229
|
+
'',
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
content: `${lines.join('\n').replace(/\n{3,}$/g, '\n\n')}\n`,
|
|
234
|
+
generatedTitle: title,
|
|
235
|
+
generatedToc: toc,
|
|
236
|
+
}
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.17.1",
|
|
@@ -34,14 +34,17 @@
|
|
|
34
34
|
"./vitepress/*": "./vitepress/*"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
+
"commands/",
|
|
37
38
|
"config/",
|
|
38
39
|
"core/",
|
|
40
|
+
"markdown/",
|
|
39
41
|
"services/",
|
|
40
42
|
"utils/",
|
|
41
43
|
"dist/",
|
|
42
44
|
"templates/",
|
|
43
45
|
"vitepress/",
|
|
44
|
-
"types/"
|
|
46
|
+
"types/",
|
|
47
|
+
"workspace/"
|
|
45
48
|
],
|
|
46
49
|
"scripts": {
|
|
47
50
|
"build": "tsup",
|
|
@@ -61,7 +64,8 @@
|
|
|
61
64
|
"vue": "^3.5.0"
|
|
62
65
|
},
|
|
63
66
|
"dependencies": {
|
|
64
|
-
"@tnotesjs/mindmap-core": "^0.2.
|
|
67
|
+
"@tnotesjs/mindmap-core": "^0.2.2",
|
|
68
|
+
"@tnotesjs/ui": "^0.1.1",
|
|
65
69
|
"echarts": "^6.0.0",
|
|
66
70
|
"github-slugger": "^2.0.0",
|
|
67
71
|
"markdown-it-container": "^4.0.0",
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, promises as fsPromises } from 'fs'
|
|
8
|
-
import { join } from 'path'
|
|
8
|
+
import { dirname, join } from 'path'
|
|
9
9
|
|
|
10
10
|
import { safeExecute } from './internal'
|
|
11
11
|
import { REPO_NOTES_URL } from '../../config/constants'
|
|
12
12
|
import { generateNoteTitle } from '../../config/templates'
|
|
13
|
+
import { reconcileTocFromFiles } from '../reconcileToc'
|
|
13
14
|
|
|
14
15
|
import type { EventScheduler } from './eventScheduler'
|
|
15
16
|
import type { WatchState } from './watchState'
|
|
@@ -17,7 +18,6 @@ import type { NoteIndexCache } from '../../core/NoteIndexCache'
|
|
|
17
18
|
import type { Logger } from '../../utils'
|
|
18
19
|
import type { NoteService } from '../note/service'
|
|
19
20
|
import type { ReadmeService } from '../readme/service'
|
|
20
|
-
import type { TocService } from '../toc/service'
|
|
21
21
|
|
|
22
22
|
const RENAME_REVERT_DELAY_MS = 2000
|
|
23
23
|
|
|
@@ -41,8 +41,6 @@ interface FolderChangeHandlerConfig {
|
|
|
41
41
|
noteService: NoteService
|
|
42
42
|
/** README 服务实例 */
|
|
43
43
|
readmeService: ReadmeService
|
|
44
|
-
/** TOC 服务实例 */
|
|
45
|
-
tocService: TocService
|
|
46
44
|
/** 笔记索引缓存实例 */
|
|
47
45
|
noteIndexCache: NoteIndexCache
|
|
48
46
|
/** 日志记录器 */
|
|
@@ -89,7 +87,7 @@ export class FolderChangeHandler {
|
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
async handleFolderDeletion(deletedFolderName: string): Promise<void> {
|
|
92
|
-
const { scheduler, watchState, noteIndexCache,
|
|
90
|
+
const { scheduler, watchState, noteIndexCache, logger, notesDir } =
|
|
93
91
|
this.config
|
|
94
92
|
|
|
95
93
|
if (scheduler.getUpdating()) return
|
|
@@ -106,12 +104,11 @@ export class FolderChangeHandler {
|
|
|
106
104
|
watchState.clearNoteCaches(deletedFolderName)
|
|
107
105
|
noteIndexCache.delete(noteIndex)
|
|
108
106
|
|
|
109
|
-
//
|
|
107
|
+
// files→TOC 对齐(Workspace):TOC 行随目录消失而移除
|
|
110
108
|
await safeExecute(
|
|
111
109
|
`删除笔记 ${noteIndex}`,
|
|
112
110
|
async () => {
|
|
113
|
-
await
|
|
114
|
-
await tocService.regenerateSidebar()
|
|
111
|
+
await reconcileTocFromFiles(dirname(notesDir))
|
|
115
112
|
this.config.onNoteStructureChanged?.(`watcher:delete:${noteIndex}`)
|
|
116
113
|
},
|
|
117
114
|
logger,
|
|
@@ -202,7 +199,7 @@ export class FolderChangeHandler {
|
|
|
202
199
|
noteIndex: string,
|
|
203
200
|
newName: string,
|
|
204
201
|
): Promise<void> {
|
|
205
|
-
const { noteIndexCache,
|
|
202
|
+
const { noteIndexCache, logger, notesDir } = this.config
|
|
206
203
|
|
|
207
204
|
logger.info(`笔记索引未变 (${noteIndex}),只更新标题`)
|
|
208
205
|
noteIndexCache.updateFolderName(noteIndex, newName)
|
|
@@ -214,8 +211,12 @@ export class FolderChangeHandler {
|
|
|
214
211
|
logger,
|
|
215
212
|
)
|
|
216
213
|
|
|
217
|
-
|
|
218
|
-
await
|
|
214
|
+
// files→TOC 对齐(Workspace):标题以目录名为准,行位置/完成标记保留
|
|
215
|
+
await safeExecute(
|
|
216
|
+
`标题重命名同步目录 ${noteIndex}`,
|
|
217
|
+
() => reconcileTocFromFiles(dirname(notesDir)),
|
|
218
|
+
logger,
|
|
219
|
+
)
|
|
219
220
|
this.config.onNoteStructureChanged?.(`watcher:rename-title:${noteIndex}`)
|
|
220
221
|
logger.success(`标题更新完成`)
|
|
221
222
|
}
|
|
@@ -266,11 +267,16 @@ export class FolderChangeHandler {
|
|
|
266
267
|
oldNoteIndex: string,
|
|
267
268
|
newNoteIndex: string,
|
|
268
269
|
): Promise<void> {
|
|
269
|
-
const { noteService, noteIndexCache,
|
|
270
|
+
const { noteService, noteIndexCache, logger, notesDir } = this.config
|
|
270
271
|
|
|
271
272
|
logger.info(`笔记索引变更: ${oldNoteIndex} → ${newNoteIndex}`)
|
|
272
273
|
|
|
273
|
-
|
|
274
|
+
// files→TOC 对齐(Workspace):旧索引行随目录消失移除,新索引行按编号追加
|
|
275
|
+
await safeExecute(
|
|
276
|
+
`索引变更同步目录 ${oldNoteIndex}→${newNoteIndex}`,
|
|
277
|
+
() => reconcileTocFromFiles(dirname(notesDir)),
|
|
278
|
+
logger,
|
|
279
|
+
)
|
|
274
280
|
|
|
275
281
|
const newNote = noteService.getNoteByIndex(newNoteIndex)
|
|
276
282
|
|
|
@@ -278,8 +284,6 @@ export class FolderChangeHandler {
|
|
|
278
284
|
noteIndexCache.delete(oldNoteIndex)
|
|
279
285
|
noteIndexCache.add(newNote)
|
|
280
286
|
|
|
281
|
-
await tocService.appendNoteToToc(newNoteIndex)
|
|
282
|
-
await tocService.regenerateSidebar()
|
|
283
287
|
this.config.onNoteStructureChanged?.(
|
|
284
288
|
`watcher:rename-index:${oldNoteIndex}->${newNoteIndex}`,
|
|
285
289
|
)
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* services/file-watcher/globalUpdateCoordinator.ts
|
|
3
3
|
*
|
|
4
|
-
* 全局更新协调:应用配置更新、更新 README
|
|
4
|
+
* 全局更新协调:应用配置更新、更新 README 列表。
|
|
5
|
+
* TOC/sidebar 的写入统一走 Workspace 的 `toc.reconcileFromFiles()`
|
|
6
|
+
* (0004 方案 B),不再直接操作 TocService。
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import { dirname } from 'path'
|
|
10
|
+
|
|
7
11
|
import { safeExecute } from './internal'
|
|
12
|
+
import { reconcileTocFromFiles } from '../reconcileToc'
|
|
8
13
|
|
|
9
14
|
import type { WatchEvent } from './internal'
|
|
10
|
-
import type { NoteIndexCache } from '../../core/NoteIndexCache'
|
|
11
15
|
import type { Logger } from '../../utils'
|
|
12
16
|
import type { ReadmeService } from '../readme/service'
|
|
13
|
-
import type { TocService } from '../toc/service'
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
interface GlobalUpdateCoordinatorConfig {
|
|
20
|
+
/** 笔记目录路径 */
|
|
21
|
+
notesDir: string
|
|
17
22
|
/** README 服务实例,用于更新笔记 README */
|
|
18
23
|
readmeService: ReadmeService
|
|
19
|
-
/** TOC 服务实例,用于更新 TOC.md 与 sidebar */
|
|
20
|
-
tocService: TocService
|
|
21
|
-
/** 笔记索引缓存实例 */
|
|
22
|
-
noteIndexCache: NoteIndexCache
|
|
23
24
|
/** 日志记录器 */
|
|
24
25
|
logger: Logger
|
|
25
26
|
}
|
|
@@ -30,26 +31,19 @@ export class GlobalUpdateCoordinator {
|
|
|
30
31
|
async applyConfigUpdates(changedNoteIndexes: string[]): Promise<void> {
|
|
31
32
|
if (changedNoteIndexes.length === 0) return
|
|
32
33
|
|
|
33
|
-
const {
|
|
34
|
-
|
|
35
|
-
logger.info('
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
logger.info(`增量更新 TOC.md 中的笔记: ${noteIndex}`)
|
|
47
|
-
},
|
|
48
|
-
logger,
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
await tocService.regenerateSidebar()
|
|
34
|
+
const { logger, notesDir } = this.config
|
|
35
|
+
|
|
36
|
+
logger.info('检测到笔记状态变化,同步全局文件...')
|
|
37
|
+
|
|
38
|
+
// files→TOC 对齐(Workspace):config.done 等以文件为真值,
|
|
39
|
+
// 同步到 TOC 行标记与 sidebar。
|
|
40
|
+
await safeExecute(
|
|
41
|
+
'配置变更同步目录',
|
|
42
|
+
() => reconcileTocFromFiles(dirname(notesDir)),
|
|
43
|
+
logger,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
logger.info(`已同步配置变更的笔记: ${changedNoteIndexes.join(', ')}`)
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
async updateNoteReadmesOnly(events: WatchEvent[]): Promise<void> {
|
|
@@ -24,7 +24,6 @@ import { NoteIndexCache } from '../../core/NoteIndexCache'
|
|
|
24
24
|
import { logger } from '../../utils'
|
|
25
25
|
import { NoteService } from '../note/service'
|
|
26
26
|
import { ReadmeService } from '../readme/service'
|
|
27
|
-
import { TocService } from '../toc/service'
|
|
28
27
|
|
|
29
28
|
import type { WatchEvent } from './internal'
|
|
30
29
|
|
|
@@ -49,7 +48,6 @@ export class FileWatcherService {
|
|
|
49
48
|
private adapter!: FsWatcherAdapter
|
|
50
49
|
private noteService!: NoteService
|
|
51
50
|
private readmeService!: ReadmeService
|
|
52
|
-
private tocService!: TocService
|
|
53
51
|
private noteIndexCache!: NoteIndexCache
|
|
54
52
|
private unlockTimer: NodeJS.Timeout | null = null
|
|
55
53
|
|
|
@@ -68,7 +66,6 @@ export class FileWatcherService {
|
|
|
68
66
|
private init(): void {
|
|
69
67
|
this.noteService = NoteService.getInstance()
|
|
70
68
|
this.readmeService = ReadmeService.getInstance()
|
|
71
|
-
this.tocService = TocService.getInstance()
|
|
72
69
|
this.noteIndexCache = NoteIndexCache.getInstance()
|
|
73
70
|
|
|
74
71
|
this.watchState = this.initWatchState()
|
|
@@ -103,7 +100,6 @@ export class FileWatcherService {
|
|
|
103
100
|
scheduler: this.scheduler,
|
|
104
101
|
noteService: this.noteService,
|
|
105
102
|
readmeService: this.readmeService,
|
|
106
|
-
tocService: this.tocService,
|
|
107
103
|
noteIndexCache: this.noteIndexCache,
|
|
108
104
|
logger,
|
|
109
105
|
onRenameSuccess: (payload) => {
|
|
@@ -228,9 +224,8 @@ export class FileWatcherService {
|
|
|
228
224
|
|
|
229
225
|
private initCoordinator(): GlobalUpdateCoordinator {
|
|
230
226
|
return new GlobalUpdateCoordinator({
|
|
227
|
+
notesDir: this.notesDir,
|
|
231
228
|
readmeService: this.readmeService,
|
|
232
|
-
tocService: this.tocService,
|
|
233
|
-
noteIndexCache: this.noteIndexCache,
|
|
234
229
|
logger,
|
|
235
230
|
})
|
|
236
231
|
}
|
package/services/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ export { FileWatcherService } from './file-watcher'
|
|
|
8
8
|
export { GitService } from './git'
|
|
9
9
|
export { NoteService } from './note'
|
|
10
10
|
export { ReadmeService } from './readme'
|
|
11
|
-
export { TocService } from './toc'
|
|
12
11
|
export { VitepressService } from './vitepress'
|
|
13
12
|
export { InitSubRepoService } from './init-sub-repo'
|
|
14
13
|
export * from './init-sub-repo/initSubRepoLogic'
|
package/services/note/service.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { writeFileSync, readFileSync, promises as fsPromises } from 'fs'
|
|
|
8
8
|
import { join } from 'path'
|
|
9
9
|
import { v4 as uuidv4 } from 'uuid'
|
|
10
10
|
|
|
11
|
-
import { NOTES_PATH, CONSTANTS, REPO_NOTES_URL } from '../../config/constants'
|
|
11
|
+
import { NOTES_PATH, CONSTANTS, REPO_NOTES_URL, ROOT_DIR_PATH } from '../../config/constants'
|
|
12
12
|
import {
|
|
13
13
|
generateNoteTitle,
|
|
14
14
|
getNewNoteReadmeBody,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { NoteIndexCache } from '../../core/NoteIndexCache'
|
|
17
17
|
import { NoteManager } from '../../core/NoteManager'
|
|
18
18
|
import { ensureDirectory, logger } from '../../utils'
|
|
19
|
-
import {
|
|
19
|
+
import { reconcileTocFromFiles } from '../reconcileToc'
|
|
20
20
|
|
|
21
21
|
import type { NoteInfo, NoteConfig } from '../../types'
|
|
22
22
|
|
|
@@ -247,10 +247,8 @@ export class NoteService {
|
|
|
247
247
|
logger.info(`检测到全局字段变更 (${noteIndex}),正在增量更新全局文件...`)
|
|
248
248
|
|
|
249
249
|
// 使用增量更新
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
await tocService.updateNoteInToc(noteIndex, updates)
|
|
253
|
-
await tocService.regenerateSidebar()
|
|
250
|
+
// files→TOC 对齐(Workspace):config 已写盘,同步 TOC 行标记与 sidebar
|
|
251
|
+
await reconcileTocFromFiles(ROOT_DIR_PATH)
|
|
254
252
|
|
|
255
253
|
logger.info(`全局文件增量更新完成 (${noteIndex})`)
|
|
256
254
|
} else {
|