スキップしてメイン コンテンツに移動

Google Blogger でマークダウンとシンタックスハイライトを使えるようにする

動機

Qiita からの乗り換えを考えている。

実現方法

マークダウンに markdown-it, シンタックスハイライトに highlight.js を使えば実現できる。
マークダウンは micromarkdown.js が良さげだったけど、highlight.js との組み合わせが分からなかったので markdown-it にした。

Google Blogger のテーマの html ファイルを編集し、</body> タグの直前(要するに body タグ内の一番最後)に以下を追加する。

<style>
div.md-toc ul {
  list-style: none;
  margin-top: 0px;
  margin-bottom: 0px;
  padding-inline-start: 0px;
}
div.md-toc ul ul {
  padding-inline-start: 1em;
}
div.md-toc li {
  list-style-type: none;
}
div.md-toc li a {
  color: rgba(0, 0, 0, 0.8);
  text-decoration: none;
  font-size: 14px;
}
div.md-toc li a:hover {
  color: rgba(0, 0, 0, 1.0);
}
blockquote::before {
  background-color: rgb(73, 75, 75);
  content: "";
  height: 100%;
  position: absolute;
  top: 0px;
  left: 0px;
  width: 4px;
  border-radius: 4px;
}
blockquote {
  position: relative;
  padding: 6px 16px;
  margin: 6px 0px;
}
blockquote p {
  color: rgba(0, 0, 0, 0.6);
  padding: 0px;
  margin: 0px;
}
code.hljs {
  tab-size: 4;
  border-radius: 8px;
  font-family: Consolas, "Liberation Mono" Menlo, Courier, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, monospace;
}
.code-lang {
  display: inline-block;
  color: rgb(255, 255, 255);
  background-color: rgb(94, 96, 96);
  border-radius: 0px 0px 8px 8px;
  font-family: YakuHanJPs, -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
  font-weight: 300;
  margin: 0px 0px 15px 0px;
  padding: 4px 10px 4px 10px;
}
</style>
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/atom-one-dark.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script> <!-- ハイライトを対応したい言語を適宜追加する https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.2/markdown-it.min.js"></script>
<script>/*<![CDATA[*/
(() => {
  const toc = document.querySelector('div.md-toc');
  let tocParentStack;
  let tocParentLevel;

  const md = markdownit({
    breaks: true,
    linkify: true,
    highlight: function (str, lang_and_filename) {
      let lang, filename;

      if (lang_and_filename) {
        if (lang_and_filename.indexOf(':') >= 0) {
          [lang, filename] = lang_and_filename.split(':');
        } else {
          lang = lang_and_filename;
        }
      }

      let preCode;
      let codeLang;

      if (filename == null) {
        preCode = '<pre><code class="hljs">';
        codeLang = '';
      } else {
        preCode = '<pre><code class="hljs" style="padding-top: 0px;">';
        codeLang = `<div class="code-lang"><span>${filename}</span></div><br/>`;
      }

      if (lang && hljs.getLanguage(lang)) {
        try {
          return preCode + codeLang +
                 hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
                 '</code></pre>';
        } catch (e) {
          console.error(e);
        }
      }

      return preCode + codeLang + md.utils.escapeHtml(str) + '</code></pre>';
    }
  })
  .use(markdownItAnchor, {
    level: [1, 2, 3],
    callback: toc == null ? undefined : function(token, { slug, title }) {
      const li = document.createElement('li');
      const a = document.createElement('a');

      a.href = '#' + slug;
      a.textContent = title;
      li.appendChild(a);

      const currentLevel = token.markup.length;

      if (tocParentLevel < currentLevel) {
        for (let i = 0; i < currentLevel - tocParentLevel; ++i) {
          tocParentStack[tocParentStack.length] = document.createElement('ul');
        }
      } else if (tocParentLevel > currentLevel) {
        for (let i = 0; i < tocParentLevel - currentLevel; ++i) {
          const li = document.createElement('li');

          li.appendChild(tocParentStack[tocParentStack.length - 1]);
          tocParentStack[tocParentStack.length - 2].appendChild(li);
          --tocParentStack.length;
        }
      }
      tocParentStack[tocParentStack.length - 1].appendChild(li);
      tocParentLevel = currentLevel;
    }
  });
  md.linkify.set({ fuzzyEmail: false });
  document.querySelectorAll('script[type="md"]').forEach(elem => {
    try {
      if (toc != null) {
        tocParentStack = [document.createElement('ul')];
        tocParentLevel = 1;
      }

      const div = document.createElement('div');

      div.className = 'markdown';
      div.innerHTML = md.render(elem.innerHTML.replaceAll('<' + 'mdscript', '<' + 'script').replaceAll('<' + '/mdscript', '<' + '/script').replaceAll(/&#(\d+);/g, (_, $1) => String.fromCharCode($1)));
      elem.parentNode.replaceChild(div, elem);

      if (toc != null) {
        while (tocParentStack.length > 0) {
          if (tocParentStack[0].childNodes.length <= 0) {
            tocParentStack.shift();
          } else {
            break;
          }
        }
        while (tocParentStack.length > 1) {
          const li = document.createElement('li');

          li.appendChild(tocParentStack[tocParentStack.length - 1]);
          tocParentStack[tocParentStack.length - 2].appendChild(li);
          --tocParentStack.length;
        }
        if (tocParentStack[0]?.childNodes?.length > 0) {
          toc.appendChild(tocParentStack[0]);
        }
      }
    } catch (e) {
      console.error(e);
    }
  });
})();
/*]]>*/</script>

Markdown を <script type="md"></script> タグで囲めば自動で html に変換される。
markdown-it のサンプル とほぼ同じだが、Qiita のような以下のようなファイル名の表示にも対応するため若干変更している。

マークダウン:
```js:sample.js
let a = 0;
```
実際の表示:
sample.js

let a = 0;

改行するためにスペース 2 個を入れるのはわずらしいため breaks: true を設定している。

注意事項

テキストをクライアントサイドで変換しているため、マークダウン中に </script> 閉じタグがある場合、マークダウン開始タグ(<script type="md">)を閉じてしまう問題がある。(ブラウザは <script> 開始タグを見つけると、単に文字列検索し次に文字列 "</script>" がある箇所を script タグの閉じタグと認識する。script タグについては、それが書かれている文脈やネスト構造などは考慮されない。)
これを防ぐにはマークダウン中では script タグではなく mdscript タグにしておくと、冒頭のコード内で表示上は単なる script タグに変換される。

blockquote 絡みの style はシンタックスハイライトとは関係ないが、引用を以下のような見た目で表示するために作成している。
シンタックスハイライトが必要なだけであれば消してよい。

引用です

名前の一部に toc が付いているスタイルや処理(callback プロパティに渡している function(token, { slug, title }) など)は、Table of contents(TOC) を生成する処理になっている。
<div class="md-toc"> な要素があれば、要素内に TOC を生成する処理になっている。
これも不要であれば消してよい。

コメント