<!DOCTYPE html><html lang="en" 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>Markdown Rendering in React · Cytrogen 的个人博客</title><meta name="description" content="This article is a practical guide on rendering Markdown in React, using the react-markdown library. It covers adding support for GitHub Flavored Markdown (GFM) with remark-gfm and implementing syntax highlighting for code blocks with react-syntax-highlighter. The tutorial provides a unique workaround for a common issue in newer versions of react-markdown where the inline prop is deprecated, offering a clever solution to correctly style code blocks. This guide is ideal for developers seeking an up-to-date method for creating rich Markdown experiences in their React applications."><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/f27d.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="../en/">HOME</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>English</span></button><div class="language-dropdown"><a class="language-option" href="/posts/c0fc.html">中文</a></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>MORE</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="../en/archives/index.html">ARCHIVE</a></li><li class="dropdown-item"><a class="nav-link" href="../en/categories/index.html">CATEGORIES</a></li><li class="dropdown-item"><a class="nav-link" href="../en/tags/index.html">TAGS</a></li><li class="dropdown-item"><a class="nav-link" href="../en/about/index.html">ABOUT</a></li><li class="dropdown-item"><a class="nav-link" href="../en/sitemap/index.html">SITEMAP</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="../en/">HOME</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../en/archives/index.html">ARCHIVE</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../en/categories/index.html">CATEGORIES</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../en/tags/index.html">TAGS</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../en/about/index.html">ABOUT</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../en/sitemap/index.html">SITEMAP</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/f27d.html">永久链接</a><div class="p-summary visually-hidden"><p>One of our project requirements was to support Markdown rendering, aiming to replicate effects similar to ChatGPT and Claude.</p>
<p>This article aims to document the problems I encountered and their solutions while implementing this feature.</p></div><div class="visually-hidden"><a class="p-category" href="../categories/Programming-Notes/">Programming Notes</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">Markdown Rendering in React</h1><div class="post-info"><time class="post-date dt-published" datetime="2025-02-21T01:46:08.000Z">2/20/2025</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.733Z"></time><a class="post-from u-translation-of" href="/posts/c0fc.html" target="_blank" title="/posts/c0fc.html">Translate · Original Link</a></div><div class="post-content e-content"><p>One of our project requirements was to support Markdown rendering, aiming to replicate effects similar to ChatGPT and Claude.</p>
<p>This article aims to document the problems I encountered and their solutions while implementing this feature.</p>
<span id="more"></span>
<div class="danger">
<p>This is an English translation of an article originally published in Chinese on <strong>February 20, 2024</strong>. The content of the original article may be <strong>outdated or deprecated</strong>. Please verify the current status of any tools, libraries, or methods mentioned before implementing them in your projects.</p>
</div>
<h1 id="react-markdown"><a class="markdownIt-Anchor" href="#react-markdown"></a> <code>react-markdown</code></h1>
<p>First, install <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>Let's test using the official example text from the <code>react-markdown</code> library:</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"> <Markdown rehypePlugins={[rehypeHighlight]}>{markdown}</Markdown>,</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">> 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"><blockquote></span><br><span class="line"> 👆 Use the toggle above to add the plugin.</span><br><span class="line"></blockquote></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"> <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 <MyFancyRule {...rest} /></span><br><span class="line"> }</span><br><span class="line"> }}</span><br><span class="line"> ></span><br><span class="line"> {markdown}</span><br><span class="line"> </Markdown>,</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>In the test text, there are also code blocks, but since I'm using Hexo for my blog, these texts that should be in code blocks (as I set) were all rendered as code blocks.</p>
<p>Therefore, I added a backslash after each code block to prevent them from being rendered as code blocks. <strong>You can remove these backslashes when testing</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">`{The Markdown test text mentioned earlier}`</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"><></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">ReactMarkdown</span>></span>{markdownContent}<span class="tag"></<span class="name">ReactMarkdown</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></></span></span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>The rendered effect (remember to replace the value of <code>markdownContent</code>):</p>
<p><img src="/posts/f27d/1.png" alt="Rendered Markdown without table, task lists, and strikethrough"></p>
<p>As you can see, there are some differences compared to Typora or GitHub's rendering effects. For example, the code blocks and regular text styles are too similar, and we'd prefer code blocks with distinct background colors and syntax highlighting.</p>
<p>Additionally, special styles like tables, task lists, and strikethrough weren't rendered.</p>
<h1 id="remark-gfm"><a class="markdownIt-Anchor" href="#remark-gfm"></a> <code>remark-gfm</code></h1>
<p>Actually, the test text already told us why: these are GFM (GitHub Flavored Markdown) features, and <code>react-markdown</code> doesn't support GFM by default.</p>
<p>So we need to install the <code>remark-gfm</code> plugin to support 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>Using plugins with <code>react-markdown</code> is quite simple:</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"><<span class="name">ReactMarkdown</span> <span class="attr">remarkPlugins</span>=<span class="string">{[remarkGfm]}</span>></span>{markdownContent}<span class="tag"></<span class="name">ReactMarkdown</span>></span></span></span><br></pre></td></tr></tbody></table></figure>
<p>The rendered effect:</p>
<p><img src="/posts/f27d/2.png" alt="Rendered Markdown without table borders"></p>
<p>At this point, you're probably wondering: where are the table borders??</p>
<p>If we inspect the table's styles using developer tools, we'll find that the table is indeed rendered as a <code>table</code> tag, but the User Agent Stylesheet has applied some processing to the <code>table</code> tag. So we need to write some styles to override these default styles:</p>
<figure class="highlight css"><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>Add <code>!important</code> to all styles to override the User Agent Stylesheet styles.</p>
<p>The rendered effect now:</p>
<p><img src="/posts/f27d/3.png" alt="Rendered Markdown with bold table border"></p>
<div class="danger">
<p>Due to security considerations, I decided not to use HTML support, so I won't demonstrate it here.</p>
</div>
<p>Now we've completed a simple Markdown rendering feature. However, there's one crucial feature in this requirement: code block highlighting.</p>
<h1 id="code-block-highlighting"><a class="markdownIt-Anchor" href="#code-block-highlighting"></a> Code Block Highlighting</h1>
<p>Due to project requirements, code blocks must be prominent and have syntax highlighting, which applies to inline code blocks as well.</p>
<p>Here we can use <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> has two engines: <code>prism</code> and <code>highlight.js</code>. You can search online for their detailed differences.</p>
<p>Here we'll use the <code>prism</code> engine with the <code>oneDark</code> theme:</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>In the <code>component</code> prop of <code>ReactMarkdown</code>, we can customize how code blocks are rendered. If you've seen other tutorials online, they almost all write it like this:</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"><<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 && match ? (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<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"> /></span></span></span><br><span class="line"> ) : (</span><br><span class="line"> <span class="language-xml"><span class="tag"><<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> /></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"> {markdownContent}</span><br><span class="line"></<span class="title class_">ReactMarkdown</span>></span><br></pre></td></tr></tbody></table></figure>
<p>Here we customized the rendering behavior of the <code>code</code> component.</p>
<blockquote>
<p>The parameters in <code>code({ node, inline, className, children, ...props }) {}</code> represent:</p>
<ul>
<li><code>node</code>: current node</li>
<li><code>inline</code>: whether it's an inline code block</li>
<li><code>className</code>: class name</li>
<li><code>children</code>: child nodes (content in code block)</li>
<li><code>...props</code>: other properties</li>
</ul>
</blockquote>
<p>Then we use regex to match <code>language-xxx</code> in <code>className</code>, and if there's a match and it's not an inline code block, we use <code>SyntaxHighlighter</code> to render the code block; otherwise, we use the default <code>code</code> tag.</p>
<p><em><strong>But!!!</strong></em></p>
<p>The <code>inline</code> property has been deprecated and won't be passed as a parameter in the new version of <code>react-markdown</code>!!!</p>
<p>This really gave me a headache at the time, and I couldn't find a solution after searching online. I decided to temporarily solve another problem: rendering code blocks without defined languages.</p>
<p>After rendering, the biggest difference between code blocks and inline code blocks is that code blocks have a <code>pre</code> tag wrapper. Since the <code>code</code> tag as a child tag can't get the parent tag's styles from <code>props</code>, but thinking reversely, the <code>pre</code> tag can get the <code>code</code> tag's styles!</p>
<p>So I wrote this wild solution:</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"><<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"><<span class="name">pre</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<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"> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">pre</span>></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"><<span class="name">pre</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<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"> /></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></<span class="name">pre</span>></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">></span><br><span class="line"> {markdownContent}</span><br><span class="line"></<span class="title class_">Markdown</span>></span><br></pre></td></tr></tbody></table></figure>
<p>Directly check the <code>children</code>'s type in the <code>pre</code> tag - if it's <code>code</code>, then it must be a code block, and then match the language type in <code>className</code>.</p>
<p>If there's no match, it will cause an error, so I temporarily added a <code>try...catch</code> statement - if there's no match, just default to rendering it as a Python language code block.</p>
<p>The effect is not bad:</p>
<p><img src="/posts/f27d/4.png" alt="Rendered Code Block with styles"></p>
<h1 id="afterword"><a class="markdownIt-Anchor" href="#afterword"></a> Afterword</h1>
<p>There are still some issues with this implementation, such as unresolved inline code block rendering, the <code>try...catch</code> statement not being a good solution, and the technical debt of defaulting to Python language for undefined language code blocks is quite messy!</p>
<p>However, due to project time constraints, I didn't delve deeper into these issues. Perhaps when I have more time in the future, I'll revisit and solve these problems. That's for another time.</p>
</div></article></div></main><footer><div class="paginator"><a class="prev" href="5e08.html">PREV POST</a><a class="next" href="6976.html">NEXT POST</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/f27d.html" data-full-url="https://cytrogen.icu/posts/f27d.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>No Webmentions yet</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">BLOGROLL</a><span class="footer-separator"> ·</span><a href="../links/index.html">LINKS</a><span class="footer-separator"> ·</span><a href="../contact/index.html">CONTACT</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">COLOPHON</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 = 'Expand';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? 'Expand' : 'Fold';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = 'Copy';
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 = 'Copied';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = 'Copy';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = 'Copy failed';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 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>