@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.
- package/dist/chunk-3R7QSABB.cjs +502 -0
- package/dist/chunk-CZXRQPFJ.js +11 -0
- package/dist/{chunk-XCWFZLER.js → chunk-DUTS75WQ.js} +27 -493
- package/dist/chunk-HXED7KVT.cjs +11 -0
- package/dist/chunk-IHFUHB4J.cjs +176 -0
- package/dist/chunk-MZIXIY2Y.cjs +216 -0
- package/dist/chunk-TJ4527KA.cjs +5264 -0
- package/dist/chunk-U6IBLREL.js +502 -0
- package/dist/chunk-UNFSW5C2.js +176 -0
- package/dist/cli/index.cjs +936 -0
- package/dist/cli/index.d.cts +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +9 -6
- package/dist/index.cjs +8 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.ts +56 -0
- package/dist/markdown/index.cjs +82 -0
- package/dist/markdown/index.d.cts +36 -0
- package/dist/markdown/index.d.ts +36 -0
- package/dist/markdown/index.js +82 -0
- package/dist/note-oGm44Rw8.d.cts +84 -0
- package/dist/note-oGm44Rw8.d.ts +84 -0
- package/dist/types-CpsEy8lA.d.cts +221 -0
- package/dist/types-CwBWwNVv.d.ts +221 -0
- package/dist/vitepress/config/index.cjs +1669 -0
- package/dist/vitepress/config/index.d.cts +12 -0
- package/dist/vitepress/config/index.d.ts +12 -0
- package/dist/vitepress/config/index.js +26 -192
- package/dist/workspace/index.cjs +1207 -0
- package/dist/workspace/index.d.cts +14 -0
- package/dist/workspace/index.d.ts +14 -0
- package/dist/workspace/index.js +1207 -0
- package/package.json +16 -3
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +189 -34
- package/vitepress/components/MindmapPreview/MindmapViewIcon.vue +41 -0
- package/vitepress/components/MindmapPreview/markdown.test.ts +67 -0
- package/vitepress/components/MindmapPreview/markdown.ts +83 -0
- package/vitepress/components/MindmapPreview/wheelInteraction.test.ts +23 -0
- package/vitepress/components/MindmapPreview/wheelInteraction.ts +7 -0
- package/vitepress/components/Settings/Settings.vue +3 -3
- package/vitepress/components/SidebarCard/SidebarCard.vue +3 -3
- package/vitepress/components/constants.ts +0 -5
- package/vitepress/configs/markdown.config.ts +6 -199
- package/vitepress/plugins/localSearchReindexLogic.ts +11 -3
- package/vitepress/theme/index.ts +0 -2
- package/vitepress/components/MindmapPreview/compat.test.ts +0 -93
- package/vitepress/components/MindmapPreview/compat.ts +0 -128
- package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +0 -57
|
@@ -13,9 +13,9 @@ import path from 'path'
|
|
|
13
13
|
import { generateAnchor } from '../../utils'
|
|
14
14
|
import {
|
|
15
15
|
normalizeMindmapMarkdown,
|
|
16
|
-
|
|
16
|
+
parseMindmapFence,
|
|
17
17
|
parseMindmapReference,
|
|
18
|
-
} from '../components/MindmapPreview/
|
|
18
|
+
} from '../components/MindmapPreview/markdown'
|
|
19
19
|
|
|
20
20
|
import type MarkdownIt from 'markdown-it'
|
|
21
21
|
import type { MarkdownOptions } from 'vitepress'
|
|
@@ -70,196 +70,7 @@ const simpleMermaidMarkdown = (md: MarkdownIt) => {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
74
|
-
* Mindmap 容器配置(兼容旧的 markmap 围栏名)
|
|
75
|
-
*/
|
|
76
|
-
function configureMindmapContainer(md: MarkdownIt) {
|
|
77
|
-
md.use(markdownItContainer, 'markmap', {
|
|
78
|
-
marker: '`',
|
|
79
|
-
validate(params: string) {
|
|
80
|
-
return (params || '').trim().startsWith('markmap')
|
|
81
|
-
},
|
|
82
|
-
render() {
|
|
83
|
-
return ''
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// 在 core 阶段把整个 container 区间替换成一个 html_block(MarkMap 组件标签)
|
|
88
|
-
// 这样渲染时就只输出 <MarkMap ...>,中间的列表 token 已被移除
|
|
89
|
-
md.core.ruler.after('block', 'tn_replace_markmap_container', (state) => {
|
|
90
|
-
const src = state.env.source || ''
|
|
91
|
-
const lines = src.split('\n')
|
|
92
|
-
const tokens = state.tokens
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
95
|
-
const t = tokens[i]
|
|
96
|
-
if (t.type === 'container_markmap_open') {
|
|
97
|
-
const containerName = 'markmap'
|
|
98
|
-
const closeType = 'container_markmap_close'
|
|
99
|
-
// 找到对应的 close token
|
|
100
|
-
let j = i + 1
|
|
101
|
-
while (
|
|
102
|
-
j < tokens.length &&
|
|
103
|
-
tokens[j].type !== closeType
|
|
104
|
-
)
|
|
105
|
-
j++
|
|
106
|
-
if (j >= tokens.length) continue // safety
|
|
107
|
-
|
|
108
|
-
// 使用 token.map 提取源文件对应行(open.token.map 存着 container 起止行)
|
|
109
|
-
const open = t
|
|
110
|
-
const startLine = open.map ? open.map[0] + 1 : null
|
|
111
|
-
const endLine = open.map ? open.map[1] - 1 : null
|
|
112
|
-
|
|
113
|
-
// 1) 从开头 fence 行解析参数(支持 `{a=1 b="x"}`、`a=1 b="x"`,并支持单个数字 shorthand)
|
|
114
|
-
const params: { [key: string]: any; initialExpandLevel?: number } = {}
|
|
115
|
-
let explicitTitle: string | undefined
|
|
116
|
-
|
|
117
|
-
if (open.map && typeof open.map[0] === 'number') {
|
|
118
|
-
const openLine = (lines[open.map[0]] || '').trim()
|
|
119
|
-
const fenceOptions = parseMarkmapFence(openLine)
|
|
120
|
-
explicitTitle = fenceOptions.title
|
|
121
|
-
let paramPart = ''
|
|
122
|
-
|
|
123
|
-
// 优先匹配大括号形式 ```markmap{...}
|
|
124
|
-
const braceMatch = openLine.match(/\{([^}]*)\}/)
|
|
125
|
-
if (braceMatch) {
|
|
126
|
-
paramPart = braceMatch[1].trim()
|
|
127
|
-
} else {
|
|
128
|
-
// 否则尝试去掉前缀 ``` 和 markmap,剩下的作为参数部分
|
|
129
|
-
const after = openLine.replace(/^`+\s*/, '')
|
|
130
|
-
if (after.startsWith(containerName)) {
|
|
131
|
-
paramPart = after.slice(containerName.length).trim()
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (fenceOptions.initialExpandLevel !== undefined) {
|
|
135
|
-
params.initialExpandLevel = fenceOptions.initialExpandLevel
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (paramPart) {
|
|
139
|
-
// 使用正则按 token 切分:保持用引号包裹的片段为单个 token(支持包含空格)
|
|
140
|
-
const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || []
|
|
141
|
-
|
|
142
|
-
// 如果第一个 token 是纯数字,把它当作 initialExpandLevel
|
|
143
|
-
let startIdx = 0
|
|
144
|
-
if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0] as string)) {
|
|
145
|
-
params.initialExpandLevel = Number(tokenArr[0])
|
|
146
|
-
startIdx = 1
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 解析剩余 token 为 key=value(支持 key=val 或 key:val)
|
|
150
|
-
for (let k = startIdx; k < tokenArr.length; k++) {
|
|
151
|
-
const pair = tokenArr[k]
|
|
152
|
-
if (!pair) continue
|
|
153
|
-
const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/)
|
|
154
|
-
if (m) {
|
|
155
|
-
const key = m[1]
|
|
156
|
-
let val = m[3]
|
|
157
|
-
|
|
158
|
-
// 去除外层引号(若存在)
|
|
159
|
-
if (
|
|
160
|
-
(/^".*"$/.test(val) && val.length >= 2) ||
|
|
161
|
-
(/^'.*'$/.test(val) && val.length >= 2)
|
|
162
|
-
) {
|
|
163
|
-
val = val.slice(1, -1)
|
|
164
|
-
} else if (/^\d+$/.test(val)) {
|
|
165
|
-
// 纯数字转字符串
|
|
166
|
-
val = String(Number(val))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
params[key] = val
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 2) 提取内容(支持文件引用语法 `<<< ./path/to/file.md`)
|
|
176
|
-
let content = ''
|
|
177
|
-
if (startLine !== null && endLine !== null) {
|
|
178
|
-
for (let k = startLine; k <= endLine && k < lines.length; k++) {
|
|
179
|
-
content += lines[k] + '\n'
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
// 回退:如果没有 map 信息,尝试用中间 tokens 拼接文本
|
|
183
|
-
for (let k = i + 1; k < j; k++) {
|
|
184
|
-
content += tokens[k].content || ''
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// --- 检查第一非空行是否为引用语法 ---
|
|
189
|
-
const firstNonEmptyLine =
|
|
190
|
-
(content || '').split('\n').find((ln) => ln.trim() !== '') || ''
|
|
191
|
-
const reference = parseMindmapReference(firstNonEmptyLine)
|
|
192
|
-
let referencedTitle: string | undefined
|
|
193
|
-
if (reference) {
|
|
194
|
-
const refRaw = reference.path
|
|
195
|
-
referencedTitle = reference.title
|
|
196
|
-
|
|
197
|
-
// 尝试同步读取文件内容(兼容常见 Node 环境)
|
|
198
|
-
try {
|
|
199
|
-
// 尝试根据当前 markdown 文件位置解析相对路径
|
|
200
|
-
const env = state.env || {}
|
|
201
|
-
const possibleRel =
|
|
202
|
-
env.relativePath || env.path || env.filePath || env.file || ''
|
|
203
|
-
let refFullPath = refRaw
|
|
204
|
-
|
|
205
|
-
if (!path.isAbsolute(refRaw)) {
|
|
206
|
-
if (possibleRel) {
|
|
207
|
-
// 将 relativePath 视作相对于项目根的路径(例如 'notes/foo/bar.md'),取其目录
|
|
208
|
-
const currentDir = path.dirname(possibleRel)
|
|
209
|
-
// 解析到 process.cwd()
|
|
210
|
-
refFullPath = path.resolve(process.cwd(), currentDir, refRaw)
|
|
211
|
-
} else {
|
|
212
|
-
// 没有相对文件信息,则相对于项目根解析
|
|
213
|
-
refFullPath = path.resolve(process.cwd(), refRaw)
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
// 绝对路径直接使用(按系统路径)
|
|
217
|
-
refFullPath = refRaw
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// console.log('refFullPath:', refFullPath)
|
|
221
|
-
const fileContent = fs.readFileSync(refFullPath, 'utf-8')
|
|
222
|
-
content = fileContent
|
|
223
|
-
} catch (err) {
|
|
224
|
-
// 读取失败:将错误写入 content 以便排查(不会让流程直接崩溃)
|
|
225
|
-
const errorMsg = err instanceof Error ? err.message : String(err)
|
|
226
|
-
content = `- Failed to load referenced file: ${esc(String(refRaw))}\n - Error: ${esc(errorMsg)}`
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
content = normalizeMindmapMarkdown(content, {
|
|
231
|
-
title: explicitTitle || referencedTitle,
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
// 3) 构造组件标签并把参数注入为 props
|
|
235
|
-
const encodedContent = encodeURIComponent(content.trim())
|
|
236
|
-
let propsStr = `content="${encodedContent}"`
|
|
237
|
-
|
|
238
|
-
for (const [k, v] of Object.entries(params)) {
|
|
239
|
-
if (typeof v === 'number' || /^\d+$/.test(String(v))) {
|
|
240
|
-
propsStr += ` :${k}="${v}"`
|
|
241
|
-
} else {
|
|
242
|
-
const safe = String(v).replace(/"/g, '"')
|
|
243
|
-
propsStr += ` ${k}="${safe}"`
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const html = `<MindmapPreview ${propsStr}></MindmapPreview>\n`
|
|
248
|
-
|
|
249
|
-
// 创建 html_block token
|
|
250
|
-
const htmlToken = new state.Token('html_block', '', 0)
|
|
251
|
-
htmlToken.content = html
|
|
252
|
-
|
|
253
|
-
// 用单个 html_token 替换 open..close 区间
|
|
254
|
-
tokens.splice(i, j - i + 1, htmlToken as any)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return true
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Canonical `mindmap` fence. Legacy `markmap` stays on its historical container path. */
|
|
73
|
+
/** Canonical `mindmap` fence. */
|
|
263
74
|
function configureMindmapFence(md: MarkdownIt) {
|
|
264
75
|
const fence = md.renderer.rules.fence
|
|
265
76
|
? md.renderer.rules.fence.bind(md.renderer.rules)
|
|
@@ -268,11 +79,8 @@ function configureMindmapFence(md: MarkdownIt) {
|
|
|
268
79
|
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
|
269
80
|
const token = tokens[index]
|
|
270
81
|
const info = token.info.trim()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const fenceOptions = parseMarkmapFence(info)
|
|
82
|
+
const fenceOptions = parseMindmapFence(info)
|
|
83
|
+
if (!fenceOptions) return fence(tokens, index, options, env, slf)
|
|
276
84
|
let content = token.content
|
|
277
85
|
const firstNonEmptyLine = content.split('\n').find((line) => line.trim()) ?? ''
|
|
278
86
|
const reference = parseMindmapReference(firstNonEmptyLine)
|
|
@@ -396,8 +204,7 @@ export function getMarkdownConfig(): MarkdownOptions {
|
|
|
396
204
|
// 添加 Mermaid 支持
|
|
397
205
|
simpleMermaidMarkdown(md)
|
|
398
206
|
|
|
399
|
-
//
|
|
400
|
-
configureMindmapContainer(md)
|
|
207
|
+
// 添加规范的 Mindmap 围栏支持
|
|
401
208
|
configureMindmapFence(md)
|
|
402
209
|
|
|
403
210
|
// 添加任务列表支持
|
|
@@ -34,11 +34,19 @@ export function getDocIdFromFile(input: {
|
|
|
34
34
|
rewrites?: Record<string, string>
|
|
35
35
|
absoluteOrRelativeFile: string
|
|
36
36
|
}): string {
|
|
37
|
-
|
|
37
|
+
// A knowledge base can be indexed on a different host platform in tests,
|
|
38
|
+
// build workers or cached metadata. Select the path implementation from the
|
|
39
|
+
// value instead of assuming the current Node.js platform.
|
|
40
|
+
const pathApi = /^[a-zA-Z]:[\\/]/.test(
|
|
41
|
+
input.absoluteOrRelativeFile || input.srcDir,
|
|
42
|
+
)
|
|
43
|
+
? path.win32
|
|
44
|
+
: path
|
|
45
|
+
const absoluteFile = pathApi.isAbsolute(input.absoluteOrRelativeFile)
|
|
38
46
|
? input.absoluteOrRelativeFile
|
|
39
|
-
:
|
|
47
|
+
: pathApi.join(input.srcDir, input.absoluteOrRelativeFile)
|
|
40
48
|
|
|
41
|
-
let relFile = slash(
|
|
49
|
+
let relFile = slash(pathApi.relative(input.srcDir, absoluteFile))
|
|
42
50
|
if (input.rewrites?.[relFile]) {
|
|
43
51
|
relFile = input.rewrites[relFile]
|
|
44
52
|
}
|
package/vitepress/theme/index.ts
CHANGED
|
@@ -45,8 +45,6 @@ function registerCoreComponents(ctx: EnhanceAppContext) {
|
|
|
45
45
|
app.component('Footprints', Footprints)
|
|
46
46
|
app.component('F', Footprints)
|
|
47
47
|
app.component('SidebarCard', SidebarCard)
|
|
48
|
-
// Keep the historical component name so existing note fences need no migration.
|
|
49
|
-
app.component('MarkMap', MindmapPreview)
|
|
50
48
|
app.component('MindmapPreview', MindmapPreview)
|
|
51
49
|
app.component('Mermaid', Mermaid)
|
|
52
50
|
app.component('NotesTable', NotesTable)
|
|
@@ -1,93 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,128 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
})
|