技术笔记

把 Markdown 一键变成公文 Word:我做了一个原生 Mac 转换器

•18 分钟阅读
文章目录

把 Markdown 一键变成公文 Word:我做了一个原生 Mac 转换器

我习惯用 Markdown 写材料:标题层级清楚,改结构方便,也不用在写作过程中反复调整字体、缩进和行距。

但到了交付环节,很多工作材料必须变成有明确版式要求的 Word 文档。标题用什么字体、正文用几号字、一级标题和二级标题如何区分、每页多少行、每行多少字、落款放在哪里,都有固定规则。

过去的流程通常是:在 Markdown 里写完,复制进 Word,再从头排一遍。内容越长,机械劳动越多,而且很容易漏掉某个标题或数字字体。

于是我做了一个只解决这件事的原生 Mac 小工具:把 Markdown 文件拖进去,点击一次,得到排好版的 DOCX;或者直接在 Finder 里右键 Markdown 文件,选择“Markdown 转公文 Word”。

Markdown 转公文 Word 的抽象示意图

隐私说明: 本文不展示真实单位名称、个人路径、内部材料或原始截图。配图中的界面、文件内容和机构名称均为重新制作的匿名示例;示例只保留技术结构,不含真实业务信息。

一、最后做成了什么

这个工具当前提供两种入口。

第一种是普通 Mac App:

  1. 拖入或选择一个 .md 文件;
  2. 选择排版模板;
  3. 检查所需字体是否安装;
  4. 点击“转换为 DOCX”;
  5. 转换完成后直接打开文档,或在 Finder 中定位。

第二种更适合日常使用:

Finder 中选中 Markdown 文件
→ 右键
→ 快速操作
→ Markdown 转公文 Word
→ 在原文件旁生成 DOCX

支持同时选中多个 Markdown 文件批量转换。如果输出文件已经存在,快速操作会自动追加时间戳,避免覆盖旧版本。

Markdown 公文转换器的本地工作流

整个过程都在本地完成,不上传文档,也不要求安装 Python、Pandoc 或额外服务。

二、为什么没有直接使用 Pandoc

Pandoc 很适合通用格式转换,例如从 Markdown 生成普通 Word 文档,再通过 reference.docx 复用样式。

但公文排版有一些更细的要求:

  • 中文和英文数字需要分别指定字体;
  • 标题层级不仅影响外观,还应写入 Word 大纲级别;
  • 页面需要设置字符网格和行网格;
  • 页边距、页眉、页脚、页码位置必须可控;
  • 标题下方的单位和日期,与文末落款的对齐方式不同;
  • 表格、空行、段落缩进不能只依赖 Markdown 的默认解释。

这些细节如果全部交给通用转换器,再通过模板和后处理修正,调试成本并不低。

我最后选择直接生成 DOCX 的 OOXML。这样每一个段落、字体、缩进和页面参数都能明确写入文档,不需要猜测转换器会怎样处理。

三、为什么改成纯 Swift

最早的原型是一个 Python 脚本,使用 python-docx 生成 Word。这个方向开发快,也很适合验证规则。

但如果最终目标是一个 Mac App,就会遇到运行环境问题:

  • 用户是否已经安装 Python;
  • 应该依赖系统 Python、虚拟环境,还是把解释器一起打包;
  • App 签名和分发时如何携带 Python 依赖;
  • Finder 快速操作如何稳定找到脚本和运行环境。

因此最终版本改成了纯 Swift:

SwiftUI
├── 文件拖放与选择
├── 模板与输出设置
├── 字体状态检查
└── 转换结果与错误提示

Swift 转换核心
├── Markdown 结构解析
├── OOXML 文档生成
├── DOCX 包结构写入
└── 系统 zip 封装

这样生成的 App 很小,启动快,也没有额外运行时。Finder 快速操作只需要调用 App 内的可执行文件。

四、DOCX 本质上是什么

DOCX 并不是一个不可理解的二进制文件,而是一个符合约定目录结构的 ZIP 包。最小可用文档大致包含:

[Content_Types].xml
_rels/
  .rels
docProps/
  app.xml
  core.xml
word/
  document.xml
  styles.xml
  settings.xml
  fontTable.xml
  footer1.xml
  _rels/
    document.xml.rels

其中:

| 文件 | 作用 | |---|---| | word/document.xml | 正文段落、标题、表格和页面设置 | | word/styles.xml | 默认字体、字号和段落样式 | | word/settings.xml | 字体语言、字段更新等文档设置 | | word/fontTable.xml | 文档声明使用的字体 | | word/footer1.xml | 页脚和页码域 | | document.xml.rels | 正文与样式、页脚等部件的关系 |

Swift 负责生成这些 XML 文件,写入临时目录,再调用 macOS 自带的 /usr/bin/zip 封装成 .docx。

生成后还要做两类检查:

unzip -t output.docx
xmllint --noout word/document.xml

前者检查压缩包是否完整,后者检查 XML 是否有效。文件能生成,不代表 Word 一定能正确打开;这两步能提前发现不少低级错误。

五、Markdown 如何映射到公文结构

为了让源稿简单可读,我采用了一套很克制的约定:

| Markdown | Word 中的含义 | |---|---| | # 标题 | 公文大标题,居中 | | ## 一、标题 | 一级标题 | | ### (一)标题 | 二级标题 | | #### 1. 标题 | 三级标题 | | 普通段落 | 正文,首行缩进两个字符 | | **重点内容** | 行内加粗 | | Markdown 表格 | Word 表格 |

一份匿名示例可以这样写:

# 关于年度重点工作推进情况的报告

某某单位
2026年9月26日

今年以来,我单位围绕年度目标,稳步推进各项重点任务。

## 一、总体情况

重点任务按计划实施,协同机制持续完善。

### (一)完善工作机制

建立定期调度和跟踪反馈机制。

#### 1. 加强协同配合

及时研究解决实际问题。

生成后的匿名示例效果如下。这里的标题、日期和正文均为虚构内容。

使用虚构内容生成的脱敏 DOCX 预览

六、最容易忽略的细节:空行不是空段落

开发过程中最直观的一个问题,是生成的 Word 段落之间出现了大量空白。

原因并不在 Word,而在 Markdown 语义处理:Markdown 中的空行通常只表示“前后是两个段落”,它本身不是一个需要显示的空段落。

如果把每个 Markdown 空行都转换为 Word 空段落,再叠加固定值 30 磅行距,页面就会被一行一行地撑开。

错误逻辑类似这样:

if line.isEmpty {
    blocks.append(.blank)
}

修正后,普通空行只负责分隔,不产生任何 Word 节点:

if line.isEmpty {
    index += 1
    continue
}

if line == " " {
    blocks.append(.blank)
}

也就是说,只有源稿明确写了一行  ,转换器才会有意保留一个空白行。

这个问题很小,却说明了格式转换中的一个基本原则:不能只看字符,还要理解源格式的语义。

七、标题下日期与文末落款不是一回事

仅靠“检测日期格式,然后右对齐”还不够。

同样是一行日期:

  • 位于标题下方时,通常和单位名称一起居中;
  • 位于正文末尾时,通常属于落款区域,需要右对齐。

因此解析器还要维护上下文状态:是否已经读到标题、正文是否开始、日期前一行是不是短单位名称。根据位置把内容分别标记为:

case centeredMetadata(String)
case signature(String)

生成 OOXML 时,再应用不同的对齐方式。

这种上下文判断比单纯的正则表达式更可靠,同时仍然允许用户通过 Markdown 结构自然表达文档。

八、字体问题比想象中复杂

在公文排版中,字体往往不是“看起来相似”就可以。常见配置会明确指定:

  • 标题字体;
  • 正文字体;
  • 一级、二级、三级标题字体;
  • 数字和英文字体。

但字体文件名、字体的中文显示名和字体内部 PostScript 名称可能完全不同。例如一个字体文件可能同时暴露中文名称、英文名称和缩写别名。

因此 App 的字体检测不能只比较一个字符串,而要维护别名集合:

("某中文字体名", ["EnglishAlias", "PostScriptName"])

即使本机没有安装字体,转换器仍然可以把目标字体名称写进 DOCX;但当前电脑预览时可能使用替代字体。为了避免误判,App 会显示字体状态,并允许用户打开字体文件夹自行安装。

字体文件可能有单独的授权限制,所以我没有把第三方字体直接打进 App。工具只负责检测和引用,字体使用仍应遵守各自的许可证。

九、把转换做成 Finder 右键操作

有了命令行转换入口以后,Finder 快速操作就很轻量了。

App 支持下面这种调用方式:

MarkdownGongwen --convert input.md output.docx

然后创建一个 Automator 快速操作,让它接收 Finder 传入的文件列表。核心逻辑可以概括为:

APP="$HOME/Applications/Markdown公文转换器.app/Contents/MacOS/MarkdownGongwen"

for input in "$@"; do
  extension="${input##*.}"
  [[ "${extension:l}" == "md" ]] || continue

  output_base="${input%.*}_公文格式"
  output="${output_base}.docx"

  if [[ -e "$output" ]]; then
    output="${output_base}_$(date +%Y%m%d-%H%M%S).docx"
  fi

  "$APP" --convert "$input" "$output"
done

快速操作安装到:

~/Library/Services/Markdown 转公文 Word.workflow

App 则安装到当前用户自己的应用目录:

~/Applications/Markdown公文转换器.app

这样不需要管理员权限,也不会依赖开发项目原来的绝对路径。

十、页面参数如何落到 OOXML

Word 的页面尺寸和边距使用 twip 作为单位。1 英寸等于 1440 twip,1 厘米约等于 567 twip。

以 A4 页面为例,页面设置写在 w:sectPr 中:

<w:sectPr>
  <w:pgSz w:w="11906" w:h="16838"/>
  <w:pgMar
    w:top="..."
    w:right="..."
    w:bottom="..."
    w:left="..."
    w:header="..."
    w:footer="..."/>
  <w:docGrid
    w:type="linesAndChars"
    w:linePitch="600"
    w:charSpace="..."/>
</w:sectPr>

固定值 30 磅行距则写在段落属性中:

<w:spacing
  w:before="0"
  w:after="0"
  w:line="600"
  w:lineRule="exact"/>

这里最重要的不是某一组具体数字,而是把单位内部规范抽象成模板参数。不同单位或不同材料类型可以使用不同配置,不必修改 Markdown 内容本身。

十一、如何验证生成结果

文档生成器不能只测试“文件存在”。我最终保留了四层验证:

1. 解析层

确认标题、正文、表格、日期和落款被识别成正确的内容块。

2. 包结构层

使用 unzip -t 检查 DOCX 是否完整,使用 XML 工具检查每个部件是否符合语法。

3. 渲染层

使用 macOS Quick Look 和办公软件打开匿名示例,检查:

  • 中文是否完整显示;
  • 标题和落款是否对齐;
  • 段落之间是否出现意外空行;
  • 页码是否位于页脚;
  • 字体缺失时是否给出明确提示。

4. 使用层

实际执行 Finder 快速操作,验证它能接收文件路径、处理中文文件名、批量转换,并避免覆盖已有结果。

这四层分别覆盖“理解错了、写坏了、显示错了、用不了”四类问题。

十二、隐私和安全方面的取舍

这个工具处理的往往是尚未公开的工作材料,所以本地优先不是一句口号,而是产品约束:

  • 文档不上传到任何服务器;
  • 不调用在线转换接口;
  • 不记录正文内容;
  • 不把真实单位名称写入程序默认示例;
  • 快速操作只接收用户主动选择的文件;
  • 输出默认放在源文件旁,路径清晰可见;
  • 同名输出不直接覆盖。

写这篇文章时,我也采用了同一套原则:不使用真实工作文档截图,而是重新生成一份只含“某某单位”的虚构材料;所有路径都使用 $HOME 或占位符;配图不包含真实文件名、账号、组织名称和内部标准文件。

十三、目前的边界

当前版本主要解决“普通文字材料从 Markdown 转为规范正文 DOCX”,还不是完整的电子公文制作系统。

暂未覆盖的内容包括:

  • 红色版头和分隔线;
  • 发文字号、签发人;
  • 完整版记;
  • 图片题注和复杂图文混排;
  • 跨页复杂表格;
  • Word 修订、批注和域的高级行为;
  • Apple 公证和面向公众的正式分发。

对于内部汇报、总结、通知正文和常规材料,它已经能够省掉大量重复排版。涉及正式发文时,仍应使用本单位审定模板,并在提交前人工复核。

十四、这次开发带来的几个结论

第一,自动化最值得处理的,往往不是技术上最难的步骤,而是每天都要重复的小动作。

第二,格式转换不能只做“文本搬运”。Markdown 的空行、标题、列表和 Word 的段落、样式、大纲之间,需要一层明确的语义映射。

第三,真正好用的工具不应要求用户记住命令。拖放是一个入口,Finder 右键才是更接近日常习惯的入口。

第四,文档类工具一定要用真实渲染结果验收。XML 正确、压缩包完整,只能说明文件没有坏,不能说明排版已经对。

现在我的日常流程变成了:在熟悉的 Markdown 编辑器中专心写内容,写完以后右键转换,再在 Word 中做最后一次检查。Markdown 负责写作,DOCX 负责交付,两者终于不再互相妥协。

最后更新:2026年9月26日

评论

加载中...
加载评论中...