@tnotesjs/core 0.3.0 → 0.4.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/dist/vitepress/config/index.js +157 -16
- package/package.json +3 -6
- package/vitepress/components/MindmapPreview/InlineRuns.ts +25 -0
- package/vitepress/components/MindmapPreview/MindmapOutlineNode.vue +62 -0
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +287 -0
- package/vitepress/components/MindmapPreview/compat.test.ts +93 -0
- package/vitepress/components/MindmapPreview/compat.ts +128 -0
- package/vitepress/components/MindmapPreview/expandLevel.test.ts +40 -0
- package/vitepress/components/MindmapPreview/expandLevel.ts +28 -0
- package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +57 -0
- package/vitepress/configs/markdown.config.ts +77 -20
- package/vitepress/theme/index.ts +4 -2
- package/vitepress/theme/styles/index.scss +0 -1
- package/vitepress/components/MarkMap/MarkMap.vue +0 -618
- package/vitepress/theme/styles/components/markmap.scss +0 -122
|
@@ -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
|
+
})
|
|
@@ -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
|
-
*
|
|
74
|
+
* Mindmap 容器配置(兼容旧的 markmap 围栏名)
|
|
70
75
|
*/
|
|
71
|
-
function
|
|
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
|
-
|
|
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 !==
|
|
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(
|
|
125
|
-
paramPart = after.slice(
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const refRaw =
|
|
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 =
|
|
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 = `<
|
|
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
|
-
|
|
399
|
+
// 添加 Mindmap 支持,并继续兼容旧 MarkMap 围栏
|
|
400
|
+
configureMindmapContainer(md)
|
|
401
|
+
configureMindmapFence(md)
|
|
345
402
|
|
|
346
403
|
// 添加任务列表支持
|
|
347
404
|
md.use(markdownItTaskLists)
|
package/vitepress/theme/index.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|