md2ui 1.0.18 → 1.0.20

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 (80) hide show
  1. package/README.md +51 -58
  2. package/bin/build.js +95 -9
  3. package/bin/md2ui.js +102 -13
  4. package/package.json +24 -10
  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 +88 -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/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
  14. package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. package/public/docs/06-/351/230/205/350/257/273/344/275/223/351/252/214/assets/img-1777261394722.png +0 -0
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. package/src/App.vue +111 -12
  38. package/src/components/AppSidebar.vue +181 -21
  39. package/src/components/CodeBlockNodeView.vue +72 -0
  40. package/src/components/DocContent.vue +25 -14
  41. package/src/components/EditorContent.vue +257 -0
  42. package/src/components/EditorToolbar.vue +264 -0
  43. package/src/components/ImageZoom.vue +88 -5
  44. package/src/components/MathBlockNodeView.vue +160 -0
  45. package/src/components/MathInlineNodeView.vue +145 -0
  46. package/src/components/MermaidNodeView.vue +157 -0
  47. package/src/components/MobileSearch.vue +97 -0
  48. package/src/components/TableOfContents.vue +174 -32
  49. package/src/components/TopBar.vue +69 -4
  50. package/src/components/TreeNode.vue +232 -39
  51. package/src/components/WelcomePage.vue +2 -2
  52. package/src/composables/useDocHash.js +9 -1
  53. package/src/composables/useDocManager.js +452 -105
  54. package/src/composables/useDocTree.js +33 -2
  55. package/src/composables/useExportWord.js +73 -10
  56. package/src/composables/useFileWatcher.js +45 -0
  57. package/src/composables/useFrontmatter.js +2 -2
  58. package/src/composables/useMarkdown.js +450 -52
  59. package/src/composables/useMermaidCache.js +15 -0
  60. package/src/composables/useScroll.js +354 -27
  61. package/src/composables/useSearch.js +12 -11
  62. package/src/config.js +1 -4
  63. package/src/extensions/CodeBlockCustom.js +113 -0
  64. package/src/extensions/MathBlock.js +107 -0
  65. package/src/extensions/MathInline.js +100 -0
  66. package/src/extensions/MermaidBlock.js +73 -0
  67. package/src/extensions/TableControls.js +670 -0
  68. package/src/services/DocService.js +168 -0
  69. package/src/style.css +2416 -36
  70. package/src/utils/imageConverter.js +129 -0
  71. package/vite-plugin-doc-api.js +369 -0
  72. package/vite.config.js +7 -2
  73. package/public/docs/02-Mermaid/345/233/276/350/241/250.md +0 -102
  74. 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
  75. 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
  76. 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
  77. package/public/docs/04-API/345/217/202/350/200/203/01-/347/273/204/344/273/266API.md +0 -80
  78. package/public/docs/04-API/345/217/202/350/200/203/02-Composables.md +0 -92
  79. package/src/api/docs.js +0 -106
  80. package/src/components/SearchPanel.vue +0 -90
@@ -1,5 +1,26 @@
1
1
  // 文档树操作:查找、展开、扁平化等纯逻辑
2
2
 
3
+ // ===== Hash 索引(O(1) 查找 + 碰撞检测) =====
4
+ // 文档列表变更时由 buildHashIndex 重建
5
+ let _hashIndex = new Map() // hash → doc
6
+
7
+ // 构建 hash → doc 索引,同时检测碰撞
8
+ export function buildHashIndex(items, docHashFn) {
9
+ _hashIndex = new Map()
10
+ const allDocs = flattenDocsList(items)
11
+ for (const doc of allDocs) {
12
+ const hash = docHashFn(doc.key)
13
+ if (_hashIndex.has(hash)) {
14
+ const existing = _hashIndex.get(hash)
15
+ console.warn(
16
+ `[md2ui] hash 碰撞检测:「${doc.key}」与「${existing.key}」生成了相同的 hash「${hash}」,` +
17
+ '后者将被覆盖。请考虑重命名其中一个文档。'
18
+ )
19
+ }
20
+ _hashIndex.set(hash, doc)
21
+ }
22
+ }
23
+
3
24
  // 在文档树中查找文档
4
25
  export function findDoc(items, key) {
5
26
  for (const item of items) {
@@ -40,8 +61,12 @@ export function findReadmeDoc(items) {
40
61
  return null
41
62
  }
42
63
 
43
- // 根据 hash 查找文档
64
+ // 根据 hash 查找文档(优先走索引,O(1);索引未建时回退全树遍历)
44
65
  export function findDocByHash(items, hash, docHash) {
66
+ if (_hashIndex.size > 0) {
67
+ return _hashIndex.get(hash) || null
68
+ }
69
+ // 回退:索引未建时全树遍历
45
70
  for (const item of items) {
46
71
  if (item.type === 'file' && docHash(item.key) === hash) return item
47
72
  if (item.type === 'folder' && item.children) {
@@ -55,7 +80,11 @@ export function findDocByHash(items, hash, docHash) {
55
80
  // 展开文档所在的所有父级文件夹
56
81
  export function expandParents(items, targetKey) {
57
82
  for (const item of items) {
58
- if (item.type === 'file' && item.key === targetKey) return true
83
+ if (item.key === targetKey) {
84
+ // 如果目标本身是文件夹,也展开它
85
+ if (item.type === 'folder') item.expanded = true
86
+ return true
87
+ }
59
88
  if (item.type === 'folder' && item.children) {
60
89
  if (expandParents(item.children, targetKey)) {
61
90
  item.expanded = true
@@ -94,3 +123,5 @@ export function collapseAll(items) {
94
123
  }
95
124
  })
96
125
  }
126
+
127
+
@@ -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 词/分钟)