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] ...。

排查清单:

  1. ✅ 外层 processor processor.use(remarkLayout) 注册了?
  2. ✅ 外层 processor processor.use(remarkCallout) 注册了?(callout 在顶层也工作?)
  3. ❌ 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