博客后台需要一个把 Markdown 转成 HTML 的功能。引一个 marked 当然可以,但这个转换本身只有几十行,值得自己写一遍。

难点不是替换,是顺序

如果直接按顺序做正则替换,代码块里的星号会被当成加粗,链接里的括号会被图片语法吃掉。解决办法:先把代码块和图片抽出来换成占位符,等所有行内替换做完再塞回去。

JS
const store = [];
const hold = html => { store.push(html); return '\0' + (store.length - 1) + '\0'; };

const FENCE = '`'.repeat(3);
src = src.replace(new RegExp(FENCE + '(\\w*)\\n([\\s\\S]*?)' + FENCE, 'g'),
  (m, lang, code) => hold('<pre><code data-lang="' + lang + '">' + esc(code) + '</code></pre>'));

块级语法

标题、引用、列表都是行首锚定的正则,注意 m 标志让 ^ 匹配每一行开头:

JS
html = html.replace(/^###\s*(.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^##\s*(.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^&gt;\s*(.+)$/gm, '<blockquote>$1</blockquote>');
html = html.replace(/^-\s+(.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, m => '<ul>' + m + '</ul>');

别忘了转义

顺序是:先转义,再解析。用户写的 <script> 必须先变成 &lt;script&gt;,否则解析器就是个 XSS 生成器。

唯一例外是占位符本身。用 \0 包住的数字不可能出现在转义后的文本里,最后一步统一还原即可。

剩下的坑

  • 嵌套列表要递归,行首空格数决定层级
  • 表格没有标准语法,不同实现差异最大
  • 行内代码里的星号不该被解析,得给它比行内替换更高的优先级

写完这一套大概 40 行,够个人博客用了。真正的复杂度在容错,而容错靠的是测试用例,不是更聪明的正则。