~cytrogen/blog-public

ref: 88eebf3dfdd8ab819fa1a84e1976a8a75d5af2b6 blog-public/posts/c0fc.html -rw-r--r-- 48.8 KiB
88eebf3dCytrogen Deploy 2026-02-19 08:34:27 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>React 的 Markdown 渲染 · Cytrogen 的个人博客</title><meta name="description" content="本文是一篇在 React 中实现 Markdown 渲染的实战教程,重点解决了新版 react-markdown 库中代码高亮的难题。文章首先介绍了如何使用 react-markdown 和 remark-gfm 插件来支持 GitHub 风格的 Markdown(如表格、删除线)。核心部分针对 react-markdown 新版废弃 inline 属性导致旧教程失效的问题,提供了一个创新的解决方案:通过自定义 pre 组件来准确识别并使用 react-syntax-highlighter 渲染代码块。本文为希望在 React 项目中实现现代化 Markdown 渲染的开发者提供了一套与时俱进的实用方法。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/c0fc.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
  font-family: 'Open Sans';
  src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  size-adjust: 107%;
  ascent-override: 97%;
  descent-override: 25%;
  line-gap-override: 0%;
}
</style><script>(function() {
  try {
    // 优先级:用户选择 > 系统偏好 > 默认浅色
    const saved = localStorage.getItem('theme');
    const theme = saved || 
      (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    
    document.documentElement.setAttribute('data-theme', theme);
    document.documentElement.style.colorScheme = theme;
  } catch (error) {
    // 失败时使用默认主题,不阻塞渲染
    document.documentElement.setAttribute('data-theme', 'light');
  }
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
  document.addEventListener('DOMContentLoaded', function() {
    const theme = document.documentElement.getAttribute('data-theme');
    const pageWrapper = document.getElementById('page-wrapper');
    if (pageWrapper && theme) {
      pageWrapper.setAttribute('data-theme', theme);
    }
  });
})();

</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/c0fc.html">永久链接</a><div class="p-summary visually-hidden"><p>项目需求中有一个功能是支持 Markdown 渲染,尽量仿照 ChatGPT、Claude 的效果。</p>
<p>该文章的目的是记录我在实现这个功能时遇到的问题和解决方案。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/JavaScript/">JavaScript</a><a class="p-category" href="../tags/React-js/">React.js</a></div><h1 class="post-title p-name">React 的 Markdown 渲染</h1><div class="post-info"><time class="post-date dt-published" datetime="2024-02-21T04:11:13.000Z">2/20/2024</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:55.001Z"></time></div><div class="post-content e-content"><html><head></head><body><p>项目需求中有一个功能是支持 Markdown 渲染,尽量仿照 ChatGPT、Claude 的效果。</p>
<p>该文章的目的是记录我在实现这个功能时遇到的问题和解决方案。</p>
<span id="more"></span>
<h2 id="react-markdown"><a class="markdownIt-Anchor" href="#react-markdown"></a> <code>react-markdown</code></h2>
<p>先安装 <code>react-markdown</code></p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install react-markdown</span><br></pre></td></tr></tbody></table></figure>
<p>此处运用 <code>react-markdown</code> 库的官方示例中的文本来进行测试:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br></pre></td><td class="code"><pre><span class="line"># A demo of `react-markdown`</span><br><span class="line"></span><br><span class="line">`react-markdown` is a markdown component for React.</span><br><span class="line"></span><br><span class="line">👉 Changes are re-rendered as you type.</span><br><span class="line"></span><br><span class="line">👈 Try writing some markdown on the left.</span><br><span class="line"></span><br><span class="line">## Overview</span><br><span class="line"></span><br><span class="line">* Follows [CommonMark](https://commonmark.org)</span><br><span class="line">* Optionally follows [GitHub Flavored Markdown](https://github.github.com/gfm/)</span><br><span class="line">* Renders actual React elements instead of using `dangerouslySetInnerHTML`</span><br><span class="line">* Lets you define your own components (to render `MyHeading` instead of `'h1'`)</span><br><span class="line">* Has a lot of plugins</span><br><span class="line"></span><br><span class="line">## Contents</span><br><span class="line"></span><br><span class="line">Here is an example of a plugin in action</span><br><span class="line">([`remark-toc`](https://github.com/remarkjs/remark-toc)).</span><br><span class="line">**This section is replaced by an actual table of contents**.</span><br><span class="line"></span><br><span class="line">## Syntax highlighting</span><br><span class="line"></span><br><span class="line">Here is an example of a plugin to highlight code:</span><br><span class="line">[`rehype-highlight`](https://github.com/rehypejs/rehype-highlight).</span><br><span class="line"></span><br><span class="line">```js</span><br><span class="line">import React from 'react'</span><br><span class="line">import ReactDOM from 'react-dom'</span><br><span class="line">import Markdown from 'react-markdown'</span><br><span class="line">import rehypeHighlight from 'rehype-highlight'</span><br><span class="line"></span><br><span class="line">const markdown = `</span><br><span class="line"># Your markdown here</span><br><span class="line">`</span><br><span class="line"></span><br><span class="line">ReactDOM.render(</span><br><span class="line">  &lt;Markdown rehypePlugins={[rehypeHighlight]}&gt;{markdown}&lt;/Markdown&gt;,</span><br><span class="line">  document.querySelector('#content')</span><br><span class="line">)</span><br><span class="line">\```</span><br><span class="line"></span><br><span class="line">&gt; Pretty neat, eh?</span><br><span class="line"></span><br><span class="line">## GitHub flavored markdown (GFM)</span><br><span class="line"></span><br><span class="line">For GFM, you can *also* use a plugin:</span><br><span class="line">[`remark-gfm`](https://github.com/remarkjs/react-markdown#use).</span><br><span class="line">It adds support for GitHub-specific extensions to the language:</span><br><span class="line">tables, strikethrough, tasklists, and literal URLs.</span><br><span class="line"></span><br><span class="line">These features **do not work by default**.</span><br><span class="line">👆 Use the toggle above to add the plugin.</span><br><span class="line"></span><br><span class="line">| Feature    | Support              |</span><br><span class="line">| ---------: | :------------------- |</span><br><span class="line">| CommonMark | 100%                 |</span><br><span class="line">| GFM        | 100% w/ `remark-gfm` |</span><br><span class="line"></span><br><span class="line">~~strikethrough~~</span><br><span class="line"></span><br><span class="line">* [ ] task list</span><br><span class="line">* [x] checked item</span><br><span class="line"></span><br><span class="line">https://example.com</span><br><span class="line"></span><br><span class="line">## HTML in markdown</span><br><span class="line"></span><br><span class="line">⚠️ HTML in markdown is quite unsafe, but if you want to support it, you can</span><br><span class="line">use [`rehype-raw`](https://github.com/rehypejs/rehype-raw).</span><br><span class="line">You should probably combine it with</span><br><span class="line">[`rehype-sanitize`](https://github.com/rehypejs/rehype-sanitize).</span><br><span class="line"></span><br><span class="line">&lt;blockquote&gt;</span><br><span class="line">  👆 Use the toggle above to add the plugin.</span><br><span class="line">&lt;/blockquote&gt;</span><br><span class="line"></span><br><span class="line">## Components</span><br><span class="line"></span><br><span class="line">You can pass components to change things:</span><br><span class="line"></span><br><span class="line">```markdown</span><br><span class="line">import React from 'react'</span><br><span class="line">import ReactDOM from 'react-dom'</span><br><span class="line">import Markdown from 'react-markdown'</span><br><span class="line">import MyFancyRule from './components/my-fancy-rule.js'</span><br><span class="line"></span><br><span class="line">const markdown = `</span><br><span class="line"># Your markdown here</span><br><span class="line">`</span><br><span class="line"></span><br><span class="line">ReactDOM.render(</span><br><span class="line">  &lt;Markdown</span><br><span class="line">    components={{</span><br><span class="line">      // Use h2s instead of h1s</span><br><span class="line">      h1: 'h2',</span><br><span class="line">      // Use a component instead of hrs</span><br><span class="line">      hr(props) {</span><br><span class="line">        const {node, ...rest} = props</span><br><span class="line">        return &lt;MyFancyRule {...rest} /&gt;</span><br><span class="line">      }</span><br><span class="line">    }}</span><br><span class="line">  &gt;</span><br><span class="line">    {markdown}</span><br><span class="line">  &lt;/Markdown&gt;,</span><br><span class="line">  document.querySelector('#content')</span><br><span class="line">)</span><br><span class="line">/```</span><br></pre></td></tr></tbody></table></figure>
<div class="danger">
<p>测试文本中也有代码块,但是因为我用的是 Hexo 写的博客,这些原本应该在(我自己设置的)代码块内的文本都被渲染成了代码块。</p>
<p>因此我在每个代码块的后面都加了个斜杠,以使其不被渲染成代码块。你们在测试时可以 <strong>将这些斜杠去掉</strong></p>
</div>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">ReactMarkdown</span> <span class="keyword">from</span> <span class="string">"react-markdown"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) {</span><br><span class="line">    <span class="keyword">const</span> markdownContent = <span class="string">`{刚才说的Markdown测试文本}`</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        <span class="language-xml"><span class="tag">&lt;&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">ReactMarkdown</span>&gt;</span>{markdownContent}<span class="tag">&lt;/<span class="name">ReactMarkdown</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/&gt;</span></span></span><br><span class="line">    );</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>显示出来的效果(记得将 <code>markdownContent</code> 的值替换掉):</p>
<p><img src="/posts/c0fc/1.png" alt="img.png"></p>
<p>可以看出和 Typora 或者 GitHub 的渲染效果有一些差异,比方说代码块和普通文字的样式过于贴近,我们还是更适用于有着明显背景色以及代码高亮的代码块。</p>
<p>并且像是表格、任务列表、删除线等特殊样式也没有被渲染出来。</p>
<h2 id="remark-gfm"><a class="markdownIt-Anchor" href="#remark-gfm"></a> <code>remark-gfm</code></h2>
<p>其实测试文本都告诉了我们为什么:那些是 GFM,也就是 GitHub Flavored Markdown 的特性,而 <code>react-markdown</code> 默认是不支持 GFM 的。</p>
<p>所以我们需要安装 <code>remark-gfm</code> 插件来支持 GFM:</p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install remark-gfm</span><br></pre></td></tr></tbody></table></figure>
<p><code>react-markdown</code> 引用插件的方式也很简单:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> remarkGfm <span class="keyword">from</span> <span class="string">"remark-gfm"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">ReactMarkdown</span> <span class="attr">remarkPlugins</span>=<span class="string">{[remarkGfm]}</span>&gt;</span>{markdownContent}<span class="tag">&lt;/<span class="name">ReactMarkdown</span>&gt;</span></span></span><br></pre></td></tr></tbody></table></figure>
<p>展现出来的效果:</p>
<p><img src="/posts/c0fc/2.png" alt="img.png"></p>
<p>看到这里,你肯定很是疑惑:表格的边框呢??</p>
<p>如果我们用开发者工具查看这个表格的样式,会发现表格确确实实被渲染成 <code>table</code> 标签,但是 User Agent Stylesheet 中的样式对 <code>table</code> 标签做了一些处理。 所以我们需要自己写一些样式来覆盖这些默认样式:</p>
<figure class="highlight css"><figcaption><span>index.CSS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">table</span> {</span><br><span class="line">    <span class="attribute">border-spacing</span>: <span class="number">0</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">border-collapse</span>: collapse <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">border-color</span>: inherit <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">display</span>: block <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">width</span>: max-content <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">max-width</span>: <span class="number">100%</span> <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">overflow</span>: auto <span class="meta">!important</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">tbody</span>, <span class="selector-tag">td</span>, <span class="selector-tag">tfoot</span>, <span class="selector-tag">th</span>, <span class="selector-tag">thead</span>, <span class="selector-tag">tr</span> {</span><br><span class="line">    <span class="attribute">border-color</span>: inherit <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">border-style</span>: solid <span class="meta">!important</span>;</span><br><span class="line">    <span class="attribute">border-width</span>: <span class="number">2px</span> <span class="meta">!important</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>把所有的样式统统加上 <code>!important</code>,这样就可以覆盖掉 User Agent Stylesheet 中的样式了。</p>
<p>再次展现出来的效果:</p>
<p><img src="/posts/c0fc/3.png" alt="img.png"></p>
<div class="danger">
<p>HTML 支持因为安全性考虑,我决定不使用,所以这里就不再展示了。</p>
</div>
<p>这样我们就完成了一个简单的 Markdown 渲染功能。不过在这个需求中有一个最为重要的功能:代码块的高亮。</p>
<h2 id="代码块的高亮"><a class="markdownIt-Anchor" href="#代码块的高亮"></a> 代码块的高亮</h2>
<p>因为项目的要求,代码块必须是突显出来、语句高亮的,这对行内代码块也一样。</p>
<p>这里我们可以使用 <code>react-syntax-highlighter</code></p>
<figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install react-syntax-highlighter</span><br></pre></td></tr></tbody></table></figure>
<p><code>react-syntax-highlighter</code> 具有两个引擎:<code>prism</code><code>highlight.js</code>。两者的区别详细可以直接去网上搜索。</p>
<p>这里我们使用 <code>prism</code> 引擎以及 <code>oneDark</code> 主题:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">Prism</span> <span class="keyword">as</span> <span class="title class_">SyntaxHighlighter</span> } <span class="keyword">from</span> <span class="string">"react-syntax-highlighter"</span>;</span><br><span class="line"><span class="keyword">import</span> { oneDark } <span class="keyword">from</span> <span class="string">"react-syntax-highlighter/dist/esm/styles/prism"</span>;</span><br></pre></td></tr></tbody></table></figure>
<p><code>ReactMarkdown</code><code>components</code> 属性中,我们可以自定义代码块的渲染方式。如果你看过网上的其他教程,近乎清一色的都是这样写的:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">ReactMarkdown</span></span><br><span class="line">    remarkPlugins={[remarkGfm]}</span><br><span class="line">    components={{</span><br><span class="line">        <span class="title function_">code</span>(<span class="params">{ node, inline, className, children, ...props }</span>) {</span><br><span class="line">            <span class="keyword">const</span> match = <span class="regexp">/language-(\w+)/</span>.<span class="title function_">exec</span>(className || <span class="string">''</span>)</span><br><span class="line">            <span class="keyword">return</span> !inline &amp;&amp; match ? (</span><br><span class="line">                <span class="language-xml"><span class="tag">&lt;<span class="name">SyntaxHighlighter</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">style</span>=<span class="string">{nightOwl}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">language</span>=<span class="string">{match[1]}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">PreTag</span>=<span class="string">"div"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    <span class="attr">children</span>=<span class="string">{String(children).replace(/\n$/,</span> '')}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                    {<span class="attr">...props</span>}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                /&gt;</span></span></span><br><span class="line">            ) : (</span><br><span class="line">                <span class="language-xml"><span class="tag">&lt;<span class="name">code</span> <span class="attr">className</span>=<span class="string">{className}</span> {<span class="attr">...props</span>} <span class="attr">children</span>=<span class="string">{children}</span> /&gt;</span></span></span><br><span class="line">            )</span><br><span class="line">        }</span><br><span class="line">    }}</span><br><span class="line">&gt;</span><br><span class="line">    {markdownContent}</span><br><span class="line">&lt;/<span class="title class_">ReactMarkdown</span>&gt;</span><br></pre></td></tr></tbody></table></figure>
<p>这里我们自定义了 <code>code</code> 组件的渲染行为。</p>
<blockquote>
<p><code>code({ node, inline, className, children, ...props }) {}</code> 中的参数们分别表示了:</p>
<ul>
<li><code>node</code>:当前节点</li>
<li><code>inline</code>:是否是行内代码块</li>
<li><code>className</code>:类名</li>
<li><code>children</code>:子节点(代码块中的内容)</li>
<li><code>...props</code>:其他属性</li>
</ul>
</blockquote>
<p>接着我们使用正则表达式来匹配 <code>className</code> 中的 <code>language-xxx</code>,如果匹配到并且不是行内代码块,就使用 <code>SyntaxHighlighter</code> 来渲染代码块,否则使用默认的 <code>code</code> 标签。</p>
<p><em><strong>但是!!!!</strong></em></p>
<p><code>inline</code> 属性在新版本的 <code>react-markdown</code> 中已经被废弃、不会作为参数传入了!!!</p>
<p>这让当时的我非常头疼,网上的资料翻了翻也没有找到解决方案,一时选择去解决另一个问题:没有定义语言的代码块的渲染。</p>
<p>代码块被渲染后,和行内代码块最大的区别是外面贴了一套 <code>pre</code> 标签。由于 <code>code</code> 标签作为子标签无法从 <code>props</code> 中获取到父标签的样式,但是反过来想,<code>pre</code> 标签是可以获取到 <code>code</code> 标签的样式的呀!</p>
<p>于是我嘎嘎乱写:</p>
<figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Markdown</span></span><br><span class="line">    remarkPlugins={[remarkGfm]}</span><br><span class="line">    components={{</span><br><span class="line">        <span class="title function_">pre</span>(<span class="params">{node, className, children, ...props}</span>) {</span><br><span class="line">            <span class="keyword">if</span> (children[<span class="string">"type"</span>] === <span class="string">"code"</span>) {</span><br><span class="line">                <span class="keyword">try</span> {</span><br><span class="line">                    <span class="keyword">const</span> match = children[<span class="string">"props"</span>][<span class="string">"className"</span>].<span class="title function_">match</span>(<span class="regexp">/language-(\w+)/</span>)</span><br><span class="line">                    <span class="keyword">return</span> (</span><br><span class="line">                        <span class="language-xml"><span class="tag">&lt;<span class="name">pre</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                            <span class="tag">&lt;<span class="name">SyntaxHighlighter</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">style</span>=<span class="string">{oneDark}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">language</span>=<span class="string">{match[1]}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">PreTag</span>=<span class="string">"div"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">showLineNumbers</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">wrapLongLines</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">children</span>=<span class="string">{String(children[</span>"<span class="attr">props</span>"]["<span class="attr">children</span>"])<span class="attr">.replace</span>(/\<span class="attr">n</span>$/, '')}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                            /&gt;</span></span></span><br><span class="line"><span class="language-xml">                        <span class="tag">&lt;/<span class="name">pre</span>&gt;</span></span></span><br><span class="line">                    )</span><br><span class="line">                } <span class="keyword">catch</span> (e) {</span><br><span class="line">                    <span class="keyword">return</span> (</span><br><span class="line">                        <span class="language-xml"><span class="tag">&lt;<span class="name">pre</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">                            <span class="tag">&lt;<span class="name">SyntaxHighlighter</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">style</span>=<span class="string">{oneDark}</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">language</span>=<span class="string">"python"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">PreTag</span>=<span class="string">"div"</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">showLineNumbers</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">wrapLongLines</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                                <span class="attr">children</span>=<span class="string">{String(children[</span>"<span class="attr">props</span>"]["<span class="attr">children</span>"])<span class="attr">.replace</span>(/\<span class="attr">n</span>$/, '')}</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">                            /&gt;</span></span></span><br><span class="line"><span class="language-xml">                        <span class="tag">&lt;/<span class="name">pre</span>&gt;</span></span></span><br><span class="line">                    )</span><br><span class="line">                }</span><br><span class="line">            }</span><br><span class="line">        }</span><br><span class="line">    }}</span><br><span class="line">&gt;</span><br><span class="line">    {markdownContent}</span><br><span class="line">&lt;/<span class="title class_">Markdown</span>&gt;</span><br></pre></td></tr></tbody></table></figure>
<p>直接在 <code>pre</code> 标签中判断 <code>children</code> 的类型,如果是 <code>code</code> 那就肯定是代码块了,然后再去匹配 <code>className</code> 中的语言类型。</p>
<p>如果匹配不到就会导致错误,所以我临时加了一个 <code>try...catch</code> 语句,匹配不到的话就默认渲染成 <code>python</code> 语言的代码块吧。</p>
<p>效果还是不错的:</p>
<p><img src="/posts/c0fc/4.png" alt="img.png"></p>
<h2 id="后话"><a class="markdownIt-Anchor" href="#后话"></a> 后话</h2>
<p>这个需求的实现还是有一些问题的,比方说行内代码块的渲染就没有解决、 <code>try...catch</code> 语句并不是一个好的解决方案,以及没有定义语言就默认渲染成 <code>python</code> 语言的代码块的技术债太鬼畜了!</p>
<p>但是由于项目的时间紧迫,我也没有再去深究这个问题。或许以后时间充裕了我会再去解决这个问题。以后再说吧。</p>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="de2c.html">上一篇</a><a class="next" href="14d5.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/c0fc.html" data-full-url="https://cytrogen.icu/posts/c0fc.html" data-mode="static">
              <h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
              <div class="webmention-list"></div>
              <span>暂无 Webmentions</span>
            </div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
  const codeBlocks = document.querySelectorAll('figure.highlight');
  
  codeBlocks.forEach(block => {
    let caption = block.querySelector('figcaption');
    if (!caption) {
      caption = document.createElement('figcaption');
      block.insertBefore(caption, block.firstChild);
    }

    const info = document.createElement('div');
    info.className = 'info';
    
    const filename = caption.querySelector('span');
    if (filename) {
      filename.className = 'filename';
      info.appendChild(filename);
    }
    
    const lang = block.className.split(' ')[1];
    if (lang) {
      const langSpan = document.createElement('span');
      langSpan.className = 'lang-name';
      langSpan.textContent = lang;
      info.appendChild(langSpan);
    }

    const sourceLink = caption.querySelector('a');
    if (sourceLink) {
      sourceLink.className = 'source-link';
      info.appendChild(sourceLink);
    }

    const actions = document.createElement('div');
    actions.className = 'actions';

    const codeHeight = block.scrollHeight;
    const threshold = 300;

    if (codeHeight > threshold) {
      block.classList.add('folded');
      
      const toggleBtn = document.createElement('button');
      toggleBtn.textContent = '展开';
      toggleBtn.addEventListener('click', () => {
        block.classList.toggle('folded');
        toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
      });
      actions.appendChild(toggleBtn);
    }

    const copyBtn = document.createElement('button');
    copyBtn.textContent = '复制';
    copyBtn.addEventListener('click', async () => {
      const codeLines = block.querySelectorAll('.code .line');
      const code = Array.from(codeLines)
        .map(line => line.textContent)
        .join('\n')
        .replace(/\n\n/g, '\n');
      
      try {
        await navigator.clipboard.writeText(code);
        copyBtn.textContent = '已复制';
        copyBtn.classList.add('copied');
        
        setTimeout(() => {
          copyBtn.textContent = '复制';
          copyBtn.classList.remove('copied');
        }, 3000);
      } catch (err) {
        console.error('复制失败:', err);
        copyBtn.textContent = '复制失败';
        
        setTimeout(() => {
          copyBtn.textContent = '复制';
        }, 3000);
      }
    });
    actions.appendChild(copyBtn);

    caption.innerHTML = '';
    caption.appendChild(info);
    caption.appendChild(actions);

    const markedLines = block.getAttribute('data-marked-lines');
    if (markedLines) {
      const lines = markedLines.split(',');
      lines.forEach(range => {
        if (range.includes('-')) {
          const [start, end] = range.split('-').map(Number);
          for (let i = start; i <= end; i++) {
            const line = block.querySelector(`.line-${i}`);
            if (line) line.classList.add('marked');
          }
        } else {
          const line = block.querySelector(`.line-${range}`);
          if (line) line.classList.add('marked');
        }
      });
    }
  });
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
  document.addEventListener('DOMContentLoaded', function() {
    const themeToggle = document.querySelector('.theme-toggle');
    
    if (!themeToggle) return;
    
    const getCurrentTheme = () => {
      return document.documentElement.getAttribute('data-theme') || 'light';
    };
    
    const updateUI = (theme) => {
      const isDark = theme === 'dark';
      themeToggle.setAttribute('aria-pressed', isDark.toString());
    };
    
    const setTheme = (theme) => {
      document.documentElement.setAttribute('data-theme', theme);
      document.documentElement.style.colorScheme = theme;
      
      const pageWrapper = document.getElementById('page-wrapper');
      if (pageWrapper) {
        pageWrapper.setAttribute('data-theme', theme);
      }
      
      // Find and remove the temporary anti-flicker style tag if it exists.
      // This ensures the main stylesheet takes full control after the initial load.
      const antiFlickerStyle = document.getElementById('anti-flicker-style');
      if (antiFlickerStyle) {
        antiFlickerStyle.remove();
      }
      
      localStorage.setItem('theme', theme);
      updateUI(theme);
    };
    
    const toggleTheme = () => {
      const current = getCurrentTheme();
      const newTheme = current === 'light' ? 'dark' : 'light';
      setTheme(newTheme);
    };
    
    updateUI(getCurrentTheme());
    
    themeToggle.addEventListener('click', toggleTheme);
    
    if (window.matchMedia) {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
      mediaQuery.addEventListener('change', function(e) {
        if (!localStorage.getItem('theme')) {
          const theme = e.matches ? 'dark' : 'light';
          setTheme(theme);
        }
      });
    }
  });
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
  document.addEventListener('DOMContentLoaded', function() {
    const backToTopBtn = document.querySelector('.back-to-top');
    
    if (!backToTopBtn) return;
    
    const toggleButtonVisibility = () => {
      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      const shouldShow = scrollTop > 200;
      
      if (shouldShow) {
        backToTopBtn.classList.add('is-visible');
      } else {
        backToTopBtn.classList.remove('is-visible');
      }
    };
    
    let ticking = false;
    const handleScroll = () => {
      if (!ticking) {
        requestAnimationFrame(() => {
          toggleButtonVisibility();
          ticking = false;
        });
        ticking = true;
      }
    };
    
    const scrollToTop = (event) => {
      event.preventDefault();
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    };
    
    window.addEventListener('scroll', handleScroll);
    backToTopBtn.addEventListener('click', scrollToTop);
    
    toggleButtonVisibility();
  });
})();</script></body></html>