Nested DSL

Rho 的所有 DSL 块(callout / layout / tabs / stepper / modal / interact / timeline)都能互相嵌套——你可以把 callout 放进 layout 卡片、把 interact 放进 stepper 步骤、把 tabs 放进 modal、把 layout 放进 timeline event ……组合无上限。


When to use nesting

  • ✅ 复合内容结构(layout 里每张卡有自己的 callout / 代码 / interact)
  • ✅ 教程式 demo(stepper 每一步含可交互的演示)
  • ✅ 多视图 + 隐藏深度(tabs 切视图,每个 tab 内含 modal 展开细节)
  • ✅ roadmap 里直接展示功能(timeline event 内嵌 demo)
  • ❌ 嵌套 4 层以上(视觉混乱,读者迷失)
  • ❌ 同类型自嵌套(tabs 套 tabs / stepper 套 stepper —— UI 混乱)

核心机制:Inner Processor

Rho 的每个容器型 DSL(callout / layout / tabs / stepper / modal / timeline)在解析自己的内部内容时,会再跑一次完整的 markdown + Rho DSL pipeline——这叫 inner processor。

外层 markdown parse
  ↓
遇到 ```layout 块
  ↓ inner processor 接管
  对 layout 内每个 :::card 内容**再 parse 一次** markdown + Rho DSLs
    ↓ 遇到 :::card 内的 ```interact
    ↓ 把 interact 也正常解析 + hydrate

结果:嵌套自然 work——你不用关心"谁先解析谁",只要语法正确,Rho 自动处理。

集成开发者注意:@rho/md 库需要先调 registerAllInnerPlugins(),否则嵌套 DSL 不识别。详见 Developer Reference.


推荐嵌套模式

模式 1:Layout + Callout(推荐)

\```layout grid cols=2

:::card accent=green
**推荐方案**

> [!INFO]
> A 方案适合 90% 用户,零配置。

详细描述……
:::

:::card accent=red
**慎用方案**

> [!WARN]
> B 方案需自维护服务器。

详细描述……
:::
\```

Card 提供布局空间,callout 提供语义高亮——职责分明,组合自然。

模式 2:Stepper + Interact(教程式 demo)

\```stepper
:::step title="第 1 步:理解参数"
拖一下滑块感受参数空间:

\```interact
slider rate 0 0.2 0.05 0.01
template stl:
[利率] -> [{(rate*100):.1f}%]
\```
:::

:::step title="第 2 步:完整公式"
现在加上本金和年限:

\```interact
slider rate 0 0.2 0.05 0.01
slider years 1 30 10 1
slider principal 1000 100000 10000 1000
computed compound = principal * pow(1+rate, years)
template stl:
[终值] -> [${compound:.0f}]
\```
:::
\```

教学型 walkthrough——每一步给小演示让读者动手感受,比纯文字效果好 10 倍。

模式 3:Tabs + Modal(多视图 + 深度可选)

\```tabs
:::tab title=快速入门
3 步搞定:
1. 安装
2. 写 demo
3. 渲染

\```modal trigger="完整安装文档(含各平台细节)"
(详细安装步骤……)
\```
:::

:::tab title=API 集成
\```js
import { hydrateAll } from '@rho/md';
hydrateAll(container);
\```

\```modal trigger="完整 unified pipeline 配置"
(详细 setup 代码……)
\```
:::
\```

主线在 tabs 里清爽,深度细节藏 modal——读者按需展开。

模式 4:Timeline + Layout 内嵌

\```timeline
:::event date="2026 Q1" title="✅ v0.3 - 基础能力"
\```layout grid cols=2
:::card accent=blue
12 项静态/交互 capability
:::
:::card accent=green
插件生态 baseline
:::
\```
:::

:::event date="2026 Q2" title="✅ v0.6 - 动画 + SVG"
新增 timer + computed + svg = 15 capability
:::
\```

Timeline event 内嵌 layout 做更结构化展示,比纯 prose 信息密度高。


哪些组合不推荐

1. 同类型自嵌套

\```tabs
:::tab title=外
\```tabs        ← ⚠️ tabs 套 tabs
:::tab title=内
\```
:::
\```

问题:两层 tab bar 视觉冲突,读者分不清外层和内层。

改进:把内层 tabs 提升到外层 tab content 之外,或用其他容器(modal / accordion 风格)。

同样逻辑:

  • ❌ stepper 套 stepper(两层进度条)
  • ❌ timeline 套 timeline(两条轴)
  • ❌ modal 套 modal(弹层叠加)
  • ❌ layout 套 layout(多层 grid 视觉破碎)

2. 嵌套 4 层以上

\```layout
:::card
\```stepper
:::step
\```tabs
:::tab
\```layout       ← 第 4 层 layout,读者迷路
:::
\```
:::
\```
:::
\```
:::
\```

3 层是舒适上限。4 层视觉嵌套读者已经看不清结构边界。重新设计 —— 把多层结构拍平到 2 层,或拆成多个独立块。

3. interact 套 interact(state 共享要用 namespace)

\```interact
slider x 0 10 5 1
\```interact          ← ❌ 这不是 nested DSL;这是两个独立 interact 块
slider y 0 10 5 1
\```
\```

问题:两个 interact 块默认 state 隔离——拖一个不影响另一个。 改进:用 Shared state (namespace):

\```interact namespace foo
slider x 0 10 5 1
\```

\```interact namespace foo
slider y 0 10 5 1
template stl:
[x+y] -> [{x + y}]
\```

性能 / 渲染顺序

1. Inner processor 不是 free

每嵌套一层就再跑一遍 markdown parse + 各 DSL plugin。深度大或重复多 → 性能下降。

实测经验(2025 年的浏览器 / @rho/md v0.1):

  • 1 层嵌套:< 1ms 增量
  • 2 层嵌套:~2-5ms
  • 3 层嵌套:~10-20ms
  • 4 层嵌套:~50ms+,可能感到卡

实用建议:保持 ≤ 3 层。

2. Hydration 顺序

外层 → 内层(自顶向下)。Layout / tabs / stepper 等容器先 hydrate,然后再 hydrate 容器内的 interact / modal / 等。通常你不需要关心,但调试 hydration bug 时知道顺序有用。

3. 重复定义的代价

嵌套时不要重复声明同一控件——用 shared-state 做 namespace 共享。

:::tab title="A 视图"
\```interact namespace data
slider rate 0 0.15 0.05 0.01
template stl: [利率] -> [{rate}]
\```
:::

:::tab title="B 视图"
\```interact namespace data
template vega-lite:        ← ✅ 复用 namespace,不重新声明 slider
{ ... uses {rate} ... }
\```
:::

Plain-text fallback behavior

嵌套不影响 plain-text fallback——所有层都各自 fallback。

例:layout 内嵌 callout 内嵌 interact,在 GitHub web 上:

  • 外层 layout :::card 字面量可见
  • 中层 callout [!INFO] 渲染为 GFM alert(GitHub 支持)
  • 最内层 interact 显示为代码块(含 sliders + template)

每一层都按自己的 fallback 规则降级,整体仍可读。


Common pitfalls

1. Inner plugin 没注册(开发者方)

import { remarkPlugins, hydrateAll } from '@rho/md';
// 缺:registerAllInnerPlugins();

→ 嵌套块不解析——layout 内的 callout 显示为字面量。集成时必须调 registerAllInnerPlugins()。详见 Developer Reference.

2. Fenced code 内嵌 fenced code 引号问题

\```layout
:::card
\```interact     ← ❌ 跟外层 \``` 冲突,markdown 把这里当 close
slider x 0 10 5 1
\```
:::
\```

→ 嵌套 fenced code 块时,内层用更多反引号:

\````layout              ← 4 反引号
:::card
\```interact            ← 3 反引号(少于外层)
slider x 0 10 5 1
\```
:::
\````

或反过来——外层 3、内层 4,规则是外层和内层反引号数不能相同。

3. 嵌套破坏 plain-text 可读性

\```layout grid cols=4
:::card
\```layout grid cols=3
:::card
\```layout
:::card
内容
:::
\```
:::
\```
:::
\```

→ 即使语法对,plain-text reader 看到一堆 ::: 和 ``` 标记,完全看不懂在干什么。嵌套 ≤ 3 层 + 保持每层职责清晰。

4. 嵌套带来的 namespace 期待错位

:::card
\```interact
slider x 0 10 5 1
\```
:::

:::card
\```interact
template stl: [x] -> [{x}]      ← ❌ x 没在这块声明
\```
:::

→ 不同 card 内的 interact 块默认 state 隔离。要共享需用 namespace。详见 Shared state.

5. 嵌套渲染高度变化

\```layout grid cols=2
:::card accent=blue
\```interact
slider x 0 10 5 1
template stl: [{x}]
\```
:::
:::card accent=red
固定文本
:::
\```

→ 拖左卡 slider 时,左卡内容高度可能变(template 输出长度变化)→ 整个 layout 行高跟着抖。给 interact 块设固定 min-height或减少 template 行数变化。

6. timeline 内嵌 stepper

\```timeline
:::event date=2026 title="V1.0"
\```stepper
:::step title="特性 1"
:::
\```
:::
\```

→ 技术合法但语义混乱——timeline 是时间轴,stepper 是教学步骤,混用让读者困惑。重新设计:把 stepper 提升到外面,timeline event 只放该版本特性的总结链接。


推荐的 mental model

把容器型 DSL 分为结构容器和状态容器:

类别 含 嵌套行为
结构容器 callout / layout / tabs / stepper / modal / timeline 内嵌任意 DSL,inner processor 处理
状态容器 interact 不嵌套(同名变量会冲突);多块共享用 namespace

结构容器 ↔ 状态容器:自由组合(callout 内放 interact、layout 内放 interact、stepper 内放 interact 全合法)。

结构容器 ↔ 结构容器:可以嵌套(layout 内放 callout、tabs 内放 modal),但避免同名嵌套(tabs 套 tabs)。

状态容器 ↔ 状态容器:用 namespace 共享,不嵌套。


See also