Plain-text fallback principle
Rho's core promise: any
.mdfile you write opens in any tool — worst case, the reader sees syntax-highlighted source, not a failed render or broken binary. This principle constrains every AINP block's design — and is the fundamental difference between AINP and MDX / Notion-flavored markdown / other "enhanced markdown" formats.
Markdown's core promise
Why did markdown beat wiki / RTF / docx / Notion's proprietary formats and become the de facto standard?
Any tool can open it. Worst case you see plain text — not a failed render or broken binary.
That promise lets .md files flow across:
- GitHub / GitLab / Bitbucket
- VS Code / Sublime / IntelliJ
- Obsidian / Bear / Typora
- Email clients (Apple Mail / Gmail / Outlook)
- Command line (
cat/less/vim) - Any text editor (including Notepad)
Readers never can't open your file because they switched tools.
How Rho honors it
All AINP blocks must look like legal markdown — in non-Rho readers they show as:
- Standard fenced code blocks (
```-wrapped) - Standard directive containers (
:::-wrapped) - Standard blockquotes (
>-prefixed)
Per-DSL fallback:
| AINP block | In a non-supporting reader |
|---|---|
Callout (> [!INFO]) |
Standard blockquote + [!INFO] literal visible (GitHub natively renders some types as GFM alerts) |
Layout (```layout) |
Code block + :::card literal + card markdown renders normally |
Tabs (```tabs) |
Code block + :::tab literal + tab content inlined |
Stepper (```stepper) |
Code block + :::step literal + steps inlined sequentially |
Modal (```modal) |
Code block (trigger + body all visible — no "hidden" effect) |
Timeline (```timeline) |
Code block + :::event literal + events inlined sequentially |
Annotate (```annotate) |
Code block (body + annotation table) |
Interact (```interact) |
Code block (slider/template literals) |
STL syntax (```stl) |
Code block (no 6-color highlighting; plain text readable) |
Key observation: every DSL lives inside markdown-already-legal containers — code blocks and directive containers are CommonMark / GFM legal syntax; they just aren't recognized as Rho-specific semantics elsewhere.
Comparison with other "enhanced markdown"
| Format | Breaks plain-text fallback? | How |
|---|---|---|
| MDX | ❌ Breaks | .md contains JSX <Component />; outside an MDX reader → file is completely broken (won't render + isn't legal markdown) |
| Notion-flavored markdown | ❌ Breaks | Notion's custom blocks (embed / database / synced) export to .md that's unreadable elsewhere |
| Bear-flavored | ❌ Breaks | Bear's custom tag / link syntax shows as gibberish elsewhere |
| MyST | ⚠️ Partly | MyST uses :::{directive} similar to Rho; plain markdown sees them as literals; but MyST is complex with broader fallback challenges |
| Quarto | ⚠️ Partly | Quarto's {r} / {python} chunks fall back as code-block literals (OK), but inline {=ref} etc. show as gibberish in plain markdown |
| GitHub-flavored markdown (GFM) | ✅ Doesn't break | task lists / tables / strikethrough all have reasonable fallback |
| Rho | ✅ Doesn't break | All DSLs live in fenced code / directive containers; always plain-text readable |
Cost of Rho's design: DSL shape is constrained by markdown grammar (must live in ``` or :::).
Benefit: your .md files never break because you switched readers.
MDX takes the opposite path: embrace React ecosystem, abandon plain-text fallback. That's a reasonable engineering choice, but Rho doesn't accept that trade-off — Rho aims to be markdown's next evolution, not React's markdown adapter.
How to verify your doc actually fallbacks plain-text
After writing a Rho doc, test in at least one fallback reader:
1. Push to GitHub
GitHub is the strictest fallback test environment — it doesn't render any non-standard markdown extensions.
git add my-doc.md
git commit -m "test"
git push
Open my-doc.md on GitHub, check:
- ✅ Standard markdown (headings / lists / code blocks / tables) renders normally
- ✅ Callout
[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]renders (GFM alerts) - ⚠️ Callout
[!INFO]/[!WARN]/[!ZEN]shows as[!INFO]literal + plain blockquote - ⚠️
:::card:::tab:::step:::eventshow as literals + content normal - ⚠️
```layout```interact```timelineshow as code blocks (with full source)
Bottom line: all content readable, no breakage (no 404 / gibberish / chaos).
2. VS Code default preview
Open .md → Cmd+Shift+V (default markdown preview):
- Standard markdown renders
- Rho-specific syntax shows as literals
3. Command-line cat
cat my-doc.md
This is the most extreme fallback — readers see only raw plain text. Every Rho source should be readable — sliders are spelled out, templates show their placeholders.
How to actively maintain fallback while writing
1. Don't use any "non-standard markdown" outside AINP blocks
<custom-element>This isn't legal markdown</custom-element> ← ❌ Risky to use HTML outside Rho
Use extension syntax only inside AINP blocks. Regular paragraphs stay 100% CommonMark / GFM legal.
2. interact templates clearly self-explain
```interact
slider weight range=40..120 step=1 default=70 label="weight"
template stl:
[Weight] -> [{weight} kg] ← ✅ Template self-explanatory
```
→ Plain-text readers seeing slider weight range=40..120 step=1 default=70 label="weight" + [Weight] -> [{weight} kg], can guess "drag a slider, change weight".
Don't use mysterious abbreviations / custom syntax that confuses plain-text readers.
3. Don't hide critical content in modal
```modal trigger="Details"
(Critical explanation)
```
→ Plain-text readers see \```modal trigger="Details" + content fully expanded — no "hide" effect. So hiding critical content in a modal = not actually hidden in plain text = you think you hid it but didn't, while in Rho you hid it from users.
Conclusion: modals only hide "optional depth."
4. annotate's positions / colors must be self-explained in the table
```annotate
The quick brown fox
---
4-9|red|adjective
10-15|blue|noun
```
→ Plain-text readers see positions + colors + descriptions and can manually count to verify, understanding the original. Don't use unlabeled annotations — loses fallback value.
5. STL nodes use semantic names
```stl
[A] -> [B] ::mod(rule="causal") ← ⚠️ "A" / "B" too abstract
[Rate_Increase] -> [Inflation_Rise] ::mod(rule="causal") ← ✅ Semantic names
```
→ Plain-text readers see semantic names and immediately understand what nodes mean; A/B/X/Y are just letters in plain text.
When violating this principle is acceptable
Almost never. But two exceptions:
1. Internal tools / personal scripts generating .md
If the file only flows within your Rho-only pipeline (will never be opened on GitHub by anyone), you can relax — but write down why you abandoned fallback for future traceability.
2. Embedding raw HTML / Vue / React in Rho
Some integrating readers may extend support. Such extensions work only in that reader; outside, broken. Rho's official spec doesn't encourage this — if you do it, isolate in a dedicated plugin / section, don't mix into general docs.
Common pitfalls
1. Using HTML tags in place of an AINP block
<details>
<summary>Click to expand</summary>
content
</details>
→ HTML <details> renders foldable on GitHub / Obsidian (legal), but:
- Can't combine with Rho interact / computed / etc.
- Inconsistent with Rho's brand
- Loses "pure markdown" advantage
Use Rho's Modal instead for consistency.
2. Raw HTML for layout
<div class="grid">
<div>Left</div>
<div>Right</div>
</div>
→ GitHub doesn't render <div> CSS (GitHub strips style). Use Rho's Layout.
3. Emoji as semantic signals — but plain-text loses structure
🟢 Recommended
🔴 Forbidden
🟡 Caution
→ Plain-text readers see only emoji chars, no visual weight. Use Callout:
> [!INFO]
> Recommended
> [!CAUTION]
> Forbidden
> [!WARN]
> Caution
Callouts in fallback are blockquote + [!TYPE] literal — structural information preserved.
4. Thinking plain-text fallback is a "security mechanism"
❌ Misconception: I can hide sensitive data in modal / interact since non-Rho users can't see it. ✅ Reality: every AINP block in fallback shows all content as plain text. Rho is a UX tool, not a security tool — sensitive data should not enter markdown at all.
5. Nesting destroys plain-text readability
```layout grid cols=4
:::card
```layout grid cols=3
...5 levels deep...
→ Even with valid syntax, plain-text readers see 30 ::: and ``` markers and can't understand structure. Keep ≤ 3 levels of nesting, with each level's responsibility clear. See Nested DSL.
6. interact templates with mysterious expressions
template stl:
[Result] -> [{round((p*pow(1+r,n)-p)/(p*r), 4)}] ← ❌ Plain-text reader has no clue
→ Use computed to break it apart and name intermediates:
computed gain = principal * pow(1+rate, years) - principal
computed roi = gain / (principal * rate)
template stl:
[ROI] -> [{round(roi, 4)}]
Plain-text readers see computed gain = ... + computed roi = ... + template ... [{round(roi, 4)}] and can follow the logic.
7. Assuming the reader "definitely supports" Rho
Drag the slider below to see the result:
```interact
slider x range=0..10 step=1 default=5 label="x"
template stl: [{x}]
```
→ If the reader views on GitHub, there's nothing to drag. Rewrite to be plain-text-friendly:
Below is an interactive slider (draggable in Rho / rho.md; shown as code elsewhere):
```interact
slider x range=0..10 step=1 default=5 label="x"
template stl: [{x}]
```
Let plain-text readers also understand what's happening.
Philosophy: why this principle can't bend
Markdown is markdown not because it has fancy capabilities — other formats are more capable. Markdown's victory rests entirely on simple + universal + indestructible.
Breaking plain-text fallback = breaking the root that makes markdown markdown.
Rho is markdown's next evolution — evolution, not revolution. We extend capabilities but preserve the root. This principle never bends.
"If users' files break because they switched tools, you've destroyed all of markdown's value."
See also
- What is Rho? — One of Rho's three core principles is this one
- Nested DSL — Nesting still falls back
- Shared state — namespace also falls back (namespace literal visible)
- The "Plain-text fallback behavior" sections in every capability doc