md2ui 1.0.16 → 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.
- package/README.md +3 -55
- package/bin/build.js +82 -7
- package/bin/md2ui.js +80 -4
- package/package.json +23 -9
- package/public/docs/00-/345/277/253/351/200/237/345/274/200/345/247/213.md +48 -28
- package/public/docs/01-/345/212/237/350/203/275/347/211/271/346/200/247.md +55 -40
- 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
- package/public/docs/02-Markdown/346/270/262/346/237/223/01-/344/273/243/347/240/201/345/235/227.md +91 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/02-/350/241/250/346/240/274.md +187 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/03-Mermaid/345/233/276/350/241/250.md +101 -0
- package/public/docs/02-Markdown/346/270/262/346/237/223/04-Frontmatter.md +32 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/src/App.vue +130 -6
- package/src/components/AppSidebar.vue +181 -21
- package/src/components/CodeBlockNodeView.vue +72 -0
- package/src/components/DocContent.vue +25 -14
- package/src/components/EditorContent.vue +257 -0
- package/src/components/EditorToolbar.vue +264 -0
- package/src/components/ImageZoom.vue +199 -2
- package/src/components/MathBlockNodeView.vue +160 -0
- package/src/components/MathInlineNodeView.vue +145 -0
- package/src/components/MermaidNodeView.vue +149 -0
- package/src/components/TableBubbleMenu.vue +177 -0
- package/src/components/TableOfContents.vue +138 -32
- package/src/components/TopBar.vue +69 -4
- package/src/components/TreeNode.vue +232 -39
- package/src/components/WelcomePage.vue +2 -2
- package/src/composables/useDocHash.js +9 -1
- package/src/composables/useDocManager.js +325 -68
- package/src/composables/useDocTree.js +56 -1
- package/src/composables/useExportPdf.js +102 -0
- package/src/composables/useExportWord.js +73 -10
- package/src/composables/useFileWatcher.js +45 -0
- package/src/composables/useFrontmatter.js +2 -2
- package/src/composables/useMarkdown.js +529 -42
- package/src/composables/useScroll.js +47 -5
- package/src/config.js +1 -1
- package/src/extensions/CodeBlockCustom.js +113 -0
- package/src/extensions/MathBlock.js +107 -0
- package/src/extensions/MathInline.js +100 -0
- package/src/extensions/MermaidBlock.js +73 -0
- package/src/extensions/TableControls.js +670 -0
- package/src/services/DocService.js +184 -0
- package/src/style.css +2194 -39
- package/vite-plugin-doc-api.js +368 -0
- package/vite.config.js +2 -1
- package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
- 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
- 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
- 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
- package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
- package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')
|
|
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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')
|
|
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
|
+
})
|