# render-markdown

一个基于 React 的 Markdown 渲染组件，支持 GFM（GitHub Flavored Markdown）Tabs 标签、代码高亮、Mermaid 图表等功能。

渲染效果
![整体渲染效果](https://remons.cn:3008/upload/md_assets/%E6%95%B4%E4%BD%93%E6%B8%B2%E6%9F%93%E6%95%88%E6%9E%9C.png)

## 技术方案

### 技术栈

| 类别 | 技术选型 | 说明 |
|------|---------|------|
| 框架 | React 18 | 组件库基础框架 |
| 构建工具 | Vite | 基于 Rollup 的极速构建器 |
| 语言 | TypeScript | 提供类型定义 |
| 样式方案 | CSS Modules / 内联样式 | 轻量无外部依赖 |
| 代码规范 | ESLint + Prettier | 统一代码风格 |
| 发布工具 | changesets | 语义化版本管理与 changelog 生成 |

### 核心功能

- **GFM 支持**: 完整的 GitHub Flavored Markdown 渲染
- **增量渲染**: 基于 morphdom 的 DOM 差异化更新，适用于流式输出场景
- **代码高亮**: 基于 highlight.js 的代码语法高亮
- **Mermaid 图表**: 支持 Mermaid 语法渲染流程图、时序图等
- **Tab 标签页**: 支持 Tab 标签页语法
- **Alert 提示框**: 支持警告、提示等信息框
- **目录锚点**: 自动生成文档目录和锚点链接（自行实现目录渲染）
- **代码复制**: 一键复制代码块
- **代码折叠**: 支持代码块折叠展开
- **深色模式**: 自动适配深色/浅色主题

## API

### 安装

```bash
npm install remons-render-markdown
```

### 导出内容

```typescript
import RenderMarkdown, { markdownFormat, languagesCommon,  initHighlighter, useIncrementalRender } from 'remons-render-markdown';
```

#### 默认导出

- **RenderMarkdown**: React 组件，用于渲染 Markdown 内容

#### 命名导出

- **markdownFormat**: Markdown 解析函数，返回 `{ anchor: AnchorItem[], info: string }`
- **languagesCommon**: 默认支持的语言包配置（javascript, typescript, css, json, bash, xml, plaintext）
- **initHighlighter**: 初始化高亮语言包的函数
- **useIncrementalRender**: 增量渲染 hook，使用 morphdom 进行 DOM 差异化更新，适用于流式输出场景
- **renderMermaid**: 独立的 Mermaid 图表渲染组件

### RenderMarkdown 组件 Props

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| content | string | ✅ | - | Markdown 内容 |
| createTime | string | ❌ | - | 创建时间，用于显示文档更新时间 |
| isSlotMermaid | boolean | ❌ | true | 是否使用 Mermaid 插件渲染图表 |
| isShowCollapsed | boolean | ❌ | true | 是否显示代码折叠按钮 |
| codeType | string | ❌ | - | 指定代码类型（如 'javascript' 等），不传则按 Markdown 渲染 |
| editButton | React.ReactNode | ❌ | - | 自定义编辑按钮 |
| backTopTarget | HTMLElement | ❌ | body | 返回顶部按钮监听的容器 |
| showDriverGuide | boolean | ❌ | false | 是否显示新手引导 |
| showToc | boolean | ❌ | false | 是否显示目录（TOC）按钮和侧边栏 |

### markdownFormat 函数

解析 Markdown 内容并返回结构化数据。

**参数：**

- `content: string` - Markdown 文本内容

**返回值：**

```typescript
{
  anchor: Array<{
    title: string;        // 标题文本
    href: string;         // 锚点链接
    nodeName: string;     // 节点名称，如 "H1", "H2"
    nodeTitle: string;    // 完整节点 HTML
    children: Array<...>; // 子标题
  }>;
  info: string;           // 渲染后的 HTML 字符串
}
```

### initHighlighter 函数

注册自定义的代码高亮语言。

**参数：**

- `languages: Record<string, any>` - 语言包对象，格式为 `{ languageName: languageModule }`

**示例：**

```typescript
import javascript from 'highlight.js/lib/languages/javascript';
import python from 'highlight.js/lib/languages/python';

initHighlighter({
  javascript,
  python,
});
```

## 使用方式

### 基础用法

```tsx
import RenderMarkdown from 'remons-render-markdown';
import 'render-markdown/dist/index.css';

function App() {
  const markdownContent = `
# Hello World

This is a **Markdown** renderer.

\`\`\`javascript
console.log('Hello');
\`\`\`
  `;

  return <RenderMarkdown content={markdownContent} />;
}
```

### 完整示例

```tsx
import RenderMarkdown from 'remons-render-markdown';
import 'render-markdown/dist/index.css';

function ArticlePage() {
  const content = `
# 文章标题

这是一篇示例文章。

## 代码示例

\`\`\`javascript
const greeting = 'Hello World';
console.log(greeting);
\`\`\`

## Mermaid 图表

\`\`\`mermaid
graph TD
  A[开始] --> B[处理]
  B --> C[结束]
\`\`\`
  `;

  return (
    <RenderMarkdown
      content={content}
      createTime="2026-07-23T12:00:00Z"
      isSlotMermaid={true}
      isShowCollapsed={true}
      editButton={<button>编辑</button>}
    />
  );
}
```

### 使用 markdownFormat 获取目录结构

```tsx
import { markdownFormat } from 'remons-render-markdown';

async function TableOfContents() {
  const content = '# 标题1\n## 标题1.1\n## 标题1.2\n# 标题2';
  const { anchor, info } = await markdownFormat(content);
  
  console.log(anchor); 
  // [
  //   { title: '标题1', href: '标题1', nodeName: 'H1', children: [...] },
  //   { title: '标题2', href: '标题2', nodeName: 'H1', children: [] }
  // ]
  
  return <div dangerouslySetInnerHTML={{ __html: info }} />;
}
```

### 自定义代码高亮语言

```tsx
import RenderMarkdown, { initHighlighter, languagesCommon } from 'remons-render-markdown';
import 'render-markdown/dist/index.css';
import python from 'highlight.js/lib/languages/python';
import go from 'highlight.js/lib/languages/go';

// 注册额外的语言
initHighlighter({
  ...languagesCommon,  // 包含默认语言
  python,
  go,
});

function App() {
  return (
    <RenderMarkdown
      content={`
\`\`\`python
def hello():
    print('Hello')
\`\`\`

\`\`\`go
package main
import "fmt"
func main() {
    fmt.Println("Hello")
}
\`\`\`
      `}
    />
  );
}
```

### 仅渲染特定代码类型

```tsx
import RenderMarkdown from 'remons-render-markdown';
import 'render-markdown/dist/index.css';

function CodeViewer() {
  const code = `
const x = 1;
const y = 2;
console.log(x + y);
  `;

  return (
    <RenderMarkdown
      content={code}
      codeType="javascript"
      isSlotMermaid={false}
    />
  );
}
```

### 使用 useIncrementalRender 实现增量渲染

`useIncrementalRender` 适用于流式输出场景（如 AI 对话），通过 morphdom 实现高效的 DOM 差异化更新。

```tsx
import { useIncrementalRender } from 'remons-render-markdown';

function StreamingContent({ content }) {
  const { anchors, hasContent, setInnerRef } = useIncrementalRender({
    content,
    // 可选：节流时间，默认 16ms
    throttleMs: 16,
  });

  return (
    <div>
      {hasContent && <div ref={setInnerRef} />}
      {anchors.length > 0 && (
        <ul>
          {anchors.map((item) => (
            <li key={item.href}>
              <a href={`#${item.href}`}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

**参数：**

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| content | string | ✅ | - | Markdown 内容 |
| codeType | string | ❌ | - | 代码类型，不传则按 Markdown 渲染 |
| customRenderers | MarkdownPlugin[] | ❌ | - | 自定义 markdown-it 插件 |
| throttleMs | number | ❌ | 16 | 节流时间（ms） |
| onNodeDiscarded | (el: HTMLElement) => void | ❌ | - | 节点被 diff 移除后触发（含被移除子树的子节点），用于清理 React Root 等挂载状态 |

**返回值：**

| 属性 | 类型 | 说明 |
|------|------|------|
| anchors | AnchorItem[] | 文档锚点列表 |
| hasContent | boolean | 是否有内容 |
| setInnerRef | (node: HTMLDivElement) => void | 内容容器的 ref 回调 |

**增量渲染与自定义插件（customRenderers）的配合约定：**

1. 插件产出的容器节点（如 `:::badge` / `:::linkCard` / `<plugin-container>`）按"标签 + className"语义匹配，内容位移时不同类型的块不会被错配到旧节点上。
2. 元素上 JS 后加的 `data-*` 属性（如初始化守卫位 `data-xxx-inited`）在 diff 后会被保留，"只初始化一次"的守卫逻辑不会在增量渲染中被重复触发。
3. JS 动态插入的额外子节点（如 React 挂载点、收起按钮）默认会被 diff 移除；若需跨增量渲染保留，请给节点设置 `data-md-persist` 属性（见导出常量 `MD_PERSIST_ATTR`）。
4. 被 diff 移除的节点会触发 `onNodeDiscarded`，请在此卸载挂载在其上的 React Root，避免内存泄漏。

### 关于语法

#### mermaid 图表

```code
---
title: 图表标题
---
......... 图表内容
```

- 折叠图
![mermaid 渲染图](https://remons.cn:3008/upload/md_assets/mermaid%20%E7%BC%A9%E7%95%A5%E5%9B%BE.png)

- 缩略图
![mermaid 渲染图](https://remons.cn:3008/upload/md_assets/mermaid%20%E5%B1%95%E5%BC%80%E5%9B%BE.png)
#### Tabs 标签页

依赖于 `@mdit/plugin-tab` 插件，参考 [mdit-plugin-tab](https://mdit-plugins.github.io/zh/tab.html) 的文档。
*暂不支持 tabs 嵌套*

语法示例：

```markdown
:::markdown-tabs

@tab:active tab1
    ```javascript
        console.log('Hello');
    ```

@tab tab2 
    ```typescript
        console.log('World');
    ```
:::
```
![tabs 渲染图](https://remons.cn:3008/upload/md_assets/tabs.png)

#### Alert 提示框

语法示例：

```markdown
> [!warning]
> 我是一个警告信息
```

![alert 渲染图](https://remons.cn:3008/upload/md_assets/alert.png)

#### renderMermaid 方法

支持渲染独立的 Mermaid 图表渲染组件，支持缩放、全屏、下载、源码查看等功能。

```tsx
import { renderMermaid } from 'remons-render-markdown';
import 'remons-render-markdown/dist/index.css';

function MermaidComponent() {
  const mermaidCode = `
---
title: 流程图示例
---
graph TD
  A[开始] --> B[处理]
  B --> C[结束]
  `;

  return (
    <div>
      {
        renderMermaid({ source: mermaidCode })
      }
    </div>
  );
}
```

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| source | string | ✅ | - | Mermaid 代码字符串，支持设置标题 |
| debounceMs | number | ❌ | 300 | 图表渲染时的 debounce 时间（毫秒） |
| enablePanzoom | boolean | ❌ | true | 是否启用缩放和平移功能 |
| showDownload | boolean | ❌ | true | 是否显示下载按钮（支持 SVG/PNG 格式） |
| showSourceView | boolean | ❌ | false | 是否显示源码查看按钮和工具栏 |
| showCollapse | boolean | ❌ | false | 是否显示折叠/展开按钮 |
| defaultCollapsed | boolean | ❌ | true | 默认折叠状态 |
| className | string | ❌ | "" | 自定义样式类名 |
| minHeight | number | ❌ | 200 | 最小高度（像素） |

**功能特性：**
- 缩放：支持放大、缩小、重置视图
- 全屏：支持浏览器原生全屏模式
- 下载：支持下载 SVG 和 PNG 格式
- 源码查看：点击按钮可查看 Mermaid 源码
- 折叠/展开：支持图表的折叠和展开
- 缩略图模式：最小化时显示缩略图
- 深色模式：自动适配主题


## 注意事项

1. **样式引入**：务必引入 `remons-render-markdown/dist/index.css` 以确保正确的样式渲染
2. **Mermaid 支持**：当 `isSlotMermaid` 为 `true` 时，会自动渲染 mermaid 代码块为图表
3. **代码高亮**：请使用 `initHighlighter` 注册（必须）
4. **安全性**：外部链接会自动添加 `target="_blank"` 和 `rel="noopener"` 属性
5. **性能优化**：Mermaid 渲染会延迟执行，以优化首屏加载速度

