Blogger の投稿エディタを Markdown エディタに改造する

Markdown で記事を書くことが多いのですが、現在お世話になっている Blogger の投稿エディタは残念ながら Markdown に対応していません。

そのため、Markdown で記事を書くときは以下のような手順を踏んでいます。

  1. Markdown エディタで記事を書く
  2. 記事を HTML に変換
  3. HTML を Blogger の投稿エディタに貼り付け
  4. 画像や修正を加えたのちに投稿

1と2に関しては1個のサービスで賄えるところもありますが、外部サービスを使う必要があるのは同じです。全部 Blogger でできたらいいのに!

そんなわけで、Blogger の投稿エディタを Markdown エディタに改造してみました。

Markdown エディタの実装方針

Markdown を HTML に変換するライブラリを使えば、Blogger でも Markdown を使えます。数ある Markdown のライブラリの中でも、Blogger 界隈では marked.js がよく用いられているようです。

以下の記事に Blogger に marked.js を導入する方法が書かれています。

これらの方法を試した結果、使いやすいライブラリだとわかったので、今回は Markdown パーサーとして maked.js v15.0.0 を採用しました。

Blogger に Markdown エディタを実装する上でこだわったのは以下の3点です。

  • Markdown を投稿エディタに直接記述できる(textarea などの中に記述しない)
  • 変換された HTML をコピーできる
  • Markdown で書くときだけ Markdown スクリプトを読み込む

これらの条件を満たしたカスタマイズをこれから紹介していきます。

Markdown エディタの導入方法

このカスタマイズは Blogger のテーマ編集を伴います。必ずテーマのバックアップを取り、テスト用のブログで試した上での導入をおすすめします。

はじめに、Blogger のテーマ編集内で以下を検索します。

<data:post.body/>

次のような記事本文を表示させるコードが見つかったら、<data:post.body/> を囲んでいる要素の属性と属性値を控えておいてください。

<!-- ↓ここの属性と属性値を控えておく -->
<div class='post-body'>
  <data:post.body/>
</div>

記事表示部分はお使いのテーマによって異なるため、上記の通りではありません。

さらに、上のような記事本文表示のコードを以下のコードに置き換えます。

<b:comment>Markdown エディタ</b:comment>
<b:with value='&quot;&lt;!-- markdown-mode-on --&gt;&quot;' var='md_comment'>
<b:with value='data:post.body contains data:md_comment' var='md_enabled'>
  
  <b:comment>メッセージ</b:comment>
  <b:if cond='data:view.isPreview'>
    <div class='md-message'>
      <b:if cond='data:md_enabled'>
        <p>Markdown モードがオンになっています&#12290;オフにしたい場合は投稿から <input expr:value='data:md_comment.escaped' type='text'/> を削除してください&#12290;</p>
        <details class='md-convert'>
          <summary>変換後の HTML</summary>
          <textarea aria-label='変換後の HTML' class='md-output'/>
          <button aria-label='コードを全選択する' class='md-select'>コード全選択</button>
        </details>
      <b:else/>
        <p>Markdown モードをオンにしたい場合は投稿に <input expr:value='data:md_comment.escaped' type='text'/> を追加してください&#12290;</p>
      </b:if>
    </div>
  </b:if>
  
  <b:comment>投稿, ページ本文</b:comment>
  <div class='post-body'>
    <b:if cond='data:md_enabled'>
      <b:class cond='data:md_enabled' name='md-input'/>
      <data:post.body.escaped/>
    <b:else/>
      <data:post.body/>
    </b:if>
  </div>
  
  <b:comment>Markdown 変換</b:comment>
  <b:if cond='data:md_enabled'>
    <script src='https://cdn.jsdelivr.net/npm/marked/marked.min.js'/>
    
<script>
(() =&gt; {
  const comment = &#39;<data:md_comment/>&#39;;
//<![CDATA[
  const input = document.querySelector('div.md-input');
  const output = document.querySelector('textarea.md-output');
  const select = document.querySelector('button.md-select');

  if(!input) return;

  const converted = marked.parse(input.textContent.replace(comment, ''));
  input.innerHTML = converted;
  input.classList.remove('md-input');

  if(output){
    output.value = converted.replace(/<a name=["']more["']>.*?<\/a>/, '<!--more-->').replace(/\n{3,}/g, '\n\n').trim();
  }

  if(!select) return;

  select.addEventListener('click', () => {
    output.focus();
    output.setSelectionRange(0, output.value.length);
  });
})();
//]]></script>
  </b:if>
</b:with>
</b:with>

上のコード内の記事本文部分である以下の div に、先ほど控えておいた属性と属性値を追加してください。不要な場合、.post-body を消しても大丈夫です。

<!-- ↓ここに控えた属性追加 -->
<div class='post-body'>
  <b:if cond='data:md_enabled'>
    <b:class cond='data:md_enabled' name='md-input'/>
    <data:post.body.escaped/>
  <b:else/>
    <data:post.body/>
  </b:if>
</div>

最後にこちらの CSS を head 内に追加し、テーマを保存すれば導入完了です!

<b:comment>Markdown モード用 CSS</b:comment>
<b:if cond='data:view.isSingleItem'>
<style>/*<![CDATA[*/
.md-input{
  display: none;
}
/*]]>*/</style>
<noscript><style>/*<![CDATA[*/
.md-input{
  display: block;
  white-space: pre-wrap;
}
/*]]>*/</style></noscript>
<b:if cond='data:view.isPreview'>
<style>/*<![CDATA[*/
.blogger-clickTrap{
  display: none!important;
}
.md-message{
  margin: 32px 0;
  padding: 16px;
  background: #eee;
  color: #333;
  line-height: 1.6;
  font-size: 15px;
}
.md-message input{
  field-sizing: content;
}
.md-convert{
  margin-top: 16px;
}
.md-convert summary{
  padding: 8px 16px;
  background: #333;
  color: #fff;
}
.md-output{
  display: block;
  padding: 16px;
  width: 100%;
  height: 40vh;
  max-height: 400px;
  background: #fff;
  color: #333;
  font-size: 16px;
  line-height: 1.6;
}
.md-select{
  display: inline-block;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border: 0;
  background: #ccc;
  margin-top:16px;
  padding: 8px 16px;
  background: #333;
  color: #fff;
}
.md-select:hover, .md-select:focus{
  background: #999;
}
/*]]>*/</style>
</b:if>
</b:if>

Markdown エディタの使用方法

前項の作業が完了すると、Blogger の投稿エディタ で Markdown モードのオン・オフの切り替えができます。デフォルトでは Markdown モードはオフです。

オンにするためには、Markdown で書きたい記事の投稿エディタにアクセスし、HTML ビューのエディタに以下のコメントを追加します。

<!-- markdown-mode-on -->

markdown-mode-on の両側の半角スペースをどちらかでも消してしまうと Markdown モードがオンになりません。

コメントの文字列を忘れた場合は、投稿のプレビュー画面を開くと以下のようなメッセージが表示されるので、入力欄のコメントをコピーしてください。

Markdown モードがオフのとき Blogger のプレビュー画面に表示されるメッセージ。「Markdown モードをオンにしたい場合は投稿にコメント <!-- markdown-mode-on --> を追加してください」と表示されている。コメントは input 要素内に書かれている

プレビュー全体を覆って誤クリックを防ぐ .blogger-clickTrap という要素を非表示にすることで、プレビュー内のクリックを有効にしています。詳細や誤クリックの防止については、以下の記事をご覧ください。

エディタにコメントが追加できたら、プレビュー画面を更新します。画像のようにメッセージに「Markdown モードがオンになっています」と表示されていたらオッケーです!

Markdown モードがオンのとき Blogger のプレビュー画面に表示されるメッセージ。「Markdown モードがオンになっています。オフにしたい場合は投稿からコメント <!-- markdown-mode-on --> を削除してください」と表示されている

反対にオフにしたい場合は、上記のコメントをエディタから削除します。

コメントを追加した状態で HTML ビューの投稿エディタに直接 Markdown を書いていくと、投稿や固定ページで HTML で表示されます。

Markdown モードがオンのときの Blogger の投稿エディタ(左)とプレビュー画面(右)。投稿エディタには Markdown で記事が書かれており、プレビュー画面にはその文章が HTML で表示されている

メッセージの details を開くと、 textarea に変換された HTML が出力されています。下の「コード全選択」ボタンをクリックして、コンテキストメニューや Ctrl + C(Mac の場合は Command + C)などでコピーしてください。

Markdown モードがオンのとき、Blogger のプレビュー画面に表示される details 要素。summary 要素には「変換後の HTML」と書かれており、展開すると HTML が出力された textarea が表示される。その下には「コード全選択」と書かれたボタンがある

(プレビュー画面では Clipboard API が使えません。execCommand('copy') は非推奨の機能なのであまり使いたくない) 

コピーした HTML は Markdown をオンにするためのコメントを含みません。そのため、エディタ内を総置換すると自動的に Markdown モードがオフになります。

Markdown モードがオンの場合の注意点として、エディタに書かれた script タグが一切実行されません。これは innerHTML の仕様です。詳細は以下のサイトをご覧ください。

記事内で実行させたいスクリプトがあるときもやはり、プレビュー画面でコピーした HTML をお使いください。

Markdown エディタのカスタマイズ

Blogger に実装した Markdown エディタに対してさらに手を加えていきます。

出力される HTML を上書きする

marked.js には、出力される HTML を自分好みに上書きできる renderer という拡張機能があります。

デフォルトでどういう HTML が出力されるかのかを以下のページで確認できます。

TypeScript で書かれているので JavaScript と若干コードが違います。たとえば TypeScript の場合、見出しに関するコードは以下のようになっています。

heading({tokens, depth}: Tokens.Heading): string {
  return `<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n`;
}

上のコードを JavaScript で使用する場合は、以下のように : Tokens.Heading: string を削除します。

heading({tokens, depth}) {
  return `<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n`;
}

見出し以外の要素も同じ箇所を削除したら、あとは関数の中身を書き換えていきます。

marked.js には tokens という、ざっくり言うと変数・定数のようなものがあり(上のコードの tokensdepth がそれです)、これを利用して JavaScript のコードと出力された HTML を照らし合わせながら、理想の HTML に近付けていきます。

marked.js が出力した HTML は、デフォルトだとタグとタグの間に空行がなかったり、li タグがインデントされていなくて見づらかったので、この機能を使って書き換えてみました。

以下のコードを、marked.parse() を使う部分の前に挿入します。

const escapeHTML = (text) => {
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

const renderer = {
  code({text, lang}){
    const code = escapeHTML(text);
    return `\n<pre${lang ? ' data-title="' + lang + '"' : ''}><code>${code}</code></pre>\n\n`;
  },

  blockquote({tokens}){
    const body = this.parser.parse(tokens).replace(/\n$/, '');
    return `\n<blockquote>${body}</blockquote>\n\n`;
  },

  heading({tokens, depth}){
    return `\n<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n\n`;
  },

  hr(token){
    return '\n<hr/>\n\n';
  },

  list(token){
    const ordered = token.ordered;
    const start = token.start;

    let body = '';

    token.items.forEach(item => {
      body += '  ' + this.listitem(item);
    })

    const type = ordered ? 'ol' : 'ul';
    const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : '';
    return '\n<' + type + startAttr + '>\n' + body + '</' + type + '>\n\n';
  },

  paragraph({tokens}){
    const text = this.parser.parseInline(tokens);
    const regex = /^\s*(<a name=["']more['"]>).*?(<\/a>)\s*$/;
    if(regex.test(text)){
      return `\n${text.replace(regex, '$1$2')}\n`;
    }else{
      return `<p>${text}</p>\n`;
    }
  }
}

marked.use({renderer});

ブロックレベル要素の上下に空行を空け、li タグをインデントし、そのほか自分の使いやすいように HTML を書き換えました。コードブロック(上のコードの code({text, lang}){...})でデフォルトのエスケープ機能がうまく動かなかったので、escapeHTML 関数を作って対応しています。

書き換え前後の HTML を見比べてみると、冒頭の一部分だけでも書き換え後のほうが明らかに見やすくなっているのがわかると思います。

renderer 適用前の Blogger のプレビュー画面に表示される HTML が出力された textarea。p 要素と ol 要素の間に空行がなく、li 要素の前にはインデントがないのでコードが見づらい
renderer 適用前
renderer 適用後の Blogger のプレビュー画面に表示される HTML が出力された textarea。p 要素と ol 要素の間に1行空行が入り、li 要素の前にはスペース2個分のインデントがあるためでコードが見やすい
renderer 適用後

出力される HTML をサニタイズ(無害化)する

サニタイズとは、HTML 内に含まれる悪意あるスクリプトやコメントを取り除いて無害化することを指します。これにより、HTML に悪質なスクリプトを埋め込むクロスサイトスクリプティング(XXS)攻撃を防ぐことができます。

ブログ管理者だけが書き込めるエディタに対してやる必要はないと思いますが、marked.js 公式がサニタイズを推奨しているので、一応やり方を書いておきます。

marked.js 自体にサニタイズ機能はないため、別のライブラリを使う必要があります。公式が推奨している DOMPurify を使ったサニタイズの手順を紹介します。

まず、以下のコードを marked.jsscript タグの上か下に追加します。

<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"/>

次に、marked.js で Markdown を HTML に変換するコードを以下のように変更します。

const converted = marked.parse(input.textContent.replace(comment, ''));
const converted = DOMPurify.sanitize(marked.parse(input.textContent.replace(comment, '')));

これで marked.js が出力する HTML を無害化できました。

marked.js を遅延読み込みする

記事本文の真下にスクリプトを置いているため、PageSpeed Insightsで「レンダリングを妨げるリソースの除外」を指摘されることがあります。

スクリプトを </body> の直前に置けば改善しますが、Markdown モードがオンのときのみ marked.js を読み込むようにしているため、設置位置はできれば変更したくありません。

そこでおすすめなのが、marked.js の遅延読み込みです。

まず、marked.js を読み込む script タグに defer='defer' を付けます。

<script src='https://cdn.jsdelivr.net/npm/marked/marked.min.js' defer='defer'/>

さらにその下の script タグを以下のように変更します。

<script>
(() =&gt; {
  // 省略
})()
//]]></script>
<script>
document.addEventListener('DOMContentLoaded', () =&gt; {
  // 省略
})
//]]></script>

deferの付いた scriptDOMContentLoaded イベントが発生する前に実行されるため、marked.js が遅延読み込みされても実行順が崩れません。

ただし、JavaScript で投稿に目次等を表示させたい場合、maked.js に変換された HTML が完全に表示されてからそれらのスクリプトを実行させる必要があります。

あとがき

Blogger の投稿エディタを Markdown エディタに改造する方法に加え、そのエディタをカスタマイズする方法も紹介しました。投稿エディタに直接 Markdown が書ける、変換された HTML がコピーできる、Markdown で書かないときはスクリプトを読み込まないという3点は自分の中でどうしても捨てきれなかったので、なんとか全て実現できてよかったです。

テストも兼ねてここ1ヶ月くらいの記事は全てこの Markdown エディタで書いていました。一度導入さえしてしまえば Markdown で書くところから公開まで Blogger だけで行えるのですごく楽です。手前味噌な発言になりますが、導入してよかったです。

この記事が Blogger で Markdown を書きたいと思う方の助けになれば幸いです。最後まで読んでいただきありがとうございました!

編集
ホーム