Markdown 导出 Word 文档技术方案

Markdown 导出 Word 文档技术方案

一、整体架构

Markdown 源文本
marked.js 渲染
HTML 字符串
DOM 解析器
docx 库构建
Word 文档
Mermaid 预处理
SVG → PNG

核心设计原则:统一 HTML 源架构

  • Markdown 渲染后维护一份干净的 HTML 变量
  • 预览展示和 Word 导出共用同一 HTML 源
  • Mermaid SVG 转图片逻辑复用,确保一致性

二、导出流程详解

2.1 第一阶段:Markdown → HTML

javascript 复制代码
// 使用 marked.js 将 Markdown 转为 HTML
import { marked } from 'marked'
const htmlContent = marked.parse(markdownText)

2.2 第二阶段:Mermaid 预处理



遍历所有 pre.mermaid
是否已渲染 SVG?
提取 SVG 内容
调用 mermaid.render
SVG → PNG 转换
存入 mermaidImages Map

关键函数

  • collectMermaidImages() - 预收集所有 Mermaid 图表
  • svgToPngForWord() - SVG 转 PNG(base64)

2.3 第三阶段:HTML → docx 元素

h1-h6
p
pre
ul/ol
img
blockquote
table
创建临时 DOM 容器
解析 HTML 字符串
递归遍历 DOM 节点
节点类型?
创建 Heading
创建 Paragraph
创建代码块
递归处理列表
创建 ImageRun
创建引用块
创建 Table

2.4 第四阶段:生成 Word 文件

javascript 复制代码
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> 内容合并后再输出

问题代码结构

html 复制代码
<!-- 简单结构(正常)-->
<ol>
  <li>项目一</li>
</ol>

<!-- 复杂结构(编号丢失)-->
<ol>
  <li><p>项目一</p></li>
</ol>

修复逻辑

javascript 复制代码
if (childTag === 'p' && isFirst && !textBuffer.trim()) {
    // 首个子元素是 <p>,合并 prefix 和 <p> 内容
    textBuffer = child.textContent
    flushTextBuffer()  // 此时 prefix + textBuffer 一起输出
}

问题 3:嵌套列表内容丢失

项目 描述
现象 多级嵌套的 <ul>/<ol> 只输出第一层
原因 仅使用 li.textContent 获取内容,未递归遍历子元素
方案 实现通用递归解析架构,支持任意深度嵌套

错误做法

javascript 复制代码
// ❌ 只取文本,嵌套结构丢失
const text = li.textContent

正确做法

javascript 复制代码
// ✅ 递归处理所有子元素
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

元素分类

javascript 复制代码
// 块级元素(触发换行)
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 + {...} 模式,显式跳过不输出

检测逻辑

javascript 复制代码
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 中兼容处理,检测类型后统一转换
javascript 复制代码
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 样式中手动覆盖
css 复制代码
.prose :deep(ol) {
    list-style-type: decimal;
}
.prose :deep(ul) {
    list-style-type: disc;
}

四、架构演进历程

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,解析时直接查表
相关推荐
凡人叶枫1 小时前
Effective C++ 条款16:成对使用 new 和 delete 时要采取相同形式
开发语言·c++·effective c++
不吃土豆的马铃薯1 小时前
C++ 高性能网络缓冲区 Buffer 源码解析
linux·服务器·开发语言·网络·c++
数据法师1 小时前
QuickSay :基于 Qt 的轻量级快捷短语管理工具
开发语言·qt
DS随心转插件1 小时前
AI 导出鸭实测:Markdown TO Word 本地化转换能力深度评测,多角度拆解本地化转换真实表现
人工智能·ai·word·wps·deepseek·ai导出鸭
caimouse2 小时前
Reactos 第1章 概述
c语言·开发语言·架构
.千余2 小时前
【C++】C++继承入门(下):友元、静态成员与菱形继承的底层逻辑
开发语言·c++·笔记·学习·其他
小短腿的代码世界2 小时前
行情快照与增量更新引擎:Qt在高频交易数据分发中的核心架构——你的行情推送为什么延迟了500ms?
开发语言·qt·架构
初中就开始混世的大魔王2 小时前
6 Fast DDS-传输层
开发语言·c++·中间件·信息与通信
啊森要自信2 小时前
【GUI自动化测试】控件、鼠标键盘操作与多场景自动化
c语言·开发语言·python·adb·ipython
花北城2 小时前
【C#】ABP框架服务端开发
开发语言·c#·abp