md2ui 1.0.18 → 1.0.19

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 (74) hide show
  1. package/README.md +3 -55
  2. package/bin/build.js +82 -7
  3. package/bin/md2ui.js +80 -4
  4. package/package.json +23 -9
  5. package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
  6. package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
  7. package/public/docs/02-Markdown/346/270/262/346/237/223/00-/345/237/272/347/241/200/350/257/255/346/263/225.md +86 -0
  8. package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
  9. package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
  10. package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
  11. package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
  12. package/public/docs/02-Markdown/346/270/262/346/237/223/05-/346/225/260/345/255/246/345/205/254/345/274/217.md +47 -0
  13. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/00-/344/270/211/346/240/217/345/270/203/345/261/200.md +33 -0
  14. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/01-/347/233/256/345/275/225/346/240/221/345/257/274/350/210/252.md +43 -0
  15. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/02-/346/226/207/346/241/243/345/244/247/347/272/262.md +51 -0
  16. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/03-/344/270/212/344/270/213/347/257/207/345/257/274/350/210/252.md +29 -0
  17. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/04-/347/253/231/345/206/205/351/223/276/346/216/245.md +39 -0
  18. package/public/docs/04-/346/220/234/347/264/242/345/212/237/350/203/275/00-/345/205/250/346/226/207/346/220/234/347/264/242.md +46 -0
  19. package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/00-/347/274/226/350/276/221/345/231/250/345/237/272/347/241/200.md +65 -0
  20. package/public/docs/05-/347/274/226/350/276/221/345/212/237/350/203/275/01-/350/207/252/345/212/250/344/277/235/345/255/230.md +38 -0
  21. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/00-/351/230/205/350/257/273/350/277/233/345/272/246.md +43 -0
  22. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/01-/345/233/276/347/211/207/346/224/276/345/244/247.md +40 -0
  23. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/02-/350/277/224/345/233/236/351/241/266/351/203/250.md +38 -0
  24. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
  25. package/public/docs/07-/347/247/273/345/212/250/347/253/257/351/200/202/351/205/215/00-/345/223/215/345/272/224/345/274/217/345/270/203/345/261/200.md +37 -0
  26. package/public/docs/08-/346/226/207/346/241/243/347/256/241/347/220/206/00-/346/226/260/345/273/272/344/270/216/345/210/240/351/231/244.md +47 -0
  27. package/public/docs/09-/345/257/274/345/207/272/345/212/237/350/203/275/00-/345/257/274/345/207/272Word.md +77 -0
  28. package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/00-CLI/345/267/245/345/205/267.md +52 -0
  29. package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/01-SSG/351/235/231/346/200/201/346/236/204/345/273/272.md +44 -0
  30. package/public/docs/10-/351/203/250/347/275/262/344/270/216/351/205/215/347/275/256/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +58 -0
  31. package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/00-/344/270/200/347/272/247/346/226/207/346/241/243.md +20 -0
  32. package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/00-/344/272/214/347/272/247/346/226/207/346/241/243.md +13 -0
  33. package/public/docs/11-/345/244/232/347/272/247/347/233/256/345/275/225/346/265/213/350/257/225/01-/345/255/220/347/233/256/345/275/225/01-/346/267/261/345/261/202/345/265/214/345/245/227/00-/344/270/211/347/272/247/346/226/207/346/241/243.md +23 -0
  34. package/src/App.vue +130 -6
  35. package/src/components/AppSidebar.vue +181 -21
  36. package/src/components/CodeBlockNodeView.vue +72 -0
  37. package/src/components/DocContent.vue +25 -14
  38. package/src/components/EditorContent.vue +257 -0
  39. package/src/components/EditorToolbar.vue +264 -0
  40. package/src/components/ImageZoom.vue +199 -2
  41. package/src/components/MathBlockNodeView.vue +160 -0
  42. package/src/components/MathInlineNodeView.vue +145 -0
  43. package/src/components/MermaidNodeView.vue +149 -0
  44. package/src/components/TableBubbleMenu.vue +177 -0
  45. package/src/components/TableOfContents.vue +138 -32
  46. package/src/components/TopBar.vue +69 -4
  47. package/src/components/TreeNode.vue +232 -39
  48. package/src/components/WelcomePage.vue +2 -2
  49. package/src/composables/useDocHash.js +9 -1
  50. package/src/composables/useDocManager.js +325 -68
  51. package/src/composables/useDocTree.js +56 -1
  52. package/src/composables/useExportPdf.js +102 -0
  53. package/src/composables/useExportWord.js +73 -10
  54. package/src/composables/useFileWatcher.js +45 -0
  55. package/src/composables/useFrontmatter.js +2 -2
  56. package/src/composables/useMarkdown.js +529 -42
  57. package/src/composables/useScroll.js +47 -5
  58. package/src/config.js +1 -1
  59. package/src/extensions/CodeBlockCustom.js +113 -0
  60. package/src/extensions/MathBlock.js +107 -0
  61. package/src/extensions/MathInline.js +100 -0
  62. package/src/extensions/MermaidBlock.js +73 -0
  63. package/src/extensions/TableControls.js +670 -0
  64. package/src/services/DocService.js +184 -0
  65. package/src/style.css +2194 -39
  66. package/vite-plugin-doc-api.js +368 -0
  67. package/vite.config.js +2 -1
  68. package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
  69. package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/01-/347/233/256/345/275/225/347/273/223/346/236/204.md +0 -55
  70. package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/02-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md +0 -63
  71. package/public/docs/03-/350/277/233/351/230/266/346/214/207/345/215/227/03-/351/203/250/347/275/262/346/226/271/346/241/210.md +0 -73
  72. package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
  73. package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
  74. package/src/api/docs.js +0 -106
@@ -5,8 +5,16 @@ const scrollProgress = ref(0)
5
5
  const showBackToTop = ref(false)
6
6
  const activeHeading = ref('')
7
7
 
8
+ // 外部注入的 tocItems 引用,用于编辑模式下通过文本匹配标题
9
+ let _tocItemsRef = null
10
+
8
11
  export function useScroll() {
9
12
 
13
+ // 注入 tocItems 引用(由 useDocManager 调用一次)
14
+ function setTocItems(tocItems) {
15
+ _tocItemsRef = tocItems
16
+ }
17
+
10
18
  // 监听滚动
11
19
  function handleScroll(e) {
12
20
  const element = e.target
@@ -21,12 +29,26 @@ export function useScroll() {
21
29
  updateActiveHeading()
22
30
  }
23
31
 
32
+ // 提取标题纯文本(去掉锚点图标等子元素)
33
+ function getHeadingText(heading) {
34
+ const clone = heading.cloneNode(true)
35
+ clone.querySelectorAll('.heading-anchor').forEach(a => a.remove())
36
+ return clone.textContent.trim()
37
+ }
38
+
39
+ // 通过文本匹配在 tocItems 中查找对应 id(编辑模式下标题无 id 时使用)
40
+ function findTocIdByText(text) {
41
+ if (!_tocItemsRef || !_tocItemsRef.value) return ''
42
+ const item = _tocItemsRef.value.find(t => t.text === text)
43
+ return item ? item.id : ''
44
+ }
45
+
24
46
  // 更新当前激活的标题
25
47
  function updateActiveHeading() {
26
48
  const content = document.querySelector('.content')
27
49
  if (!content) return
28
50
 
29
- const headings = document.querySelectorAll('.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6')
51
+ const headings = content.querySelectorAll('.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6')
30
52
  const scrollTop = content.scrollTop
31
53
 
32
54
  let currentId = ''
@@ -36,19 +58,38 @@ export function useScroll() {
36
58
  const offsetTop = rect.top - contentRect.top + scrollTop
37
59
 
38
60
  if (offsetTop <= scrollTop + 100) {
39
- currentId = heading.id
61
+ // 优先用 id,没有 id 时通过文本匹配 tocItems
62
+ currentId = heading.id || findTocIdByText(getHeadingText(heading))
40
63
  }
41
64
  })
42
65
 
43
66
  activeHeading.value = currentId
44
67
  }
45
68
 
46
- // 滚动到指定标题
69
+ // 滚动到指定标题(支持编辑模式下通过文本匹配定位)
47
70
  function scrollToHeading(id) {
48
- const el = document.getElementById(id)
71
+ // 优先通过 id 定位
72
+ let el = document.getElementById(id)
49
73
  if (el) {
50
74
  el.scrollIntoView({ behavior: 'smooth', block: 'start' })
51
75
  activeHeading.value = id
76
+ return
77
+ }
78
+ // 编辑模式下标题无 id,通过 tocItems 找到文本再匹配 DOM
79
+ if (_tocItemsRef && _tocItemsRef.value) {
80
+ const tocItem = _tocItemsRef.value.find(t => t.id === id)
81
+ if (tocItem) {
82
+ const content = document.querySelector('.content')
83
+ if (!content) return
84
+ const headings = content.querySelectorAll('.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6')
85
+ for (const heading of headings) {
86
+ if (getHeadingText(heading) === tocItem.text) {
87
+ heading.scrollIntoView({ behavior: 'smooth', block: 'start' })
88
+ activeHeading.value = id
89
+ return
90
+ }
91
+ }
92
+ }
52
93
  }
53
94
  }
54
95
 
@@ -66,6 +107,7 @@ export function useScroll() {
66
107
  activeHeading,
67
108
  handleScroll,
68
109
  scrollToHeading,
69
- scrollToTop
110
+ scrollToTop,
111
+ setTocItems
70
112
  }
71
113
  }
package/src/config.js CHANGED
@@ -1,4 +1,4 @@
1
- // 共享配置 - bin/md2ui.js 和 src/api/docs.js 共用
1
+ // 共享配置 - vite.config.js 和 DocService.js 共用
2
2
  export const config = {
3
3
  // 默认端口
4
4
  defaultPort: 3000,
@@ -0,0 +1,113 @@
1
+ import { Node, mergeAttributes, VueNodeViewRenderer, textblockTypeInputRule } from '@tiptap/vue-3'
2
+ import CodeBlockNodeView from '../components/CodeBlockNodeView.vue'
3
+
4
+ /**
5
+ * 自定义代码块扩展
6
+ * 编辑模式下渲染带 header(语言标签 + 复制按钮)的代码块,与查看模式风格一致
7
+ */
8
+ export const CodeBlockCustom = Node.create({
9
+ name: 'codeBlock',
10
+ group: 'block',
11
+ content: 'text*',
12
+ marks: '',
13
+ defining: true,
14
+ isolating: true,
15
+ code: true,
16
+
17
+ addAttributes() {
18
+ return {
19
+ language: {
20
+ default: null,
21
+ parseHTML: (element) => {
22
+ const code = element.querySelector('code')
23
+ if (!code) return null
24
+ // 从 class="language-xxx" 中提取语言
25
+ const cls = [...code.classList].find(c => c.startsWith('language-'))
26
+ return cls ? cls.replace('language-', '') : null
27
+ },
28
+ },
29
+ }
30
+ },
31
+
32
+ parseHTML() {
33
+ return [
34
+ {
35
+ tag: 'pre',
36
+ preserveWhitespace: 'full',
37
+ getAttrs: (node) => {
38
+ const code = node.querySelector('code')
39
+ if (!code) return {}
40
+ // 排除 mermaid,由 MermaidBlock 处理
41
+ if (code.classList.contains('language-mermaid')) return false
42
+ return {}
43
+ },
44
+ },
45
+ ]
46
+ },
47
+
48
+ renderHTML({ node, HTMLAttributes }) {
49
+ const lang = node.attrs.language
50
+ return [
51
+ 'pre',
52
+ mergeAttributes(HTMLAttributes),
53
+ ['code', { class: lang ? `language-${lang}` : null }, 0],
54
+ ]
55
+ },
56
+
57
+ addNodeView() {
58
+ return VueNodeViewRenderer(CodeBlockNodeView)
59
+ },
60
+
61
+ addKeyboardShortcuts() {
62
+ return {
63
+ 'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock(),
64
+ // 在代码块内按 Enter 保持在代码块中
65
+ 'Enter': ({ editor }) => {
66
+ if (!editor.isActive('codeBlock')) return false
67
+ return editor.commands.newlineInCode()
68
+ },
69
+ // 在代码块末尾按 Mod-Enter 跳出代码块
70
+ 'Mod-Enter': ({ editor }) => {
71
+ if (!editor.isActive('codeBlock')) return false
72
+ return editor.commands.exitCode()
73
+ },
74
+ // Backspace 在空代码块时退出
75
+ 'Backspace': ({ editor }) => {
76
+ const { $anchor } = editor.state.selection
77
+ if (!editor.isActive('codeBlock')) return false
78
+ if ($anchor.parent.textContent.length > 0) return false
79
+ return editor.commands.clearNodes()
80
+ },
81
+ }
82
+ },
83
+
84
+ // 输入规则:``` 触发代码块
85
+ addInputRules() {
86
+ return [
87
+ textblockTypeInputRule({
88
+ find: /^```([a-z]*)?[\s\n]$/,
89
+ type: this.type,
90
+ getAttributes: (match) => ({
91
+ language: match[1] || null,
92
+ }),
93
+ }),
94
+ ]
95
+ },
96
+
97
+ // tiptap-markdown 序列化规则
98
+ addStorage() {
99
+ return {
100
+ markdown: {
101
+ serialize(state, node) {
102
+ const lang = node.attrs.language || ''
103
+ state.write('```' + lang + '\n')
104
+ state.text(node.textContent, false)
105
+ state.ensureNewLine()
106
+ state.write('```')
107
+ state.closeBlock(node)
108
+ },
109
+ parse: {},
110
+ },
111
+ }
112
+ },
113
+ })
@@ -0,0 +1,107 @@
1
+ import { Node, mergeAttributes, VueNodeViewRenderer } from '@tiptap/vue-3'
2
+ import MathBlockNodeView from '../components/MathBlockNodeView.vue'
3
+
4
+ // HTML 属性转义
5
+ function escapeAttr(str) {
6
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
7
+ }
8
+
9
+ /**
10
+ * 块级数学公式扩展($$...$$)
11
+ * 编辑模式下渲染为可交互的 NodeView
12
+ */
13
+ export const MathBlock = Node.create({
14
+ name: 'mathBlock',
15
+ group: 'block',
16
+ atom: true,
17
+
18
+ addAttributes() {
19
+ return {
20
+ latex: { default: '' },
21
+ }
22
+ },
23
+
24
+ parseHTML() {
25
+ return [
26
+ {
27
+ tag: 'div.math-block',
28
+ getAttrs: (node) => {
29
+ const latex = node.getAttribute('data-latex') || node.textContent.trim()
30
+ return { latex }
31
+ },
32
+ },
33
+ ]
34
+ },
35
+
36
+ renderHTML({ node, HTMLAttributes }) {
37
+ return ['div', mergeAttributes(HTMLAttributes, {
38
+ class: 'math-block',
39
+ 'data-latex': node.attrs.latex,
40
+ })]
41
+ },
42
+
43
+ addNodeView() {
44
+ return VueNodeViewRenderer(MathBlockNodeView)
45
+ },
46
+
47
+ addKeyboardShortcuts() {
48
+ return {
49
+ 'Backspace': ({ editor }) => {
50
+ if (!editor.isActive('mathBlock')) return false
51
+ return editor.commands.deleteSelection()
52
+ },
53
+ }
54
+ },
55
+
56
+ addStorage() {
57
+ return {
58
+ markdown: {
59
+ serialize(state, node) {
60
+ state.write('$$\n')
61
+ state.text(node.attrs.latex || '', false)
62
+ state.ensureNewLine()
63
+ state.write('$$')
64
+ state.closeBlock(node)
65
+ },
66
+ parse: {
67
+ // 给 markdown-it 注册块级公式规则
68
+ setup(md) {
69
+ md.block.ruler.before('fence', 'math_block', (state, startLine, endLine, silent) => {
70
+ const startPos = state.bMarks[startLine] + state.tShift[startLine]
71
+ const maxPos = state.eMarks[startLine]
72
+ if (startPos + 2 > maxPos) return false
73
+ const marker = state.src.slice(startPos, startPos + 2)
74
+ if (marker !== '$$') return false
75
+ if (silent) return true
76
+ let nextLine = startLine + 1
77
+ let found = false
78
+ while (nextLine < endLine) {
79
+ const nPos = state.bMarks[nextLine] + state.tShift[nextLine]
80
+ const nMax = state.eMarks[nextLine]
81
+ if (nPos < nMax && state.src.slice(nPos, nPos + 2) === '$$') {
82
+ found = true
83
+ break
84
+ }
85
+ nextLine++
86
+ }
87
+ if (!found) return false
88
+ // 提取 $$ 之间的内容
89
+ const contentStart = state.bMarks[startLine + 1]
90
+ const contentEnd = state.eMarks[nextLine - 1]
91
+ const latex = state.src.slice(contentStart, contentEnd).trim()
92
+ const token = state.push('math_block', 'div', 0)
93
+ token.content = latex
94
+ token.map = [startLine, nextLine + 1]
95
+ state.line = nextLine + 1
96
+ return true
97
+ })
98
+ md.renderer.rules.math_block = (tokens, idx) => {
99
+ const latex = tokens[idx].content
100
+ return '<div class="math-block" data-latex="' + escapeAttr(latex) + '"></div>'
101
+ }
102
+ },
103
+ },
104
+ },
105
+ }
106
+ },
107
+ })
@@ -0,0 +1,100 @@
1
+ import { Node, mergeAttributes, VueNodeViewRenderer } from '@tiptap/vue-3'
2
+ import MathInlineNodeView from '../components/MathInlineNodeView.vue'
3
+
4
+ // HTML 属性转义
5
+ function escapeAttr(str) {
6
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
7
+ }
8
+
9
+ /**
10
+ * 行内数学公式扩展($...$)
11
+ * 编辑模式下渲染为 inline NodeView
12
+ */
13
+ export const MathInline = Node.create({
14
+ name: 'mathInline',
15
+ group: 'inline',
16
+ inline: true,
17
+ atom: true,
18
+
19
+ addAttributes() {
20
+ return {
21
+ latex: { default: '' },
22
+ }
23
+ },
24
+
25
+ parseHTML() {
26
+ return [
27
+ {
28
+ tag: 'span.math-inline-node',
29
+ getAttrs: (node) => {
30
+ const latex = node.getAttribute('data-latex') || node.textContent.trim()
31
+ return latex ? { latex } : false
32
+ },
33
+ },
34
+ ]
35
+ },
36
+
37
+ renderHTML({ node, HTMLAttributes }) {
38
+ return ['span', mergeAttributes(HTMLAttributes, {
39
+ class: 'math-inline-node',
40
+ 'data-latex': node.attrs.latex,
41
+ }), node.attrs.latex]
42
+ },
43
+
44
+ addNodeView() {
45
+ return VueNodeViewRenderer(MathInlineNodeView)
46
+ },
47
+
48
+ addInputRules() {
49
+ return [
50
+ {
51
+ find: /(?<!\$)\$([^$\n]+)\$$/,
52
+ handler: ({ state, range, match }) => {
53
+ const latex = match[1]
54
+ if (!latex || !latex.trim()) return
55
+ const node = state.schema.nodes.mathInline.create({ latex: latex.trim() })
56
+ const tr = state.tr.replaceWith(range.from, range.to, node)
57
+ tr.insertText(' ', range.from + 1)
58
+ return tr
59
+ },
60
+ },
61
+ ]
62
+ },
63
+
64
+ addStorage() {
65
+ return {
66
+ markdown: {
67
+ serialize(state, node) {
68
+ state.write('$' + (node.attrs.latex || '') + '$')
69
+ },
70
+ parse: {
71
+ // 给 markdown-it 注册行内公式规则
72
+ setup(md) {
73
+ md.inline.ruler.after('escape', 'math_inline', (state, silent) => {
74
+ if (state.src[state.pos] !== '$') return false
75
+ if (state.src[state.pos + 1] === '$') return false // 跳过 $$
76
+ const start = state.pos + 1
77
+ let end = start
78
+ while (end < state.posMax) {
79
+ if (state.src[end] === '$' && state.src[end - 1] !== '\\') break
80
+ end++
81
+ }
82
+ if (end >= state.posMax) return false
83
+ if (end === start) return false // 空公式
84
+ if (silent) return true
85
+ const latex = state.src.slice(start, end).trim()
86
+ const token = state.push('math_inline', 'span', 0)
87
+ token.content = latex
88
+ state.pos = end + 1
89
+ return true
90
+ })
91
+ md.renderer.rules.math_inline = (tokens, idx) => {
92
+ const latex = tokens[idx].content
93
+ return '<span class="math-inline-node" data-latex="' + escapeAttr(latex) + '">' + escapeAttr(latex) + '</span>'
94
+ }
95
+ },
96
+ },
97
+ },
98
+ }
99
+ },
100
+ })
@@ -0,0 +1,73 @@
1
+ import { Node, mergeAttributes, VueNodeViewRenderer } from '@tiptap/vue-3'
2
+ import MermaidNodeView from '../components/MermaidNodeView.vue'
3
+
4
+ /**
5
+ * Tiptap Mermaid 扩展
6
+ * 编辑模式下将 mermaid 代码块渲染为图表预览,右上角提供编辑按钮
7
+ */
8
+ export const MermaidBlock = Node.create({
9
+ name: 'mermaidBlock',
10
+ group: 'block',
11
+ content: 'text*',
12
+ marks: '',
13
+ defining: true,
14
+ isolating: true,
15
+ code: true,
16
+
17
+ addAttributes() {
18
+ return {
19
+ language: {
20
+ default: 'mermaid',
21
+ },
22
+ }
23
+ },
24
+
25
+ parseHTML() {
26
+ return [
27
+ {
28
+ tag: 'pre',
29
+ preserveWhitespace: 'full',
30
+ getAttrs: (node) => {
31
+ const code = node.querySelector('code')
32
+ if (!code) return false
33
+ const isMermaid = code.classList.contains('language-mermaid')
34
+ return isMermaid ? {} : false
35
+ },
36
+ // 优先级高于默认 codeBlock
37
+ priority: 60,
38
+ },
39
+ ]
40
+ },
41
+
42
+ renderHTML({ HTMLAttributes }) {
43
+ return [
44
+ 'pre',
45
+ mergeAttributes(HTMLAttributes),
46
+ ['code', { class: 'language-mermaid' }, 0],
47
+ ]
48
+ },
49
+
50
+ addNodeView() {
51
+ return VueNodeViewRenderer(MermaidNodeView)
52
+ },
53
+
54
+ // tiptap-markdown 序列化/解析规则
55
+ addStorage() {
56
+ return {
57
+ markdown: {
58
+ serialize(state, node) {
59
+ state.write('```mermaid\n')
60
+ state.text(node.textContent, false)
61
+ state.ensureNewLine()
62
+ state.write('```')
63
+ state.closeBlock(node)
64
+ },
65
+ parse: {
66
+ // 不需要额外的 parse 配置,
67
+ // Markdown 解析后会生成 <pre><code class="language-mermaid"> 结构,
68
+ // 由 parseHTML 规则匹配
69
+ },
70
+ },
71
+ }
72
+ },
73
+ })