
从 pandoc、harper 和 dprint 出发,分别练习读懂一份文档树、一张 936 条的语法规则表与一个只负责跑插件的格式化引擎,并给出今晚能跑通的最小路径。
.wasm 插件里。$ cat d1.yaml
toc: no
$ pandoc-3.12.1 --defaults=d1.yaml -f markdown -t markdown toc.md
# One
## Two {#two}
text
$ pandoc-3.13 --defaults=d1.yaml -f markdown -t markdown toc.md
Error parsing d1.yaml:
d1.yaml:1:6: toc: expected Bool, but encountered String
|
1 | toc: no
| ^data Pandoc = Pandoc Meta [Block]:元数据,加一串块。4pandoc --list-input-formats 数出 52 种输入格式,--list-output-formats 数出 77 种输出。每种格式一个模块,引擎只在中间那棵树上做事。-t native 得到 Haskell 字面量,加 -t json 得到 JSON。先看结构,再决定改哪里。$ printf '# Hi\n\nSome *text* with a [link](https://example.com).\n' | pandoc -f markdown -t native
[ Header 1 ( "hi" , [] , [] ) [ Str "Hi" ]
, Para
[ Str "Some" , Space , Emph [ Str "text" ] , Space , Str "with"
, Space , Str "a" , Space
, Link ( "" , [] , [] ) [ Str "link" ] ( "https://example.com" , "" )
, Str "."
]
]Header 1 加一个属性三元组,*text* 变成 Emph,链接变成 Link 加目标地址。pandoc 二进制 168,374,480 字节,Lua 5.4 和全部模板都编在里面了。curl -LO https://github.com/jgm/pandoc/releases/download/3.13/pandoc-3.13-linux-amd64.tar.gz
tar xzf pandoc-3.13-linux-amd64.tar.gz
./pandoc-3.13/bin/pandoc --versionpandoc 3.13,接着几行写着 Features: +server +lua 和 Scripting engine: Lua 5.4。-t json 导出来,jq '.blocks[0]' 看第一块长什么样。 function Header(el)
return pandoc.walk_block(el, {
Str = function(s) return pandoc.Str(string.upper(s.text)) end
})
end
function Link(el)
el.content:insert(pandoc.Str(" (" .. el.target .. ")"))
return el
end
```
跑出来的标题内容是 `[ Str "HELLO" , Space , Str "WORLD" ]`,链接内容是 `[ Str "link" , Str " (https://example.com)" ]`。
3. **不写 Lua 也行。** 把 `-t json` 的结果用 Python 改一遍:每个段落后面插一个 `{"t":"HorizontalRule"}`,再 `-f json -t markdown` 打回去,段落之间就多了分隔线。
4. **量一次转换成本。** 858,081 字节的 Markdown 转成 HTML 用 1.25 秒,转成 JSON 0.75 秒,转成 docx 1.14 秒,转回 Markdown 1.34 秒。JSON 中间物是 5,443,027 字节,比原文大六倍多。
5. **走一趟往返。** `-t docx` 再 `-f docx -t native`,AST 和原来逐字一致:在 docx 这条路上,格式转换只换了外层容器。
### 版本动作与限制 {#版本动作与限制}
- 装 3.13,许可 GPL-2.0。<MarkdownCitation index="6" title="pandoc 仓库" url="https://github.com/jgm/pandoc" />
- 检查自己的 defaults 与模板里有没有裸写的 `yes`/`no`/`on`/`off`/`y`/`n`。它们现在是字符串,在模板里一律按真值处理。<MarkdownCitation index="1" title="pandoc 3.13 发布说明" url="https://github.com/jgm/pandoc/releases/tag/3.13" />
- AST 定义住在单独的 pandoc-types 包里,有自己的版本号;本机导出的 JSON 第一项 `pandoc-api-version` 是 `[1,23,1,2]`。<MarkdownCitation index="4" title="pandoc-types 的文档定义" url="https://github.com/jgm/pandoc-types/blob/master/src/Text/Pandoc/Definition.hs" />
## 2. harper:九百三十六条规则,二十一个类别 {#2-harper九百三十六条规则二十一个类别}
**仓库**:[Automattic/harper](https://github.com/Automattic/harper) · Rust · Apache-2.0 · 17,151 星
### 为什么今天看 {#为什么今天看-2}
harper 3.0.0 在北京时间 10 月 27 日 06:54 发布(UTC 2026-10-26T22:54Z),离这篇文章动笔两个小时。上一版 2.13.0 在 10 月 16 日 22:54。<MarkdownCitationGroup><MarkdownCitation index="7" title="harper v3.0.0 发布记录" url="https://github.com/Automattic/harper/releases/tag/v3.0.0" /><MarkdownCitation index="8" title="harper v2.13.0 发布记录" url="https://github.com/Automattic/harper/releases/tag/v2.13.0" /></MarkdownCitationGroup>
发布页正文很短:二十一个 crate 一起跳到大版本,正文是打包工具自动生成的清单。<MarkdownCitation index="7" title="harper v3.0.0 发布记录" url="https://github.com/Automattic/harper/releases/tag/v3.0.0" />
真正的改动在提交里:从 2.13.0 到 3.0.0 一共 55 个提交、198 个文件。<MarkdownCitation index="9" title="harper:2.13.0 到 3.0.0 的提交对比" url="https://github.com/Automattic/harper/compare/v2.13.0...v3.0.0" />
其中带破坏性标记的只有一条,#2630「词典里的字母大小写处理更明确了」。剩下大多是新增规则:gonna / gotta / wanna、altar 与 alter、passed 与 past、把当动词用的 preference 改成 prefer。<MarkdownCitation index="9" title="harper:2.13.0 到 3.0.0 的提交对比" url="https://github.com/Automattic/harper/compare/v2.13.0...v3.0.0" />
大版本号来自那一条破坏性改动;这半个月新增的规则几十条,凑不成 3.0.0。
### 最值得拆的机制 {#最值得拆的机制-2}
- **第一步把字符切成带词性的 token。** 词性标注和分块用的是两个训练好的模型,`harper-brill` 目录里存成 JSON:tagger 模型 659,686 字节,chunker 模型 52,717 字节。<MarkdownCitation index="10" title="harper 的 brill 词性标注模块" url="https://github.com/Automattic/harper/tree/master/harper-brill" />
- **第二步让每条规则自己去找。** `Linter` trait 只有两个方法:`lint(&mut self, document)` 返回一组问题,`description()` 返回一句给人看的说明。<MarkdownCitation index="11" title="harper 的 linting 模块" url="https://github.com/Automattic/harper/blob/master/harper-core/src/linting/mod.rs" />
- **每条规则住在一个文件里。** `harper-core/src/linting/` 下有 368 个文件,`a_part.rs`、`ajar.rs`、`very_unique.rs` 各自独立成篇。<MarkdownCitation index="11" title="harper 的 linting 模块" url="https://github.com/Automattic/harper/blob/master/harper-core/src/linting/mod.rs" />
- **规则表能整张导出。** 运行时把配置导成 JSON:936 条规则,18,619 字节,每条的值是 `null`(用默认)、`true` 或 `false`。<MarkdownCitation index="12" title="harper.js 文档" url="https://writewithharper.com/docs/harperjs/introduction" />
- **问题分二十一类。** 拼写、重复、格式、标点、语法、用词、搭配各算一类,类别名写在每条 lint 上。<MarkdownCitation index="12" title="harper.js 文档" url="https://writewithharper.com/docs/harperjs/introduction" />
### 复现难度:中低 {#复现难度中低}
3.0.0 的发布页上没有预编译的命令行工具,能下的只有两个源码包。<MarkdownCitation index="7" title="harper v3.0.0 发布记录" url="https://github.com/Automattic/harper/releases/tag/v3.0.0" />
最省事的路是 npm 上的 `harper.js`:包里带了编译好的 WebAssembly,十几行脚本就能看到结果。<MarkdownCitation index="12" title="harper.js 文档" url="https://writewithharper.com/docs/harperjs/introduction" />
本机装它用了 0.7 秒,`node_modules` 占 91 MB。包里同时塞了完整版与精简版两份 20 MB 的 wasm,外加两份 26 MB 的内联副本,体积大半花在这里。
### 最小上手路径 {#最小上手路径-2}
```bash
mkdir harper-try && cd harper-try
npm init -y
npm install harper.jscheck.mjs:import { LocalLinter, createBinaryModuleFromUrl } from 'harper.js';
const wasm = new URL('./node_modules/harper.js/dist/harper_wasm_bg.wasm', import.meta.url);
const linter = new LocalLinter({ binary: createBinaryModuleFromUrl(wasm.href) });
await linter.setup();
const text = process.argv[2] ?? 'I definately have went to the the store.';
for (const lint of await linter.lint(text, { language: 'plaintext' })) {
const fix = lint.suggestions().map((s) => s.get_replacement_text()).join(' / ');
console.log(`${lint.lint_kind_pretty()}: ${lint.message()} → ${fix}`);
}node check.mjs 的输出:Spelling: Did you mean to spell `definately` this way? → definitely / defiantly / delicately
Grammar: Use the past participle `gone` instead of `went` when using compound tenses or passive voice. → gone
Repetition: Did you mean to repeat this word? → theawait linter.getLintConfigAsJSON() 拿到整张表,数 key 就是 936;getLintDescriptionsAsJSON() 里每条规则都有一句英文说明。SpellCheck 改成 false,再 setLintConfigWithJSON,同一句的命中从 2 条变成 0 条。He are happy. 被抓,类别是 Agreement,提示让动词与主语一致。This are a test.、These is a test.、There is many reasons for this.、The list of items are long. 四条全部原样放过。九百多条规则是一条条写的,覆盖范围看得见。The colour of the analyse was gray. 在美式英语下报两条(colour、analyse),setDialect(Dialect.British) 之后只剩 gray 一条。language: 'markdown' 只报代码块外面的错,language: 'plaintext' 连代码块里的错词一起报。行内代码同样被跳过。.weir 文件,写六行: expr main (in order to)
let message "Use `to` instead of `in order to`."
let description "Prefer the shorter infinitive marker."
let kind "WordChoice"
let becomes "to"
test "We left early in order to catch the train." "We left early to catch the train."
```
用 `packWeirpackFiles` 打包成 506 字节,再 `loadWeirpackFromBlob` 装进 linter。规则数从 936 变成 937,`We left early in order to catch the train.` 立刻多出一条命中。装包时它会先跑文件里那行 `test`,用例不通过就当场报错。<MarkdownCitation index="13" title="harper 的 Weir 规则语言" url="https://writewithharper.com/docs/weir" />
### 版本动作与限制 {#版本动作与限制-2}
- 装 3.0.0,许可 Apache-2.0。<MarkdownCitation index="14" title="harper 仓库" url="https://github.com/Automattic/harper" />
- 词典的大小写处理在 3.0.0 变了(#2630)。自己往词典里加过词的话,升级后要重看一遍。<MarkdownCitation index="9" title="harper:2.13.0 到 3.0.0 的提交对比" url="https://github.com/Automattic/harper/compare/v2.13.0...v3.0.0" />
- 它只管英语,README 说核心可以扩到别的语言。<MarkdownCitation index="14" title="harper 仓库" url="https://github.com/Automattic/harper" />
- 主语与动词一致这类规则覆盖不全,见上面第 3 条练习。<MarkdownCitation index="11" title="harper 的 linting 模块" url="https://github.com/Automattic/harper/blob/master/harper-core/src/linting/mod.rs" />
- 想要命令行,得从 [crates.io](https://crates.io) 编 `harper-cli`,本机没试这条路。<MarkdownCitation index="7" title="harper v3.0.0 发布记录" url="https://github.com/Automattic/harper/releases/tag/v3.0.0" />
## 3. dprint:引擎不认识任何一门语言 {#3-dprint引擎不认识任何一门语言}
**仓库**:[dprint/dprint](https://github.com/dprint/dprint) · Rust · MIT · 4,117 星
### 为什么今天看 {#为什么今天看-3}
dprint 0.64.3 在北京时间 10 月 23 日 09:32 发布(UTC 2026-10-23T01:32Z),0.64.2 在它之前 88 分钟。<MarkdownCitationGroup><MarkdownCitation index="15" title="dprint 0.64.3 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.3" /><MarkdownCitation index="16" title="dprint 0.64.2 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.2" /></MarkdownCitationGroup>
0.64 这条线从 10 月 19 日的 0.64.0 走到 10 月 23 日,四个版本里有三个在补插件运行时与配置边界。wasmtime 升到 50、`call_indirect` 变便宜、wasm 插件去掉原生 unwind 信息、支持带异常处理的插件。<MarkdownCitationGroup><MarkdownCitation index="16" title="dprint 0.64.2 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.2" /><MarkdownCitation index="17" title="dprint 0.64.0 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.0" /><MarkdownCitation index="18" title="dprint 0.64.1 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.1" /></MarkdownCitationGroup>
0.64.3 修的是配置边界:插件关联的通配路径以前能把文件遍历带出配置文件所在的目录,现在带不出去了。<MarkdownCitation index="15" title="dprint 0.64.3 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.3" />
### 最值得拆的机制 {#最值得拆的机制-3}
- **插件是独立的可执行单元。** 一个 `.wasm` 文件,任何能编到 WebAssembly 的语言都行;或者一个独立进程,任何语言都行。宿主只认那个通用接口,不管里面的排版怎么实现。<MarkdownCitation index="19" title="dprint 插件开发文档" url="https://dprint.dev/plugin-dev/" />
- **两种插件的待遇不同。** wasm 插件在沙箱里跑;进程插件跑在沙箱外,所以要额外给一个校验和。<MarkdownCitation index="20" title="dprint 插件列表" url="https://dprint.dev/plugins/" />
- **宿主干四件通用的事。** 遍历文件、读配置、缓存插件、在 wasm 里跑插件。它不知道 TypeScript 长什么样。
- **管哪些文件由插件自己说。** 插件声明自己负责的扩展名,宿主据此决定哪些文件进队列。
- **npm 包只是一层外壳。** 写 `npm:@dprint/typescript@0.96.1`,dprint 会下载这个包,从里面取出真正的 `plugin.wasm` 再编译。
### 复现难度:低 {#复现难度低-2}
发布页给的是静态二进制,解开就是一个文件。
### 最小上手路径 {#最小上手路径-3}
```bash
curl -LO https://github.com/dprint/dprint/releases/download/0.64.3/dprint-x86_64-unknown-linux-gnu.zip
unzip dprint-x86_64-unknown-linux-gnu.zip && chmod +x dprint
./dprint --version
mkdir try && cd try && ../dprint init -yinit -y 会看当前目录里有什么文件,自己挑插件并写 dprint.json。我放了一个 .ts 和一个 .json,它写出的是:{
"typescript": {},
"json": {},
"excludes": ["**/node_modules", "**/*-lock.json"],
"plugins": ["npm:@dprint/typescript@0.96.1", "npm:@dprint/json@0.25.2"]
}dprint fmt 先把插件编成 wasm:Compiling /home/you/.cache/dprint/cache/npm/registry.npmjs.org/@dprint/json@0.25.2/plugin.wasm
Compiling /home/you/.cache/dprint/cache/npm/registry.npmjs.org/@dprint/typescript@0.96.1/plugin.wasm
Formatted 2 files.plugin.wasm 是 4,174,185 字节,markdown 的 824,737 字节,json 的 658,096 字节。这三个数字就是「语言知识」的体积。dprint format-times 会逐文件报耗时。dprint fmt,屏幕上没有任何输出;dprint check 的退出码是 0。这条性质是它能进 CI 的前提。dprint.json 的 plugins 里删掉 typescript,再跑 dprint file-paths:.ts 文件从清单里消失了,也不会报错。引擎手上没有这门语言的知识,就当它不存在。dprint config add https://plugins.dprint.dev/markdown-0.28.2.wasm 直接按 URL 加插件,dprint fmt 会下载一个独立的 .wasm 并编译。两种写法的差别只在下载方式。20.json,里面是插件交出来的全部信息:名字、版本、配置键、配置 schema 的地址、检查更新的地址。 {"source":"remote:https://plugins.dprint.dev/markdown-0.28.2.wasm","pluginKind":"Wasm",
"info":{"name":"dprint-plugin-markdown","version":"0.28.2","configKey":"markdown",
"configSchemaUrl":"https://plugins.dprint.dev/dprint/dprint-plugin-markdown/0.28.2/schema.json",
"updateUrl":"https://plugins.dprint.dev/dprint/dprint-plugin-markdown/latest.json"}}
```
### 版本动作与限制 {#版本动作与限制-3}
- 装 0.64.3,许可 MIT。<MarkdownCitation index="21" title="dprint 仓库" url="https://github.com/dprint/dprint" />
- 插件要联网下载,第一次运行得能出网;离线机器得先把 `.wasm` 拷进来。<MarkdownCitation index="19" title="dprint 插件开发文档" url="https://dprint.dev/plugin-dev/" />
- 版本线很密,0.64 这一线四天发了四个版本。升级前先读发布说明。<MarkdownCitation index="15" title="dprint 0.64.3 发布说明" url="https://github.com/dprint/dprint/releases/tag/0.64.3" />
- 删了插件却留着 `"typescript": {}` 这样的空段落,dprint 会报一条配置诊断。
- 官方插件七个,社区九个,包装别人格式化器的二十七个,剩下一个能把任何命令行格式化器包进来。<MarkdownCitation index="20" title="dprint 插件列表" url="https://dprint.dev/plugins/" />
## 怎么选今晚的第一条路径 {#怎么选今晚的第一条路径}
| 想补的那一层 | 仓库与本期信号 | 最小条件 | 第一条可观察路径 | 主要限制 |
|---|---|---|---|---|
| 中间物:文档树 | [pandoc](https://github.com/jgm/pandoc) · [`3.13`](https://github.com/jgm/pandoc/releases/tag/3.13) 北京时间 10 月 27 日 05:37 | 36 MB 静态包,零依赖 | `-t native` 看一棵树,再用 Lua filter 改一步 | GPL-2.0;`yes`/`no` 的行为变了 |
| 规则:文本规则表 | [harper](https://github.com/Automattic/harper) · [`v3.0.0`](https://github.com/Automattic/harper/releases/tag/v3.0.0) 北京时间 10 月 27 日 06:54 | Node 22,npm 装 91 MB | 跑十几行脚本看命中,再写一条 `.weir` 规则 | 只管英语;包体很胖;命令行要自己编 |
| 运行时:插件宿主 | [dprint](https://github.com/dprint/dprint) · [`0.64.3`](https://github.com/dprint/dprint/releases/tag/0.64.3) 北京时间 10 月 23 日 09:32 | 一个静态二进制,加一次联网 | 删掉一个插件,看 `.ts` 从文件清单里消失 | 插件要下载;版本线很密 |
三个工具把同一件事拆成两半:机器负责流程,知识放在机器外面。
从 pandoc 走到 dprint,知识离引擎越来越远。pandoc 的知识跟引擎住在同一个仓库,按格式切成模块;harper 的知识是运行期加载的 JSON 与 `.weir` 文本;dprint 的知识是别人编好、运行期下载的 `.wasm`。
今晚挑一个,把它的一条知识换掉,看要动几行引擎的代码。
github.com
github.com
pandoc.org
github.com
pandoc.org
github.com
github.com
github.com
github.com
github.com
github.com
writewithharper.com
writewithharper.com
github.com
github.com
github.com
github.com
github.com
dprint.dev
dprint.dev
github.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.