众所周知,实现代码语法高亮使用最多的便是highlight.js库,而其也实现了Angular版本,然而,其却有不少的问题:

  1. Demo和网上的方案大多是静态内容(或同步方式,即内容在服务端生成后随页面一同返回)的渲染,对动态内容渲染的支持并不友好。
  2. 即便实现了动态内容的渲染,在页面前后跳转时(前进、后退,此时SSR环境下是异步方式获取文章内容,即ngAfterViewInit只执行一次)却无法更新渲染内容重新渲染。
  3. 在code只有一行时无法强制显示代码行数,虽然API中的options提供了此选项。
  4. 无法优雅地自定义显示代码行数时的HTML结构(官方实现是table方式)和预览样式。
  5. 特殊字符的转义Bug。

综上,因此,需要自行想办法解决诸此种种问题。

一种方式是使用MutationObserver监听DOM的变化,但却有种种弊端和不便,且难免引入性能问题。

另一种方式便是本文的重点——使用正则表达式自行解析文章HTML内容。这种方式的好处不言自明了,既解决了动态内容的渲染问题,也避免了事件监听引发的一连串问题,还可以轻松定制HTML结构和样式,可谓一箭三雕、一石三鸟。

以下便是实现代码:

typescriptCopy code
  • 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
this.post.postContent = this.post.postContent.replace( /<pre(?:\s+[^>]*)*>\s*<code(?:\s+[^>]*)?>([\s\S]*?)<\/code>\s*<\/pre>/ig, (preStr, codeStr: string) => { const langReg = /^<pre(?:\s+[^>]*)*\s+class="([^"]+)"(?:\s+[^>]*)*>/ig; const langResult = Array.from(preStr.matchAll(langReg)); let langStr = ''; let language = ''; if (langResult.length > 0 && langResult[0].length === 2) { const langClass = langResult[0][1].split(/\s+/i).filter( (item) => item.split('-')[0].toLowerCase() === 'language' ); if (langClass.length > 0) { langStr = langClass[0].split('-')[1] || ''; if (langStr && highlight.getLanguage(langStr)) { language = langStr; } } } // unescape: ><& codeStr = codeStr.replace(/&lt;/ig, '<') .replace(/&gt;/ig, '>') .replace(/&amp;/ig, '&'); const lines = codeStr.split(/\r\n|\r|\n/i).map((str, i) => `<li>${i + 1}</li>`).join(''); const codes = language ? highlight.highlight(codeStr, { language }).value : highlight.highlightAuto(codeStr).value; return `<pre class="i-code"${langStr ? ' data-lang="' + langStr + '"' : ''}>` + `<ul class="i-code-lines">${lines}</ul>` + `<code class="i-code-text">${codes}</code></pre>`; } );
this.post.postContent = this.post.postContent.replace( /]*)*>\s*]*)?>([\s\S]*?)<\/code>\s*<\/pre>/ig, (preStr, codeStr: string) => { const langReg = /^]*)*\s+class="([^"]+)"(?:\s+[^>]*)*>/ig; const langResult = Array.from(preStr.matchAll(langReg)); let langStr = ''; let language = ''; if (langResult.length > 0 && langResult[0].length === 2) { const langClass = langResult[0][1].split(/\s+/i).filter( (item) => item.split('-')[0].toLowerCase() === 'language' ); if (langClass.length > 0) { langStr = langClass[0].split('-')[1] || ''; if (langStr && highlight.getLanguage(langStr)) { language = langStr; } } } // unescape: ><& codeStr = codeStr.replace(/</ig, '<') .replace(/>/ig, '>') .replace(/&/ig, '&'); const lines = codeStr.split(/\r\n|\r|\n/i).map((str, i) => `
  • ${i + 1}
  • `).join(''); const codes = language ? highlight.highlight(codeStr, { language }).value : highlight.highlightAuto(codeStr).value; return `
    ` +
          `
      ${lines}
    ` + `${codes}
    `; } );

    简要介绍下代码要点:

    1. 使用惰性匹配获取所有pre>code标签,将其替换为highlight渲染后的HTML。
    2. 替换时查询代码使用的编程语言class,作为渲染的参数之一。
    3. 转义特殊字符。
    4. 解析代码行数,并生成HTML。
    5. 组装完整的渲染后的HTML。

    代码的关键便是几处正则表达式的使用,尤其是惰性匹配捕获组非捕获组的概念的使用。具体定义和Demo本文不再赘述,读者可自行查阅官方文档。

    最后,一点感慨和收获……

    代码写的越多,越发现正则作为效率利器,真是实至名归。如今,各大IDE的查找、替换,甚至包括office等都支持正则表达式了,熟悉并加以善用,节省的时间不是一星半点——其应用已远远超出编程、开发的范畴,以至各种实际的生活、办公场景。

    另,不得不感慨下ES6,以上代码倘若不是用ES6,而是用传统的ES5甚至更原始版本的引擎的话,不知要增加多少代码行数和工作量。┓( ´∀` )┏