@tnotesjs/core 0.3.0 → 0.4.1

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.
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ normalizeMindmapMarkdown,
5
+ parseMarkmapFence,
6
+ parseMindmapReference,
7
+ } from './compat'
8
+
9
+ describe('parseMarkmapFence', () => {
10
+ it.each([
11
+ ['```markmap', {}],
12
+ ['```markmap 2', { initialExpandLevel: 2 }],
13
+ ['```markmap {2}', { initialExpandLevel: 2 }],
14
+ ['```markmap {initialExpandLevel=3}', { initialExpandLevel: 3 }],
15
+ ['```markmap [ESM 模块] 2', { title: 'ESM 模块', initialExpandLevel: 2 }],
16
+ ['```markmap 2 [ESM 模块]', { title: 'ESM 模块', initialExpandLevel: 2 }],
17
+ ['```mindmap', {}],
18
+ ['```mindmap [项目架构] 1', { title: '项目架构', initialExpandLevel: 1 }],
19
+ ])('parses %s', (input, expected) => {
20
+ expect(parseMarkmapFence(input)).toEqual(expected)
21
+ })
22
+ })
23
+
24
+ describe('parseMindmapReference', () => {
25
+ it.each([
26
+ ['<<< ./assets/tree.md', { path: './assets/tree.md' }],
27
+ ['<<< ./assets/tree.md [项目架构]', { path: './assets/tree.md', title: '项目架构' }],
28
+ ['<<< "./assets/tree with spaces.md" [项目架构]', { path: './assets/tree with spaces.md', title: '项目架构' }],
29
+ ])('parses %s', (input, expected) => {
30
+ expect(parseMindmapReference(input)).toEqual(expected)
31
+ })
32
+ })
33
+
34
+ describe('normalizeMindmapMarkdown', () => {
35
+ it('injects the default root for historical unordered-list input', () => {
36
+ expect(normalizeMindmapMarkdown('- A\n - B')).toBe('# root\n\n- A\n - B\n')
37
+ })
38
+
39
+ it('promotes a single legacy root item instead of rendering duplicate roots', () => {
40
+ expect(normalizeMindmapMarkdown('- root\n - item1\n - item2')).toBe(
41
+ '# root\n\n- item1\n- item2\n',
42
+ )
43
+ expect(normalizeMindmapMarkdown('- Custom\n - item', { title: 'Custom' })).toBe(
44
+ '# Custom\n\n- item\n',
45
+ )
46
+ })
47
+
48
+ it('does not promote a matching root item when it has top-level siblings', () => {
49
+ expect(normalizeMindmapMarkdown('- root\n - nested\n- sibling')).toBe(
50
+ '# root\n\n- root\n - nested\n- sibling\n',
51
+ )
52
+ })
53
+
54
+ it('preserves an existing H1 and lets explicit fence metadata override it', () => {
55
+ const source = '# Existing\n\n- A\n'
56
+ expect(normalizeMindmapMarkdown(source)).toBe(source)
57
+ expect(normalizeMindmapMarkdown(source, { title: 'Explicit' })).toBe('# Explicit\n\n- A\n')
58
+ })
59
+
60
+ it('converts legacy H2/H3 sections into a nested unordered-list tree', () => {
61
+ const source = [
62
+ '# ESM',
63
+ '',
64
+ '## 基本语法',
65
+ '',
66
+ '### 导出方式',
67
+ '',
68
+ '- 命名导出',
69
+ ' - 统一导出',
70
+ '### 导入方式',
71
+ '- 默认导入',
72
+ ].join('\n')
73
+
74
+ expect(normalizeMindmapMarkdown(source)).toBe([
75
+ '# ESM',
76
+ '',
77
+ '- 基本语法',
78
+ '',
79
+ ' - 导出方式',
80
+ '',
81
+ ' - 命名导出',
82
+ ' - 统一导出',
83
+ ' - 导入方式',
84
+ ' - 默认导入',
85
+ '',
86
+ ].join('\n'))
87
+ })
88
+
89
+ it('is stable when normalized more than once', () => {
90
+ const once = normalizeMindmapMarkdown('- A\n - B')
91
+ expect(normalizeMindmapMarkdown(once)).toBe(once)
92
+ })
93
+ })
@@ -0,0 +1,128 @@
1
+ export interface MarkmapFenceOptions {
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
+ function promoteLegacyRootList(body: string[], rootTitle: string): string[] {
16
+ const firstContentIndex = body.findIndex((line) => line.trim() !== '')
17
+ if (firstContentIndex < 0) return body
18
+
19
+ const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/)
20
+ if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body
21
+
22
+ const descendants = body.slice(firstContentIndex + 1)
23
+ if (descendants.some((line) => line.trim() !== '' && !/^\s{2,}/.test(line))) return body
24
+
25
+ return [
26
+ ...body.slice(0, firstContentIndex),
27
+ ...descendants.map((line) => line.replace(/^ {2}/, '')),
28
+ ]
29
+ }
30
+
31
+ /** Parse the opening fence while retaining every legacy expand-level spelling. */
32
+ export function parseMarkmapFence(openLine: string): MarkmapFenceOptions {
33
+ const fenceBody = openLine.trim().replace(/^`+\s*/, '')
34
+ const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/)
35
+ if (!nameMatch) return {}
36
+ let rest = fenceBody.slice(nameMatch[1].length).trim()
37
+ const options: MarkmapFenceOptions = {}
38
+
39
+ const titleMatch = rest.match(/\[([^\]]+)\]/)
40
+ if (titleMatch) {
41
+ options.title = titleMatch[1].trim()
42
+ rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
43
+ }
44
+
45
+ const braceMatch = rest.match(/\{([^}]*)\}/)
46
+ const paramPart = braceMatch ? braceMatch[1].trim() : rest
47
+ const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? []
48
+ for (const [index, token] of tokens.entries()) {
49
+ if (/^\d+$/.test(token) && index === 0) {
50
+ options.initialExpandLevel = Number(token)
51
+ continue
52
+ }
53
+ const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/)
54
+ if (!pair || pair[1] !== 'initialExpandLevel') continue
55
+ const value = pair[2].replace(/^['"]|['"]$/g, '')
56
+ if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value)
57
+ }
58
+ return options
59
+ }
60
+
61
+ /** Parse `<<< file.md [title]`; the title is optional and paths may be quoted. */
62
+ export function parseMindmapReference(line: string): MindmapReference | null {
63
+ const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
64
+ if (!match) return null
65
+
66
+ let rest = match[1].trim()
67
+ let title: string | undefined
68
+ const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
69
+ if (titleMatch) {
70
+ title = cleanHeadingText(titleMatch[1]) || undefined
71
+ rest = rest.slice(0, titleMatch.index).trim()
72
+ }
73
+
74
+ const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
75
+ return path ? { path, title } : null
76
+ }
77
+
78
+ export interface NormalizeMindmapOptions {
79
+ title?: string
80
+ defaultTitle?: string
81
+ }
82
+
83
+ /** Convert historical VitePress markmap input into the strict mindmap-core format. */
84
+ export function normalizeMindmapMarkdown(
85
+ source: string,
86
+ options: NormalizeMindmapOptions = {},
87
+ ): string {
88
+ const lines = source.replace(/\r\n?/g, '\n').split('\n')
89
+ let existingTitle = ''
90
+ let rootIndex = -1
91
+
92
+ for (let index = 0; index < lines.length; index++) {
93
+ const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/)
94
+ if (!match) continue
95
+ existingTitle = cleanHeadingText(match[1])
96
+ rootIndex = index
97
+ break
98
+ }
99
+
100
+ const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
101
+ const body: string[] = []
102
+ let headingDepth: number | null = null
103
+
104
+ for (let index = 0; index < lines.length; index++) {
105
+ if (index === rootIndex) continue
106
+ const line = lines[index]
107
+ const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/)
108
+ if (heading) {
109
+ headingDepth = heading[1].length - 2
110
+ body.push(`${' '.repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`)
111
+ continue
112
+ }
113
+
114
+ const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/)
115
+ if (listItem && headingDepth !== null) {
116
+ body.push(`${' '.repeat(headingDepth + 1)}${line}`)
117
+ continue
118
+ }
119
+ body.push(line)
120
+ }
121
+
122
+ const normalizedBody = promoteLegacyRootList(body, rootTitle)
123
+ while (normalizedBody[0]?.trim() === '') normalizedBody.shift()
124
+ while (normalizedBody[normalizedBody.length - 1]?.trim() === '') normalizedBody.pop()
125
+ return normalizedBody.length > 0
126
+ ? `# ${rootTitle}\n\n${normalizedBody.join('\n')}\n`
127
+ : `# ${rootTitle}\n`
128
+ }
@@ -0,0 +1,40 @@
1
+ import { MindmapSession } from '@tnotesjs/mindmap-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { applyInitialExpandLevel, normalizeExpandLevel } from './expandLevel'
5
+
6
+ function createSession(): MindmapSession {
7
+ return new MindmapSession({
8
+ markdown: '# root\n\n- 一级\n - 二级\n - 三级\n',
9
+ fileName: 'expand-level.tn-mindmap.md',
10
+ })
11
+ }
12
+
13
+ function visibleLabels(session: MindmapSession): string[] {
14
+ const result: string[] = []
15
+ const visit = (node: typeof session.document.root) => {
16
+ result.push(node.content.text)
17
+ if (!node.collapsed) node.children.forEach(visit)
18
+ }
19
+ visit(session.document.root)
20
+ return result
21
+ }
22
+
23
+ describe('applyInitialExpandLevel', () => {
24
+ it('clamps the minimum level to one', () => {
25
+ expect(normalizeExpandLevel(0)).toBe(1)
26
+ expect(normalizeExpandLevel(-2)).toBe(1)
27
+ })
28
+
29
+ it('shows only root and first-level children for level one', () => {
30
+ const session = createSession()
31
+ applyInitialExpandLevel(session, 1)
32
+ expect(visibleLabels(session)).toEqual(['root', '一级'])
33
+ })
34
+
35
+ it('shows root, first-level and second-level children for level two', () => {
36
+ const session = createSession()
37
+ applyInitialExpandLevel(session, 2)
38
+ expect(visibleLabels(session)).toEqual(['root', '一级', '二级'])
39
+ })
40
+ })
@@ -0,0 +1,28 @@
1
+ import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
2
+
3
+ export function normalizeExpandLevel(value: number): number {
4
+ return Math.max(1, Math.trunc(Number(value) || 1))
5
+ }
6
+
7
+ function childLevel(node: MindmapNode, root: MindmapNode): number {
8
+ let level = 0
9
+ let current: MindmapNode | null = node
10
+ while (current && current !== root) {
11
+ level += 1
12
+ current = current.parent
13
+ }
14
+ return level
15
+ }
16
+
17
+ /**
18
+ * Level 1 renders root + direct children; level 2 additionally renders
19
+ * grandchildren. Nodes at the last visible level are collapsed.
20
+ */
21
+ export function applyInitialExpandLevel(session: MindmapSession, value: number): void {
22
+ const visibleLevel = normalizeExpandLevel(value)
23
+ const root = session.document.root
24
+ session.document.traverse((node) => {
25
+ if (node === root || node.children.length === 0) return
26
+ session.setCollapsed(node.id, childLevel(node, root) >= visibleLevel)
27
+ })
28
+ }
@@ -0,0 +1,57 @@
1
+ import { parseMarkdown } from '@tnotesjs/mindmap-core'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import {
8
+ normalizeMindmapMarkdown,
9
+ parseMarkmapFence,
10
+ parseMindmapReference,
11
+ } from './compat'
12
+
13
+ const tnotesRoot = path.resolve(fileURLToPath(new URL('../../../../', import.meta.url)))
14
+ const noteFiles = [
15
+ 'TNotes.canvas/notes/0035. 使用 ctx.drawImage 引入图像/README.md',
16
+ 'TNotes.docs/notes/0013. Mindmap/README.md',
17
+ 'TNotes.docs/notes/0014. 分仓库模式/README.md',
18
+ 'TNotes.javascript/notes/0070. CommonJS/README.md',
19
+ 'TNotes.javascript/notes/0071. ESM/README.md',
20
+ 'TNotes.sql/notes/0001. MySQL 8 从入门到精通/README.md',
21
+ 'TNotes.vite/notes/0014. vite 思维导图/README.md',
22
+ ].map((relative) => path.join(tnotesRoot, relative))
23
+
24
+ const corpusAvailable = noteFiles.every((file) => fs.existsSync(file))
25
+
26
+ describe.skipIf(!corpusAvailable)('legacy TNotes markmap corpus', () => {
27
+ it('normalizes every historical block into valid mindmap-core Markdown', () => {
28
+ const diagnostics: string[] = []
29
+ let blockCount = 0
30
+
31
+ for (const noteFile of noteFiles) {
32
+ const note = fs.readFileSync(noteFile, 'utf8')
33
+ const blocks = note.matchAll(/^(```(?:mindmap|markmap)[^\n]*)\n([\s\S]*?)^```\s*$/gm)
34
+ for (const match of blocks) {
35
+ blockCount += 1
36
+ const options = parseMarkmapFence(match[1])
37
+ let source = match[2]
38
+ const reference = parseMindmapReference(
39
+ source.split('\n').find((line) => line.trim()) ?? '',
40
+ )
41
+ if (reference) {
42
+ source = fs.readFileSync(path.resolve(path.dirname(noteFile), reference.path), 'utf8')
43
+ }
44
+ const normalized = normalizeMindmapMarkdown(source, {
45
+ title: options.title || reference?.title,
46
+ })
47
+ const result = parseMarkdown(normalized, path.basename(noteFile))
48
+ if (!result.valid) {
49
+ diagnostics.push(`${noteFile}\n${result.diagnostics.map((item) => item.message).join('\n')}`)
50
+ }
51
+ }
52
+ }
53
+
54
+ expect(blockCount).toBe(16)
55
+ expect(diagnostics).toEqual([])
56
+ })
57
+ })
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { DEFAULT_OPTIMIZE_DEPS_INCLUDE } from './dependencyOptimization'
4
+
5
+ describe('Vite dependency optimization', () => {
6
+ it('pre-bundles Mermaid nested CommonJS fastdom dependency', () => {
7
+ expect(DEFAULT_OPTIMIZE_DEPS_INCLUDE).toContain(
8
+ '@tnotesjs/core > mermaid > fastdom',
9
+ )
10
+ })
11
+ })
@@ -0,0 +1,12 @@
1
+ export const DEFAULT_OPTIMIZE_DEPS_INCLUDE = [
2
+ // VitePress 内部 CJS 依赖 —— 需要 Vite 预构建为 ESM
3
+ 'vitepress > @vscode/markdown-it-katex',
4
+ 'vitepress > @braintree/sanitize-url',
5
+ 'vitepress > dayjs',
6
+ 'vitepress > dayjs/plugin/utc',
7
+ 'vitepress > dayjs/plugin/localizedFormat',
8
+ // Mermaid 11 的 ESM chunk 默认导入 CommonJS fastdom。pnpm 的严格
9
+ // 依赖布局下 Vite 无法从知识库根目录自动发现这条嵌套依赖,开发
10
+ // 模式会直接把 fastdom.js 当作 ESM 加载并导致整页白屏。
11
+ '@tnotesjs/core > mermaid > fastdom',
12
+ ] as const
@@ -12,6 +12,7 @@ import fs from 'fs'
12
12
  import path from 'path'
13
13
  import { defineConfig, type UserConfig } from 'vitepress'
14
14
 
15
+ import { DEFAULT_OPTIMIZE_DEPS_INCLUDE } from './dependencyOptimization'
15
16
  import { ConfigManager } from '../../config/ConfigManager'
16
17
  import {
17
18
  getIgnoreList,
@@ -110,14 +111,7 @@ export function defineNotesConfig(overrides: UserConfig = {}) {
110
111
  ...overrideVite?.resolve,
111
112
  },
112
113
  optimizeDeps: {
113
- include: [
114
- // VitePress 内部 CJS 依赖 —— 需要 Vite 预构建为 ESM
115
- 'vitepress > @vscode/markdown-it-katex',
116
- 'vitepress > @braintree/sanitize-url',
117
- 'vitepress > dayjs',
118
- 'vitepress > dayjs/plugin/utc',
119
- 'vitepress > dayjs/plugin/localizedFormat',
120
- ],
114
+ include: [...DEFAULT_OPTIMIZE_DEPS_INCLUDE],
121
115
  ...overrideVite?.optimizeDeps,
122
116
  },
123
117
  },
@@ -11,6 +11,11 @@ import markdownItTaskLists from 'markdown-it-task-lists'
11
11
  import path from 'path'
12
12
 
13
13
  import { generateAnchor } from '../../utils'
14
+ import {
15
+ normalizeMindmapMarkdown,
16
+ parseMarkmapFence,
17
+ parseMindmapReference,
18
+ } from '../components/MindmapPreview/compat'
14
19
 
15
20
  import type MarkdownIt from 'markdown-it'
16
21
  import type { MarkdownOptions } from 'vitepress'
@@ -66,17 +71,13 @@ const simpleMermaidMarkdown = (md: MarkdownIt) => {
66
71
  }
67
72
 
68
73
  /**
69
- * MarkMap 容器配置
74
+ * Mindmap 容器配置(兼容旧的 markmap 围栏名)
70
75
  */
71
- function configureMarkMapContainer(md: MarkdownIt) {
72
- // 先保留 container 的解析(负责把 ```markmap ``` 识别成 container tokens)
73
- // 但让它本身不输出任何 HTML(render 返回空)
76
+ function configureMindmapContainer(md: MarkdownIt) {
74
77
  md.use(markdownItContainer, 'markmap', {
75
78
  marker: '`',
76
79
  validate(params: string) {
77
- // 接受 "markmap", "markmap{...}" 或 "markmap key=val ..." 等写法
78
- const p = (params || '').trim()
79
- return p.startsWith('markmap')
80
+ return (params || '').trim().startsWith('markmap')
80
81
  },
81
82
  render() {
82
83
  return ''
@@ -93,11 +94,13 @@ function configureMarkMapContainer(md: MarkdownIt) {
93
94
  for (let i = 0; i < tokens.length; i++) {
94
95
  const t = tokens[i]
95
96
  if (t.type === 'container_markmap_open') {
97
+ const containerName = 'markmap'
98
+ const closeType = 'container_markmap_close'
96
99
  // 找到对应的 close token
97
100
  let j = i + 1
98
101
  while (
99
102
  j < tokens.length &&
100
- tokens[j].type !== 'container_markmap_close'
103
+ tokens[j].type !== closeType
101
104
  )
102
105
  j++
103
106
  if (j >= tokens.length) continue // safety
@@ -109,9 +112,12 @@ function configureMarkMapContainer(md: MarkdownIt) {
109
112
 
110
113
  // 1) 从开头 fence 行解析参数(支持 `{a=1 b="x"}`、`a=1 b="x"`,并支持单个数字 shorthand)
111
114
  const params: { [key: string]: any; initialExpandLevel?: number } = {}
115
+ let explicitTitle: string | undefined
112
116
 
113
117
  if (open.map && typeof open.map[0] === 'number') {
114
118
  const openLine = (lines[open.map[0]] || '').trim()
119
+ const fenceOptions = parseMarkmapFence(openLine)
120
+ explicitTitle = fenceOptions.title
115
121
  let paramPart = ''
116
122
 
117
123
  // 优先匹配大括号形式 ```markmap{...}
@@ -121,10 +127,13 @@ function configureMarkMapContainer(md: MarkdownIt) {
121
127
  } else {
122
128
  // 否则尝试去掉前缀 ``` 和 markmap,剩下的作为参数部分
123
129
  const after = openLine.replace(/^`+\s*/, '')
124
- if (after.startsWith('markmap')) {
125
- paramPart = after.slice('markmap'.length).trim()
130
+ if (after.startsWith(containerName)) {
131
+ paramPart = after.slice(containerName.length).trim()
126
132
  }
127
133
  }
134
+ if (fenceOptions.initialExpandLevel !== undefined) {
135
+ params.initialExpandLevel = fenceOptions.initialExpandLevel
136
+ }
128
137
 
129
138
  if (paramPart) {
130
139
  // 使用正则按 token 切分:保持用引号包裹的片段为单个 token(支持包含空格)
@@ -179,10 +188,11 @@ function configureMarkMapContainer(md: MarkdownIt) {
179
188
  // --- 检查第一非空行是否为引用语法 ---
180
189
  const firstNonEmptyLine =
181
190
  (content || '').split('\n').find((ln) => ln.trim() !== '') || ''
182
- const refMatch = firstNonEmptyLine.trim().match(/^<<<\s*(.+)$/)
183
- if (refMatch) {
184
- // 提取引用路径,支持引号包裹
185
- const refRaw = refMatch[1].trim().replace(/^['"]|['"]$/g, '')
191
+ const reference = parseMindmapReference(firstNonEmptyLine)
192
+ let referencedTitle: string | undefined
193
+ if (reference) {
194
+ const refRaw = reference.path
195
+ referencedTitle = reference.title
186
196
 
187
197
  // 尝试同步读取文件内容(兼容常见 Node 环境)
188
198
  try {
@@ -213,12 +223,14 @@ function configureMarkMapContainer(md: MarkdownIt) {
213
223
  } catch (err) {
214
224
  // 读取失败:将错误写入 content 以便排查(不会让流程直接崩溃)
215
225
  const errorMsg = err instanceof Error ? err.message : String(err)
216
- content = `Failed to load referenced file: ${esc(
217
- String(refRaw)
218
- )}\n\nError: ${esc(errorMsg)}`
226
+ content = `- Failed to load referenced file: ${esc(String(refRaw))}\n - Error: ${esc(errorMsg)}`
219
227
  }
220
228
  }
221
229
 
230
+ content = normalizeMindmapMarkdown(content, {
231
+ title: explicitTitle || referencedTitle,
232
+ })
233
+
222
234
  // 3) 构造组件标签并把参数注入为 props
223
235
  const encodedContent = encodeURIComponent(content.trim())
224
236
  let propsStr = `content="${encodedContent}"`
@@ -232,7 +244,7 @@ function configureMarkMapContainer(md: MarkdownIt) {
232
244
  }
233
245
  }
234
246
 
235
- const html = `<MarkMap ${propsStr}></MarkMap>\n`
247
+ const html = `<MindmapPreview ${propsStr}></MindmapPreview>\n`
236
248
 
237
249
  // 创建 html_block token
238
250
  const htmlToken = new state.Token('html_block', '', 0)
@@ -247,6 +259,50 @@ function configureMarkMapContainer(md: MarkdownIt) {
247
259
  })
248
260
  }
249
261
 
262
+ /** Canonical `mindmap` fence. Legacy `markmap` stays on its historical container path. */
263
+ function configureMindmapFence(md: MarkdownIt) {
264
+ const fence = md.renderer.rules.fence
265
+ ? md.renderer.rules.fence.bind(md.renderer.rules)
266
+ : () => ''
267
+
268
+ md.renderer.rules.fence = (tokens, index, options, env, slf) => {
269
+ const token = tokens[index]
270
+ const info = token.info.trim()
271
+ if (!/^mindmap(?=\s|\{|\[|$)/.test(info)) {
272
+ return fence(tokens, index, options, env, slf)
273
+ }
274
+
275
+ const fenceOptions = parseMarkmapFence(info)
276
+ let content = token.content
277
+ const firstNonEmptyLine = content.split('\n').find((line) => line.trim()) ?? ''
278
+ const reference = parseMindmapReference(firstNonEmptyLine)
279
+
280
+ if (reference) {
281
+ const possibleRel = env?.relativePath || env?.path || env?.filePath || env?.file || ''
282
+ const refFullPath = path.isAbsolute(reference.path)
283
+ ? reference.path
284
+ : path.resolve(process.cwd(), possibleRel ? path.dirname(possibleRel) : '', reference.path)
285
+ try {
286
+ content = fs.readFileSync(refFullPath, 'utf8')
287
+ } catch (error) {
288
+ const message = error instanceof Error ? error.message : String(error)
289
+ content = `- Failed to load referenced file: ${reference.path}\n - Error: ${message}`
290
+ }
291
+ }
292
+
293
+ content = normalizeMindmapMarkdown(content, {
294
+ title: fenceOptions.title || reference?.title,
295
+ })
296
+ const props = [
297
+ `content="${encodeURIComponent(content.trim())}"`,
298
+ fenceOptions.initialExpandLevel === undefined
299
+ ? ''
300
+ : `:initialExpandLevel="${fenceOptions.initialExpandLevel}"`,
301
+ ].filter(Boolean).join(' ')
302
+ return `<MindmapPreview ${props}></MindmapPreview>\n`
303
+ }
304
+ }
305
+
250
306
  /**
251
307
  * Swiper 容器配置
252
308
  */
@@ -340,8 +396,9 @@ export function getMarkdownConfig(): MarkdownOptions {
340
396
  // 添加 Mermaid 支持
341
397
  simpleMermaidMarkdown(md)
342
398
 
343
- // 添加 MarkMap 支持
344
- configureMarkMapContainer(md)
399
+ // 添加 Mindmap 支持,并继续兼容旧 MarkMap 围栏
400
+ configureMindmapContainer(md)
401
+ configureMindmapFence(md)
345
402
 
346
403
  // 添加任务列表支持
347
404
  md.use(markdownItTaskLists)
@@ -22,8 +22,8 @@ import Footprints from '../components/Footprints/Footprints.vue'
22
22
  import { useRenameOverlay } from '../components/Layout/composables/useRenameOverlay'
23
23
  import { redirectAfterRename } from '../components/Layout/composables/useRenameRedirect'
24
24
  import Layout from '../components/Layout/Layout.vue'
25
- import MarkMap from '../components/MarkMap/MarkMap.vue'
26
25
  import Mermaid from '../components/Mermaid/Mermaid.vue'
26
+ import MindmapPreview from '../components/MindmapPreview/MindmapPreview.vue'
27
27
  import NotesTable from '../components/NotesTable/NotesTable.vue'
28
28
  import SidebarCard from '../components/SidebarCard/SidebarCard.vue'
29
29
  import Tooltip from '../components/Tooltip/Tooltip.vue'
@@ -45,7 +45,9 @@ function registerCoreComponents(ctx: EnhanceAppContext) {
45
45
  app.component('Footprints', Footprints)
46
46
  app.component('F', Footprints)
47
47
  app.component('SidebarCard', SidebarCard)
48
- app.component('MarkMap', MarkMap)
48
+ // Keep the historical component name so existing note fences need no migration.
49
+ app.component('MarkMap', MindmapPreview)
50
+ app.component('MindmapPreview', MindmapPreview)
49
51
  app.component('Mermaid', Mermaid)
50
52
  app.component('NotesTable', NotesTable)
51
53
  app.component('N', NotesTable)
@@ -17,7 +17,6 @@
17
17
 
18
18
  /* 4. 组件全局层:跨组件共享的样式 */
19
19
  @use './components/swiper';
20
- @use './components/markmap';
21
20
  @use './components/collapse';
22
21
  /**
23
22
  * !404 不是一个有效的 Sass 标识符(不能以数字开头),需要为它添加一个别名