云计算百科
云计算领域专业知识百科平台

Markdown 导出 Word 文档技术方案

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,解析时直接查表
赞(0)
未经允许不得转载:网硕互联帮助中心 » Markdown 导出 Word 文档技术方案
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!