
不用装一堆花里胡哨的编辑器也不用对着在线网页担心格式丢失一条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-doccustom-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 浏览器里预览 MarkdownChrome 插件的思路还有一种需求是快速预览不想本地装一堆开发环境。Chrome 上有很多 Markdown 预览插件装一个就能把.md文件拖进浏览器看渲染效果。这类插件一般支持 GitHub 风格和自定义 CSS适合非技术同事快速审阅。我有时候临时写个.md发群里同事没安装任何编辑器用 Chrome 插件打开看一眼就够了。用 VSCode 的同学也可以用 Markdown Preview Enhanced预览功能很强大支持图表和导出但要注意上面说的“预览器兼容性”问题——它渲染没问题不代表 GitHub 上也一定没问题最终以目标平台的渲染为准。7. 结语bm md背后真正的价值是“文档即代码”把这条命令搭完我最深的体会是写 Markdown 本身并不难难的是让文档的生成、更新、流转变得可控。bm md这个工具本质上把文档变成了代码的一部分模板放在版本控制里数据源直接从接口读任何人对文档做的修改都是可审计可回退的。如果你也想搭一把我建议不要一上来就模仿别人做得很重的框架先想清楚三件事你的 Markdown 主要是给谁看的数据源是从哪里来的以及最终交付目标是什么平台。这三个问题回答清楚了再动手写代码基本不会走偏。最后还有一个小技巧很多人在生成表格时喜欢手写对齐方式实测下来程序生成时直接全部使用左对齐最省心因为自动判断每一列的对齐方式在中文场景下反而容易出幺蛾子。少一些花哨指令模板更容易维护最终文档的稳定性也更高。