Quick start (unified pipeline)

装完 @rho/md 后 5 分钟内跑出第一个 Rho 渲染。


完整最小集成(~30 行)

index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Rho demo</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="root" class="markdown-body"></div>
  <script type="module" src="./main.js"></script>
</body>
</html>

main.js:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { registerAllInnerPlugins, remarkPlugins, hydrateAll } from '@rho/md';
import '@rho/md/css';

// 1. 注册 inner plugins(嵌套 DSL 必须)
registerAllInnerPlugins();

// 2. 构建 unified processor
const processor = unified()
  .use(remarkParse)
  .use(remarkGfm);

// 3. 注册 Rho 的 7 个 remark plugins
remarkPlugins.forEach((p) => processor.use(p));

// 4. 转 HTML AST + stringify
processor
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeStringify, { allowDangerousHtml: true });

// 5. 你的 markdown 源
const markdown = `
# Hello Rho

> [!INFO]
> 这是一个 callout

\`\`\`interact
slider x 0 10 5 1
template stl:
[选中值] -> [{x}]
\`\`\`
`;

// 6. process → 注入 DOM
const html = String(await processor.process(markdown));
const container = document.getElementById('root');
container.innerHTML = html;

// 7. hydrate 交互块(slider / tabs / etc.)
hydrateAll(container);

打开 index.html → 你看到:

  • 标题 "Hello Rho"
  • 蓝色 callout
  • 可拖动的 slider + 实时输出 "选中值 = 5"

6 步详解

Step 1:registerAllInnerPlugins()

registerAllInnerPlugins();

做什么:告诉 Rho 的 inner processor "嵌套 DSL 时所有 7 个 remark plugin 都要应用"。 为什么必须:嵌套 callout / layout / interact 等需要 inner processor 知道有哪些 DSL plugin。详见 Inner processor。

常见错误:忘加这行 → layout 内的 callout 显示为字面量。

Step 2-4:构建 unified processor

const processor = unified()
  .use(remarkParse)
  .use(remarkGfm);

remarkPlugins.forEach((p) => processor.use(p));

processor
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeStringify, { allowDangerousHtml: true });
Step 作用
remarkParse markdown 字符串 → markdown AST
remarkGfm GFM 扩展(task list / table / 等)
remarkPlugins (7 个) Rho 的 callout / layout / tabs / stepper / timeline / annotate / modal 转换
remarkRehype markdown AST → HTML AST
rehypeStringify HTML AST → HTML 字符串

allowDangerousHtml: true 必填 —— Rho DSL 转换出的 HTML 含 marker classes / data attributes,被 rehype 视为"dangerous"——必须允许。Rho 的 SVG sanitizer 在内部处理安全。

Step 5:你的 markdown

任何合法 markdown + Rho DSL 都行:

const markdown = `
# 标题

正文段落。

> [!ZEN]
> Rho 的灵魂

\`\`\`layout grid cols=2
:::card accent=blue
左卡内容
:::
:::card accent=red
右卡内容
:::
\`\`\`

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

Step 6:process → 注入 DOM

const html = String(await processor.process(markdown));
container.innerHTML = html;

String(...) 必须:process() 返回 VFile,String() 取 .value。

Step 7:hydrate

hydrateAll(container);

做什么:扫 container 内所有 Rho marker class(.rho-interact / .rho-tabs / .rho-stepper / .rho-modal),把它们从静态 HTML 变成可交互组件。

必须在浏览器里跑——SSR 的话,把 hydrate 放在 client-side bundle 里。


完整 file structure

my-rho-demo/
├── package.json
├── index.html
├── main.js
└── style.css           ← 可选,扩展 Rho 默认样式

package.json:

{
  "name": "my-rho-demo",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@rho/md": "^0.1.0",
    "unified": "^11.0.5",
    "remark-parse": "^11.0.0",
    "remark-gfm": "^4.0.1",
    "remark-rehype": "^11.0.0",
    "rehype-stringify": "^10.0.0",
    "vega-embed": "^7.0.0"
  }
}

启动:

npm install
npx vite          # 或任意支持 ES module 的 dev server

框架集成

React

import { useEffect, useRef } from 'react';
import { /* ... */ } from '@rho/md';

function RhoMarkdown({ source }) {
  const ref = useRef(null);

  useEffect(() => {
    const html = renderRho(source);   // 调你的 unified pipeline
    ref.current.innerHTML = html;
    hydrateAll(ref.current);
  }, [source]);

  return <div className="markdown-body" ref={ref} />;
}

Vue 3

<template>
  <div class="markdown-body" ref="root"></div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue';
import { hydrateAll } from '@rho/md';

const props = defineProps(['source']);
const root = ref(null);

const update = async () => {
  const html = await renderRho(props.source);
  root.value.innerHTML = html;
  hydrateAll(root.value);
};

onMounted(update);
watch(() => props.source, update);
</script>

Astro / Next.js / 其他静态站

build 时跑 unified pipeline 生成 HTML,runtime 在 client-side script 里调 hydrateAll。


SSR + hydration

服务端:

// SSR (Node)
const html = String(await processor.process(markdown));
return `<html>...<div id="root">${html}</div>...<script src="/client.js">...`;

客户端 (client.js):

import { hydrateAll } from '@rho/md';
import '@rho/md/css';

hydrateAll(document.getElementById('root'));

Common issues

嵌套 callout 在 layout 里不渲染

→ 忘 registerAllInnerPlugins()。

chart 不渲染但其他都正常

→ 忘装 vega-embed。

slider 不可拖

→ 忘 hydrateAll(container),或 hydrate 跑在 SSR 端而不是 client。

TypeScript 抱怨类型

→ 检查 tsconfig.json 的 module: "esnext" 和 moduleResolution: "bundler"。


接下来

→ API reference — 全 API surface → Hydration utilities — 部分 hydrate / 时机控制 → CSS theming — 改样式 → Inner processor — 嵌套 DSL 详解