@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.
- package/dist/vitepress/config/index.js +172 -24
- 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/config/dependencyOptimization.test.ts +11 -0
- package/vitepress/config/dependencyOptimization.ts +12 -0
- package/vitepress/config/index.ts +2 -8
- 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
|
@@ -17,6 +17,20 @@ import fs3 from "fs";
|
|
|
17
17
|
import path5 from "path";
|
|
18
18
|
import { defineConfig } from "vitepress";
|
|
19
19
|
|
|
20
|
+
// vitepress/config/dependencyOptimization.ts
|
|
21
|
+
var DEFAULT_OPTIMIZE_DEPS_INCLUDE = [
|
|
22
|
+
// VitePress 内部 CJS 依赖 —— 需要 Vite 预构建为 ESM
|
|
23
|
+
"vitepress > @vscode/markdown-it-katex",
|
|
24
|
+
"vitepress > @braintree/sanitize-url",
|
|
25
|
+
"vitepress > dayjs",
|
|
26
|
+
"vitepress > dayjs/plugin/utc",
|
|
27
|
+
"vitepress > dayjs/plugin/localizedFormat",
|
|
28
|
+
// Mermaid 11 的 ESM chunk 默认导入 CommonJS fastdom。pnpm 的严格
|
|
29
|
+
// 依赖布局下 Vite 无法从知识库根目录自动发现这条嵌套依赖,开发
|
|
30
|
+
// 模式会直接把 fastdom.js 当作 ESM 加载并导致整页白屏。
|
|
31
|
+
"@tnotesjs/core > mermaid > fastdom"
|
|
32
|
+
];
|
|
33
|
+
|
|
20
34
|
// vitepress/configs/constants.ts
|
|
21
35
|
function getIgnoreList(config) {
|
|
22
36
|
return [...config.ignore_dirs.map((dir) => `**/${dir}/**`)];
|
|
@@ -49,6 +63,103 @@ import markdownItContainer from "markdown-it-container";
|
|
|
49
63
|
import mila from "markdown-it-link-attributes";
|
|
50
64
|
import markdownItTaskLists from "markdown-it-task-lists";
|
|
51
65
|
import path from "path";
|
|
66
|
+
|
|
67
|
+
// vitepress/components/MindmapPreview/compat.ts
|
|
68
|
+
function cleanHeadingText(value) {
|
|
69
|
+
return value.trim().replace(/\s+#+\s*$/, "").trim();
|
|
70
|
+
}
|
|
71
|
+
function promoteLegacyRootList(body, rootTitle) {
|
|
72
|
+
const firstContentIndex = body.findIndex((line) => line.trim() !== "");
|
|
73
|
+
if (firstContentIndex < 0) return body;
|
|
74
|
+
const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/);
|
|
75
|
+
if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body;
|
|
76
|
+
const descendants = body.slice(firstContentIndex + 1);
|
|
77
|
+
if (descendants.some((line) => line.trim() !== "" && !/^\s{2,}/.test(line))) return body;
|
|
78
|
+
return [
|
|
79
|
+
...body.slice(0, firstContentIndex),
|
|
80
|
+
...descendants.map((line) => line.replace(/^ {2}/, ""))
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
function parseMarkmapFence(openLine) {
|
|
84
|
+
const fenceBody = openLine.trim().replace(/^`+\s*/, "");
|
|
85
|
+
const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/);
|
|
86
|
+
if (!nameMatch) return {};
|
|
87
|
+
let rest = fenceBody.slice(nameMatch[1].length).trim();
|
|
88
|
+
const options = {};
|
|
89
|
+
const titleMatch = rest.match(/\[([^\]]+)\]/);
|
|
90
|
+
if (titleMatch) {
|
|
91
|
+
options.title = titleMatch[1].trim();
|
|
92
|
+
rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
|
|
93
|
+
}
|
|
94
|
+
const braceMatch = rest.match(/\{([^}]*)\}/);
|
|
95
|
+
const paramPart = braceMatch ? braceMatch[1].trim() : rest;
|
|
96
|
+
const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
97
|
+
for (const [index, token] of tokens.entries()) {
|
|
98
|
+
if (/^\d+$/.test(token) && index === 0) {
|
|
99
|
+
options.initialExpandLevel = Number(token);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/);
|
|
103
|
+
if (!pair || pair[1] !== "initialExpandLevel") continue;
|
|
104
|
+
const value = pair[2].replace(/^['"]|['"]$/g, "");
|
|
105
|
+
if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value);
|
|
106
|
+
}
|
|
107
|
+
return options;
|
|
108
|
+
}
|
|
109
|
+
function parseMindmapReference(line) {
|
|
110
|
+
const match = line.trim().match(/^<<<\s+(.+?)\s*$/);
|
|
111
|
+
if (!match) return null;
|
|
112
|
+
let rest = match[1].trim();
|
|
113
|
+
let title;
|
|
114
|
+
const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/);
|
|
115
|
+
if (titleMatch) {
|
|
116
|
+
title = cleanHeadingText(titleMatch[1]) || void 0;
|
|
117
|
+
rest = rest.slice(0, titleMatch.index).trim();
|
|
118
|
+
}
|
|
119
|
+
const path6 = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2").trim();
|
|
120
|
+
return path6 ? { path: path6, title } : null;
|
|
121
|
+
}
|
|
122
|
+
function normalizeMindmapMarkdown(source, options = {}) {
|
|
123
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
124
|
+
let existingTitle = "";
|
|
125
|
+
let rootIndex = -1;
|
|
126
|
+
for (let index = 0; index < lines.length; index++) {
|
|
127
|
+
const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/);
|
|
128
|
+
if (!match) continue;
|
|
129
|
+
existingTitle = cleanHeadingText(match[1]);
|
|
130
|
+
rootIndex = index;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || "root") || "root";
|
|
134
|
+
const body = [];
|
|
135
|
+
let headingDepth = null;
|
|
136
|
+
for (let index = 0; index < lines.length; index++) {
|
|
137
|
+
if (index === rootIndex) continue;
|
|
138
|
+
const line = lines[index];
|
|
139
|
+
const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/);
|
|
140
|
+
if (heading) {
|
|
141
|
+
headingDepth = heading[1].length - 2;
|
|
142
|
+
body.push(`${" ".repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/);
|
|
146
|
+
if (listItem && headingDepth !== null) {
|
|
147
|
+
body.push(`${" ".repeat(headingDepth + 1)}${line}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
body.push(line);
|
|
151
|
+
}
|
|
152
|
+
const normalizedBody = promoteLegacyRootList(body, rootTitle);
|
|
153
|
+
while (normalizedBody[0]?.trim() === "") normalizedBody.shift();
|
|
154
|
+
while (normalizedBody[normalizedBody.length - 1]?.trim() === "") normalizedBody.pop();
|
|
155
|
+
return normalizedBody.length > 0 ? `# ${rootTitle}
|
|
156
|
+
|
|
157
|
+
${normalizedBody.join("\n")}
|
|
158
|
+
` : `# ${rootTitle}
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// vitepress/configs/markdown.config.ts
|
|
52
163
|
function esc(s = "") {
|
|
53
164
|
return s.replace(
|
|
54
165
|
/[&<>"']/g,
|
|
@@ -80,12 +191,11 @@ var simpleMermaidMarkdown = (md) => {
|
|
|
80
191
|
return fence(tokens, index, options, env, slf);
|
|
81
192
|
};
|
|
82
193
|
};
|
|
83
|
-
function
|
|
194
|
+
function configureMindmapContainer(md) {
|
|
84
195
|
md.use(markdownItContainer, "markmap", {
|
|
85
196
|
marker: "`",
|
|
86
197
|
validate(params) {
|
|
87
|
-
|
|
88
|
-
return p.startsWith("markmap");
|
|
198
|
+
return (params || "").trim().startsWith("markmap");
|
|
89
199
|
},
|
|
90
200
|
render() {
|
|
91
201
|
return "";
|
|
@@ -98,26 +208,34 @@ function configureMarkMapContainer(md) {
|
|
|
98
208
|
for (let i = 0; i < tokens.length; i++) {
|
|
99
209
|
const t = tokens[i];
|
|
100
210
|
if (t.type === "container_markmap_open") {
|
|
211
|
+
const containerName = "markmap";
|
|
212
|
+
const closeType = "container_markmap_close";
|
|
101
213
|
let j = i + 1;
|
|
102
|
-
while (j < tokens.length && tokens[j].type !==
|
|
214
|
+
while (j < tokens.length && tokens[j].type !== closeType)
|
|
103
215
|
j++;
|
|
104
216
|
if (j >= tokens.length) continue;
|
|
105
217
|
const open = t;
|
|
106
218
|
const startLine = open.map ? open.map[0] + 1 : null;
|
|
107
219
|
const endLine = open.map ? open.map[1] - 1 : null;
|
|
108
220
|
const params = {};
|
|
221
|
+
let explicitTitle;
|
|
109
222
|
if (open.map && typeof open.map[0] === "number") {
|
|
110
223
|
const openLine = (lines[open.map[0]] || "").trim();
|
|
224
|
+
const fenceOptions = parseMarkmapFence(openLine);
|
|
225
|
+
explicitTitle = fenceOptions.title;
|
|
111
226
|
let paramPart = "";
|
|
112
227
|
const braceMatch = openLine.match(/\{([^}]*)\}/);
|
|
113
228
|
if (braceMatch) {
|
|
114
229
|
paramPart = braceMatch[1].trim();
|
|
115
230
|
} else {
|
|
116
231
|
const after = openLine.replace(/^`+\s*/, "");
|
|
117
|
-
if (after.startsWith(
|
|
118
|
-
paramPart = after.slice(
|
|
232
|
+
if (after.startsWith(containerName)) {
|
|
233
|
+
paramPart = after.slice(containerName.length).trim();
|
|
119
234
|
}
|
|
120
235
|
}
|
|
236
|
+
if (fenceOptions.initialExpandLevel !== void 0) {
|
|
237
|
+
params.initialExpandLevel = fenceOptions.initialExpandLevel;
|
|
238
|
+
}
|
|
121
239
|
if (paramPart) {
|
|
122
240
|
const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
123
241
|
let startIdx = 0;
|
|
@@ -153,9 +271,11 @@ function configureMarkMapContainer(md) {
|
|
|
153
271
|
}
|
|
154
272
|
}
|
|
155
273
|
const firstNonEmptyLine = (content || "").split("\n").find((ln) => ln.trim() !== "") || "";
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
274
|
+
const reference = parseMindmapReference(firstNonEmptyLine);
|
|
275
|
+
let referencedTitle;
|
|
276
|
+
if (reference) {
|
|
277
|
+
const refRaw = reference.path;
|
|
278
|
+
referencedTitle = reference.title;
|
|
159
279
|
try {
|
|
160
280
|
const env = state.env || {};
|
|
161
281
|
const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
|
|
@@ -174,13 +294,13 @@ function configureMarkMapContainer(md) {
|
|
|
174
294
|
content = fileContent;
|
|
175
295
|
} catch (err) {
|
|
176
296
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
177
|
-
content =
|
|
178
|
-
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
Error: ${esc(errorMsg)}`;
|
|
297
|
+
content = `- Failed to load referenced file: ${esc(String(refRaw))}
|
|
298
|
+
- Error: ${esc(errorMsg)}`;
|
|
182
299
|
}
|
|
183
300
|
}
|
|
301
|
+
content = normalizeMindmapMarkdown(content, {
|
|
302
|
+
title: explicitTitle || referencedTitle
|
|
303
|
+
});
|
|
184
304
|
const encodedContent = encodeURIComponent(content.trim());
|
|
185
305
|
let propsStr = `content="${encodedContent}"`;
|
|
186
306
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -191,7 +311,7 @@ Error: ${esc(errorMsg)}`;
|
|
|
191
311
|
propsStr += ` ${k}="${safe}"`;
|
|
192
312
|
}
|
|
193
313
|
}
|
|
194
|
-
const html = `<
|
|
314
|
+
const html = `<MindmapPreview ${propsStr}></MindmapPreview>
|
|
195
315
|
`;
|
|
196
316
|
const htmlToken = new state.Token("html_block", "", 0);
|
|
197
317
|
htmlToken.content = html;
|
|
@@ -201,6 +321,40 @@ Error: ${esc(errorMsg)}`;
|
|
|
201
321
|
return true;
|
|
202
322
|
});
|
|
203
323
|
}
|
|
324
|
+
function configureMindmapFence(md) {
|
|
325
|
+
const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
|
|
326
|
+
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
|
327
|
+
const token = tokens[index];
|
|
328
|
+
const info = token.info.trim();
|
|
329
|
+
if (!/^mindmap(?=\s|\{|\[|$)/.test(info)) {
|
|
330
|
+
return fence(tokens, index, options, env, slf);
|
|
331
|
+
}
|
|
332
|
+
const fenceOptions = parseMarkmapFence(info);
|
|
333
|
+
let content = token.content;
|
|
334
|
+
const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
|
|
335
|
+
const reference = parseMindmapReference(firstNonEmptyLine);
|
|
336
|
+
if (reference) {
|
|
337
|
+
const possibleRel = env?.relativePath || env?.path || env?.filePath || env?.file || "";
|
|
338
|
+
const refFullPath = path.isAbsolute(reference.path) ? reference.path : path.resolve(process.cwd(), possibleRel ? path.dirname(possibleRel) : "", reference.path);
|
|
339
|
+
try {
|
|
340
|
+
content = fs.readFileSync(refFullPath, "utf8");
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
343
|
+
content = `- Failed to load referenced file: ${reference.path}
|
|
344
|
+
- Error: ${message}`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
content = normalizeMindmapMarkdown(content, {
|
|
348
|
+
title: fenceOptions.title || reference?.title
|
|
349
|
+
});
|
|
350
|
+
const props = [
|
|
351
|
+
`content="${encodeURIComponent(content.trim())}"`,
|
|
352
|
+
fenceOptions.initialExpandLevel === void 0 ? "" : `:initialExpandLevel="${fenceOptions.initialExpandLevel}"`
|
|
353
|
+
].filter(Boolean).join(" ");
|
|
354
|
+
return `<MindmapPreview ${props}></MindmapPreview>
|
|
355
|
+
`;
|
|
356
|
+
};
|
|
357
|
+
}
|
|
204
358
|
function configureSwiperContainer(md) {
|
|
205
359
|
let __tn_swiper_uid = 0;
|
|
206
360
|
let __tn_rules_stack = [];
|
|
@@ -269,7 +423,8 @@ function getMarkdownConfig() {
|
|
|
269
423
|
return true;
|
|
270
424
|
});
|
|
271
425
|
simpleMermaidMarkdown(md);
|
|
272
|
-
|
|
426
|
+
configureMindmapContainer(md);
|
|
427
|
+
configureMindmapFence(md);
|
|
273
428
|
md.use(markdownItTaskLists);
|
|
274
429
|
md.use(mila, {
|
|
275
430
|
attrs: {
|
|
@@ -1648,14 +1803,7 @@ function defineNotesConfig(overrides = {}) {
|
|
|
1648
1803
|
...overrideVite?.resolve
|
|
1649
1804
|
},
|
|
1650
1805
|
optimizeDeps: {
|
|
1651
|
-
include: [
|
|
1652
|
-
// VitePress 内部 CJS 依赖 —— 需要 Vite 预构建为 ESM
|
|
1653
|
-
"vitepress > @vscode/markdown-it-katex",
|
|
1654
|
-
"vitepress > @braintree/sanitize-url",
|
|
1655
|
-
"vitepress > dayjs",
|
|
1656
|
-
"vitepress > dayjs/plugin/utc",
|
|
1657
|
-
"vitepress > dayjs/plugin/localizedFormat"
|
|
1658
|
-
],
|
|
1806
|
+
include: [...DEFAULT_OPTIMIZE_DEPS_INCLUDE],
|
|
1659
1807
|
...overrideVite?.optimizeDeps
|
|
1660
1808
|
}
|
|
1661
1809
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.17.1",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"vue": "^3.5.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"
|
|
52
|
+
"@tnotesjs/mindmap-core": "^0.2.0",
|
|
53
53
|
"echarts": "^6.0.0",
|
|
54
54
|
"github-slugger": "^2.0.0",
|
|
55
55
|
"markdown-it-container": "^4.0.0",
|
|
@@ -57,11 +57,8 @@
|
|
|
57
57
|
"markdown-it-mathjax3": "^4.3.2",
|
|
58
58
|
"markdown-it-task-lists": "^2.1.1",
|
|
59
59
|
"marked": "^15.0.11",
|
|
60
|
-
"minisearch": "^7.2.0",
|
|
61
|
-
"markmap-lib": "^0.18.12",
|
|
62
|
-
"markmap-toolbar": "^0.18.12",
|
|
63
|
-
"markmap-view": "^0.18.12",
|
|
64
60
|
"mermaid": "^11.5.0",
|
|
61
|
+
"minisearch": "^7.2.0",
|
|
65
62
|
"sass-embedded": "^1.90.0",
|
|
66
63
|
"swiper": "^11.2.1",
|
|
67
64
|
"uuid": "^11.1.0",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseInlineSegments } from '@tnotesjs/mindmap-core'
|
|
2
|
+
import { defineComponent, h } from 'vue'
|
|
3
|
+
|
|
4
|
+
export default defineComponent({
|
|
5
|
+
name: 'MindmapInlineRuns',
|
|
6
|
+
props: {
|
|
7
|
+
raw: { type: String, default: '' },
|
|
8
|
+
},
|
|
9
|
+
setup(props) {
|
|
10
|
+
return () => h('span', { class: 'mindmap-inline-runs' }, parseInlineSegments(props.raw).map((segment) => {
|
|
11
|
+
const classes = Object.entries(segment.marks)
|
|
12
|
+
.filter(([, enabled]) => enabled)
|
|
13
|
+
.map(([name]) => `is-${name}`)
|
|
14
|
+
if (segment.link) {
|
|
15
|
+
return h('a', {
|
|
16
|
+
class: ['mindmap-inline-run', 'is-link', ...classes],
|
|
17
|
+
href: segment.link.url,
|
|
18
|
+
target: '_blank',
|
|
19
|
+
rel: 'noopener noreferrer',
|
|
20
|
+
}, segment.text)
|
|
21
|
+
}
|
|
22
|
+
return h('span', { class: ['mindmap-inline-run', ...classes] }, segment.text)
|
|
23
|
+
}))
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import InlineRuns from './InlineRuns'
|
|
3
|
+
|
|
4
|
+
import type { MindmapNode } from '@tnotesjs/mindmap-core'
|
|
5
|
+
|
|
6
|
+
defineOptions({ name: 'MindmapOutlineNode' })
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
node: MindmapNode
|
|
10
|
+
version: number
|
|
11
|
+
root?: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
defineEmits<{
|
|
15
|
+
toggle: [id: string]
|
|
16
|
+
}>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<li class="mindmap-outline-node" :class="{ 'is-root': root, 'is-done': node.content.checked === true }">
|
|
21
|
+
<div class="mindmap-outline-row">
|
|
22
|
+
<button
|
|
23
|
+
v-if="node.children.length > 0"
|
|
24
|
+
type="button"
|
|
25
|
+
class="mindmap-outline-toggle"
|
|
26
|
+
:aria-label="node.collapsed ? '展开主题' : '折叠主题'"
|
|
27
|
+
:aria-expanded="!node.collapsed"
|
|
28
|
+
@click="$emit('toggle', node.id)"
|
|
29
|
+
>
|
|
30
|
+
{{ node.collapsed ? '›' : '⌄' }}
|
|
31
|
+
</button>
|
|
32
|
+
<span v-else class="mindmap-outline-leaf" aria-hidden="true">•</span>
|
|
33
|
+
<input
|
|
34
|
+
v-if="node.content.checked !== null && !root"
|
|
35
|
+
class="mindmap-outline-checkbox"
|
|
36
|
+
type="checkbox"
|
|
37
|
+
:checked="node.content.checked"
|
|
38
|
+
disabled
|
|
39
|
+
aria-label="只读待办状态"
|
|
40
|
+
/>
|
|
41
|
+
<span class="mindmap-outline-label">
|
|
42
|
+
<InlineRuns :raw="node.content.image ? node.content.text : node.content.raw" />
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<img
|
|
46
|
+
v-if="node.content.image"
|
|
47
|
+
class="mindmap-outline-image"
|
|
48
|
+
:src="node.content.image.src"
|
|
49
|
+
:alt="node.content.image.alt"
|
|
50
|
+
:style="node.content.image.width ? { width: `${node.content.image.width}px` } : undefined"
|
|
51
|
+
/>
|
|
52
|
+
<ul v-if="node.children.length > 0 && !node.collapsed" class="mindmap-outline-children">
|
|
53
|
+
<MindmapOutlineNode
|
|
54
|
+
v-for="child in node.children"
|
|
55
|
+
:key="child.id"
|
|
56
|
+
:node="child"
|
|
57
|
+
:version="version"
|
|
58
|
+
@toggle="$emit('toggle', $event)"
|
|
59
|
+
/>
|
|
60
|
+
</ul>
|
|
61
|
+
</li>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
3
|
+
import { onContentUpdated, useData } from 'vitepress'
|
|
4
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
5
|
+
|
|
6
|
+
import { normalizeMindmapMarkdown } from './compat'
|
|
7
|
+
import { applyInitialExpandLevel } from './expandLevel'
|
|
8
|
+
import MindmapOutlineNode from './MindmapOutlineNode.vue'
|
|
9
|
+
|
|
10
|
+
type PreviewView = 'mindmap' | 'outline' | 'source'
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
content?: string
|
|
14
|
+
initialExpandLevel?: number
|
|
15
|
+
}>(), {
|
|
16
|
+
content: '',
|
|
17
|
+
initialExpandLevel: 3,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const { isDark } = useData()
|
|
21
|
+
const activeView = ref<PreviewView>('mindmap')
|
|
22
|
+
const canvasHost = ref<HTMLElement | null>(null)
|
|
23
|
+
const session = shallowRef<MindmapSession | null>(null)
|
|
24
|
+
const renderVersion = ref(0)
|
|
25
|
+
let viewer: CanvasViewer | null = null
|
|
26
|
+
let mounted = false
|
|
27
|
+
|
|
28
|
+
function decodeContent(value: string): string {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(value)
|
|
31
|
+
} catch {
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizedContent = computed(() => normalizeMindmapMarkdown(decodeContent(props.content)))
|
|
37
|
+
|
|
38
|
+
function createViewer(): void {
|
|
39
|
+
if (!mounted || !canvasHost.value || !session.value || viewer) return
|
|
40
|
+
viewer = new CanvasViewer(canvasHost.value, session.value, {
|
|
41
|
+
theme: isDark.value ? 'dark' : 'light',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rebuildSession(): void {
|
|
46
|
+
viewer?.destroy()
|
|
47
|
+
viewer = null
|
|
48
|
+
const next = new MindmapSession({
|
|
49
|
+
markdown: normalizedContent.value,
|
|
50
|
+
fileName: 'mindmap-preview.tn-mindmap.md',
|
|
51
|
+
})
|
|
52
|
+
applyInitialExpandLevel(next, props.initialExpandLevel)
|
|
53
|
+
const invalidate = () => { renderVersion.value += 1 }
|
|
54
|
+
next.on('collapseChange', invalidate)
|
|
55
|
+
next.on('focusChange', invalidate)
|
|
56
|
+
next.on('change', invalidate)
|
|
57
|
+
session.value = next
|
|
58
|
+
renderVersion.value += 1
|
|
59
|
+
void nextTick(createViewer)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setView(view: PreviewView): void {
|
|
63
|
+
activeView.value = view
|
|
64
|
+
if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toggleNode(id: string): void {
|
|
68
|
+
session.value?.toggleCollapse(id)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function exitFocusTo(index: number): void {
|
|
72
|
+
session.value?.exitFocusTo(index)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
|
|
76
|
+
watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
mounted = true
|
|
80
|
+
createViewer()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
onContentUpdated(() => {
|
|
84
|
+
if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
onBeforeUnmount(() => {
|
|
88
|
+
mounted = false
|
|
89
|
+
viewer?.destroy()
|
|
90
|
+
viewer = null
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<section class="mindmap-preview" :class="{ 'is-dark': isDark }">
|
|
96
|
+
<header class="mindmap-preview-header">
|
|
97
|
+
<nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
|
|
98
|
+
<button
|
|
99
|
+
v-for="item in ([['mindmap', '脑图'], ['outline', '大纲'], ['source', '源码']] as const)"
|
|
100
|
+
:key="item[0]"
|
|
101
|
+
type="button"
|
|
102
|
+
:class="{ 'is-active': activeView === item[0] }"
|
|
103
|
+
@click="setView(item[0])"
|
|
104
|
+
>
|
|
105
|
+
{{ item[1] }}
|
|
106
|
+
</button>
|
|
107
|
+
</nav>
|
|
108
|
+
<button v-if="activeView === 'mindmap'" type="button" class="mindmap-fit" @click="viewer?.zoomToFit()">
|
|
109
|
+
适应视口
|
|
110
|
+
</button>
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
|
|
114
|
+
<button type="button" @click="exitFocusTo(0)">全部</button>
|
|
115
|
+
<template v-for="(node, index) in session.focusPath" :key="node.id">
|
|
116
|
+
<span aria-hidden="true">/</span>
|
|
117
|
+
<button type="button" @click="exitFocusTo(index + 1)">{{ node.content.text }}</button>
|
|
118
|
+
</template>
|
|
119
|
+
</nav>
|
|
120
|
+
|
|
121
|
+
<div v-show="activeView === 'mindmap'" ref="canvasHost" class="mindmap-canvas-host" />
|
|
122
|
+
|
|
123
|
+
<div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
|
|
124
|
+
<ul class="mindmap-outline-root">
|
|
125
|
+
<MindmapOutlineNode
|
|
126
|
+
:node="session.focusRootNode"
|
|
127
|
+
:version="renderVersion"
|
|
128
|
+
root
|
|
129
|
+
@toggle="toggleNode"
|
|
130
|
+
/>
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<pre v-if="activeView === 'source'" class="mindmap-source"><code>{{ normalizedContent }}</code></pre>
|
|
135
|
+
</section>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<style scoped lang="scss">
|
|
139
|
+
.mindmap-preview {
|
|
140
|
+
--mindmap-panel: var(--vp-c-bg-soft);
|
|
141
|
+
--mindmap-border: var(--vp-c-divider);
|
|
142
|
+
margin: 1.5rem 0;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
border: 1px solid var(--mindmap-border);
|
|
145
|
+
border-radius: 10px;
|
|
146
|
+
background: var(--vp-c-bg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.mindmap-preview-header {
|
|
150
|
+
display: flex;
|
|
151
|
+
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);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.mindmap-preview-tabs {
|
|
160
|
+
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
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.mindmap-fit,
|
|
179
|
+
.mindmap-focus-path button {
|
|
180
|
+
color: var(--vp-c-text-2);
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
|
|
183
|
+
&:hover { color: var(--vp-c-brand-1); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.mindmap-focus-path {
|
|
187
|
+
display: flex;
|
|
188
|
+
gap: 6px;
|
|
189
|
+
align-items: center;
|
|
190
|
+
padding: 6px 12px;
|
|
191
|
+
overflow-x: auto;
|
|
192
|
+
border-bottom: 1px solid var(--mindmap-border);
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
color: var(--vp-c-text-3);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.mindmap-canvas-host {
|
|
198
|
+
position: relative;
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 440px;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
background: var(--vp-c-bg);
|
|
203
|
+
touch-action: none;
|
|
204
|
+
user-select: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.mindmap-canvas-host:deep(.mm-canvas),
|
|
208
|
+
.mindmap-canvas-host:deep(.mm-overlay) {
|
|
209
|
+
position: absolute;
|
|
210
|
+
inset: 0;
|
|
211
|
+
width: 100%;
|
|
212
|
+
height: 100%;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.mindmap-canvas-host:deep(.mm-overlay) { pointer-events: none; }
|
|
216
|
+
.mindmap-canvas-host:deep(.mm-editor) { outline: none; }
|
|
217
|
+
|
|
218
|
+
.mindmap-outline {
|
|
219
|
+
max-height: 560px;
|
|
220
|
+
padding: 18px 22px 22px;
|
|
221
|
+
overflow: auto;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.mindmap-outline-root,
|
|
225
|
+
.mindmap-outline :deep(ul) {
|
|
226
|
+
margin: 0;
|
|
227
|
+
padding: 0;
|
|
228
|
+
list-style: none;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.mindmap-outline :deep(.mindmap-outline-children) {
|
|
232
|
+
margin-left: 10px;
|
|
233
|
+
padding-left: 19px;
|
|
234
|
+
border-left: 1px solid var(--vp-c-divider);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.mindmap-outline :deep(.mindmap-outline-row) {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: flex-start;
|
|
240
|
+
gap: 7px;
|
|
241
|
+
min-height: 30px;
|
|
242
|
+
padding: 3px 0;
|
|
243
|
+
color: var(--vp-c-text-1);
|
|
244
|
+
line-height: 24px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.mindmap-outline :deep(.mindmap-outline-toggle),
|
|
248
|
+
.mindmap-outline :deep(.mindmap-outline-leaf) {
|
|
249
|
+
flex: 0 0 18px;
|
|
250
|
+
width: 18px;
|
|
251
|
+
color: var(--vp-c-text-3);
|
|
252
|
+
text-align: center;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.mindmap-outline :deep(.mindmap-outline-toggle:hover) { color: var(--vp-c-brand-1); }
|
|
256
|
+
.mindmap-outline :deep(.mindmap-outline-checkbox) { margin-top: 5px; }
|
|
257
|
+
.mindmap-outline :deep(.mindmap-outline-label) { min-width: 0; overflow-wrap: anywhere; }
|
|
258
|
+
.mindmap-outline :deep(.mindmap-outline-node.is-root > .mindmap-outline-row) { font-size: 18px; font-weight: 700; }
|
|
259
|
+
.mindmap-outline :deep(.mindmap-outline-node.is-done > .mindmap-outline-row .mindmap-outline-label) { opacity: .58; text-decoration: line-through; }
|
|
260
|
+
.mindmap-outline :deep(.mindmap-outline-image) { display: block; max-width: min(100%, 560px); max-height: 360px; margin: 5px 0 12px 25px; border-radius: 6px; }
|
|
261
|
+
.mindmap-outline :deep(.is-bold) { font-weight: 700; }
|
|
262
|
+
.mindmap-outline :deep(.is-italic) { font-style: italic; }
|
|
263
|
+
.mindmap-outline :deep(.is-underline) { text-decoration: underline; }
|
|
264
|
+
.mindmap-outline :deep(.is-strike) { text-decoration: line-through; }
|
|
265
|
+
.mindmap-outline :deep(.is-highlight) { padding: 0 2px; border-radius: 2px; background: #ffe56b; color: #252525; }
|
|
266
|
+
.mindmap-outline :deep(.is-code) { padding: 1px 5px; border-radius: 4px; background: var(--vp-c-bg-soft); color: var(--vp-c-danger-1); font-family: var(--vp-font-family-mono); }
|
|
267
|
+
.mindmap-outline :deep(.is-link) { color: var(--vp-c-brand-1); text-decoration: underline; text-underline-offset: 3px; }
|
|
268
|
+
|
|
269
|
+
.mindmap-source {
|
|
270
|
+
max-height: 560px;
|
|
271
|
+
margin: 0;
|
|
272
|
+
padding: 18px 22px;
|
|
273
|
+
overflow: auto;
|
|
274
|
+
border-radius: 0;
|
|
275
|
+
background: var(--vp-code-block-bg);
|
|
276
|
+
color: var(--vp-code-block-color);
|
|
277
|
+
font-size: 13px;
|
|
278
|
+
line-height: 1.65;
|
|
279
|
+
white-space: pre;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (max-width: 768px) {
|
|
283
|
+
.mindmap-canvas-host { height: 360px; }
|
|
284
|
+
.mindmap-preview-tabs button { padding-inline: 9px; }
|
|
285
|
+
.mindmap-outline { padding-inline: 12px; }
|
|
286
|
+
}
|
|
287
|
+
</style>
|