docx转为markdown

202 阅读2分钟

1.转化为html

nodejs存在一些库能够解析docx文档:

库名称主要功能使用场景优点缺点
docx生成 DOCX动态创建 Word 文档(如报告、合同)- 纯 JS,无依赖
- 支持表格、图片、样式
- API 直观
- 不支持解析现有 DOCX
- 仅适用于生成新文件
mammoth.js解析 DOCX → HTML/Markdown提取 DOCX 内容并转换格式- 轻量级
- 保留基本样式(如标题、列表)
- 不支持修改 DOCX
- 复杂格式可能丢失
docxtemplater模板化 DOCX(变量替换)批量生成文档(如证书、发票)- 支持循环、条件逻辑
- 保留原格式
- 需要预定义模板
- 不适用于复杂文档生成
officegen生成 DOCX/PPTX/XLSX创建 Office 文档(兼容旧版 Node)- 支持多种格式
- 简单 API
- 维护较少
- 功能较基础
pandoc(需 CLI)DOCX ↔ HTML/Markdown/PDF 等格式转换(如 DOCX 转 Markdown)- 支持多种格式
- 高质量转换
- 需安装外部工具
- 不适合动态生成
unzipper + XML 解析底层操作 DOCX需要直接修改 DOCX 内部结构- 完全控制文件内容- 开发复杂
- 需手动处理 XML

相较之下mammoth比较合适使用在当前场景,该工具本身支持直接转为markdown

mammoth document.docx --output-format=markdown

但实践中效果并不是很好,文档会产生一些转义符、锚点处生成了a标签、无法转换表格等问题。 因此决定先通过mammoth,先将docx文档转化为html,再解析html转为markdown

mammoth 转为html的这一步将docxxml格式会剔除字体、字号等冗余信息,结构也变得简化,给转换markdown提供了基础。

2.转化为markdown

这一步没什么特别的,只是简单的根据html标签转化为markdown的语法

此处没有递归处理各节点,因为目前看到的mammoth生成的html没有深层嵌套节点,有需要进一步优化可以加入递归转化

另外需要注意将html实体编码进行解码处理。

const dom = document.querySelector('#container');
const markdownText = htmlToMarkdown(dom);
const decodeMarkdownText = decodeHTMLEntities(markdownText);

function htmlToMarkdown(element) {
  const tagMap = {
    H1: "#",
    H2: "##",
    H3: "###",
    H4: "####",
    H5: "#####",
    H6: "######",
  };

  let result = [];

  function processNode(node) {
    if (node.nodeType === Node.ELEMENT_NODE) {
      const tagName = node.tagName;

      switch (tagName) {
        case "H1":
        case "H2":
        case "H3":
        case "H4":
        case "H5":
        case "H6":
          result.push(`${tagMap[tagName]} ${node.textContent.trim()}\n`);
          break;

        case "P":
          let pText = Array.from(node.childNodes).reduce((acc, child) => {
            if (child.tagName === "BR") {
              acc += "  \n";
            } else if (child.nodeType === Node.TEXT_NODE) {
              acc += child.textContent.trim();
            } else {
              acc += child.textContent.trim();
            }
            return acc;
          }, "");
          result.push(pText);
          break;

        case "UL":
          Array.from(node.children).forEach(child => {
            result.push(`- ${child.textContent.trim()}`);
          });
          result.push("");
          break;

        case "OL":
          Array.from(node.children).forEach((child, index) => {
            result.push(`${index + 1}. ${child.textContent.trim()}`);
          });
          result.push("");
          break;

        case "A":
          const href = node.getAttribute("href");
          result.push(`[${node.textContent}](${href})`);
          break;

        case "STRONG":
          result.push(`**${node.textContent}**`);
          break;

        case "EM":
          result.push(`*${node.textContent}*`);
          break;

        case "TABLE":
          const rows = Array.from(node.querySelectorAll("tr"));
          if (rows.length > 0) {
            const headers = Array.from(rows[0].querySelectorAll("th, td")).map(cell =>
              cell.textContent.trim()
            );
            const separator = headers.map(() => "---");

            result.push(`\n| ${headers.join(" | ")} |\n| ${separator.join(" | ")} |`);

            for (let i = 1; i < rows.length; i++) {
              const cells = Array.from(rows[i].querySelectorAll("td")).map(cell =>
                cell.textContent.trim()
              );
              result.push(`| ${cells.join(" | ")} |`);
            }
          }
          break;

        default:
          Array.from(node.childNodes).forEach(processNode);
      }
    } else if (node.nodeType === Node.TEXT_NODE) {
      result.push(node.textContent.trim());
    } else if (node.nodeType === Node.ELEMENT_NODE && node.hasChildNodes()) {
      Array.from(node.childNodes).forEach(processNode);
    }
  }

  processNode(element);
  return result.join("\n").replace(/\n{3,}/g, "\n\n");
}

function decodeHTMLEntities(text) {
  const textArea = document.createElement("textarea");
  textArea.innerHTML = text;
  return textArea.value;
}