Markdown 导出 Word 文档技术方案
一、整体架构
#mermaid-svg-AKEAFDx2t2lLuD2L{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AKEAFDx2t2lLuD2L .error-icon{fill:#552222;}#mermaid-svg-AKEAFDx2t2lLuD2L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AKEAFDx2t2lLuD2L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AKEAFDx2t2lLuD2L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AKEAFDx2t2lLuD2L .marker.cross{stroke:#333333;}#mermaid-svg-AKEAFDx2t2lLuD2L svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AKEAFDx2t2lLuD2L p{margin:0;}#mermaid-svg-AKEAFDx2t2lLuD2L .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L .cluster-label text{fill:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L .cluster-label span{color:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L .cluster-label span p{background-color:transparent;}#mermaid-svg-AKEAFDx2t2lLuD2L .label text,#mermaid-svg-AKEAFDx2t2lLuD2L span{fill:#333;color:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L .node rect,#mermaid-svg-AKEAFDx2t2lLuD2L .node circle,#mermaid-svg-AKEAFDx2t2lLuD2L .node ellipse,#mermaid-svg-AKEAFDx2t2lLuD2L .node polygon,#mermaid-svg-AKEAFDx2t2lLuD2L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AKEAFDx2t2lLuD2L .rough-node .label text,#mermaid-svg-AKEAFDx2t2lLuD2L .node .label text,#mermaid-svg-AKEAFDx2t2lLuD2L .image-shape .label,#mermaid-svg-AKEAFDx2t2lLuD2L .icon-shape .label{text-anchor:middle;}#mermaid-svg-AKEAFDx2t2lLuD2L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AKEAFDx2t2lLuD2L .rough-node .label,#mermaid-svg-AKEAFDx2t2lLuD2L .node .label,#mermaid-svg-AKEAFDx2t2lLuD2L .image-shape .label,#mermaid-svg-AKEAFDx2t2lLuD2L .icon-shape .label{text-align:center;}#mermaid-svg-AKEAFDx2t2lLuD2L .node.clickable{cursor:pointer;}#mermaid-svg-AKEAFDx2t2lLuD2L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AKEAFDx2t2lLuD2L .arrowheadPath{fill:#333333;}#mermaid-svg-AKEAFDx2t2lLuD2L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AKEAFDx2t2lLuD2L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AKEAFDx2t2lLuD2L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKEAFDx2t2lLuD2L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AKEAFDx2t2lLuD2L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKEAFDx2t2lLuD2L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AKEAFDx2t2lLuD2L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AKEAFDx2t2lLuD2L .cluster text{fill:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L .cluster span{color:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AKEAFDx2t2lLuD2L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AKEAFDx2t2lLuD2L rect.text{fill:none;stroke-width:0;}#mermaid-svg-AKEAFDx2t2lLuD2L .icon-shape,#mermaid-svg-AKEAFDx2t2lLuD2L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKEAFDx2t2lLuD2L .icon-shape p,#mermaid-svg-AKEAFDx2t2lLuD2L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AKEAFDx2t2lLuD2L .icon-shape rect,#mermaid-svg-AKEAFDx2t2lLuD2L .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKEAFDx2t2lLuD2L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AKEAFDx2t2lLuD2L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AKEAFDx2t2lLuD2L :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Markdown 源文本
marked.js 渲染
HTML 字符串
DOM 解析器
docx 库构建
Word 文档
Mermaid 预处理
SVG → PNG
核心设计原则:统一 HTML 源架构
- Markdown 渲染后维护一份干净的 HTML 变量
- 预览展示和 Word 导出共用同一 HTML 源
- Mermaid SVG 转图片逻辑复用,确保一致性
二、导出流程详解
2.1 第一阶段:Markdown → HTML
// 使用 marked.js 将 Markdown 转为 HTML
import { marked } from 'marked'
const htmlContent = marked.parse(markdownText)
2.2 第二阶段:Mermaid 预处理
#mermaid-svg-UyVfBl6kI4c1YF77{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UyVfBl6kI4c1YF77 .error-icon{fill:#552222;}#mermaid-svg-UyVfBl6kI4c1YF77 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UyVfBl6kI4c1YF77 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UyVfBl6kI4c1YF77 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UyVfBl6kI4c1YF77 .marker.cross{stroke:#333333;}#mermaid-svg-UyVfBl6kI4c1YF77 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UyVfBl6kI4c1YF77 p{margin:0;}#mermaid-svg-UyVfBl6kI4c1YF77 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 .cluster-label text{fill:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 .cluster-label span{color:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 .cluster-label span p{background-color:transparent;}#mermaid-svg-UyVfBl6kI4c1YF77 .label text,#mermaid-svg-UyVfBl6kI4c1YF77 span{fill:#333;color:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 .node rect,#mermaid-svg-UyVfBl6kI4c1YF77 .node circle,#mermaid-svg-UyVfBl6kI4c1YF77 .node ellipse,#mermaid-svg-UyVfBl6kI4c1YF77 .node polygon,#mermaid-svg-UyVfBl6kI4c1YF77 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UyVfBl6kI4c1YF77 .rough-node .label text,#mermaid-svg-UyVfBl6kI4c1YF77 .node .label text,#mermaid-svg-UyVfBl6kI4c1YF77 .image-shape .label,#mermaid-svg-UyVfBl6kI4c1YF77 .icon-shape .label{text-anchor:middle;}#mermaid-svg-UyVfBl6kI4c1YF77 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UyVfBl6kI4c1YF77 .rough-node .label,#mermaid-svg-UyVfBl6kI4c1YF77 .node .label,#mermaid-svg-UyVfBl6kI4c1YF77 .image-shape .label,#mermaid-svg-UyVfBl6kI4c1YF77 .icon-shape .label{text-align:center;}#mermaid-svg-UyVfBl6kI4c1YF77 .node.clickable{cursor:pointer;}#mermaid-svg-UyVfBl6kI4c1YF77 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UyVfBl6kI4c1YF77 .arrowheadPath{fill:#333333;}#mermaid-svg-UyVfBl6kI4c1YF77 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UyVfBl6kI4c1YF77 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UyVfBl6kI4c1YF77 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UyVfBl6kI4c1YF77 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UyVfBl6kI4c1YF77 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UyVfBl6kI4c1YF77 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UyVfBl6kI4c1YF77 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UyVfBl6kI4c1YF77 .cluster text{fill:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 .cluster span{color:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UyVfBl6kI4c1YF77 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UyVfBl6kI4c1YF77 rect.text{fill:none;stroke-width:0;}#mermaid-svg-UyVfBl6kI4c1YF77 .icon-shape,#mermaid-svg-UyVfBl6kI4c1YF77 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UyVfBl6kI4c1YF77 .icon-shape p,#mermaid-svg-UyVfBl6kI4c1YF77 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UyVfBl6kI4c1YF77 .icon-shape rect,#mermaid-svg-UyVfBl6kI4c1YF77 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UyVfBl6kI4c1YF77 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UyVfBl6kI4c1YF77 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UyVfBl6kI4c1YF77 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
遍历所有 pre.mermaid
是否已渲染 SVG?
提取 SVG 内容
调用 mermaid.render
SVG → PNG 转换
存入 mermaidImages Map
关键函数:
- collectMermaidImages() – 预收集所有 Mermaid 图表
- svgToPngForWord() – SVG 转 PNG(base64)
2.3 第三阶段:HTML → docx 元素
#mermaid-svg-5KwiajroAsXBY754{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5KwiajroAsXBY754 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5KwiajroAsXBY754 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5KwiajroAsXBY754 .error-icon{fill:#552222;}#mermaid-svg-5KwiajroAsXBY754 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5KwiajroAsXBY754 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5KwiajroAsXBY754 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5KwiajroAsXBY754 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5KwiajroAsXBY754 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5KwiajroAsXBY754 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5KwiajroAsXBY754 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5KwiajroAsXBY754 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5KwiajroAsXBY754 .marker.cross{stroke:#333333;}#mermaid-svg-5KwiajroAsXBY754 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5KwiajroAsXBY754 p{margin:0;}#mermaid-svg-5KwiajroAsXBY754 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-5KwiajroAsXBY754 .cluster-label text{fill:#333;}#mermaid-svg-5KwiajroAsXBY754 .cluster-label span{color:#333;}#mermaid-svg-5KwiajroAsXBY754 .cluster-label span p{background-color:transparent;}#mermaid-svg-5KwiajroAsXBY754 .label text,#mermaid-svg-5KwiajroAsXBY754 span{fill:#333;color:#333;}#mermaid-svg-5KwiajroAsXBY754 .node rect,#mermaid-svg-5KwiajroAsXBY754 .node circle,#mermaid-svg-5KwiajroAsXBY754 .node ellipse,#mermaid-svg-5KwiajroAsXBY754 .node polygon,#mermaid-svg-5KwiajroAsXBY754 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5KwiajroAsXBY754 .rough-node .label text,#mermaid-svg-5KwiajroAsXBY754 .node .label text,#mermaid-svg-5KwiajroAsXBY754 .image-shape .label,#mermaid-svg-5KwiajroAsXBY754 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5KwiajroAsXBY754 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5KwiajroAsXBY754 .rough-node .label,#mermaid-svg-5KwiajroAsXBY754 .node .label,#mermaid-svg-5KwiajroAsXBY754 .image-shape .label,#mermaid-svg-5KwiajroAsXBY754 .icon-shape .label{text-align:center;}#mermaid-svg-5KwiajroAsXBY754 .node.clickable{cursor:pointer;}#mermaid-svg-5KwiajroAsXBY754 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5KwiajroAsXBY754 .arrowheadPath{fill:#333333;}#mermaid-svg-5KwiajroAsXBY754 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5KwiajroAsXBY754 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5KwiajroAsXBY754 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5KwiajroAsXBY754 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5KwiajroAsXBY754 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5KwiajroAsXBY754 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5KwiajroAsXBY754 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5KwiajroAsXBY754 .cluster text{fill:#333;}#mermaid-svg-5KwiajroAsXBY754 .cluster span{color:#333;}#mermaid-svg-5KwiajroAsXBY754 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5KwiajroAsXBY754 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5KwiajroAsXBY754 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5KwiajroAsXBY754 .icon-shape,#mermaid-svg-5KwiajroAsXBY754 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5KwiajroAsXBY754 .icon-shape p,#mermaid-svg-5KwiajroAsXBY754 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5KwiajroAsXBY754 .icon-shape rect,#mermaid-svg-5KwiajroAsXBY754 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5KwiajroAsXBY754 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5KwiajroAsXBY754 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5KwiajroAsXBY754 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
h1-h6
p
pre
ul/ol
img
blockquote
table
创建临时 DOM 容器
解析 HTML 字符串
递归遍历 DOM 节点
节点类型?
创建 Heading
创建 Paragraph
创建代码块
递归处理列表
创建 ImageRun
创建引用块
创建 Table
2.4 第四阶段:生成 Word 文件
const doc = new Document({
sections: [{
children: docxElements // 转换后的元素数组
}]
})
const buffer = await Packer.toBlob(doc)
await saveBlobToFile(buffer, filename)
三、遇到的问题及解决方案
问题 1:第三方库支持不足
| 现象 | 使用 html-to-docx 库导出时,代码块和图片丢失 |
| 原因 | 该库对复杂 HTML 结构(嵌套 table、inline style 包装)支持差 |
| 方案 | 弃用 html-to-docx,改用 docx 库自建解析器 |
| 经验 | 第三方库有局限性,核心功能需自主可控 |
问题 2:有序列表编号丢失
| 现象 | <ol> 导出后无编号,只有文字内容 |
| 原因 | 当 <li> 第一个子元素是 <p> 时(如 <li><p>内容</p></li>),textBuffer 为空,flushTextBuffer() 不输出 prefix |
| 方案 | 检测到 <p> 是首个子元素时,将 prefix 与 <p> 内容合并后再输出 |
问题代码结构:
<!– 简单结构(正常)–>
<ol>
<li>项目一</li>
</ol>
<!– 复杂结构(编号丢失)–>
<ol>
<li><p>项目一</p></li>
</ol>
修复逻辑:
if (childTag === 'p' && isFirst && !textBuffer.trim()) {
// 首个子元素是 <p>,合并 prefix 和 <p> 内容
textBuffer = child.textContent
flushTextBuffer() // 此时 prefix + textBuffer 一起输出
}
问题 3:嵌套列表内容丢失
| 现象 | 多级嵌套的 <ul>/<ol> 只输出第一层 |
| 原因 | 仅使用 li.textContent 获取内容,未递归遍历子元素 |
| 方案 | 实现通用递归解析架构,支持任意深度嵌套 |
错误做法:
// ❌ 只取文本,嵌套结构丢失
const text = li.textContent
正确做法:
// ✅ 递归处理所有子元素
function processElement(element, indentLevel) {
for (const child of element.children) {
if (child.tagName === 'UL' || child.tagName === 'OL') {
result.push(…processElement(child, indentLevel + 1))
}
// … 其他元素处理
}
}
问题 4:内联元素错误换行
| 现象 | 这是**粗体**文字 被拆成三行 |
| 原因 | <strong>、<em> 等内联元素被误判为块级元素,触发换行 |
| 方案 | 精确区分内联/块级元素,内联元素追加到 textBuffer,块级元素才触发 flush |
元素分类:
// 块级元素(触发换行)
const blockTags = ['p', 'pre', 'div', 'ul', 'ol', 'li', 'blockquote', 'table', 'img']
// 内联元素(不换行,追加到 buffer)
const inlineTags = ['strong', 'em', 'code', 'a', 'span', 's', 'del', 'b', 'i', 'u']
问题 5:Mermaid CSS 残留污染
| 现象 | 导出的 Word 中出现 #mermaid-xxx{fill:#333;…} 乱码 |
| 原因 | Mermaid 渲染后 <pre> 中残留 CSS 样式代码,被当作普通代码块输出 |
| 方案 | 正则检测 #mermaid-xxx + {…} 模式,显式跳过不输出 |
检测逻辑:
const text = preElement.textContent
const isMermaidCss = /#mermaid-[^\\s]+/.test(text) && /\\{[^}]*\\}/.test(text)
if (isMermaidCss) {
continue // 跳过,不输出
}
问题 6:Buffer 类型不兼容
| 现象 | saveBlobToFile 报错:blob.arrayBuffer is not a function |
| 原因 | html-to-docx 返回的是 Node.js Buffer,而非浏览器 Blob |
| 方案 | 在 fileSaver.js 中兼容处理,检测类型后统一转换 |
let arrayBuffer
if (blobOrBuffer instanceof Blob) {
arrayBuffer = await blobOrBuffer.arrayBuffer()
} else if (Buffer.isBuffer(blobOrBuffer)) {
arrayBuffer = blobOrBuffer.buffer
}
问题 7:HTML 预览列表无样式
| 现象 | 预览区的 <ol> 无编号,<ul> 无符号 |
| 原因 | Tailwind CSS Preflight 重置了 list-style-type: none |
| 方案 | 在 .prose 样式中手动覆盖 |
.prose :deep(ol) {
list-style-type: decimal;
}
.prose :deep(ul) {
list-style-type: disc;
}
四、架构演进历程
#mermaid-svg-jpRESI60lCUMYhhm{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jpRESI60lCUMYhhm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jpRESI60lCUMYhhm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jpRESI60lCUMYhhm .error-icon{fill:#552222;}#mermaid-svg-jpRESI60lCUMYhhm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jpRESI60lCUMYhhm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jpRESI60lCUMYhhm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jpRESI60lCUMYhhm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jpRESI60lCUMYhhm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jpRESI60lCUMYhhm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jpRESI60lCUMYhhm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jpRESI60lCUMYhhm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jpRESI60lCUMYhhm .marker.cross{stroke:#333333;}#mermaid-svg-jpRESI60lCUMYhhm svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jpRESI60lCUMYhhm p{margin:0;}#mermaid-svg-jpRESI60lCUMYhhm .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-jpRESI60lCUMYhhm .cluster-label text{fill:#333;}#mermaid-svg-jpRESI60lCUMYhhm .cluster-label span{color:#333;}#mermaid-svg-jpRESI60lCUMYhhm .cluster-label span p{background-color:transparent;}#mermaid-svg-jpRESI60lCUMYhhm .label text,#mermaid-svg-jpRESI60lCUMYhhm span{fill:#333;color:#333;}#mermaid-svg-jpRESI60lCUMYhhm .node rect,#mermaid-svg-jpRESI60lCUMYhhm .node circle,#mermaid-svg-jpRESI60lCUMYhhm .node ellipse,#mermaid-svg-jpRESI60lCUMYhhm .node polygon,#mermaid-svg-jpRESI60lCUMYhhm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jpRESI60lCUMYhhm .rough-node .label text,#mermaid-svg-jpRESI60lCUMYhhm .node .label text,#mermaid-svg-jpRESI60lCUMYhhm .image-shape .label,#mermaid-svg-jpRESI60lCUMYhhm .icon-shape .label{text-anchor:middle;}#mermaid-svg-jpRESI60lCUMYhhm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jpRESI60lCUMYhhm .rough-node .label,#mermaid-svg-jpRESI60lCUMYhhm .node .label,#mermaid-svg-jpRESI60lCUMYhhm .image-shape .label,#mermaid-svg-jpRESI60lCUMYhhm .icon-shape .label{text-align:center;}#mermaid-svg-jpRESI60lCUMYhhm .node.clickable{cursor:pointer;}#mermaid-svg-jpRESI60lCUMYhhm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jpRESI60lCUMYhhm .arrowheadPath{fill:#333333;}#mermaid-svg-jpRESI60lCUMYhhm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jpRESI60lCUMYhhm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jpRESI60lCUMYhhm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jpRESI60lCUMYhhm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jpRESI60lCUMYhhm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jpRESI60lCUMYhhm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jpRESI60lCUMYhhm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jpRESI60lCUMYhhm .cluster text{fill:#333;}#mermaid-svg-jpRESI60lCUMYhhm .cluster span{color:#333;}#mermaid-svg-jpRESI60lCUMYhhm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jpRESI60lCUMYhhm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jpRESI60lCUMYhhm rect.text{fill:none;stroke-width:0;}#mermaid-svg-jpRESI60lCUMYhhm .icon-shape,#mermaid-svg-jpRESI60lCUMYhhm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jpRESI60lCUMYhhm .icon-shape p,#mermaid-svg-jpRESI60lCUMYhhm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jpRESI60lCUMYhhm .icon-shape rect,#mermaid-svg-jpRESI60lCUMYhhm .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jpRESI60lCUMYhhm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jpRESI60lCUMYhhm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jpRESI60lCUMYhhm :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
v1.0.2 完善
v1.0.1 重构
v1.0.0 初版
html-to-docx 库
简单 HTML 结构
问题多:图片丢失、代码块丢失
迁移到 docx 库
自建 DOM 解析器
解决图片和代码块问题
通用递归架构
支持任意嵌套
精确内联/块级区分
智能文件命名
五、核心经验总结
| 1 | 自建解析器更可控:第三方库有局限性,核心功能需自主实现 |
| 2 | 递归处理是必须的:HTML 结构可无限嵌套,必须支持任意深度 |
| 3 | 内联/块级必须精确区分:错误分类会导致格式混乱 |
| 4 | CSS 框架有副作用:Tailwind Preflight 会重置原生样式 |
| 5 | 统一 HTML 源架构:预览和导出共用同一数据源,确保一致性 |
| 6 | 预收集机制:Mermaid 图表先收集到 Map,解析时直接查表 |
网硕互联帮助中心![[python]共享舞蹈课程预约系统 健身房的小程序设计视频(编号:91761267)-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/02/20260224145806-699dbc7eddca3-220x150.jpg)




评论前必须登录!
注册