Inner processor
嵌套 DSL 怎么 work——layout 内的 callout / stepper 里的 interact 是怎么被识别和渲染的。 集成方常见 bug 来源:忘
registerAllInnerPlugins()。
问题:为什么需要 inner processor
考虑这段嵌套:
\```layout grid cols=2
:::card accent=blue
> [!INFO]
> Callout 嵌在 card 里
:::
:::card accent=red
\```interact
slider x 0 10 5 1
template stl: [{x}]
\```
:::
\```
外层 unified processor 跑 remarkLayout 解析到 :::card block——拿到 card 的内部内容字符串。
但 card 内部还有 markdown(callout / interact)需要再解析一次。
remarkLayout plugin 不知道外层注册了哪些 Rho remark plugin——它只知道自己负责 layout。
所以它不能自己 re-run 完整 pipeline。
这就是 inner processor 的作用:一个全局共享的 secondary processor,专门处理嵌套内容。
注册流程
import { registerAllInnerPlugins } from '@rho/md';
registerAllInnerPlugins(); // 1. 在外层 processor build 之前调
做什么:把 remarkCallout / remarkLayout / remarkAnnotate / remarkTabs / remarkStepper / remarkTimeline / remarkModal 全部注册到 inner processor 单例。
调用时机:应用启动时调一次——之后所有嵌套解析都用同一组 plugin。
单独注册
registerAllInnerPlugins() 是 convenience。如果你只想支持部分嵌套:
import { registerInnerPlugin, remarkCallout, remarkLayout } from '@rho/md';
registerInnerPlugin(remarkCallout);
registerInnerPlugin(remarkLayout);
// 之后嵌套支持 callout + layout,但不支持 stepper / tabs / 等
⚠️ 注意:选择性注册的常见错误是忘了对应的 outer plugin。比如外层 processor 注册了 remarkTabs 但 inner 没注册——结果是顶层 tabs 工作,但 layout 内的 tabs 不工作。保持 outer 和 inner 注册集合一致。
嵌套解析流程
外层 processor 跑 markdown:
-> remarkParse → AST
-> remarkGfm → AST
-> remarkLayout → 找到 :::card;提取 card 内部 string
│
▼
调 inner processor
│
-> remarkCallout → 处理 > [!INFO]
-> 其他 inner plugins
│
▼
inner 输出 HTML 片段
│
▼
嵌入 layout 的 card HTML
-> remarkRehype → ...
-> rehypeStringify → 最终 HTML
关键事实:inner processor 不重复跑 remarkParse —— 嵌套内容会被 inner 处理后再合并。Rho 内部细节封装好,集成方只看到"嵌套自然 work"。
调试嵌套不渲染
最常见症状:外层 layout 内的 callout 显示为字面量 > [!INFO] ...。
排查清单:
- ✅ 外层 processor
processor.use(remarkLayout)注册了? - ✅ 外层 processor
processor.use(remarkCallout)注册了?(callout 在顶层也工作?) - ❌
registerAllInnerPlugins()调了? ← 大概率是这个
修复:在 build pipeline 启动处加一行:
import { registerAllInnerPlugins } from '@rho/md';
registerAllInnerPlugins();
应放在任何 processor.process() 调用之前——通常在 module 顶层 / app entry。
parseInner(content) —— 高级 API
如果你想手动调 inner processor(例如自己写新的 remark plugin 需要解析嵌套内容):
import { parseInner } from '@rho/md';
const innerHTML = parseInner('> [!INFO]\n> nested content');
// → '<aside class="rho-callout">...</aside>'
普通集成不需要这个 API——它是给写 Rho 的扩展 plugin 的人用的。
性能 / 限制
嵌套层数
每嵌套一层 = inner processor 跑一次。实测开销(2025 年 V8):
| 嵌套层 | 增量耗时 |
|---|---|
| 1 | < 1ms |
| 2 | ~2-5ms |
| 3 | ~10-20ms |
| 4 | ~50ms+ |
写作者侧建议 ≤ 3 层(详见 Nested DSL (writer))。集成方需要告诉用户这个限制,避免他们写极深嵌套。
inner processor 是单例
整个应用只有一个 inner processor 实例。
- 优点:性能好、配置一次到处用
- 缺点:不能多 "flavor"(不能 page A 用 plugin set X,page B 用 set Y)
如果真需要多 flavor,目前的 workaround 是重新调 registerAllInnerPlugins——但这会影响全局。通常单一注册集合就够。
嵌套 fence 的反引号约定
嵌套 fenced code 时反引号数量必须不同:
\````layout ← 4 反引号外层
:::card
\```interact ← 3 反引号内层
slider x 0 10 5 1
\```
:::
\````
inner processor 也按 markdown 标准处理——外层反引号数 > 内层 或反过来都行,但不能相同。
写作者文档 Nested DSL pitfall A1 提到了这个,但作为集成方你的用户可能踩——可以在你的 reader UI / 错误提示里加 hint。
自己实现 reader 时的嵌套
如果你不用 @rho/md 而从 spec 自实现 reader(见 Implementing your own reader),需要自己设计 inner processor 等价机制:
- 解析
:::card时拿到 card 内部 string - 对 string 再跑一次完整 markdown + Rho DSL pipeline
- 把结果嵌进 card HTML
不一定要单例——可以是函数 / 上下文传递 / 任何你的设计。关键是 "嵌套内容必须能再走一遍完整 pipeline"。
See also
- Quick start —
registerAllInnerPlugins()在最简集成里的位置 - Remark plugins — 各 plugin 的 inner registration 行为
- API reference
- Nested DSL (writer) — 写作者侧的嵌套规则
- Implementing your own reader — 自实现时的嵌套设计