OREMATOPEE

プログラミング、気になったこと、メモ書き...etc

はてなブログのコードブロックにテーマを設定する(シンタックスハイライト)

はてなブログで配布されているテーマをそのまま使っていたが、コードブロックが白と黒一色だったりしてぱっと見で変数や関数がわかりづらかったので、ちゃんとコードの構成要素に沿った形で色付け表示してみた。

highlight.jsというツールを使うことで簡単に導入可能で、少しはてなブログをカスタマイズするだけ。 まず、現在の状態を確認しておく。

f:id:npakk:20210725001335p:plain
コードブロックが白と黒で表現されていて見づらい...

highlight.jsを導入することでこのような表示になる。

f:id:npakk:20210725001532p:plain
エディタで見るような色付けがされていて見やすい

導入方法

以下のコードをはてなブログ管理画面 → 設定 → 詳細設定 → headに要素を追加に貼り付ける。

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/styles/base16/tomorrow-night.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/languages/erb.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/languages/plaintext.min.js">
</script>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
  document.querySelectorAll('pre.code').forEach((block) => {
    hljs.highlightBlock(block);
    hljs.lineNumbersBlock(block);

    var classes = block.classList;
    if(classes.length > 0){
      if(classes[1].indexOf(':')){
        var values = classes[1].split(':');
        var filename = values[1];
        if(filename){
          block.setAttribute('data-filename', filename)
          block.classList.remove(classes[1]);
          block.classList.add(values[0]);

          var containerEle = document.createElement('div');
          containerEle.classList.add('filename-container');
          var filenameEle = document.createElement('span');
          filenameEle.classList.add('filename');
          filenameEle.append(document.createTextNode(filename));
          containerEle.append(filenameEle)

          block.parentNode.insertBefore(containerEle, block);
        }
      }
    }

    // ちらつき解消
    block.classList.add("visible");
  });
});
</script>
<style>
/* ファイル名表示 */
.filename-container {
  position: relative;
  z-index: 10;
  top: 29px;
}
.filename {
  display: inline-block;
  vertical-align: top;
  max-width: 100%;
  background: rgba(177,197,247,.25);
  color: #fff;
  font-size: 0.8em;
  height: 24px;
  line-height: 24px;
  padding: 0 6px 0 8px;
  font-family: monospace;
  border-radius: 4px 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* ちらつき解消 */
.entry-content pre.code {
  opacity: 0;
  transition: opacity 0.5s ease;
}
.entry-content pre.code.visible {
  opacity: 1;
}
.hljs-ln-numbers {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  text-align: center;
  color: #666;
}
/* 枠線の削除 */
.hljs-ln {
  margin-bottom: 0;
  border: none;
}
.hljs-ln tr, .hljs-ln td {
  border: none;
}
</style>

参考記事からそのまま貼り付けたものもあるので、もっとキレイに書けるはずです...。

行番号を表示したくない場合はこちら

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/styles/base16/tomorrow-night.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/languages/erb.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/languages/plaintext.min.js">
</script>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
  document.querySelectorAll('pre.code').forEach((block) => {
    hljs.highlightBlock(block);

    var classes = block.classList;
    if(classes.length > 0){
      if(classes[1].indexOf(':')){
        var values = classes[1].split(':');
        var filename = values[1];
        if(filename){
          block.setAttribute('data-filename', filename)
          block.classList.remove(classes[1]);
          block.classList.add(values[0]);

          var containerEle = document.createElement('div');
          containerEle.classList.add('filename-container');
          var filenameEle = document.createElement('span');
          filenameEle.classList.add('filename');
          filenameEle.append(document.createTextNode(filename));
          containerEle.append(filenameEle)

          block.parentNode.insertBefore(containerEle, block);
        }
      }
    }

    // ちらつき解消
    block.classList.add("visible");
  });
});
</script>
<style>
/* ファイル名表示 */
.filename-container {
  position: relative;
  z-index: 10;
  top: 29px;
}
.filename {
  display: inline-block;
  vertical-align: top;
  max-width: 100%;
  background: rgba(177,197,247,.25);
  color: #fff;
  font-size: 0.8em;
  height: 24px;
  line-height: 24px;
  padding: 0 6px 0 8px;
  font-family: monospace;
  border-radius: 4px 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* ちらつき解消 */
.entry-content pre.code {
  opacity: 0;
  transition: opacity 0.5s ease;
}
.entry-content pre.code.visible {
  opacity: 1;
}
</style>

何をやっているのか

特別難しいことはしていません。
CDN経由でhighlight.jsと拡張機能であるhighlightjs-line-number.jsを読みこんでカスタマイズし、あとはCSSで見た目を整えているだけ。
ただ、実装が少し複雑でこのポストの独自性があるところでいえば、ファイル名を表示させている箇所です。

QiitaZennはコードブロックのコードソースを記述したあと、:以降でファイル名を表示できます。 f:id:npakk:20210725012437p:plain

f:id:npakk:20210725012158p:plain
左上に「Dockerfile」とファイル名が表示されていますね。

Markdownの記法には色々方言があるのですが、:を使ってファイル名を表示されているこの記法は拡張されたものであり、はてなブログでは使えません。
どうにか実装する方法はないかと、こちらに行き着きましたが、要件は満たせているものの横スクロール時にファイル名がついてきてしまい断念...。
データ属性としてdata-filenameというものを用意しているのは便利なのですが、MarkdownがHTMLに出力されるときに使われるpre要素の疑似要素としてファイル名を表示させているため、親要素であるpreのスクロールに追従してしまうんですね。(回避方法はあるかと思いますが、思いつきませんでした。)

諦めてZennの実装ではどうなっているのか見てみると、ファイル名表示をpre要素とは別にdiv要素で実装していました。
data-filenameのデータ属性を見つけたらpre要素とは別でファイル名を表示するためのdivを生成すればいける!」とのことで、ソースの以下の箇所を実装しました。

    var classes = block.classList;
    if(classes.length > 0){
      if(classes[1].indexOf(':')){
        var values = classes[1].split(':');
        var filename = values[1];
        if(filename){
          block.setAttribute('data-filename', filename)
          block.classList.remove(classes[1]);
          block.classList.add(values[0]);

          var containerEle = document.createElement('div');
          containerEle.classList.add('filename-container');
          var filenameEle = document.createElement('span');
          filenameEle.classList.add('filename');
          filenameEle.append(document.createTextNode(filename));
          containerEle.append(filenameEle)

          block.parentNode.insertBefore(containerEle, block);
        }
      }
    }

data-filenameが記載された要素を見つけ、その親との間にdivを新たに生成しています。

テーマを変える

以下のソースのbase16/tomorrow-nightの部分を好きなテーマの名前に変更します。

~
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.1.0/styles/base16/tomorrow-night.min.css">
~

highlight.jsのdemoに対応しているテーマがありますのでまずはテーマを選ぶ。
適当なURLではうまくcdnを取り込めないので、ここで選んだテーマの正確なURLをコピーして、上記ソースを書き換えてください。

参考文献

はてなブログで highlight.js を使う (Markdown記法向け) - ぺんぎんの布団
[Markdown] Qiitaのようにコードに自動でファイル名を付ける
highlight.jsに行番号を追加する方法 - Kotonoha
GitHub - wcoder/highlightjs-line-numbers.js: Line numbering plugin for Highlight.js
【JavaScript】要素を追加するinsertBeforeとappendChildについて - TASK NOTES
JavaScriptで親や兄弟要素を取得する | cly7796.net
JavaScript | 複数のノードをまとめて追加(DocumentFragment)

最後に

実装中にこんな記事を見かけてzennを併用している私としては、絶対こっちの方がおすすめです... zenn.dev