@tnotesjs/core 0.7.0 → 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.
Files changed (55) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/dist/markdown/index.cjs +6 -9
  26. package/dist/markdown/index.js +6 -9
  27. package/dist/vitepress/config/index.cjs +214 -58
  28. package/dist/vitepress/config/index.js +208 -52
  29. package/markdown/components.ts +86 -0
  30. package/markdown/index.ts +17 -0
  31. package/markdown/noteFormatter.test.ts +44 -0
  32. package/markdown/noteFormatter.ts +237 -0
  33. package/package.json +7 -3
  34. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  35. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  36. package/vitepress/components/Footprints/Footprints.vue +15 -537
  37. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  38. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  39. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  40. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  41. package/vitepress/configs/markdown.config.ts +170 -26
  42. package/vitepress/theme/index.ts +9 -13
  43. package/vitepress/theme/styles/base.scss +15 -0
  44. package/workspace/atomic.ts +113 -0
  45. package/workspace/errors.ts +27 -0
  46. package/workspace/index.ts +40 -0
  47. package/workspace/mutationQueue.ts +28 -0
  48. package/workspace/paths.ts +64 -0
  49. package/workspace/reconcile.test.ts +300 -0
  50. package/workspace/reconcile.ts +95 -0
  51. package/workspace/scanner.ts +292 -0
  52. package/workspace/types.ts +224 -0
  53. package/workspace/workspace.test.ts +333 -0
  54. package/workspace/workspace.ts +1020 -0
  55. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -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.7.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.1",
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",
@@ -1,24 +1,15 @@
1
- <!--
2
- vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue
1
+ <!--
2
+ Deprecated local shell. Canonical implementation lives in @tnotesjs/ui as
3
+ BilibiliVideo. Kept so deep path imports do not break during migration.
3
4
  -->
4
-
5
5
  <template>
6
- <iframe
7
- style="width: 100%; aspect-ratio: 16/9; margin: 1rem 0"
8
- :src="'//player.bilibili.com/player.html?isOutside=true&bvid=' + id"
9
- scrolling="no"
10
- border="0"
11
- frameborder="no"
12
- framespacing="0"
13
- allowfullscreen="true"
14
- ></iframe>
6
+ <BilibiliVideo :id="id" />
15
7
  </template>
16
8
 
17
9
  <script setup lang="ts">
18
- defineProps({
19
- id: {
20
- type: String,
21
- required: true,
22
- },
23
- })
10
+ import { BilibiliVideo } from '@tnotesjs/ui'
11
+
12
+ defineProps<{
13
+ id: string
14
+ }>()
24
15
  </script>