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
@@ -0,0 +1,102 @@
1
+ import { ref } from 'vue'
2
+
3
+ /**
4
+ * PDF 导出 — 基于浏览器 window.print() 实现
5
+ * 通过创建独立的打印窗口,只包含文档内容,生成干净的 PDF
6
+ */
7
+ export function useExportPdf() {
8
+ const exportingPdf = ref(false)
9
+
10
+ async function exportToPdf(title = '文档') {
11
+ exportingPdf.value = true
12
+ try {
13
+ const contentEl = document.querySelector('.markdown-content')
14
+ if (!contentEl) return
15
+
16
+ // 克隆内容,移除不需要打印的元素
17
+ const clone = contentEl.cloneNode(true)
18
+ clone.querySelectorAll('.code-block-actions').forEach(el => el.remove())
19
+ clone.querySelectorAll('.image-copy-btn, .mermaid-copy-btn').forEach(el => el.remove())
20
+ clone.querySelectorAll('.table-toolbar').forEach(el => el.remove())
21
+
22
+ // 收集页面中的样式
23
+ const styles = []
24
+ for (const sheet of document.styleSheets) {
25
+ try {
26
+ for (const rule of sheet.cssRules) {
27
+ styles.push(rule.cssText)
28
+ }
29
+ } catch {
30
+ // 跨域样式表忽略
31
+ }
32
+ }
33
+
34
+ // 创建打印窗口
35
+ const printWindow = window.open('', '_blank')
36
+ if (!printWindow) {
37
+ alert('请允许弹出窗口以导出 PDF')
38
+ return
39
+ }
40
+
41
+ const safeTitle = title.replace(/[<>&"]/g, c =>
42
+ ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c]))
43
+
44
+ printWindow.document.write(`<!DOCTYPE html>
45
+ <html lang="zh-CN">
46
+ <head>
47
+ <meta charset="UTF-8">
48
+ <title>${safeTitle}</title>
49
+ <style>
50
+ ${styles.join('\n')}
51
+ body {
52
+ margin: 0;
53
+ padding: 20px 40px;
54
+ background: #fff;
55
+ color: #1a1e1b;
56
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
57
+ font-size: 14px;
58
+ line-height: 1.6;
59
+ }
60
+ .markdown-content {
61
+ max-width: 100%;
62
+ padding: 0;
63
+ margin: 0;
64
+ }
65
+ .code-block-wrapper { break-inside: avoid; border: 1px solid #ddd; }
66
+ .code-block-header { background: #f5f5f5; }
67
+ table { break-inside: avoid; }
68
+ .mermaid { break-inside: avoid; }
69
+ .heading-anchor { display: none !important; }
70
+ @media print {
71
+ a[href^="http"]::after {
72
+ content: " (" attr(href) ")";
73
+ font-size: 0.85em;
74
+ color: #666;
75
+ word-break: break-all;
76
+ }
77
+ a[data-doc-key]::after { content: none; }
78
+ }
79
+ </style>
80
+ </head>
81
+ <body>
82
+ <article class="markdown-content">${clone.innerHTML}</article>
83
+ </body>
84
+ </html>`)
85
+ printWindow.document.close()
86
+
87
+ // 等待加载完成后触发打印
88
+ printWindow.onload = () => {
89
+ setTimeout(() => {
90
+ printWindow.print()
91
+ printWindow.onafterprint = () => printWindow.close()
92
+ }, 500)
93
+ }
94
+ } catch (err) {
95
+ console.error('导出 PDF 失败:', err)
96
+ } finally {
97
+ exportingPdf.value = false
98
+ }
99
+ }
100
+
101
+ return { exportingPdf, exportToPdf }
102
+ }
@@ -3,10 +3,26 @@ import {
3
3
  Document, Packer, Paragraph, TextRun, HeadingLevel,
4
4
  Table, TableRow, TableCell, WidthType, BorderStyle,
5
5
  ImageRun, AlignmentType, ShadingType,
6
- ExternalHyperlink,
6
+ ExternalHyperlink, TableLayoutType,
7
7
  convertInchesToTwip
8
8
  } from 'docx'
9
- import { saveAs } from 'file-saver'
9
+ // 使用原生方式下载文件,兼容 Chrome / Safari / Firefox
10
+ // 使用原生方式下载文件,兼容 Chrome / Safari / Firefox
11
+ function downloadBlob(blob, fileName) {
12
+ // 将 Blob 包装为 File 对象,Chrome 会从 File.name 获取文件名
13
+ const file = new File([blob], fileName, { type: blob.type })
14
+ const url = URL.createObjectURL(file)
15
+ const a = document.createElement('a')
16
+ a.href = url
17
+ a.download = fileName
18
+ a.style.display = 'none'
19
+ document.body.appendChild(a)
20
+ a.click()
21
+ setTimeout(() => {
22
+ document.body.removeChild(a)
23
+ URL.revokeObjectURL(url)
24
+ }, 200)
25
+ }
10
26
 
11
27
  // ---- SVG 转 PNG ----
12
28
 
@@ -160,16 +176,35 @@ function extractTextRuns(node, inherited = {}) {
160
176
  return runs
161
177
  }
162
178
 
163
- // 解析表格
179
+ // 解析表格(兼容 Office + WPS)
164
180
  function parseTable(tableEl) {
181
+ // A4 内容区宽度(缇),左右页边距各 1.2 英寸,纸宽 8.27 英寸
182
+ const TABLE_WIDTH_DXA = 9024
165
183
  const rows = []
166
184
  const allTr = tableEl.querySelectorAll('tr')
185
+
186
+ // 1. 扫描最大列数(考虑 colspan)
187
+ let maxCols = 0
188
+ for (const tr of allTr) {
189
+ let colCount = 0
190
+ for (const td of tr.querySelectorAll('th, td')) {
191
+ colCount += parseInt(td.getAttribute('colspan') || '1', 10)
192
+ }
193
+ maxCols = Math.max(maxCols, colCount)
194
+ }
195
+ if (maxCols === 0) return null
196
+
197
+ // 2. 每列均分宽度
198
+ const colWidth = Math.floor(TABLE_WIDTH_DXA / maxCols)
199
+
200
+ // 3. 构建行(处理 colspan)
167
201
  for (const tr of allTr) {
168
202
  const cells = []
169
203
  const tds = tr.querySelectorAll('th, td')
170
204
  const isHeader = tr.querySelector('th') !== null
171
205
  for (const td of tds) {
172
- cells.push(new TableCell({
206
+ const colspan = parseInt(td.getAttribute('colspan') || '1', 10)
207
+ const cellOpts = {
173
208
  children: [new Paragraph({
174
209
  children: extractTextRuns(td, isHeader ? { bold: true } : {}),
175
210
  spacing: { before: 40, after: 40 },
@@ -177,17 +212,23 @@ function parseTable(tableEl) {
177
212
  shading: isHeader
178
213
  ? { type: ShadingType.CLEAR, fill: 'f0f0f0' }
179
214
  : undefined,
180
- width: { size: 100 / tds.length, type: WidthType.PERCENTAGE },
181
- }))
215
+ width: { size: colWidth * colspan, type: WidthType.DXA },
216
+ }
217
+ if (colspan > 1) cellOpts.columnSpan = colspan
218
+ cells.push(new TableCell(cellOpts))
182
219
  }
183
220
  if (cells.length > 0) {
184
221
  rows.push(new TableRow({ children: cells }))
185
222
  }
186
223
  }
187
224
  if (rows.length === 0) return null
225
+
226
+ // 4. FIXED 布局 + columnWidths(tblGrid),WPS 严格依赖这两项
188
227
  return new Table({
189
228
  rows,
190
- width: { size: 100, type: WidthType.PERCENTAGE },
229
+ width: { size: TABLE_WIDTH_DXA, type: WidthType.DXA },
230
+ layout: TableLayoutType.FIXED,
231
+ columnWidths: Array(maxCols).fill(colWidth),
191
232
  })
192
233
  }
193
234
 
@@ -410,8 +451,8 @@ async function parseDomToDocx(contentEl) {
410
451
  continue
411
452
  }
412
453
 
413
- // 表格(可能被 table-wrapper 包裹)
414
- if (tag === 'TABLE' || node.classList?.contains('table-wrapper')) {
454
+ // 表格(可能被 table-outer 或 table-wrapper 包裹)
455
+ if (tag === 'TABLE' || node.classList?.contains('table-wrapper') || node.classList?.contains('table-outer')) {
415
456
  const tableEl = tag === 'TABLE' ? node : node.querySelector('table')
416
457
  if (tableEl) {
417
458
  const table = parseTable(tableEl)
@@ -601,6 +642,24 @@ export function useExportWord() {
601
642
  alignment: AlignmentType.LEFT,
602
643
  },
603
644
  },
645
+ heading1: {
646
+ run: { font: 'Microsoft YaHei', size: 36, bold: true, color: '333333' },
647
+ },
648
+ heading2: {
649
+ run: { font: 'Microsoft YaHei', size: 32, bold: true, color: '333333' },
650
+ },
651
+ heading3: {
652
+ run: { font: 'Microsoft YaHei', size: 28, bold: true, color: '333333' },
653
+ },
654
+ heading4: {
655
+ run: { font: 'Microsoft YaHei', size: 26, bold: true, color: '333333' },
656
+ },
657
+ heading5: {
658
+ run: { font: 'Microsoft YaHei', size: 24, bold: true, color: '333333' },
659
+ },
660
+ heading6: {
661
+ run: { font: 'Microsoft YaHei', size: 22, bold: true, color: '333333' },
662
+ },
604
663
  },
605
664
  },
606
665
  sections: [{
@@ -619,8 +678,12 @@ export function useExportWord() {
619
678
  })
620
679
 
621
680
  const blob = await Packer.toBlob(doc)
681
+ // 确保 blob 的 MIME type 正确
682
+ const wordBlob = new Blob([blob], {
683
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
684
+ })
622
685
  const fileName = title.replace(/[\\/:*?"<>|]/g, '_') + '.docx'
623
- saveAs(blob, fileName)
686
+ downloadBlob(wordBlob, fileName)
624
687
  } catch (err) {
625
688
  console.error('导出 Word 失败:', err)
626
689
  } finally {
@@ -0,0 +1,45 @@
1
+ import { ref, onMounted, onBeforeUnmount } from 'vue'
2
+ import { pollDocsList, pollDocContent } from '../services/DocService.js'
3
+
4
+ /**
5
+ * 轮询监听文件变更
6
+ * 通过 DocService 统一处理模式检测和 ETag,这里只管定时调用
7
+ */
8
+ export function useFileWatcher({ getCurrentDocPath, onDocsListChange, onDocContentChange }) {
9
+ const watching = ref(false)
10
+ let timer = null
11
+ let polling = false
12
+
13
+ async function poll() {
14
+ if (polling) return
15
+ polling = true
16
+ try {
17
+ // 轮询文档列表
18
+ const newTree = await pollDocsList()
19
+ if (newTree) onDocsListChange?.(newTree)
20
+
21
+ // 轮询当前文档内容
22
+ const docPath = getCurrentDocPath?.()
23
+ const content = await pollDocContent(docPath)
24
+ if (content !== null) onDocContentChange?.(content)
25
+ } finally {
26
+ polling = false
27
+ }
28
+ }
29
+
30
+ function start() {
31
+ if (watching.value) return
32
+ watching.value = true
33
+ timer = setInterval(poll, 500)
34
+ }
35
+
36
+ function stop() {
37
+ watching.value = false
38
+ if (timer) { clearInterval(timer); timer = null }
39
+ }
40
+
41
+ onMounted(start)
42
+ onBeforeUnmount(stop)
43
+
44
+ return { watching, start, stop }
45
+ }
@@ -4,7 +4,7 @@
4
4
  // 支持 title / description / order / hidden 字段
5
5
  export function parseFrontmatter(markdown) {
6
6
  const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
7
- if (!match) return { data: {}, content: markdown }
7
+ if (!match) return { data: {}, content: markdown, rawYaml: '' }
8
8
  const yamlStr = match[1]
9
9
  const data = {}
10
10
  for (const line of yamlStr.split('\n')) {
@@ -20,7 +20,7 @@ export function parseFrontmatter(markdown) {
20
20
  else val = val.replace(/^['"]|['"]$/g, '')
21
21
  data[m[1]] = val
22
22
  }
23
- return { data, content: markdown.slice(match[0].length) }
23
+ return { data, content: markdown.slice(match[0].length), rawYaml: yamlStr }
24
24
  }
25
25
 
26
26
  // 计算阅读时间(中文 400 字/分钟,英文 200 词/分钟)