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 之前做一次轻量归类:
- 检测正文里是否存在明显的流程图符号,例如
┌、└、│、─、▶、◀、▼、①、②、❗。 - 如果连续多行都像流程图,就自动在这段内容外包一层
text代码块。 - 再交给
ReactMarkdown渲染。 - 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 语法。