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
- Shared state (namespace) — 多个 interact 块共用 state
- Plain-text fallback principle — 嵌套依然 fallback
- Layout grid + cards
- Stepper
- Modal
- Tabs
- Developer Reference: inner processor — 集成方调
registerAllInnerPlugins()