@tnotesjs/core 0.4.2 → 0.6.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 (48) hide show
  1. package/dist/chunk-3R7QSABB.cjs +502 -0
  2. package/dist/chunk-CZXRQPFJ.js +11 -0
  3. package/dist/{chunk-XCWFZLER.js → chunk-DUTS75WQ.js} +27 -493
  4. package/dist/chunk-HXED7KVT.cjs +11 -0
  5. package/dist/chunk-IHFUHB4J.cjs +176 -0
  6. package/dist/chunk-MZIXIY2Y.cjs +216 -0
  7. package/dist/chunk-TJ4527KA.cjs +5264 -0
  8. package/dist/chunk-U6IBLREL.js +502 -0
  9. package/dist/chunk-UNFSW5C2.js +176 -0
  10. package/dist/cli/index.cjs +936 -0
  11. package/dist/cli/index.d.cts +2 -0
  12. package/dist/cli/index.d.ts +2 -0
  13. package/dist/cli/index.js +9 -6
  14. package/dist/index.cjs +8 -0
  15. package/dist/index.d.cts +56 -0
  16. package/dist/index.d.ts +56 -0
  17. package/dist/markdown/index.cjs +82 -0
  18. package/dist/markdown/index.d.cts +36 -0
  19. package/dist/markdown/index.d.ts +36 -0
  20. package/dist/markdown/index.js +82 -0
  21. package/dist/note-oGm44Rw8.d.cts +84 -0
  22. package/dist/note-oGm44Rw8.d.ts +84 -0
  23. package/dist/types-CpsEy8lA.d.cts +221 -0
  24. package/dist/types-CwBWwNVv.d.ts +221 -0
  25. package/dist/vitepress/config/index.cjs +1669 -0
  26. package/dist/vitepress/config/index.d.cts +12 -0
  27. package/dist/vitepress/config/index.d.ts +12 -0
  28. package/dist/vitepress/config/index.js +26 -192
  29. package/dist/workspace/index.cjs +1207 -0
  30. package/dist/workspace/index.d.cts +14 -0
  31. package/dist/workspace/index.d.ts +14 -0
  32. package/dist/workspace/index.js +1207 -0
  33. package/package.json +16 -3
  34. package/vitepress/components/MindmapPreview/MindmapPreview.vue +189 -34
  35. package/vitepress/components/MindmapPreview/MindmapViewIcon.vue +41 -0
  36. package/vitepress/components/MindmapPreview/markdown.test.ts +67 -0
  37. package/vitepress/components/MindmapPreview/markdown.ts +83 -0
  38. package/vitepress/components/MindmapPreview/wheelInteraction.test.ts +23 -0
  39. package/vitepress/components/MindmapPreview/wheelInteraction.ts +7 -0
  40. package/vitepress/components/Settings/Settings.vue +3 -3
  41. package/vitepress/components/SidebarCard/SidebarCard.vue +3 -3
  42. package/vitepress/components/constants.ts +0 -5
  43. package/vitepress/configs/markdown.config.ts +6 -199
  44. package/vitepress/plugins/localSearchReindexLogic.ts +11 -3
  45. package/vitepress/theme/index.ts +0 -2
  46. package/vitepress/components/MindmapPreview/compat.test.ts +0 -93
  47. package/vitepress/components/MindmapPreview/compat.ts +0 -128
  48. package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +0 -57
package/package.json CHANGED
@@ -1,18 +1,30 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
7
7
  "bin": {
8
- "tnotes": "./dist/cli/index.js"
8
+ "tnotes": "dist/cli/index.js"
9
9
  },
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
+ "require": "./dist/index.cjs",
13
14
  "import": "./dist/index.js"
14
15
  },
16
+ "./workspace": {
17
+ "types": "./dist/workspace/index.d.ts",
18
+ "require": "./dist/workspace/index.cjs",
19
+ "import": "./dist/workspace/index.js"
20
+ },
21
+ "./markdown": {
22
+ "types": "./dist/markdown/index.d.ts",
23
+ "require": "./dist/markdown/index.cjs",
24
+ "import": "./dist/markdown/index.js"
25
+ },
15
26
  "./vitepress/config": {
27
+ "require": "./dist/vitepress/config/index.cjs",
16
28
  "import": "./dist/vitepress/config/index.js"
17
29
  },
18
30
  "./vitepress/theme": {
@@ -59,6 +71,7 @@
59
71
  "marked": "^15.0.11",
60
72
  "mermaid": "^11.5.0",
61
73
  "minisearch": "^7.2.0",
74
+ "prettier": "^3.6.2",
62
75
  "sass-embedded": "^1.90.0",
63
76
  "swiper": "^11.2.1",
64
77
  "uuid": "^11.1.0",
@@ -85,7 +98,7 @@
85
98
  "license": "MIT",
86
99
  "repository": {
87
100
  "type": "git",
88
- "url": "https://github.com/tnotesjs/core"
101
+ "url": "git+https://github.com/tnotesjs/core.git"
89
102
  },
90
103
  "keywords": [
91
104
  "tnotes",
@@ -3,9 +3,12 @@ import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
3
3
  import { onContentUpdated, useData } from 'vitepress'
4
4
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
5
5
 
6
- import { normalizeMindmapMarkdown } from './compat'
7
6
  import { applyInitialExpandLevel } from './expandLevel'
7
+ import { normalizeMindmapMarkdown } from './markdown'
8
8
  import MindmapOutlineNode from './MindmapOutlineNode.vue'
9
+ import MindmapViewIcon from './MindmapViewIcon.vue'
10
+ import { gateMindmapWheel } from './wheelInteraction'
11
+ import { icon__fullscreen, icon__fullscreen_exit, icon__zoom_fit } from '../../assets/icons'
9
12
 
10
13
  type PreviewView = 'mindmap' | 'outline' | 'source'
11
14
 
@@ -19,12 +22,21 @@ const props = withDefaults(defineProps<{
19
22
 
20
23
  const { isDark } = useData()
21
24
  const activeView = ref<PreviewView>('mindmap')
25
+ const previewRoot = ref<HTMLElement | null>(null)
22
26
  const canvasHost = ref<HTMLElement | null>(null)
23
27
  const session = shallowRef<MindmapSession | null>(null)
24
28
  const renderVersion = ref(0)
29
+ const isFullscreen = ref(false)
30
+ const isCanvasActive = ref(false)
25
31
  let viewer: CanvasViewer | null = null
26
32
  let mounted = false
27
33
 
34
+ const viewOptions = [
35
+ { value: 'mindmap', label: '脑图' },
36
+ { value: 'outline', label: '大纲' },
37
+ { value: 'source', label: '源码' },
38
+ ] as const
39
+
28
40
  function decodeContent(value: string): string {
29
41
  try {
30
42
  return decodeURIComponent(value)
@@ -61,6 +73,7 @@ function rebuildSession(): void {
61
73
 
62
74
  function setView(view: PreviewView): void {
63
75
  activeView.value = view
76
+ if (view !== 'mindmap') isCanvasActive.value = false
64
77
  if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
65
78
  }
66
79
 
@@ -72,11 +85,51 @@ function exitFocusTo(index: number): void {
72
85
  session.value?.exitFocusTo(index)
73
86
  }
74
87
 
88
+ async function toggleFullscreen(): Promise<void> {
89
+ const root = previewRoot.value
90
+ if (!root) return
91
+
92
+ try {
93
+ if (document.fullscreenElement === root) await document.exitFullscreen()
94
+ else await root.requestFullscreen()
95
+ } catch {
96
+ // Fullscreen can be rejected by the browser or an embedded document policy.
97
+ }
98
+ }
99
+
100
+ function handleFullscreenChange(): void {
101
+ isFullscreen.value = document.fullscreenElement === previewRoot.value
102
+ if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
103
+ }
104
+
105
+ function activateCanvas(): void {
106
+ isCanvasActive.value = true
107
+ }
108
+
109
+ function handleCanvasWheelCapture(event: WheelEvent): void {
110
+ gateMindmapWheel(event, isCanvasActive.value)
111
+ }
112
+
113
+ function handleDocumentPointerDown(event: PointerEvent): void {
114
+ if (event.target instanceof Node && !previewRoot.value?.contains(event.target)) {
115
+ isCanvasActive.value = false
116
+ }
117
+ }
118
+
119
+ function handleDocumentKeydown(event: KeyboardEvent): void {
120
+ if (event.key !== 'Escape' || !isCanvasActive.value) return
121
+ isCanvasActive.value = false
122
+ canvasHost.value?.blur()
123
+ }
124
+
75
125
  watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
76
126
  watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
77
127
 
78
128
  onMounted(() => {
79
129
  mounted = true
130
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
131
+ document.addEventListener('pointerdown', handleDocumentPointerDown, true)
132
+ document.addEventListener('keydown', handleDocumentKeydown, true)
80
133
  createViewer()
81
134
  })
82
135
 
@@ -86,29 +139,58 @@ onContentUpdated(() => {
86
139
 
87
140
  onBeforeUnmount(() => {
88
141
  mounted = false
142
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
143
+ document.removeEventListener('pointerdown', handleDocumentPointerDown, true)
144
+ document.removeEventListener('keydown', handleDocumentKeydown, true)
89
145
  viewer?.destroy()
90
146
  viewer = null
91
147
  })
92
148
  </script>
93
149
 
94
150
  <template>
95
- <section class="mindmap-preview" :class="{ 'is-dark': isDark }">
96
- <header class="mindmap-preview-header">
151
+ <section
152
+ ref="previewRoot"
153
+ class="mindmap-preview"
154
+ :class="{ 'is-dark': isDark, 'is-fullscreen': isFullscreen }"
155
+ :data-version="renderVersion"
156
+ >
157
+ <div class="mindmap-preview-actions">
97
158
  <nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
98
159
  <button
99
- v-for="item in ([['mindmap', '脑图'], ['outline', '大纲'], ['source', '源码']] as const)"
100
- :key="item[0]"
160
+ v-for="item in viewOptions"
161
+ :key="item.value"
101
162
  type="button"
102
- :class="{ 'is-active': activeView === item[0] }"
103
- @click="setView(item[0])"
163
+ class="mindmap-preview-action"
164
+ :class="{ 'is-active': activeView === item.value }"
165
+ :aria-label="item.label"
166
+ :aria-pressed="activeView === item.value"
167
+ :title="item.label"
168
+ @click="setView(item.value)"
104
169
  >
105
- {{ item[1] }}
170
+ <MindmapViewIcon :view="item.value" />
106
171
  </button>
107
172
  </nav>
108
- <button v-if="activeView === 'mindmap'" type="button" class="mindmap-fit" @click="viewer?.zoomToFit()">
109
- 适应视口
173
+ <span class="mindmap-preview-action-divider" aria-hidden="true" />
174
+ <button
175
+ v-if="activeView === 'mindmap'"
176
+ type="button"
177
+ class="mindmap-preview-action"
178
+ aria-label="适应视口"
179
+ title="适应视口"
180
+ @click="viewer?.zoomToFit()"
181
+ >
182
+ <img :src="icon__zoom_fit" alt="" />
110
183
  </button>
111
- </header>
184
+ <button
185
+ type="button"
186
+ class="mindmap-preview-action"
187
+ :aria-label="isFullscreen ? '退出全屏' : '全屏查看'"
188
+ :title="isFullscreen ? '退出全屏' : '全屏查看'"
189
+ @click="toggleFullscreen"
190
+ >
191
+ <img :src="isFullscreen ? icon__fullscreen_exit : icon__fullscreen" alt="" />
192
+ </button>
193
+ </div>
112
194
 
113
195
  <nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
114
196
  <button type="button" @click="exitFocusTo(0)">全部</button>
@@ -118,7 +200,14 @@ onBeforeUnmount(() => {
118
200
  </template>
119
201
  </nav>
120
202
 
121
- <div v-show="activeView === 'mindmap'" ref="canvasHost" class="mindmap-canvas-host" />
203
+ <div
204
+ v-show="activeView === 'mindmap'"
205
+ ref="canvasHost"
206
+ class="mindmap-canvas-host"
207
+ :class="{ 'is-interaction-active': isCanvasActive }"
208
+ @pointerdown.capture="activateCanvas"
209
+ @wheel.capture="handleCanvasWheelCapture"
210
+ />
122
211
 
123
212
  <div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
124
213
  <ul class="mindmap-outline-root">
@@ -139,6 +228,7 @@ onBeforeUnmount(() => {
139
228
  .mindmap-preview {
140
229
  --mindmap-panel: var(--vp-c-bg-soft);
141
230
  --mindmap-border: var(--vp-c-divider);
231
+ position: relative;
142
232
  margin: 1.5rem 0;
143
233
  overflow: hidden;
144
234
  border: 1px solid var(--mindmap-border);
@@ -146,36 +236,75 @@ onBeforeUnmount(() => {
146
236
  background: var(--vp-c-bg);
147
237
  }
148
238
 
149
- .mindmap-preview-header {
239
+ .mindmap-preview-actions {
240
+ position: absolute;
241
+ top: 8px;
242
+ right: 8px;
243
+ z-index: 20;
150
244
  display: flex;
151
245
  align-items: center;
152
- justify-content: space-between;
153
- min-height: 42px;
154
- padding: 5px 8px;
155
- border-bottom: 1px solid var(--mindmap-border);
156
- background: var(--mindmap-panel);
246
+ gap: 4px;
247
+ padding: 2px;
248
+ border: .1px solid var(--vp-c-divider);
249
+ border-radius: 7px;
250
+ background-color: color-mix(in srgb, var(--vp-code-block-bg) 92%, transparent);
251
+ box-shadow: var(--vp-shadow-1);
252
+ opacity: 0;
253
+ pointer-events: none;
254
+ transition: opacity .2s;
157
255
  }
158
256
 
159
257
  .mindmap-preview-tabs {
160
258
  display: flex;
161
- gap: 3px;
162
-
163
- button {
164
- padding: 5px 12px;
165
- border-radius: 6px;
166
- color: var(--vp-c-text-2);
167
- font-size: 13px;
168
- font-weight: 600;
169
-
170
- &:hover,
171
- &.is-active {
172
- color: var(--vp-c-text-1);
173
- background: var(--vp-c-bg);
174
- }
259
+ gap: 4px;
260
+ }
261
+
262
+ .mindmap-preview:hover > .mindmap-preview-actions,
263
+ .mindmap-preview-actions:focus-within {
264
+ opacity: 1;
265
+ pointer-events: auto;
266
+ }
267
+
268
+ .mindmap-preview-action {
269
+ display: inline-flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ width: 32px;
273
+ height: 32px;
274
+ padding: 0;
275
+ border: 0;
276
+ border-radius: 6px;
277
+ background: transparent;
278
+ color: var(--vp-c-brand-1);
279
+ cursor: pointer;
280
+ transition: background-color .2s, transform .2s;
281
+
282
+ svg,
283
+ img {
284
+ width: 18px;
285
+ height: 18px;
286
+ }
287
+
288
+ &:hover,
289
+ &.is-active {
290
+ background-color: var(--vp-c-default-soft);
291
+ }
292
+
293
+ &.is-active {
294
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 35%, transparent);
175
295
  }
296
+
297
+ &:hover { transform: scale(1.05); }
298
+ &:active { transform: scale(.95); }
299
+ }
300
+
301
+ .mindmap-preview-action-divider {
302
+ width: 1px;
303
+ height: 20px;
304
+ margin: 0 1px;
305
+ background: var(--vp-c-divider);
176
306
  }
177
307
 
178
- .mindmap-fit,
179
308
  .mindmap-focus-path button {
180
309
  color: var(--vp-c-text-2);
181
310
  font-size: 12px;
@@ -202,6 +331,10 @@ onBeforeUnmount(() => {
202
331
  background: var(--vp-c-bg);
203
332
  touch-action: none;
204
333
  user-select: none;
334
+
335
+ &.is-interaction-active {
336
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 38%, transparent);
337
+ }
205
338
  }
206
339
 
207
340
  .mindmap-canvas-host:deep(.mm-canvas),
@@ -279,9 +412,31 @@ onBeforeUnmount(() => {
279
412
  white-space: pre;
280
413
  }
281
414
 
415
+ .mindmap-preview:fullscreen {
416
+ display: flex;
417
+ width: 100%;
418
+ height: 100%;
419
+ margin: 0;
420
+ border: 0;
421
+ border-radius: 0;
422
+ background: var(--vp-c-bg);
423
+ flex-direction: column;
424
+
425
+ .mindmap-canvas-host,
426
+ .mindmap-outline,
427
+ .mindmap-source {
428
+ flex: 1 1 auto;
429
+ max-height: none;
430
+ min-height: 0;
431
+ }
432
+
433
+ .mindmap-canvas-host { height: auto; }
434
+ }
435
+
282
436
  @media (max-width: 768px) {
283
437
  .mindmap-canvas-host { height: 360px; }
284
- .mindmap-preview-tabs button { padding-inline: 9px; }
438
+ .mindmap-preview-actions { top: 6px; right: 6px; }
439
+ .mindmap-preview-action { width: 28px; height: 28px; }
285
440
  .mindmap-outline { padding-inline: 12px; }
286
441
  }
287
442
  </style>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ view: 'mindmap' | 'outline' | 'source'
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <svg v-if="view === 'mindmap'" aria-hidden="true" viewBox="0 0 48 48">
9
+ <path d="M0 0h48v48H0z" fill="none" />
10
+ <path
11
+ d="M26 24h16M26 38h16M26 10h16M18 24H6h4m8 14c-6-2-2-14-8-14m8-14c-6 2-2 14-8 14"
12
+ fill="none"
13
+ stroke="#636cff"
14
+ stroke-linecap="round"
15
+ stroke-linejoin="round"
16
+ stroke-width="4"
17
+ />
18
+ </svg>
19
+ <svg v-else-if="view === 'outline'" aria-hidden="true" viewBox="0 0 48 48">
20
+ <path d="M0 0h48v48H0z" fill="none" />
21
+ <path
22
+ d="M26 24h18m-30 0h4m0 14h26M6 38h4m8-28h26M6 10h4"
23
+ fill="none"
24
+ stroke="#636cff"
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ stroke-width="4"
28
+ />
29
+ </svg>
30
+ <svg v-else aria-hidden="true" viewBox="0 0 24 24">
31
+ <path d="M0 0h24v24H0z" fill="none" />
32
+ <path
33
+ d="m17 8 1.84 1.85c.773.778 1.16 1.167 1.16 1.65s-.387.872-1.16 1.65L17 15M7 8 5.16 9.85C4.387 10.628 4 11.017 4 11.5s.387.872 1.16 1.65L7 15m7.5-11-5 16"
34
+ fill="none"
35
+ stroke="#636cff"
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ stroke-width="1.5"
39
+ />
40
+ </svg>
41
+ </template>
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ normalizeMindmapMarkdown,
5
+ parseMindmapFence,
6
+ parseMindmapReference,
7
+ } from './markdown'
8
+
9
+ describe('parseMindmapFence', () => {
10
+ it.each([
11
+ ['```mindmap', {}],
12
+ ['```mindmap [项目架构]', { title: '项目架构' }],
13
+ ['```mindmap [项目架构] 2', { title: '项目架构', initialExpandLevel: 2 }],
14
+ ['```mindmap 2 [项目架构]', { title: '项目架构', initialExpandLevel: 2 }],
15
+ ['```mindmap 0', { initialExpandLevel: 1 }],
16
+ ])('parses %s', (input, expected) => {
17
+ expect(parseMindmapFence(input)).toEqual(expected)
18
+ })
19
+
20
+ it.each([
21
+ '```markmap',
22
+ '```markmap 2',
23
+ '```markmap {2}',
24
+ '```mindmap {2}',
25
+ '```mindmap {initialExpandLevel=3}',
26
+ ])('rejects removed syntax: %s', (input) => {
27
+ expect(parseMindmapFence(input)).toBeNull()
28
+ })
29
+ })
30
+
31
+ describe('parseMindmapReference', () => {
32
+ it.each([
33
+ ['<<< ./assets/tree.md', { path: './assets/tree.md' }],
34
+ ['<<< ./assets/tree.md [项目架构]', { path: './assets/tree.md', title: '项目架构' }],
35
+ ['<<< "./assets/tree with spaces.md" [项目架构]', { path: './assets/tree with spaces.md', title: '项目架构' }],
36
+ ])('parses %s', (input, expected) => {
37
+ expect(parseMindmapReference(input)).toEqual(expected)
38
+ })
39
+ })
40
+
41
+ describe('normalizeMindmapMarkdown', () => {
42
+ it('injects the default root for unordered-list input', () => {
43
+ expect(normalizeMindmapMarkdown('- A\n - B')).toBe('# root\n\n- A\n - B\n')
44
+ })
45
+
46
+ it('does not promote an explicit root list item', () => {
47
+ expect(normalizeMindmapMarkdown('- root\n - item1\n - item2')).toBe(
48
+ '# root\n\n- root\n - item1\n - item2\n',
49
+ )
50
+ })
51
+
52
+ it('preserves an existing H1 and lets explicit fence metadata override it', () => {
53
+ const source = '# Existing\n\n- A\n'
54
+ expect(normalizeMindmapMarkdown(source)).toBe(source)
55
+ expect(normalizeMindmapMarkdown(source, { title: 'Explicit' })).toBe('# Explicit\n\n- A\n')
56
+ })
57
+
58
+ it('does not convert H2-H6 headings into list nodes', () => {
59
+ const source = '# Existing\n\n## Section\n\n- A'
60
+ expect(normalizeMindmapMarkdown(source)).toBe('# Existing\n\n## Section\n\n- A\n')
61
+ })
62
+
63
+ it('is stable when normalized more than once', () => {
64
+ const once = normalizeMindmapMarkdown('- A\n - B')
65
+ expect(normalizeMindmapMarkdown(once)).toBe(once)
66
+ })
67
+ })
@@ -0,0 +1,83 @@
1
+ export interface MindmapFenceOptions {
2
+ title?: string
3
+ initialExpandLevel?: number
4
+ }
5
+
6
+ export interface MindmapReference {
7
+ path: string
8
+ title?: string
9
+ }
10
+
11
+ function cleanHeadingText(value: string): string {
12
+ return value.trim().replace(/\s+#+\s*$/, '').trim()
13
+ }
14
+
15
+ /** Parse the canonical `mindmap [title] 2` fence metadata. */
16
+ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null {
17
+ const fenceBody = openLine.trim().replace(/^`+\s*/, '')
18
+ const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/)
19
+ if (!nameMatch) return null
20
+
21
+ let rest = fenceBody.slice(nameMatch[0].length).trim()
22
+ const options: MindmapFenceOptions = {}
23
+ const titleMatch = rest.match(/\[([^\]]+)\]/)
24
+ if (titleMatch) {
25
+ options.title = cleanHeadingText(titleMatch[1]) || undefined
26
+ rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
27
+ }
28
+
29
+ if (rest && !/^\d+$/.test(rest)) return null
30
+ if (rest) {
31
+ options.initialExpandLevel = Math.max(1, Number(rest))
32
+ }
33
+ return options
34
+ }
35
+
36
+ /** Parse `<<< file.md [title]`; the title is optional and paths may be quoted. */
37
+ export function parseMindmapReference(line: string): MindmapReference | null {
38
+ const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
39
+ if (!match) return null
40
+
41
+ let rest = match[1].trim()
42
+ let title: string | undefined
43
+ const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
44
+ if (titleMatch) {
45
+ title = cleanHeadingText(titleMatch[1]) || undefined
46
+ rest = rest.slice(0, titleMatch.index).trim()
47
+ }
48
+
49
+ const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
50
+ return path ? { path, title } : null
51
+ }
52
+
53
+ export interface NormalizeMindmapOptions {
54
+ title?: string
55
+ defaultTitle?: string
56
+ }
57
+
58
+ /** Ensure canonical mindmap Markdown has exactly one H1 root title. */
59
+ export function normalizeMindmapMarkdown(
60
+ source: string,
61
+ options: NormalizeMindmapOptions = {},
62
+ ): string {
63
+ const lines = source.replace(/\r\n?/g, '\n').split('\n')
64
+ let existingTitle = ''
65
+ let rootIndex = -1
66
+
67
+ for (let index = 0; index < lines.length; index++) {
68
+ const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/)
69
+ if (!match) continue
70
+ existingTitle = cleanHeadingText(match[1])
71
+ rootIndex = index
72
+ break
73
+ }
74
+
75
+ const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
76
+ const body = lines.filter((_, index) => index !== rootIndex)
77
+ while (body[0]?.trim() === '') body.shift()
78
+ while (body[body.length - 1]?.trim() === '') body.pop()
79
+
80
+ return body.length > 0
81
+ ? `# ${rootTitle}\n\n${body.join('\n')}\n`
82
+ : `# ${rootTitle}\n`
83
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { gateMindmapWheel } from './wheelInteraction'
4
+
5
+ describe('gateMindmapWheel', () => {
6
+ it('keeps document scrolling available before the canvas is activated', () => {
7
+ const stop = vi.fn()
8
+ const event = { stopImmediatePropagation: stop } as unknown as WheelEvent
9
+
10
+ gateMindmapWheel(event, false)
11
+
12
+ expect(stop).toHaveBeenCalledOnce()
13
+ })
14
+
15
+ it('lets CanvasViewer handle wheel input after activation', () => {
16
+ const stop = vi.fn()
17
+ const event = { stopImmediatePropagation: stop } as unknown as WheelEvent
18
+
19
+ gateMindmapWheel(event, true)
20
+
21
+ expect(stop).not.toHaveBeenCalled()
22
+ })
23
+ })
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CanvasViewer 会在自身 wheel 监听器中阻止默认事件并移动画布。
3
+ * 未激活时先在捕获阶段终止分发,但不 preventDefault,让滚轮继续交给文档页面。
4
+ */
5
+ export function gateMindmapWheel(event: WheelEvent, active: boolean): void {
6
+ if (!active) event.stopImmediatePropagation()
7
+ }
@@ -180,6 +180,7 @@ vitepress/components/Settings/Settings.vue
180
180
  <script setup lang="ts">
181
181
  import { ref, computed, onMounted } from 'vue'
182
182
 
183
+ import { useLocalIde } from '../composables/useLocalIde'
183
184
  import {
184
185
  NOTES_DIR_KEY,
185
186
  LOCAL_IDE_KEY,
@@ -188,14 +189,13 @@ import {
188
189
  SIDEBAR_UNDONE_PREFIX_KEY,
189
190
  CONTENT_WIDTH_MODE_KEY,
190
191
  } from '../constants'
191
- import { useLocalIde } from '../composables/useLocalIde'
192
+ // @ts-expect-error - VitePress Data Loader
193
+ import { data as tnotesConfig } from '../tnotes-config.data'
192
194
  import {
193
195
  DEFAULT_LOCAL_IDE,
194
196
  normalizeLocalIde,
195
197
  type LocalIdeId,
196
198
  } from '../utils/vscodePaths'
197
- // @ts-expect-error - VitePress Data Loader
198
- import { data as tnotesConfig } from '../tnotes-config.data'
199
199
 
200
200
  type ContentWidthMode = 'wide' | 'standard'
201
201
 
@@ -6,6 +6,8 @@
6
6
  import { useData } from 'vitepress'
7
7
  import { ref, computed, watch } from 'vue'
8
8
 
9
+ import FolderTreeItems from './FolderTreeItems.vue'
10
+ import NotesTrendChart from './NotesTrendChart.vue'
9
11
  import {
10
12
  icon__fold,
11
13
  icon__github,
@@ -14,10 +16,8 @@ import {
14
16
  icon__number_gray,
15
17
  icon__number_purple,
16
18
  } from '../../assets/icons'
17
- import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
18
19
  import { useLocalIde } from '../composables/useLocalIde'
19
- import FolderTreeItems from './FolderTreeItems.vue'
20
- import NotesTrendChart from './NotesTrendChart.vue'
20
+ import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
21
21
  import { useSettingsDialog } from '../Settings/composables/useSettingsDialog'
22
22
  import { data as sidebarConfig } from '../sidebar.data'
23
23
  import {
@@ -53,11 +53,6 @@ export const NOTES_VIEW_KEY: string = 'NOTES_VIEW_KEY__' + REPO_NAME
53
53
  export const EN_WORD_LIST_COMP_IS_AUTO_SHOW_CARD: string =
54
54
  'EN_WORD_LIST_COMP_IS_AUTO_SHOW_CARD__' + REPO_NAME
55
55
 
56
- /**
57
- * MarkMap 默认主题配置
58
- */
59
- export const MARKMAP_THEME_KEY: string = 'MARKMAP_THEME_KEY__' + REPO_NAME
60
-
61
56
  /**
62
57
  * 侧边栏是否显示笔记编号配置
63
58
  */