Make組ブログ

Python、Webサービスや製品開発、ライブラリー開発についてhirokikyが書きます

highlight.jsで行ごとに区切りつつハイライトする

highlight.js というコードハイライトをするJavaScriptのライブラリーがあります。

highlightjs.org

このライブラリーで、「行ごと」にHTML要素を分割しつつハイライトする方法を説明します。

highilght.jsの基本的な使い方をおさらい

highlight.jsは基本的に関数を呼ぶだけでコードハイライトができるので便利です。 以下の例では "# Heading 1" という文字列をMarkdownとしてハイライトしています。

import hljs from 'highlight.js'

let result = hljs.highlight("markdown", "# Heading1")
console.log(result.value)  // '<span class="hljs-section"># Heading 1</span>'

highlight.js は '<span class="hljs-section"># Heading 1</span>' のようなハイライト用のHTMLタグが埋められた結果を返します。 .hljs-section クラスにCSSを当てることでハイライトができるというものです。

失敗例: 行を分けてからハイライトするとうまくいかない

画面の仕様により、表示するハイライト済みのコードを行ごとに区切りたいことがあります。 例えば以下のように、行番号を表示しつつコードを表示したいときなどがあります。

<table>
  <tr>
    <td>1</td>
    <td><span class="hljs-section"># Heading1</span></td>
  </tr>
  <tr>
    <td>2</td>
    <td>This is body</td>
  </tr>
</table>

もちろん行番号を表示するコードの実装方法も様々ですが、画面や動作の都合上 <table> で実装するとします。 このとき、肝心のコードの内容は # Heading 1This is body に行ごとに区切る必要があります。

以下のように行ごとにハイライトするのは良くありません。

import hljs from 'highlight.js'

let body = `# Heading 1
This is body

\`\`\`python
import this
\`\`\`
`

for (let row of body.split("\n")) {
  let result = hljs.highlight("markdown", row)
  console.log(result.value)
}

理由は、複数行にまたがるシンタックスMarkdownならコードブロックなど)に対応できないからです。 上記の例だと```pythonimport this 、 ``` が別々に解釈されるので、サブブロック(import this)のハイライトが効かなくなります。

回答: 状態を共有しながらハイライトする

highlight.jsの .highilght(...) 関数は第4引数に状態を渡せます。 ハイライトした結果の状態を引き継いで渡すことで、複数行にまたがるシンタックスでもうまく解釈されます。

function lineByLineHighilght (body) {
  let state = null
  return body.split("\n").map(function (row) {
    let result = hljs.highlight('markdown', row, true, state);
    state = result.top  // result.topの状態を次に受け渡す
    return result.value + "<br/>"
  })
}

こうすることで行ごとに区切りつつ、行ごとにハイライトが効くようになります (行ごとに毎度 <span class="...">...</span> は正しく閉じられます)。