返回首页
/markdown-auto-code-escaping

Markdown 自动转义代码块说明

创建于 2026/6/24 07:28:29
Markdown自动转义代码块渲染

Markdown 自动转义代码块说明

这篇记录的是本项目里 Markdown 渲染层的一次兼容处理:当文章正文里出现 ASCII/Unicode 流程图、时序图、框线图时,自动把它们当作 text 代码块渲染,避免 Markdown 普通段落把格式压平。

背景问题

Markdown 的普通段落不会保留连续空格和单个换行。比如下面这种内容,本质上依赖空格、换行、等宽字符和框线符号来维持排版:

┌────────────────────────────┐                     ┌─────────────────────────────┐
│        插件钱包 (Extension) │                     │      后端服务 / DApp Host    │
│  - curve25519-js (X25519)  │                     │  - X25519 / curve25519 实现  │
│  - AES-256-GCM             │                     │  - AES-256-GCM              │
└────────────┬───────────────┘                     └─────────────┬──────────────┘
             │                                                   │
             │ ① 生成本地密钥对(只在本地)                      │
             │──────────────────────────────────────────────────▶│
             │   pubWallet = generateKeyPair().public            │
             │   privWallet = generateKeyPair().private          │

如果它被 Markdown 当作普通段落,最终会变成类似这样的 HTML:

<p>┌──────┐ │ 插件钱包 │ ...</p>

浏览器会折叠连续空格,也不会按原始换行展示,所以流程图会被压成一整段横向文本,读起来很混乱。

核心方案

解决思路不是让 Markdown 变聪明,而是在交给 ReactMarkdown 之前做一次轻量归类:

  1. 检测正文里是否存在明显的流程图符号,例如
  2. 如果连续多行都像流程图,就自动在这段内容外包一层 text 代码块。
  3. 再交给 ReactMarkdown 渲染。
  4. CSS 对代码块使用 white-space: pre,保留原始空格和换行。

处理前:

┌──────┐
│ 内容 │
└──────┘

处理后:

```text
```text
┌──────┐
│ 内容 │
└──────┘
```
```

调用时机

这个逻辑发生在展示阶段,不发生在保存阶段。

调用链路是:

数据库或编辑器里的原始 content
        ↓
<MarkdownContent content={content} />
        ↓
normalizeMarkdownContent(content)
        ↓
normalizeAccidentalDocumentIndent(content)
        ↓
protectDiagramBlocks(content)
        ↓
ReactMarkdown 渲染
        ↓
页面展示

也就是说:

  • 不会改数据库里的原始正文。
  • 不会在提交文章时修改内容。
  • 文章详情页展示时会执行。
  • 编辑页 Live Preview 预览时也会执行。
  • 任何使用 MarkdownContent 的地方都会自动应用这套逻辑。

相关实现

入口组件是:

<MarkdownContent content={content} />

组件内部先生成标准化后的内容:

const normalizedContent = normalizeMarkdownContent(content);

再交给 ReactMarkdown

<ReactMarkdown rehypePlugins={[rehypeHighlight]} remarkPlugins={[remarkGfm]}>
  {normalizedContent}
</ReactMarkdown>

自动保护流程图的核心判断分为两类:

function isDiagramLine(line: string): boolean {
  // 判断当前行是否明显像流程图
}

function isDiagramContinuationLine(line: string): boolean {
  // 流程图已经开始后,继续保留缩进、多列空格的后续行
}

最终由 protectDiagramBlocks 把连续流程图区域包成 text 代码块。

CSS 配合

代码块样式里关键的是:

.markdown-content pre {
  overflow-x: auto;
  white-space: pre;
}

.markdown-content pre code {
  white-space: pre;
}

含义是:

  • white-space: pre:保留原始空格和换行。
  • overflow-x: auto:内容太宽时横向滚动,而不是强行压缩或破坏对齐。

这对流程图、命令输出、终端日志、ASCII 表格都更友好。

为什么不直接保存时转义

目前选择在渲染阶段处理,主要是为了保护原始内容:

  • 旧文章不用批量迁移。
  • 数据库里保留用户真实输入。
  • 如果后续识别规则要调整,只需要改渲染逻辑。
  • 不会因为一次保存把原文不可逆地改成另一种格式。

如果以后需要把内容永久规范化,也可以在保存接口里复用同样的判断逻辑,但那属于数据清洗,不是当前这次展示修复。

推荐手写方式

虽然项目已经支持自动识别,但以后手写这类流程图,最推荐的格式还是显式代码块:

```text
```text
┌────────────────────────────┐
│        插件钱包             │
└────────────────────────────┘
```
```

显式写法最稳定,也最容易让别人理解这段内容不是普通段落,而是一段需要保留格式的文本图。

适用范围

适合自动保护的内容包括:

  • ASCII/Unicode 流程图
  • 钱包通信流程图
  • 终端输出结构图
  • 文本表格
  • 多列对齐说明
  • 依赖空格排版的架构图

不适合自动处理的内容:

  • 普通中文段落
  • 正常 Markdown 表格
  • 已经手动写好的三反引号代码块
  • 单行很长但没有结构符号的普通文本

总结

这次修复的核心是:

识别流程图文本
        ↓
自动包成 text 代码块
        ↓
CSS 保留原始空白
        ↓
ReactMarkdown 正常渲染

它解决的是 Markdown 普通段落不保留空格和换行的问题,不改变数据库原文,也不影响正常 Markdown 语法。

游客模式