不用装一堆花里胡哨的编辑器,也不用对着在线网页担心格式丢失,一条bm md命令直接把你手里的数据、代码、或者一堆零散笔记,变成一份结构干净、能进 Git 也能直接交给下游工具处理的 Markdown 文档。这事儿我干了不止一次,今天把整个从需求拆解到落地实现的过程完整写出来,参数、命令、踩坑都在里面。
先把这个话题说透:bm在我这里的语境里就是build markdown的缩写,一个本地跑的小工具或者说工作流的名字。它解决的核心问题是三件事:一是把杂乱的数据源(可能是接口返回的 JSON、可能是数据库导出、也可能是一堆跟着命名规范走的.txt文件)统一变成结构化的 Markdown;二是让这个转换过程可以重复执行,改一次数据,重新跑一下命令,最新的.md文件就自动生成;三是保证输出的 Markdown 风格统一、表格对齐、代码块带语言标注,不用再手动去调格式。所以这不是一篇单纯讲 Markdown 语法有多全的文章,而是一篇讲怎么把文档生成这件事“自动化”和“工程化”的记录。适合的人群也很清晰:经常要写接口文档、数据报表说明、批量生成课程笔记或者维护一套静态博客草稿的开发者,以及那些不想在“排版”上浪费时间、只想让工具把文档从数据里直接“焊”出来的效率党。
1. 整体设计与思路拆解:为什么不用现成的 Markdown 编辑器
开始动手前,我先把市面上主流的 Markdown 工具在脑子里过了一遍。Typora 我也用过,所见即所得确实舒服,但它解决的是“人坐在电脑前一个字一个字敲”的场景,解决不了“每周一早上要从数据库抽出五十条记录,然后按照固定模板生成一篇周报文档”的场景。VSCode 加插件也很强,但那是拿来给人工写作用,不是拿来给脚本跑的。我需要的是一个能把“数据”变成“文档”的流水线,而不是另一个写作环境。
于是核心思路一下就清楚了:写一个命令行工具,输入是数据,输出是.md文件,中间夹着模板解析和格式生成逻辑。这个名字就叫bm md,含义很直白:把任意结构化输入 Build 成 Markdown。这个方案的优势用一句话就能概括——“一次配置,永远复用”。我不需要每次在编辑器里重新调整标题层级、表格列宽、代码块缩进,只要把正则写对、模板定好,后面所有人都能跑同一条命令拿到同一风格的文档。这比任何 WYSIWYG 编辑器都更适合团队协作和自动化流程。
整个链路的技术选型也遵循这个思路:数据输入的规范是最重要的,我严格遵守“要是数据本身是脏的,后面再怎么转都是脏的”这一基本判断。先定输入格式再做模板,模板占八成精力,输出命令反而是最简单的拼字符串。只要一步步把数据清洗干净,生成 Markdown 就只是格式化输出而已。
2. 核心细节解析与实操要点:Markdown 语法和文件结构这件事
既然要“自动化生成 Markdown”,首先得深刻理解 Markdown 本身到底是怎么一回事。它本质上是一种轻量级的标记语言,用几个特殊字符就能把纯文本变成有层级、有强调、有列表、有表格的结构化内容。这正是它能被程序轻松生成的原因,也是它能在各种编辑器、代码托管平台、博客系统里被统一渲染的原因。
2.1 Markdown 的六个常用语法块
要写出程序能稳定生成的 Markdown,必须先掌握它最常用的几类语法。我用了一段时间之后把常用的浓缩成六类,生成时只需要覆盖这些就够了:
- 标题:
#到######表示一到六级标题,注意井号和文字之间必须有一个空格,否则很多渲染器不识别。 - 列表:无序列表用
-、*或+,有序列表直接用1.2.这种数字加点。生成时如果要嵌套,子列表必须缩进两个或四个空格,这个缩进在程序里很容易漏。 - 表格:用管道符
|分隔单元格,第二行必须有|---|---|来声明对齐方式。表格是程序生成时最值得花功夫的地方,因为只要有一列漏写了分隔符,整个表格就可能渲染失败。 - 代码块:用三个反引号包裹,反引号后面紧跟着语言类型,例如
```python。 - 引用:行首加一个
>,用于备注说明或引用别人的话。 - 加粗与斜体:
**加粗**和*斜体*,在程序生成时,只要记得别把这些符号写进代码块里就行。
这些语法看着不起眼,但程序生成时最容易出问题的就是它们:要么是反引号数量不够导致代码块提前闭合,要么是表格单元格里出现了|导致整行错位,要么是行尾没加两个空格导致换行失效。这些坑后面我会在排查部分挨个说。
2.2 目录与文件命名规范的先行约定
既然要做工程化,文件命名和目录结构必须从一开始就定下来。我按“模板、数据、输出、脚本”四段式拆分:
bm-md/ templates/ api-doc-template.md report-template.md data/ raw/ clean/ output/ api/ reports/ scripts/ build-md.js package.json README.mdtemplates/放 Markdown 骨架文件,里面用占位符标出动态内容的位置,例如{{TITLE}}、{{TABLE}}、{{BODY}},比在代码里写死一长串模板字符串好维护得多。data/raw放原始数据,data/clean放清洗过的中间数据,output/是最终生成的文档。为什么这样分?因为一旦脚本跑挂了,或者产出的 Markdown 格式不对,我可以快速定位是数据源的问题、清洗逻辑的问题还是模板渲染的问题,不会在那里瞎猜。
3. 实操过程与核心环节实现:从零开始搭一个bm md工具
这一部分,我用一个真实场景贯穿始终:我需要把某个数据接口返回的“知识点列表”(每条含标题、分类、标签、内容摘要、更新时间)直接生成一篇 Markdown 格式的知识库文档。整个流程会走完依赖准备、数据清洗、模板渲染、命令行封装这四个阶段。
3.1 环境准备:Node.js 和 npm 依赖安装
我选择用 Node.js 来写这个小工具,原因很朴素:跨平台、自带文件模块、npm 生态里有现成的命令行参数解析库,团队里前端同学也能改。
先建项目并初始化:
mkdir bm-md && cd bm-md npm init -y npm install commander marked fs-extracommander用来解析命令行的--input、--output这类参数;marked是可选的,用来在本地快速验证生成的 Markdown 能否被正常渲染成 HTML,方便预览;fs-extra是对 Node 原生文件模块的增强,创建目录和拷贝文件时省不少事。
注意:如果你是国内网络环境,npm 安装依赖慢或者失败,可以临时换个 registry,例如
npm config set registry https://registry.npmmirror.com,装完再换回来。我在团队里初始化环境时就被这一步卡过二十分钟。
3.2 数据清洗:写一个专门的 cleanData 函数
拿到接口返回的 JSON 往往是脏的,例如字段名大小写不一致、某些字段有空值、标签数组里夹着空白字符串。直接拿这些东西生成 Markdown,渲染出来要么空荡荡,要么undefined满天飞。所以我在scripts/data-clean.js里写了一个清洗函数:
function cleanData(rawList) { return rawList .filter(item => item && item.title && String(item.title).trim() !== '') .map(item => { const tags = Array.isArray(item.tags) ? item.tags.map(tag => String(tag).trim()).filter(Boolean) : []; return { title: String(item.title).trim(), category: item.category ? String(item.category).trim() : '未分类', summary: item.summary ? String(item.summary).trim() : '暂无摘要', tags: tags, updatedAt: item.updatedAt || new Date().toISOString().split('T')[0] }; }) .sort((a, b) => a.category.localeCompare(b.category, 'zh-Hans-CN')); }这里面的逻辑有几个值得抠一下的点。第一,过滤条件里只滤掉了连标题都没有的数据,因为标题是一篇文档的骨架,没标题的条目生成出来没有意义;摘要和分类为空我给了默认值,保证文档完整。第二,sort按分类的中文拼音排序,这样做出来的清单不是随机堆砌,而是有分组的阅读体验。第三,toISOString().split('T')[0]是拿当天日期的稳定写法,比new Date()直接拼字符串格式靠谱,还带时区转换,避免了时区偏移导致日期差一天的问题。
3.3 模板渲染:占位符替换与 Markdown 表格生成
数据清洗完,接下来就是把干净数据填进 Markdown 模板。这一步是整个工具的灵魂。我新建了一个templates/knowledge-base-template.md,内容大概是这样的:
# {{TITLE}} > 更新日期:{{DATE}} ## 目录 {{TOC}} --- {{CONTENT}}对应地,在scripts/build-md.js里,我将数据渲染成 Markdown 的正文内容。生成目录和生成表格这两块最容易写错,我在这里多写几句:
function generateToc(list) { const categories = [...new Set(list.map(item => item.category))]; return categories.map(cat => `- [${cat}](#${cat})`).join('\n'); } function generateContent(list) { return list.map(item => { const tagStr = item.tags.length > 0 ? item.tags.map(tag => `\`${tag}\``).join(' ') : '无'; return [ `## ${item.category}`, '', `### ${item.title}`, '', `- 标签:${tagStr}`, `- 更新日期:${item.updatedAt}`, '', `${item.summary}`, '' ].join('\n'); }).join('\n'); }这个设计比直接拼一个大字符串要聪明的地方在于,我把“章节目录”和“正文内容”分开生成,模板只需要关心整体布局,细节变化交给函数去管。目录里的锚点链接#${cat}和后面的## ${item.category}要保持一致。中文标题的锚点在不同渲染器里规则有差异,GitHub 会自动处理中文标点和空格,但如果你不确定,最稳妥的办法是让分类也用简单的英文 slug 作为维护字段,避免锚点失效。这一条算是我自己在编写过程中踩过最隐蔽的坑之一。
main 函数负责把它们拼起来并写出文件:
const fs = require('fs-extra'); const path = require('path'); const { program } = require('commander'); program .option('-i, --input <path>', 'input JSON file') .option('-o, --output <path>', 'output md file') .parse(process.argv); async function main() { const options = program.opts(); if (!options.input || !options.output) { console.error('请提供 --input 和 --output 参数'); process.exit(1); } const rawData = await fs.readJson(path.resolve(options.input)); const cleanDataList = cleanData(rawData); const template = await fs.readFile( path.resolve('templates/knowledge-base-template.md'), 'utf-8' ); const title = '内部知识库清单'; const date = new Date().toISOString().split('T')[0]; const toc = generateToc(cleanDataList); const content = generateContent(cleanDataList); const finalMd = template .replace('{{TITLE}}', title) .replace('{{DATE}}', date) .replace('{{TOC}}', toc) .replace('{{CONTENT}}', content); await fs.ensureDir(path.dirname(path.resolve(options.output))); await fs.writeFile(path.resolve(options.output), finalMd, 'utf-8'); console.log(`已生成: ${options.output}`); } main().catch(err => { console.error(err); process.exit(1); });3.4 命令行封装:把bm md变成一条真正的命令
写到这里,脚本已经能在项目目录里跑了,但我不满足于node scripts/build-md.js -i data.json -o output.md这种输入方式,我想把它封装成一条真正的bm md命令。这一步用 npm 的bin字段很好解决。
在package.json里加入:
{ "bin": { "bm": "./scripts/bm-cli.js" } }在scripts/bm-cli.js里加上子命令分发逻辑:
#!/usr/bin/env node const { program } = require('commander'); program .command('md') .description('从 JSON 生成 Markdown 文档') .option('-i, --input <path>', 'input JSON path') .option('-o, --output <path>', 'output Markdown path') .action(async (cmdObj) => { const build = require('./build-md'); await build(cmdObj); }); program.parse(process.argv);然后在项目根目录执行npm link,把命令软链到全局。这样之后在终端里敲:
bm md -i ./data/raw/knowledge.json -o ./output/knowledge.md一条命令直接出文档。如果你想用文件夹方式批量跑,可以再扩展一层,遍历目录下所有 JSON 文件分别生成对应的 Markdown,代码本质上就是在外面加一个fs.readdir循环。我用这个方式把陆续积攒的月度数据全部一次性生成了对应的报告目录,节省的时间非常可观。
4. 常见问题与排查技巧实录:生成 Markdown 最容易踩的五个坑
把工具跑通只是第一步,真正让它变得可靠是后面连续踩坑和修 bug 的过程。下面这几个问题不是偶发的,几乎每个用脚本生成 Markdown 的人都会碰见,我按出现频率从高到低列成一张速查表:
| 问题现象 | 根本原因 | 快速排查方法 | 解决方案 |
|---|---|---|---|
代码块里的#被渲染成标题 | 代码块反引号数量不足或未闭合 | 检查原文本中是否存在单个反引号干扰 | 代码块统一用三个反引号,并在代码块前加空行 |
| 表格渲染错位,列数不一致 | 某一行少写一个| | 用 Python 或 Node 脚本按|切分校验每行列数 | 在生成函数里对每行列数做断言 |
| Markdown 文件里中文锚点失效 | 锚点含中文、空格或特殊字符 | 用浏览器打开 HTML 点击目录测试 | 改用英文 slug 或预生成带 id 的标题 |
| 生成的文档在 GitHub 上列表没有缩进层级 | 子列表没有缩进空格 | 查看显示为纯文本时子项前方是否有空格 | 子项统一缩进两个空格 |
| 换行不生效,变成同一行 | 行尾没有两个空格或缺少空行 | 确认相邻段落之间是否有空行 | 段落间必然加一个空行 |
这里我挑几个最典型的展开讲。
第一个坑是表格里的管道符。清洗后的摘要文本里经常出现|这个字符,用户口述里的“A 或 B”写成了A|B,放进 Markdown 表格后直接让那一行多出一列,整个表格立刻乱了。我的解决方案是在生成表格内容之前,对摘要字段做一次replace(/\|/g, '\\|')转义。这个方法效率最高,一劳永逸。
第二个坑是 Windows 平台和 macOS 平台执行命令时,文件路径里的反斜杠不一致。我在一次分享中演示时,文件夹路径里刚好有个\t开头,结果被转义成了制表符,文件直接写到了莫名其妙的位置。后来我统一使用path.resolve()处理路径参数,不在代码里手写任何含反斜杠的硬编码路径,问题就消失了。
第三个坑是 npm link 之后命令行提示找不到命令。多半是bin指向的 js 文件没有执行权限,或者在 Linux/macOS 上忘记给文件加执行位。解决办法是运行chmod +x scripts/bm-cli.js,然后重新npm link。Windows 上有时候还要检查是否用了管理员权限运行终端,这个问题很容易被忽视。
第四个坑是模板里的占位符被替换后又跑了一遍脚本,导致同一篇文档里出现两次相同内容。这是因为模板中用了{{TITLE}},但正文里的某个代码示例也写了类似的字符串。后来我在占位符上加了前缀,改成{{BM:TITLE}},这样即便原文文本里有模板语法,也不会被误替换。
第五个坑更隐蔽:我起初用marked在本地渲染生成预览,但marked的表格和 GitHub 的表格解析规则有细微差异,有些语法marked能渲染出来,推到 GitHub 上却显示异常。现在我的建议是,本地预览可以另选更贴近 GitHub 风格的渲染库,比如markdown-it,支持配置项接近 GitHub,能提前暴露很多渲染问题。
注意:在使用脚本批量生成之前,一定要先看几份生成的样本,放到目标平台(GitHub、语雀、公司 Wiki)上渲染确认。不要一次性批量生成几百个文件之后才发现模板里有一个小符号配错,返工成本非常高。
5. 进阶玩法:从 Markdown 文档反推数据模型和批处理
工具能跑通之后,我开始琢磨怎么让它的适用范围更广。既然数据能变成 Markdown,反过来,能不能从一堆 Markdown 文件里把结构化数据抽取出来?答案是肯定的。Markdown 本身就是一种轻量结构化格式,用正则和简单的解析器就能把标题、表格、列表提取出来。我在一个知识库整理项目里,就用 Node 脚本把几百个.md文件里的“一级标题 + 表格第三列”抽取成了一份 JSON 清单,整个过程不到 20 秒。
这种逆向操作最大的价值在于“格式即协议”。当你的工具链里所有文档都遵守同一种结构时,你可以在文档和 API 之间自由地转换。bm md的定位也因此从“生成文档的命令”变成了“业务数据和展示层之间的一座桥”。它不涉及任何复杂的渲染框架,也不用依赖在线服务,是一个完全本地、可审计、可测试的管道。
批量处理也值得一提。在实际工作里,数据往往不是一条而是一批。某个系统的数据导出为 300 个 JSON 文件,需要生成 300 个对应的 Markdown 页面并归入不同的目录。这时在入口脚本里加一个遍历即可:
for file in ./data/raw/*.json; do bm md -i "$file" -o "./output/$(basename "$file" .json).md" done技术上这种批跑很简单,但你要注意问题:如果一个文件生成失败,整个 for 循环会不会中断?我的建议是给build-md.js的入口包一层try...catch,失败时只打印文件名和错误信息,不中断后续文件。很多同学在这一步栽过跟头,一旦某份数据里有特殊字符导致脚本崩溃,后面 299 个文档全都不生成了,这种体验很容易让人烦躁。
把批处理加上之后,整个工具链已经完全可以胜任日常的知识库自动同步需求:数据库定时导出 JSON,脚本清洗数据,bm md批量生成 Markdown 文档,然后通过 Git 提交,推送到远程仓库。整个过程无非就是一行 cron 任务的事儿,效率和我最初手写文档相比完全不是一个量级。
6. 扩展集成:怎么把 Markdown 自动化流转到 Word 或其他下游
文档生成出来之后很多时候并不是终点。工作里更常见的诉求是:Markdown 只是中间格式,最后要交成 Word、PDF,或者喂给大模型做后续处理。这里我把我试过的几条路径整理一下,每一条都不复杂,但偏偏很多人绕远路了。
6.1 Markdown 转 Word:用 Pandoc 就够了
如果只是把 Markdown 转成 Word,我强烈推荐用 Pandoc,而不是在浏览器里复制粘贴然后手动调格式。命令相当的简单:
pandoc input.md -o output.docx如果需要自定义样式,可以引用一个参考文档:
pandoc input.md --reference-doc=custom-reference.docx -o output.docx前提是先准备一个custom-reference.docx,里面预设好字体、标题颜色、表格样式,Pandoc 会按这个模板的样式渲染所有内容。这个方案适合交付正式报告,效果比在 Word 里手动重排稳定得多。我在做季度汇报时就用bm md生成中间 Markdown,再用 Pandoc 转成正式 Word,整个流程从改数据到出文档只需要五分钟。
6.2 Markdown 交给大模型:别把格式搞太花
Markdown 交给大模型做上下文,不需要追求花哨的排版,反而要追求信息的紧凑和结构清晰。我通常会把一篇文章拆成 4 到 6 级标题、表格、列表这几类有限的模式,避免使用复杂嵌套和大量行内样式。原因是大模型理解纯文本结构的能力很强,但遇到过长或者嵌套过深的结构时,有些模型容易出现定位偏差。用bm md自动生成的文档天然就满足这种“结构有限但语义明确”的特征,因为模板是我定的,生成的格式完全可控。这一点在 Coze 或各类知识库问答场景里,直接决定了检索和回答的效果能到什么程度。
之前为了在一套工作流里把 Markdown 喂给大模型做知识库召回,我踩过不少坑,其中最严重的是文档里塞了大量非必要的 HTML 标签。后来我把所有模板统一改成纯 Markdown 语法,bm md生成的文档再进模型,命中率和流畅度都肉眼可见的提升了。
6.3 浏览器里预览 Markdown:Chrome 插件的思路
还有一种需求是快速预览,不想本地装一堆开发环境。Chrome 上有很多 Markdown 预览插件,装一个就能把.md文件拖进浏览器看渲染效果。这类插件一般支持 GitHub 风格和自定义 CSS,适合非技术同事快速审阅。我有时候临时写个.md发群里,同事没安装任何编辑器,用 Chrome 插件打开看一眼就够了。用 VSCode 的同学也可以用 Markdown Preview Enhanced,预览功能很强大,支持图表和导出,但要注意上面说的“预览器兼容性”问题——它渲染没问题不代表 GitHub 上也一定没问题,最终以目标平台的渲染为准。
7. 结语:bm md背后真正的价值是“文档即代码”
把这条命令搭完,我最深的体会是:写 Markdown 本身并不难,难的是让文档的生成、更新、流转变得可控。bm md这个工具本质上把文档变成了代码的一部分,模板放在版本控制里,数据源直接从接口读,任何人对文档做的修改都是可审计可回退的。
如果你也想搭一把,我建议不要一上来就模仿别人做得很重的框架,先想清楚三件事:你的 Markdown 主要是给谁看的,数据源是从哪里来的,以及最终交付目标是什么平台。这三个问题回答清楚了,再动手写代码,基本不会走偏。
最后还有一个小技巧,很多人在生成表格时喜欢手写对齐方式,实测下来,程序生成时直接全部使用左对齐最省心,因为自动判断每一列的对齐方式在中文场景下反而容易出幺蛾子。少一些花哨指令,模板更容易维护,最终文档的稳定性也更高。