@tnotesjs/core 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-4MO7F7Q4.js → chunk-BSTQOJVE.js} +1 -1
  3. package/dist/{chunk-SQVHJR2Z.js → chunk-O4DCXHOV.js} +0 -3
  4. package/dist/cli/index.js +2 -2
  5. package/dist/index.js +1 -1
  6. package/dist/vitepress/config/index.js +2 -2
  7. package/package.json +3 -4
  8. package/types/config.ts +60 -61
  9. package/types/index.ts +11 -11
  10. package/types/note.ts +33 -33
  11. package/vitepress/assets/icons/icon__check.svg +3 -3
  12. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  13. package/vitepress/assets/icons/icon__close.svg +1 -1
  14. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  15. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  16. package/vitepress/assets/icons/icon__copy.svg +4 -4
  17. package/vitepress/assets/icons/icon__focus.svg +1 -1
  18. package/vitepress/assets/icons/icon__fold.svg +3 -3
  19. package/vitepress/assets/icons/icon__folder.svg +1 -1
  20. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  21. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  22. package/vitepress/assets/icons/icon__github.svg +3 -3
  23. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  24. package/vitepress/assets/icons/icon__next.svg +1 -1
  25. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  26. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  27. package/vitepress/assets/icons/icon__prev.svg +1 -1
  28. package/vitepress/assets/icons/icon__restore.svg +1 -1
  29. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  30. package/vitepress/assets/icons/icon__search.svg +1 -1
  31. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  32. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  33. package/vitepress/assets/icons/icon__totop.svg +5 -5
  34. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  35. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  37. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  38. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  39. package/vitepress/assets/icons/index.ts +39 -39
  40. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  41. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  42. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -213
  43. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  44. package/vitepress/components/Discussions/Discussions.vue +244 -211
  45. package/vitepress/components/EnWordList/EnWordList.vue +663 -543
  46. package/vitepress/components/EnWordList/RightClickMenu.vue +89 -66
  47. package/vitepress/components/Footprints/Footprints.vue +472 -377
  48. package/vitepress/components/Layout/AboutModal.vue +331 -105
  49. package/vitepress/components/Layout/AboutPanel.vue +484 -270
  50. package/vitepress/components/Layout/ContentCollapse.vue +590 -603
  51. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  52. package/vitepress/components/Layout/DocBeforeControls.vue +351 -149
  53. package/vitepress/components/Layout/DocFooter.vue +233 -233
  54. package/vitepress/components/Layout/ImagePreview.vue +484 -281
  55. package/vitepress/components/Layout/Layout.vue +744 -621
  56. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +0 -2
  57. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  58. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  59. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -171
  60. package/vitepress/components/Layout/SidebarResizeHandle.vue +311 -319
  61. package/vitepress/components/Layout/Swiper.vue +167 -167
  62. package/vitepress/components/Layout/ToggleFullContent.vue +47 -35
  63. package/vitepress/components/Layout/ToggleSidebar.vue +37 -26
  64. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  65. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  66. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  67. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  68. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  69. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  70. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  71. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  72. package/vitepress/components/LoadingPage/LoadingPage.vue +1 -6
  73. package/vitepress/components/MarkMap/MarkMap.vue +564 -405
  74. package/vitepress/components/Mermaid/Mermaid.vue +643 -365
  75. package/vitepress/components/NotesTable/NotesTable.vue +174 -98
  76. package/vitepress/components/NotesTable/README.md +67 -67
  77. package/vitepress/components/Settings/Settings.vue +711 -334
  78. package/vitepress/components/Settings/SettingsDialog.vue +1 -11
  79. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  80. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  81. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -946
  82. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  83. package/vitepress/components/constants.ts +109 -109
  84. package/vitepress/components/notesConfig.data.ts +73 -73
  85. package/vitepress/components/sidebar.data.ts +59 -59
  86. package/vitepress/components/tnotes-config.data.ts +21 -21
  87. package/vitepress/components/utils.ts +26 -26
  88. package/vitepress/config/index.ts +169 -169
  89. package/vitepress/configs/constants.ts +26 -26
  90. package/vitepress/configs/head.config.ts +25 -25
  91. package/vitepress/configs/index.ts +9 -9
  92. package/vitepress/configs/markdown-it.d.ts +23 -23
  93. package/vitepress/configs/markdown.config.ts +368 -367
  94. package/vitepress/configs/theme.config.ts +108 -108
  95. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  96. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  97. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  98. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  99. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  100. package/vitepress/theme/index.ts +137 -137
  101. package/vitepress/theme/styles/base.scss +66 -64
  102. package/vitepress/theme/styles/components/404.scss +31 -31
  103. package/vitepress/theme/styles/components/collapse.scss +172 -172
  104. package/vitepress/theme/styles/components/markmap.scss +101 -101
  105. package/vitepress/theme/styles/components/swiper.scss +255 -255
  106. package/vitepress/theme/styles/index.scss +25 -25
  107. package/vitepress/theme/styles/layout.scss +78 -78
  108. package/vitepress/theme/styles/utilities.scss +39 -39
  109. package/vitepress/theme/styles/vitepress-override.scss +25 -25
  110. package/vitepress/components/Discussions/Discussions.module.scss +0 -32
  111. package/vitepress/components/EnWordList/EnWordList.module.scss +0 -124
  112. package/vitepress/components/EnWordList/RightClickMenu.module.scss +0 -22
  113. package/vitepress/components/Footprints/Footprints.module.scss +0 -93
  114. package/vitepress/components/Layout/AboutModal.module.scss +0 -233
  115. package/vitepress/components/Layout/ImagePreview.module.scss +0 -201
  116. package/vitepress/components/Layout/Layout.module.scss +0 -661
  117. package/vitepress/components/Layout/ToggleFullContent.module.scss +0 -11
  118. package/vitepress/components/Layout/ToggleSidebar.module.scss +0 -11
  119. package/vitepress/components/MarkMap/MarkMap.module.scss +0 -159
  120. package/vitepress/components/Mermaid/Mermaid.module.scss +0 -275
  121. package/vitepress/components/NotesTable/NotesTable.module.scss +0 -77
  122. package/vitepress/components/Settings/Settings.module.scss +0 -375
@@ -1,367 +1,368 @@
1
- /**
2
- * .vitepress/config/markdown.config.ts
3
- *
4
- * Markdown 配置
5
- */
6
- import fs from 'fs'
7
- import MarkdownIt from 'markdown-it'
8
- import markdownItContainer from 'markdown-it-container'
9
- import mila from 'markdown-it-link-attributes'
10
- import markdownItTaskLists from 'markdown-it-task-lists'
11
- import path from 'path'
12
- import { MarkdownOptions } from 'vitepress'
13
-
14
- import { generateAnchor } from '../../utils'
15
-
16
- /**
17
- * 辅助函数:HTML 转义
18
- */
19
- function esc(s = '') {
20
- return s.replace(
21
- /[&<>"']/g,
22
- (ch) =>
23
- ({
24
- '&': '&amp;',
25
- '<': '&lt;',
26
- '>': '&gt;',
27
- '"': '&quot;',
28
- "'": '&#39;',
29
- }[ch]!)
30
- )
31
- }
32
-
33
- /**
34
- * 简化的 Mermaid 处理函数
35
- */
36
- const simpleMermaidMarkdown = (md: MarkdownIt) => {
37
- const fence = md.renderer.rules.fence
38
- ? md.renderer.rules.fence.bind(md.renderer.rules)
39
- : () => ''
40
-
41
- md.renderer.rules.fence = (tokens, index, options, env, slf) => {
42
- const token = tokens[index]
43
-
44
- // 检查是否为 mermaid 代码块
45
- if (token.info.trim() === 'mermaid') {
46
- try {
47
- const key = `mermaid-${Date.now()}-${Math.random()
48
- .toString(36)
49
- .substr(2, 9)}`
50
- const content = token.content
51
- return `<Mermaid id="${key}" graph="${encodeURIComponent(content)}" />`
52
- } catch (err) {
53
- return `<pre>${err}</pre>`
54
- }
55
- }
56
-
57
- // 允许使用 mmd 标记显示 Mermaid 代码本身
58
- if (token.info.trim() === 'mmd') {
59
- tokens[index].info = 'mermaid'
60
- }
61
-
62
- return fence(tokens, index, options, env, slf)
63
- }
64
- }
65
-
66
- /**
67
- * MarkMap 容器配置
68
- */
69
- function configureMarkMapContainer(md: MarkdownIt) {
70
- // 先保留 container 的解析(负责把 ```markmap ``` 识别成 container tokens)
71
- // 但让它本身不输出任何 HTML(render 返回空)
72
- md.use(markdownItContainer, 'markmap', {
73
- marker: '`',
74
- validate(params: string) {
75
- // 接受 "markmap", "markmap{...}" 或 "markmap key=val ..." 等写法
76
- const p = (params || '').trim()
77
- return p.startsWith('markmap')
78
- },
79
- render() {
80
- return ''
81
- },
82
- })
83
-
84
- // 在 core 阶段把整个 container 区间替换成一个 html_block(MarkMap 组件标签)
85
- // 这样渲染时就只输出 <MarkMap ...>,中间的列表 token 已被移除
86
- md.core.ruler.after('block', 'tn_replace_markmap_container', (state) => {
87
- const src = state.env.source || ''
88
- const lines = src.split('\n')
89
- const tokens = state.tokens
90
-
91
- for (let i = 0; i < tokens.length; i++) {
92
- const t = tokens[i]
93
- if (t.type === 'container_markmap_open') {
94
- // 找到对应的 close token
95
- let j = i + 1
96
- while (
97
- j < tokens.length &&
98
- tokens[j].type !== 'container_markmap_close'
99
- )
100
- j++
101
- if (j >= tokens.length) continue // safety
102
-
103
- // 使用 token.map 提取源文件对应行(open.token.map 存着 container 起止行)
104
- const open = t
105
- const startLine = open.map ? open.map[0] + 1 : null
106
- const endLine = open.map ? open.map[1] - 1 : null
107
-
108
- // 1) 从开头 fence 行解析参数(支持 `{a=1 b="x"}`、`a=1 b="x"`,并支持单个数字 shorthand)
109
- const params: { [key: string]: any; initialExpandLevel?: number } = {}
110
-
111
- if (open.map && typeof open.map[0] === 'number') {
112
- const openLine = (lines[open.map[0]] || '').trim()
113
- let paramPart = ''
114
-
115
- // 优先匹配大括号形式 ```markmap{...}
116
- const braceMatch = openLine.match(/\{([^}]*)\}/)
117
- if (braceMatch) {
118
- paramPart = braceMatch[1].trim()
119
- } else {
120
- // 否则尝试去掉前缀 ``` 和 markmap,剩下的作为参数部分
121
- const after = openLine.replace(/^`+\s*/, '')
122
- if (after.startsWith('markmap')) {
123
- paramPart = after.slice('markmap'.length).trim()
124
- }
125
- }
126
-
127
- if (paramPart) {
128
- // 使用正则按 token 切分:保持用引号包裹的片段为单个 token(支持包含空格)
129
- const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || []
130
-
131
- // 如果第一个 token 是纯数字,把它当作 initialExpandLevel
132
- let startIdx = 0
133
- if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0] as string)) {
134
- params.initialExpandLevel = Number(tokenArr[0])
135
- startIdx = 1
136
- }
137
-
138
- // 解析剩余 token 为 key=value(支持 key=val 或 key:val)
139
- for (let k = startIdx; k < tokenArr.length; k++) {
140
- const pair = tokenArr[k]
141
- if (!pair) continue
142
- const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/)
143
- if (m) {
144
- const key = m[1]
145
- let val = m[3]
146
-
147
- // 去除外层引号(若存在)
148
- if (
149
- (/^".*"$/.test(val) && val.length >= 2) ||
150
- (/^'.*'$/.test(val) && val.length >= 2)
151
- ) {
152
- val = val.slice(1, -1)
153
- } else if (/^\d+$/.test(val)) {
154
- // 纯数字转字符串
155
- val = String(Number(val))
156
- }
157
-
158
- params[key] = val
159
- }
160
- }
161
- }
162
- }
163
-
164
- // 2) 提取内容(支持文件引用语法 `<<< ./path/to/file.md`)
165
- let content = ''
166
- if (startLine !== null && endLine !== null) {
167
- for (let k = startLine; k <= endLine && k < lines.length; k++) {
168
- content += lines[k] + '\n'
169
- }
170
- } else {
171
- // 回退:如果没有 map 信息,尝试用中间 tokens 拼接文本
172
- for (let k = i + 1; k < j; k++) {
173
- content += tokens[k].content || ''
174
- }
175
- }
176
-
177
- // --- 检查第一非空行是否为引用语法 ---
178
- const firstNonEmptyLine =
179
- (content || '').split('\n').find((ln) => ln.trim() !== '') || ''
180
- const refMatch = firstNonEmptyLine.trim().match(/^<<<\s*(.+)$/)
181
- if (refMatch) {
182
- // 提取引用路径,支持引号包裹
183
- const refRaw = refMatch[1].trim().replace(/^['"]|['"]$/g, '')
184
-
185
- // 尝试同步读取文件内容(兼容常见 Node 环境)
186
- try {
187
- // 尝试根据当前 markdown 文件位置解析相对路径
188
- const env = state.env || {}
189
- const possibleRel =
190
- env.relativePath || env.path || env.filePath || env.file || ''
191
- let refFullPath = refRaw
192
-
193
- if (!path.isAbsolute(refRaw)) {
194
- if (possibleRel) {
195
- // relativePath 视作相对于项目根的路径(例如 'notes/foo/bar.md'),取其目录
196
- const currentDir = path.dirname(possibleRel)
197
- // 解析到 process.cwd()
198
- refFullPath = path.resolve(process.cwd(), currentDir, refRaw)
199
- } else {
200
- // 没有相对文件信息,则相对于项目根解析
201
- refFullPath = path.resolve(process.cwd(), refRaw)
202
- }
203
- } else {
204
- // 绝对路径直接使用(按系统路径)
205
- refFullPath = refRaw
206
- }
207
-
208
- // console.log('refFullPath:', refFullPath)
209
- const fileContent = fs.readFileSync(refFullPath, 'utf-8')
210
- content = fileContent
211
- } catch (err) {
212
- // 读取失败:将错误写入 content 以便排查(不会让流程直接崩溃)
213
- const errorMsg = err instanceof Error ? err.message : String(err)
214
- content = `Failed to load referenced file: ${esc(
215
- String(refRaw)
216
- )}\n\nError: ${esc(errorMsg)}`
217
- }
218
- }
219
-
220
- // 3) 构造组件标签并把参数注入为 props
221
- const encodedContent = encodeURIComponent(content.trim())
222
- let propsStr = `content="${encodedContent}"`
223
-
224
- for (const [k, v] of Object.entries(params)) {
225
- if (typeof v === 'number' || /^\d+$/.test(String(v))) {
226
- propsStr += ` :${k}="${v}"`
227
- } else {
228
- const safe = String(v).replace(/"/g, '&quot;')
229
- propsStr += ` ${k}="${safe}"`
230
- }
231
- }
232
-
233
- const html = `<MarkMap ${propsStr}></MarkMap>\n`
234
-
235
- // 创建 html_block token
236
- const htmlToken = new state.Token('html_block', '', 0)
237
- htmlToken.content = html
238
-
239
- // 用单个 html_token 替换 open..close 区间
240
- tokens.splice(i, j - i + 1, htmlToken as any)
241
- }
242
- }
243
-
244
- return true
245
- })
246
- }
247
-
248
- /**
249
- * Swiper 容器配置
250
- */
251
- function configureSwiperContainer(md: MarkdownIt) {
252
- let __tn_swiper_uid = 0
253
-
254
- interface TN_RULES_STACK_ITEM {
255
- image: any
256
- pOpen: any
257
- pClose: any
258
- }
259
- let __tn_rules_stack: Array<TN_RULES_STACK_ITEM> = []
260
-
261
- // 每个文档渲染前重置计数器
262
- md.core.ruler.before('block', 'tn_swiper_reset_uid', () => {
263
- __tn_swiper_uid = 0
264
- __tn_rules_stack = []
265
- return true
266
- })
267
-
268
- md.use(markdownItContainer, 'swiper', {
269
- render: (tokens: any[], idx: number) => {
270
- if (tokens[idx].nesting === 1) {
271
- // 进容器:保存原规则 & 局部覆盖
272
- __tn_rules_stack.push({
273
- image: md.renderer.rules.image,
274
- pOpen: md.renderer.rules.paragraph_open,
275
- pClose: md.renderer.rules.paragraph_close,
276
- })
277
-
278
- md.renderer.rules.paragraph_open = () => ''
279
- md.renderer.rules.paragraph_close = () => ''
280
- md.renderer.rules.image = (tokens: any[], i: number) => {
281
- const token: any = tokens[i]
282
- const src = token.attrGet('src') || ''
283
- const alt = token.content || ''
284
- const title = alt && alt.trim() ? alt : 'img'
285
- return `<div class="swiper-slide" data-title="${esc(
286
- title
287
- )}"><img src="${esc(src)}" alt="${esc(alt)}"></div>`
288
- }
289
-
290
- const id = `tn-swiper-${++__tn_swiper_uid}`
291
- return `
292
- <div class="tn-swiper" data-swiper-id="${id}">
293
- <div class="tn-swiper-tabs"></div>
294
- <div class="swiper-container">
295
- <div class="swiper-wrapper">
296
- `
297
- } else {
298
- // 出容器:恢复原规则并收尾
299
- const prev: TN_RULES_STACK_ITEM = __tn_rules_stack.pop() || {
300
- image: null,
301
- pOpen: null,
302
- pClose: null,
303
- }
304
- md.renderer.rules.image = prev.image
305
- md.renderer.rules.paragraph_open = prev.pOpen
306
- md.renderer.rules.paragraph_close = prev.pClose
307
-
308
- return `
309
- </div>
310
- <!-- 下一页按钮 -->
311
- <!-- <div class="swiper-button-next"></div> -->
312
- <!-- 上一页按钮 -->
313
- <!-- <div class="swiper-button-prev"></div> -->
314
- <!-- 分页导航 -->
315
- <!-- <div class="swiper-pagination"></div> -->
316
- </div>
317
- </div>
318
- `
319
- }
320
- },
321
- })
322
- }
323
-
324
- /**
325
- * Markdown 配置
326
- */
327
- export function getMarkdownConfig(): MarkdownOptions {
328
- const markdown: MarkdownOptions = {
329
- lineNumbers: true,
330
- math: true,
331
- config(md) {
332
- // 添加前置规则保存原始内容
333
- md.core.ruler.before('normalize', 'save-source', (state) => {
334
- state.env.source = state.src
335
- return true
336
- })
337
-
338
- // 添加 Mermaid 支持
339
- simpleMermaidMarkdown(md)
340
-
341
- // 添加 MarkMap 支持
342
- configureMarkMapContainer(md)
343
-
344
- // 添加任务列表支持
345
- md.use(markdownItTaskLists)
346
-
347
- // 添加链接属性支持
348
- md.use(mila, {
349
- attrs: {
350
- target: '_self',
351
- rel: 'noopener',
352
- },
353
- })
354
-
355
- // 添加 Swiper 支持
356
- configureSwiperContainer(md)
357
- },
358
- anchor: {
359
- slugify: generateAnchor,
360
- },
361
- image: {
362
- lazyLoading: true,
363
- },
364
- }
365
-
366
- return markdown
367
- }
1
+ /**
2
+ * .vitepress/config/markdown.config.ts
3
+ *
4
+ * Markdown 配置
5
+ */
6
+ import fs from 'fs'
7
+ import markdownItContainer from 'markdown-it-container'
8
+ import mila from 'markdown-it-link-attributes'
9
+ import markdownItTaskLists from 'markdown-it-task-lists'
10
+ import path from 'path'
11
+
12
+ import { generateAnchor } from '../../utils'
13
+
14
+ import type MarkdownIt from 'markdown-it'
15
+ import type { MarkdownOptions } from 'vitepress'
16
+
17
+ /**
18
+ * 辅助函数:HTML 转义
19
+ */
20
+ function esc(s = '') {
21
+ return s.replace(
22
+ /[&<>"']/g,
23
+ (ch) =>
24
+ ({
25
+ '&': '&amp;',
26
+ '<': '&lt;',
27
+ '>': '&gt;',
28
+ '"': '&quot;',
29
+ "'": '&#39;',
30
+ }[ch]!)
31
+ )
32
+ }
33
+
34
+ /**
35
+ * 简化的 Mermaid 处理函数
36
+ */
37
+ const simpleMermaidMarkdown = (md: MarkdownIt) => {
38
+ const fence = md.renderer.rules.fence
39
+ ? md.renderer.rules.fence.bind(md.renderer.rules)
40
+ : () => ''
41
+
42
+ md.renderer.rules.fence = (tokens, index, options, env, slf) => {
43
+ const token = tokens[index]
44
+
45
+ // 检查是否为 mermaid 代码块
46
+ if (token.info.trim() === 'mermaid') {
47
+ try {
48
+ const key = `mermaid-${Date.now()}-${Math.random()
49
+ .toString(36)
50
+ .substr(2, 9)}`
51
+ const content = token.content
52
+ return `<Mermaid id="${key}" graph="${encodeURIComponent(content)}" />`
53
+ } catch (err) {
54
+ return `<pre>${err}</pre>`
55
+ }
56
+ }
57
+
58
+ // 允许使用 mmd 标记显示 Mermaid 代码本身
59
+ if (token.info.trim() === 'mmd') {
60
+ tokens[index].info = 'mermaid'
61
+ }
62
+
63
+ return fence(tokens, index, options, env, slf)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * MarkMap 容器配置
69
+ */
70
+ function configureMarkMapContainer(md: MarkdownIt) {
71
+ // 先保留 container 的解析(负责把 ```markmap ``` 识别成 container tokens)
72
+ // 但让它本身不输出任何 HTML(render 返回空)
73
+ md.use(markdownItContainer, 'markmap', {
74
+ marker: '`',
75
+ validate(params: string) {
76
+ // 接受 "markmap", "markmap{...}" 或 "markmap key=val ..." 等写法
77
+ const p = (params || '').trim()
78
+ return p.startsWith('markmap')
79
+ },
80
+ render() {
81
+ return ''
82
+ },
83
+ })
84
+
85
+ // core 阶段把整个 container 区间替换成一个 html_block(MarkMap 组件标签)
86
+ // 这样渲染时就只输出 <MarkMap ...>,中间的列表 token 已被移除
87
+ md.core.ruler.after('block', 'tn_replace_markmap_container', (state) => {
88
+ const src = state.env.source || ''
89
+ const lines = src.split('\n')
90
+ const tokens = state.tokens
91
+
92
+ for (let i = 0; i < tokens.length; i++) {
93
+ const t = tokens[i]
94
+ if (t.type === 'container_markmap_open') {
95
+ // 找到对应的 close token
96
+ let j = i + 1
97
+ while (
98
+ j < tokens.length &&
99
+ tokens[j].type !== 'container_markmap_close'
100
+ )
101
+ j++
102
+ if (j >= tokens.length) continue // safety
103
+
104
+ // 使用 token.map 提取源文件对应行(open.token.map 存着 container 起止行)
105
+ const open = t
106
+ const startLine = open.map ? open.map[0] + 1 : null
107
+ const endLine = open.map ? open.map[1] - 1 : null
108
+
109
+ // 1) 从开头 fence 行解析参数(支持 `{a=1 b="x"}`、`a=1 b="x"`,并支持单个数字 shorthand)
110
+ const params: { [key: string]: any; initialExpandLevel?: number } = {}
111
+
112
+ if (open.map && typeof open.map[0] === 'number') {
113
+ const openLine = (lines[open.map[0]] || '').trim()
114
+ let paramPart = ''
115
+
116
+ // 优先匹配大括号形式 ```markmap{...}
117
+ const braceMatch = openLine.match(/\{([^}]*)\}/)
118
+ if (braceMatch) {
119
+ paramPart = braceMatch[1].trim()
120
+ } else {
121
+ // 否则尝试去掉前缀 ``` markmap,剩下的作为参数部分
122
+ const after = openLine.replace(/^`+\s*/, '')
123
+ if (after.startsWith('markmap')) {
124
+ paramPart = after.slice('markmap'.length).trim()
125
+ }
126
+ }
127
+
128
+ if (paramPart) {
129
+ // 使用正则按 token 切分:保持用引号包裹的片段为单个 token(支持包含空格)
130
+ const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || []
131
+
132
+ // 如果第一个 token 是纯数字,把它当作 initialExpandLevel
133
+ let startIdx = 0
134
+ if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0] as string)) {
135
+ params.initialExpandLevel = Number(tokenArr[0])
136
+ startIdx = 1
137
+ }
138
+
139
+ // 解析剩余 token 为 key=value(支持 key=val key:val)
140
+ for (let k = startIdx; k < tokenArr.length; k++) {
141
+ const pair = tokenArr[k]
142
+ if (!pair) continue
143
+ const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/)
144
+ if (m) {
145
+ const key = m[1]
146
+ let val = m[3]
147
+
148
+ // 去除外层引号(若存在)
149
+ if (
150
+ (/^".*"$/.test(val) && val.length >= 2) ||
151
+ (/^'.*'$/.test(val) && val.length >= 2)
152
+ ) {
153
+ val = val.slice(1, -1)
154
+ } else if (/^\d+$/.test(val)) {
155
+ // 纯数字转字符串
156
+ val = String(Number(val))
157
+ }
158
+
159
+ params[key] = val
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // 2) 提取内容(支持文件引用语法 `<<< ./path/to/file.md`)
166
+ let content = ''
167
+ if (startLine !== null && endLine !== null) {
168
+ for (let k = startLine; k <= endLine && k < lines.length; k++) {
169
+ content += lines[k] + '\n'
170
+ }
171
+ } else {
172
+ // 回退:如果没有 map 信息,尝试用中间 tokens 拼接文本
173
+ for (let k = i + 1; k < j; k++) {
174
+ content += tokens[k].content || ''
175
+ }
176
+ }
177
+
178
+ // --- 检查第一非空行是否为引用语法 ---
179
+ const firstNonEmptyLine =
180
+ (content || '').split('\n').find((ln) => ln.trim() !== '') || ''
181
+ const refMatch = firstNonEmptyLine.trim().match(/^<<<\s*(.+)$/)
182
+ if (refMatch) {
183
+ // 提取引用路径,支持引号包裹
184
+ const refRaw = refMatch[1].trim().replace(/^['"]|['"]$/g, '')
185
+
186
+ // 尝试同步读取文件内容(兼容常见 Node 环境)
187
+ try {
188
+ // 尝试根据当前 markdown 文件位置解析相对路径
189
+ const env = state.env || {}
190
+ const possibleRel =
191
+ env.relativePath || env.path || env.filePath || env.file || ''
192
+ let refFullPath = refRaw
193
+
194
+ if (!path.isAbsolute(refRaw)) {
195
+ if (possibleRel) {
196
+ // relativePath 视作相对于项目根的路径(例如 'notes/foo/bar.md'),取其目录
197
+ const currentDir = path.dirname(possibleRel)
198
+ // 解析到 process.cwd()
199
+ refFullPath = path.resolve(process.cwd(), currentDir, refRaw)
200
+ } else {
201
+ // 没有相对文件信息,则相对于项目根解析
202
+ refFullPath = path.resolve(process.cwd(), refRaw)
203
+ }
204
+ } else {
205
+ // 绝对路径直接使用(按系统路径)
206
+ refFullPath = refRaw
207
+ }
208
+
209
+ // console.log('refFullPath:', refFullPath)
210
+ const fileContent = fs.readFileSync(refFullPath, 'utf-8')
211
+ content = fileContent
212
+ } catch (err) {
213
+ // 读取失败:将错误写入 content 以便排查(不会让流程直接崩溃)
214
+ const errorMsg = err instanceof Error ? err.message : String(err)
215
+ content = `Failed to load referenced file: ${esc(
216
+ String(refRaw)
217
+ )}\n\nError: ${esc(errorMsg)}`
218
+ }
219
+ }
220
+
221
+ // 3) 构造组件标签并把参数注入为 props
222
+ const encodedContent = encodeURIComponent(content.trim())
223
+ let propsStr = `content="${encodedContent}"`
224
+
225
+ for (const [k, v] of Object.entries(params)) {
226
+ if (typeof v === 'number' || /^\d+$/.test(String(v))) {
227
+ propsStr += ` :${k}="${v}"`
228
+ } else {
229
+ const safe = String(v).replace(/"/g, '&quot;')
230
+ propsStr += ` ${k}="${safe}"`
231
+ }
232
+ }
233
+
234
+ const html = `<MarkMap ${propsStr}></MarkMap>\n`
235
+
236
+ // 创建 html_block token
237
+ const htmlToken = new state.Token('html_block', '', 0)
238
+ htmlToken.content = html
239
+
240
+ // 用单个 html_token 替换 open..close 区间
241
+ tokens.splice(i, j - i + 1, htmlToken as any)
242
+ }
243
+ }
244
+
245
+ return true
246
+ })
247
+ }
248
+
249
+ /**
250
+ * Swiper 容器配置
251
+ */
252
+ function configureSwiperContainer(md: MarkdownIt) {
253
+ let __tn_swiper_uid = 0
254
+
255
+ interface TN_RULES_STACK_ITEM {
256
+ image: any
257
+ pOpen: any
258
+ pClose: any
259
+ }
260
+ let __tn_rules_stack: Array<TN_RULES_STACK_ITEM> = []
261
+
262
+ // 每个文档渲染前重置计数器
263
+ md.core.ruler.before('block', 'tn_swiper_reset_uid', () => {
264
+ __tn_swiper_uid = 0
265
+ __tn_rules_stack = []
266
+ return true
267
+ })
268
+
269
+ md.use(markdownItContainer, 'swiper', {
270
+ render: (tokens: any[], idx: number) => {
271
+ if (tokens[idx].nesting === 1) {
272
+ // 进容器:保存原规则 & 局部覆盖
273
+ __tn_rules_stack.push({
274
+ image: md.renderer.rules.image,
275
+ pOpen: md.renderer.rules.paragraph_open,
276
+ pClose: md.renderer.rules.paragraph_close,
277
+ })
278
+
279
+ md.renderer.rules.paragraph_open = () => ''
280
+ md.renderer.rules.paragraph_close = () => ''
281
+ md.renderer.rules.image = (tokens: any[], i: number) => {
282
+ const token: any = tokens[i]
283
+ const src = token.attrGet('src') || ''
284
+ const alt = token.content || ''
285
+ const title = alt && alt.trim() ? alt : 'img'
286
+ return `<div class="swiper-slide" data-title="${esc(
287
+ title
288
+ )}"><img src="${esc(src)}" alt="${esc(alt)}"></div>`
289
+ }
290
+
291
+ const id = `tn-swiper-${++__tn_swiper_uid}`
292
+ return `
293
+ <div class="tn-swiper" data-swiper-id="${id}">
294
+ <div class="tn-swiper-tabs"></div>
295
+ <div class="swiper-container">
296
+ <div class="swiper-wrapper">
297
+ `
298
+ } else {
299
+ // 出容器:恢复原规则并收尾
300
+ const prev: TN_RULES_STACK_ITEM = __tn_rules_stack.pop() || {
301
+ image: null,
302
+ pOpen: null,
303
+ pClose: null,
304
+ }
305
+ md.renderer.rules.image = prev.image
306
+ md.renderer.rules.paragraph_open = prev.pOpen
307
+ md.renderer.rules.paragraph_close = prev.pClose
308
+
309
+ return `
310
+ </div>
311
+ <!-- 下一页按钮 -->
312
+ <!-- <div class="swiper-button-next"></div> -->
313
+ <!-- 上一页按钮 -->
314
+ <!-- <div class="swiper-button-prev"></div> -->
315
+ <!-- 分页导航 -->
316
+ <!-- <div class="swiper-pagination"></div> -->
317
+ </div>
318
+ </div>
319
+ `
320
+ }
321
+ },
322
+ })
323
+ }
324
+
325
+ /**
326
+ * Markdown 配置
327
+ */
328
+ export function getMarkdownConfig(): MarkdownOptions {
329
+ const markdown: MarkdownOptions = {
330
+ lineNumbers: true,
331
+ math: true,
332
+ config(md) {
333
+ // 添加前置规则保存原始内容
334
+ md.core.ruler.before('normalize', 'save-source', (state) => {
335
+ state.env.source = state.src
336
+ return true
337
+ })
338
+
339
+ // 添加 Mermaid 支持
340
+ simpleMermaidMarkdown(md)
341
+
342
+ // 添加 MarkMap 支持
343
+ configureMarkMapContainer(md)
344
+
345
+ // 添加任务列表支持
346
+ md.use(markdownItTaskLists)
347
+
348
+ // 添加链接属性支持
349
+ md.use(mila, {
350
+ attrs: {
351
+ target: '_self',
352
+ rel: 'noopener',
353
+ },
354
+ })
355
+
356
+ // 添加 Swiper 支持
357
+ configureSwiperContainer(md)
358
+ },
359
+ anchor: {
360
+ slugify: generateAnchor,
361
+ },
362
+ image: {
363
+ lazyLoading: true,
364
+ },
365
+ }
366
+
367
+ return markdown
368
+ }