Plain-text fallback principle
Rho 的核心承诺:你写的
.md文件任何工具都能打开——最坏情况下读者看到带语法高亮的源码,不是渲染失败的白屏 / 损坏的二进制。 这条原则约束了所有 Rho DSL 的设计——也是 Rho 区别于 MDX / Notion-flavored / 其他"增强版 markdown"的根本。
Markdown 的核心承诺
为什么 markdown 战胜了 wiki / RTF / docx / Notion 私有格式成为事实标准?
任何工具都能打开它。最坏情况下你看到的是纯文本——而不是渲染失败的白屏 / 损坏的二进制。
这条承诺让 .md 文件可以横跨:
- GitHub / GitLab / Bitbucket
- VS Code / Sublime / IntelliJ
- Obsidian / Bear / Typora
- 邮件客户端(Apple Mail / Gmail / Outlook)
- 命令行 (
cat/less/vim) - 任何文本编辑器(包括 Notepad)
读者永远不会因为换工具而打不开你的文件。
Rho 怎么遵守这条
Rho 的所有 DSL 必须 看起来像合法的 markdown——在不支持 Rho 的 reader 里显示为:
- 普通 fenced code block(
```包围) - 普通 directive container(
:::包围) - 普通 blockquote(
>包围)
每种降级行为:
| Rho DSL | 在不支持 reader 里 |
|---|---|
Callout (> [!INFO]) |
普通 blockquote + [!INFO] 字面量可见(GitHub 自带 GFM alert 渲染部分类型) |
Layout (```layout) |
代码块 + :::card 字面量 + 卡内 markdown 正常渲染 |
Tabs (```tabs) |
代码块 + :::tab 字面量 + tab 内容 inline 展开 |
Stepper (```stepper) |
代码块 + :::step 字面量 + 步骤顺序展开 |
Modal (```modal) |
代码块(trigger + body 全显示,无"隐藏"效果) |
Timeline (```timeline) |
代码块 + :::event 字面量 + 事件顺序展开 |
Annotate (```annotate) |
代码块(含正文 + 标注表) |
Interact (```interact) |
代码块(slider/template 字面量) |
STL syntax (```stl) |
代码块(无 6 色高亮,纯文本可读) |
关键观察:所有 DSL 都装在 markdown 语法已合法的容器里——code block 和 directive container 是 CommonMark / GFM 约定的合法语法,只是不被识别成 Rho 特殊语义。
跟其他 "增强版 markdown" 对比
| 格式 | 是否破坏 plain-text fallback | 怎么破坏 |
|---|---|---|
| MDX | ❌ 破坏 | .md 含 JSX <Component />,离开 MDX reader → 文件完全坏掉(render 不出来 + 不是合法 markdown) |
| Notion-flavored markdown | ❌ 破坏 | Notion 自定义 block(embed / database / synced)export 出来的 .md 在别处不可读 |
| Bear-flavored | ❌ 破坏 | Bear 自定义 tag / link 语法在别处显示为乱码 |
| MyST | ⚠️ 部分破坏 | MyST 用 :::{directive} 跟 Rho 类似,普通 markdown 视为字面量;但 MyST 复杂度高,更难全程 fallback |
| Quarto | ⚠️ 部分破坏 | Quarto 的 {r} / {python} chunks 在非 Quarto reader 显示为代码块字面量(fallback OK),但 inline {=ref} 等在普通 markdown 显示为乱码 |
| GitHub-flavored markdown (GFM) | ✅ 不破坏 | task list / table / strikethrough 在普通 markdown 都有合理 fallback |
| Rho | ✅ 不破坏 | 所有 DSL 都装在 fenced code / directive container 里,永远 plain-text 可读 |
Rho 设计的代价:DSL 形式受 markdown 语法限制(必须装在 ``` 或 ::: 里)。
Rho 设计的收益:你的 .md 文件永远不会因为换 reader 而坏。
MDX 走另一条路:拥抱 React 生态、放弃 plain-text fallback。这是合理的工程选择,但 Rho 不接受这个 trade-off——Rho 的目标是 markdown 的 next evolution,不是 React 的 markdown 适配。
怎么验证你的文档真的 plain-text fallback
写完 Rho 文档后,至少测试一种 fallback reader:
1. 推到 GitHub 仓库
GitHub 是测试 fallback 最严格的环境——它不渲染任何非标准 markdown 扩展。
git add my-doc.md
git commit -m "test"
git push
打开 GitHub 上的 my-doc.md,看:
- ✅ 标准 markdown(标题 / 列表 / 代码块 / 表格)正常
- ✅ Callout
[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]正常(GFM alert) - ⚠️ Callout
[!INFO]/[!WARN]/[!ZEN]显示为[!INFO]字面量 + 普通 blockquote - ⚠️
:::card:::tab:::step:::event显示为字面量 + 内容正常 - ⚠️
```layout```interact```timeline显示为代码块(含全部源)
底线:所有内容可读,无破坏性(无 404 / 乱码 / 错乱)。
2. VS Code 默认预览
打开 .md 文件 → Cmd+Shift+V(默认 markdown preview):
- 标准 markdown 渲染
- Rho 特殊语法显示为字面量
3. 命令行 cat
cat my-doc.md
这是最极端的 fallback——读者只看到原文 plain text。所有 Rho 源应该可以阅读——sliders 写得明明白白,template 看得见占位符。
写作时怎么主动维护 fallback
1. 不要在 Rho DSL 外用任何"非标准 markdown"
<custom-element>这不是合法 markdown</custom-element> ← ❌ 在 Rho 外用 HTML 风险
只在 Rho DSL 里用扩展语法。普通段落保持 100% CommonMark / GFM 合法。
2. interact 的 template 写法明确
\```interact
slider weight 40 120 70 1
template stl:
[体重] -> [{weight} kg] ← ✅ template 自我解释
\```
→ Plain-text reader 看到 slider weight 40 120 70 1 + [体重] -> [{weight} kg],能猜出原意是"拖滑块改体重"。
不要用神秘缩写 / 自定义 syntax 让 plain-text 读者迷糊。
3. 关键内容不要藏 modal
\```modal trigger="详情"
(核心解释)
\```
→ Plain-text reader 看到 \```modal trigger="详情" + 内容全部展开——没有"折叠"效果。所以核心内容藏 modal = 在 plain-text 里也藏不住 = 你以为藏了其实没藏 + Rho reader 里反而藏了让用户看不到。
结论:modal 只藏"可选深度"。
4. annotate 的字符位置 / 颜色含义在标注表里写清楚
\```annotate
The quick brown fox
---
4-9|red|形容词
10-15|blue|名词
\```
→ Plain-text reader 看到字符位置 + 颜色 + 描述,手动数字符位置都能对应,理解原意。不要用没有 label 的标注——失去 fallback 价值。
5. STL 节点用语义名
\```stl
[A] -> [B] ::mod(rule="causal") ← ⚠️ "A" / "B" 太抽象
[Rate_Increase] -> [Inflation_Rise] ::mod(rule="causal") ← ✅ 语义名
\```
→ Plain-text reader 看到语义名能立刻理解节点是什么;A/B/X/Y 在 plain text 里就是字母。
何时违反这条原则是可接受的
几乎从不。但有两个例外场景:
1. 内部工具 / 个人脚本生成的 .md
如果文件只在你的 Rho-only pipeline 内流转(永远不会被人用 GitHub 看),可以放宽——但写下来你为什么放弃 fallback,便于未来回溯。
2. 在 Rho 内嵌 raw HTML / Vue / React 组件
部分集成 reader 可能扩展支持。这样的扩展只在该 reader 里有效,离开就坏。Rho 官方 spec 不鼓励这种模式——如果你做了,把它隔离在专门的 plugin / 章节,不要混进通用 docs。
Common pitfalls
1. 用 HTML 标签替代 Rho DSL
<details>
<summary>点击展开</summary>
内容
</details>
→ HTML <details> 在 GitHub / Obsidian 渲染成可折叠(合法),但:
- 无法跟 Rho interact / computed 等结合
- 跟 Rho 整体 brand 不一致
- 失去 "纯 markdown" 优势
改用 Rho 的 Modal 一致性更好。
2. 用 raw HTML 做 layout
<div class="grid">
<div>左</div>
<div>右</div>
</div>
→ 在 GitHub 上不渲染 <div> 的 CSS(GitHub 剥 style)。改用 Rho 的 Layout。
3. 用 emoji 当语义信号但 plain-text 看不到结构
🟢 推荐
🔴 禁用
🟡 谨慎
→ Plain-text reader 看到只是 emoji 字符,没有视觉权重。改用 Callout:
> [!INFO]
> 推荐
> [!CAUTION]
> 禁用
> [!WARN]
> 谨慎
callout 在 fallback 里是 blockquote + [!TYPE] 字面量——结构信息保留。
4. 觉得 plain-text fallback 是"安全机制"
❌ 误解:可以用 modal / interact 藏敏感数据,反正 Rho 不支持的人看不到。 ✅ 事实:所有 Rho DSL 在 fallback 里全部内容明文可见。Rho 是 UX 工具,不是安全工具——敏感数据应该完全不放进 markdown。
5. 用嵌套破坏 plain-text 可读性
\```layout grid cols=4
:::card
\```layout grid cols=3
...嵌套 5 层...
→ 即使语法合法,plain-text reader 看到 30 个 ::: 和 ``` 标记完全看不懂结构。保持 ≤ 3 层嵌套,且每层职责清晰。详见 Nested DSL.
6. interact template 用神秘表达式
template stl:
[结果] -> [{(p*pow(1+r,n)-p)/(p*r):.4f}] ← ❌ plain-text 读者完全看不懂
→ 改用 computed 拆开 + 给中间值名字:
computed gain = principal * pow(1+rate, years) - principal
computed roi = gain / (principal * rate)
template stl:
[ROI] -> [{roi:.4f}]
Plain-text reader 看到 computed gain = ... + computed roi = ... + template ... [{roi:.4f}],能跟着读懂逻辑。
7. 假设 reader "肯定支持" Rho
读者拖一下下面的滑块就能看到结果:
\```interact
slider x 0 10 5 1
template stl: [{x}]
\```
→ 如果读者在 GitHub 上看,"拖"什么也没有。改写成兼容 plain-text:
下面是一个交互式滑块(在 Rho MD 中可拖动;在其他地方显示为代码):
\```interact
slider x 0 10 5 1
template stl: [{x}]
\```
让 plain-text 读者也明白发生了什么。
哲学:为什么这条不能让
Markdown 之所以是 markdown,不是因为它有什么花哨的能力——其他格式能力都比它强。Markdown 的胜利全靠简单 + 普适 + 不可摧毁。
破坏 plain-text fallback = 破坏 markdown 之所以是 markdown 的根。
Rho 是 markdown 的 next evolution——演化不是革命。我们扩展能力但保留根。这条原则永远不让步。
"如果用户的文件因为换工具而坏掉,你就毁了 markdown 的全部价值。"
See also
- What is Rho? — Rho 的三条核心原则之一就是这条
- Nested DSL — 嵌套依然 fallback
- Shared state — namespace 也 fallback(namespace 字面量可见)
- 各 capability 文档的 "Plain-text fallback behavior" 段