基础代码展示 → rehype-highlight 语法高亮 → react-syntax-highlighter 自定义复制 / 主题切换
我们实现了基础的 md 展示、自定义标签、自定义图表的展示,接下来我们学习下如何展示代码。这一篇是 react-markdown 系列的收官篇:md 展示 + 自定义标签 + 图表 + 代码,大模型输出的能力闭环。
在 markdown 内容里直接放 ```javascript 代码围栏,react-markdown 默认就能渲染出代码块:
import React, { useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import "github-markdown-css";
import * as echarts from 'echarts';
// 添加大段代码示例
const codeExample = `
function add(a, b) {
return a + b;
}
const result = add(5, 3);
console.log(result);
`;
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
## 二级标题:Markdown 丰富示例 [[2,'我是2的id']]
### 三级标题:Markdown 丰富示例 [[3,'我是3的id']]
### 代码示例
\`\`\`javascript
${codeExample}
\`\`\`
`;
const replaceReferences = (str) => {
// 定义正则表达式
const regex = /\[\[(\d+),'(.*?)'\]\]/g;
// 使用 replace 方法进行全局替换
return str.replace(regex, (match, num, id) => {
return `<sup className="text-blue-600 cursor-pointer" data-supid="${id}">[${num}]</sup>`;
});
}
const regStr = replaceReferences(richMarkdownContent);
// 自定义渲染器
const components = {
sup: ({ children, ...rest }) => {
return (
<sup className="text-active" onClick={(event) => handleSupClick(event)} {...rest}>
{children}
</sup>
);
},
};
// 点击事件处理函数
const handleSupClick = (event) => {
const supid = event.target.dataset.supid;
console.log("Clicked sup data-supid:", supid);
// 你可以在这里进行其他操作,比如将内容传递给父组件等
};
const App = () => {
return (
<div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={components}>
{regStr}
</ReactMarkdown>
</div>
);
};
export default App;
npm install highlight.js -S
npm install rehype-highlight -S
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from 'rehype-highlight';
import "github-markdown-css";
import 'highlight.js/styles/github.css';
// 增加大段代码示例
const codeExample = `
// 定义一个复杂的 JavaScript 类
class ComplexNumber {
constructor(real, imaginary) {
this.real = real;
this.imaginary = imaginary;
}
// 加法方法
add(other) {
return new ComplexNumber(
this.real + other.real,
this.imaginary + other.imaginary
);
}
// 减法方法
subtract(other) {
return new ComplexNumber(
this.real - other.real,
this.imaginary - other.imaginary
);
}
// 乘法方法
multiply(other) {
return new ComplexNumber(
this.real * other.real - this.imaginary * other.imaginary,
this.real * other.imaginary + this.imaginary * other.real
);
}
}
`;
const richMarkdownContent = `
# 一级标题:Markdown 丰富示例 [[1,'我是1的id']]
### 代码示例
\`\`\`javascript
${codeExample}
\`\`\`
`;
const replaceReferences = (str) => {
// 定义正则表达式
const regex = /\[\[(\d+),'(.*?)'\]\]/g;
// 使用 replace 方法进行全局替换
return str.replace(regex, (match, num, id) => {
return `<sup className="text-blue-600 cursor-pointer" data-supid="${id}">[${num}]</sup>`;
});
}
const regStr = replaceReferences(richMarkdownContent);
// 自定义渲染器
const components = {
sup: ({ children, ...rest }) => {
return (
<sup className="text-active" onClick={(event) => handleSupClick(event)} {...rest}>
{children}
</sup>
);
},
};
// 点击事件处理函数
const handleSupClick = (event) => {
const supid = event.target.dataset.supid;
console.log("Clicked sup data-supid:", supid);
// 你可以在这里进行其他操作,比如将内容传递给父组件等
};
const App = () => {
return (
<div className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeHighlight]}
components={components}
>
{regStr}
</ReactMarkdown>
</div>
);
};
export default App;
rehypePlugins={[rehypeRaw, rehypeHighlight]} 多了 rehypeHighlight;同时引入 highlight.js 的 github 风格 CSS(import 'highlight.js/styles/github.css')。rehypeHighlight 会自动识别代码块语言并加高亮 class。要支持"复制代码 + 亮暗主题切换",就得自定义 code 组件。借助 react-syntax-highlighter(Prism 版)+ antd + CopyToClipboard:
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
materialLight,
materialOceanic
} from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Button, Popover, Space } from 'antd';
import { CopyOutlined, BulbOutlined, BulbFilled } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
// 代码块组件
function CodeBlock({ code, language = '' }) {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = () => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div style={{ position: 'relative', margin: '16px 0' }}>
<div style={{
position: 'absolute',
right: 8,
top: 8,
zIndex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 4,
padding: 4
}}>
<Space>
<Button
type="text"
size="small"
icon={isDarkTheme ? <BulbFilled /> : <BulbOutlined />}
onClick={() => setIsDarkTheme(!isDarkTheme)}
/>
<Popover open={copied} content="已复制!" trigger={[]}>
<CopyToClipboard text={code} onCopy={handleCopy}>
<Button type="text" size="small" icon={<CopyOutlined />} />
</CopyToClipboard>
</Popover>
</Space>
</div>
<SyntaxHighlighter
language={language.toLowerCase()}
style={isDarkTheme ? materialOceanic : materialLight}
customStyle={{
padding: '40px 20px 20px',
borderRadius: 8,
fontSize: 14,
overflowX: 'auto'
}}
PreTag="div"
>
{code.trim()}
</SyntaxHighlighter>
</div>
);
}
// Markdown 渲染器组件
export default function MarkdownCodeBlock({ content }) {
const components = {
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<CodeBlock
code={String(children).replace(/\n$/, '')}
language={match[1]}
/>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
};
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={components}
>
{content}
</ReactMarkdown>
);
}
import MarkdownCodeBlock from './CodeBlock';
const App = () => {
const markdown = `
## 示例代码
\`\`\`js
console.log('Hello World');
\`\`\`
\`\`\`css
.container {
padding: 20px;
}
\`\`\`
`;
return (
<div className="doc-container" style={{ maxWidth: 800, margin: '0 auto' }}>
<MarkdownCodeBlock content={markdown} />
</div>
);
};
export default App;
code 渲染函数里用 /language-(\w+)/ 从 className 里取语言,块级代码(!inline && match)走 CodeBlock 富组件,行内代码走默认 <code>。这就是上一篇说过的"CodeNode 分发器"思想的代码块版。基于大模型的输出,几乎也就这么点功能,大差不差:md 基础展示 → 样式美化 → 自定义标签 / 事件 → 图表 → 代码及高亮。有更好的实现方式可以分享(原文源码可下载,dev4 分支)。
| 篇目 | 能力 | 关键插件 / 组件 |
|---|---|---|
| 第 1 篇 | md 渲染 + 美化 + html | react-markdown + github-markdown-css + remark-gfm + rehype-raw |
| 第 2 篇 | 自定义标签 + 事件 | 正则替换 + components 自定义渲染器 |
| 第 3 篇 | echarts 报表 | md 里嵌容器 div + useEffect + echarts.init |
| 第 4 篇(本文) | 代码 + 高亮 + 复制/主题 | rehype-highlight 或 react-syntax-highlighter + antd |
| 术语 | 定义 |
|---|---|
| rehype-highlight | 基于 highlight.js 的 rehype 插件,自动为代码块加语法高亮 class |
| highlight.js | 轻量语法高亮库,提供多种主题 CSS |
| react-syntax-highlighter | React 代码高亮组件(Prism / hljs 双版本),支持主题与自定义容器 |
| inline / block code | 行内代码(`x`)vs 块级代码(``` 围栏),渲染路径不同 |
| language-(\w+) | 从代码块 className 中提取语言名的正则,react-markdown 的语言标记格式 |
| CopyToClipboard | 复制到剪贴板的 React 组件(react-copy-to-clipboard) |
// ========== 安装依赖 ==========
// npm i react-syntax-highlighter -S
// npm i antd @ant-design/icons -S
// npm i react-copy-to-clipboard -S
// 已有:react-markdown + remark-gfm
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { materialLight, materialOceanic } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Button, Popover, Space } from 'antd';
import { CopyOutlined, BulbOutlined, BulbFilled } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
function CodeBlock({ code, language = '' }) {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = () => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div style={{ position: 'relative', margin: '16px 0' }}>
{/* 右上角工具栏:主题切换 + 复制 */}
<div style={{
position: 'absolute', right: 8, top: 8, zIndex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.2)', borderRadius: 4, padding: 4
}}>
<Space>
<Button type="text" size="small"
icon={isDarkTheme ? <BulbFilled /> : <BulbOutlined />}
onClick={() => setIsDarkTheme(!isDarkTheme)} />
<Popover open={copied} content="已复制!" trigger={[]}>
<CopyToClipboard text={code} onCopy={handleCopy}>
<Button type="text" size="small" icon={<CopyOutlined />} />
</CopyToClipboard>
</Popover>
</Space>
</div>
<SyntaxHighlighter
language={language.toLowerCase()}
style={isDarkTheme ? materialOceanic : materialLight}
customStyle={{ padding: '40px 20px 20px', borderRadius: 8, fontSize: 14, overflowX: 'auto' }}
PreTag="div"
>
{code.trim()}
</SyntaxHighlighter>
</div>
);
}
export default function MarkdownCodeBlock({ content }) {
const components = {
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<CodeBlock code={String(children).replace(/\n$/, '')} language={match[1]} />
) : (
<code className={className} {...props}>{children}</code>
);
}
};
return (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
{content}
</ReactMarkdown>
);
}