WordPress の Markdown 移行補助ツールを開発してみた

2016年3月7日 0 開発 , , ,

先月、WordPress に Markdown と highlight.js という記事を書き、少しずつブログ記事の Markdown 化に取り組んでいた。しかしあまりにも定型作業 & 数が多いので、移行補助ツールを開発することにした。

WordPress は標準で記事を XML としてエクスポートする機能がある。これを解析して Markdown ファイルに変換する。そんなコンセプトで開発してゆき、私的に実用十分なものができたので wpxml2md としてリリースしてみた。

以下に wpxml2md の説明や開発で得られた知見などをまとめる。

もくじ

ツールを使ってみたいだけならば wpxml2md の使い方WordPress XML について のエクスポート手順を参照のこと。それ以外は開発の話が中心。

wpxml2md の使い方

まずは想定環境から。

WordPress の記事を Markdown で書く方法はいくつかあるが、このツールでは Jetpack by WordPress.com を想定している。Jetpack をインストールして設定から Markdown を有効にすると、従来の記法に加えて Markdown も解釈してくれる。

Jetpack 以外の Markdown プラグインはテストしていない。とはいえ HTML タグを対応する Markdown 記法に変換するだけだから、問題は起きにくいとは思うけれど。

また、ソースコードの構文強調は SyntaxHighlighter Evolved を想定。広く普及しているうえ WP Code Highlight.js のようにショートコードの互換運用もされていたりする。このプラグイン用に書かれたショートコードを Markdown ( GitHub Flavored Markdown ) のコードブロックに変換する。

その他のショートコードは基本、無視。そのため異なるショートコード記法の構文強調プラグインだと変換されない。

wpxml2md のインストールと使い方を解説する。

このツールは npm として実装しているので Node.js 環境が必要。Node は公式サイトのインストーラーや Homebrewcreationix/nvm などで構築できる。

wpxml2md は以下のコマンドで取得する。ここでは説明を簡単にするために -g オプションを付けてグローバルにインストールしておく。

$ npm install -g wpxml2md

これで wpxml2md というコマンドを利用できる。WordPress からエクスポートした XML ファイルの置かれた階層に cd して以下を実効すると Markdown 変換されたファイル群を得られる。

$ wpxml2md -i wp.xml -o ./ -r

あとは変換された内容を WordPress 上で対応する記事にコピペしてゆけばよい。なお npm wp2md のように WordPress へログインして直に記事を上書きすることも検討したが、以下の理由から却下した。

  • 記事の内容によっては Markdown 変換が不完全になる可能性もありえる
  • Markdown 変換が不完全な場合、上書き前に戻すのが面倒
  • 変換結果をローカルで確認してから記事に反映したい
  • npm とはいえ、ログイン情報を渡すのはユーザーとして抵抗がある

Markdown は広く普及しているので、編集やプレビューするためのツールは豊富にある。不完全な部分があれば、それらで修正してから WordPress へ反映することをオススメする。私は AtomQuiver などを利用している。

WordPress XML について

WordPress の記事を XML としてエクスポートする手順は以下。

  1. WordPress の管理画面を開く
  2. 左メニューからツールを選択
  3. エクスポート欄ですべてのコンテンツを選択
  4. エクスポートファイルをダウンロードボタンを押下

これで XML ファイルが得られる。

XML の構造

XML 解析にあたり公式仕様をあたろうと WordPress XML Specification などでググってもそれらしい資料を見つけられなかった。代りにフォーラムでWXR Specification OR WordPress Extended XML Spec という投稿があり、その中で提示されていた The WordPress eXtended Rss (WXR) Export/Import, XML Document Format Decoded and Explained. – The Developer’s Tidbits が網羅的かつ詳細だったので参考にした。

この XML は WXR ( WordPress eXtended RSS ) と呼ぶそうだ。参考記事をもとに XML 構造を自分用にまとめてみる。まずは全体図。

<?xml version="1.0" encoding="UTF-8" ?>
<rss>
  <channel>
    <item></item>
  </channel>
</rss>

channel 要素がサイト全体を表す。ユーザー情報やタグ、カテゴリなどの要素は全体的なので channel 直下に定義される。投稿や固定ページは item 要素になり、記事の数だけ定義される。

今回は投稿と固定ページを Markdown 化したいので item 要素を解析できればよい。その構造は以下となる。記事の本文など、要素内に XML などが入れ子になり得る値は CDATA セクションに定義されている。

<item>
  <title>Sample Post</title>
  <link>http://akabeko.me/blog/</link>
  <pubDate>Sun, 06 Mar 2016 12:00:00 +0000</pubDate>
  <dc:creator><![CDATA[akabeko]]></dc:creator>
  <guid isPermaLink="false">http://akabeko.me/blog/?p=46</guid>
  <description></description>
  <content:encoded><![CDATA[Sample post.]]></content:encoded>
  <excerpt:encoded><![CDATA[]]></excerpt:encoded>
  <wp:post_id>46</wp:post_id>
  <wp:post_date><![CDATA[2016-03-06 12:00:00]]></wp:post_date>
  <wp:post_date_gmt><![CDATA[2016-03-06 12:00:00]]></wp:post_date_gmt>
  <wp:comment_status><![CDATA[open]]></wp:comment_status>
  <wp:ping_status><![CDATA[open]]></wp:ping_status>
  <wp:post_name><![CDATA[Sample Post]]></wp:post_name>
  <wp:status><![CDATA[publish]]></wp:status>
  <wp:post_parent>0</wp:post_parent>
  <wp:menu_order>0</wp:menu_order>
  <wp:post_type><![CDATA[post]]></wp:post_type>
  <wp:post_password><![CDATA[]]></wp:post_password>
  <wp:is_sticky>0</wp:is_sticky>
  <category domain="post_tag" nicename="Sample"><![CDATA[Sample]]></category>
  <category domain="category" nicename="Sample"><![CDATA[Sample]]></category>
  <wp:postmeta>
    <wp:meta_key><![CDATA[syntaxhighlighter_encoded]]></wp:meta_key>
    <wp:meta_value><![CDATA[1]]></wp:meta_value>
  </wp:postmeta>
  <wp:postmeta>
    <wp:meta_key><![CDATA[_edit_last]]></wp:meta_key>
    <wp:meta_value><![CDATA[1]]></wp:meta_value>
  </wp:postmeta>
</item>

子要素の内容を簡単にまとめる。勘違いしている箇所もありそう。もし誤りを見つけたらコメント欄などで指摘してほしい。

要素 内容
title 記事のタイトル。
link 記事の URL。
pubDate 記事を公開した日時。前述の参考記事には RFC 822 とあるけど、おそらく現在なら RFC 2822 と思われる。この値は JavaScript の Date コンストラクタに指定できる。
dc:creator 記事の著者名。
guid 記事の一意な識別子。パーマリンクを設定していない状態の記事 URL になる。
description 記事の説明文。
content:encoded 記事の本文。Markdown 化の対象とするデータ。
excerpt:encoded 記事を RSS 配信するときの抜粋文。設定が全文配信なら空になるようだ。
wp:post_id 記事の識別子。WordPress 内がインクリメンタルに自動採番する。guid 要素の末尾に指定される値もこれ。
wp:post_date 投稿日時。書式は YYYY-MM-DD hh:mm:ss になっている。
wp:post_date_gmt 投稿日時の GMT 版。
wp:comment_status コメント欄の状態。openclosed になるようだ。
wp:ping_status ピンバックの状態。openclosed になるようだ。
wp:post_name URL 用の投稿名。記事の編集画面でパーマリンク欄へ入力したものになる。
wp:status 記事の状態。publishdraft など。
wp:post_parent 親となる記事の識別子。WordPress の固定ページは記事に階層を設定可能で、これはその親子関係を知るための情報。
wp:menu_order 記事メニュー上の並び順を示す番号。
wp:post_type 記事の種別。投稿なら post で固定ページは page になる。投稿と固定ページは共に item 要素として定義されるため、処理を分岐したい場合はこの値を判定する。
wp:post_password 記事にアクセス制限を掛けた場合のパスワード。
wp:is_sticky 記事がサイトのトップに固定されていることを示す値。0 = false、1 = truetrue なら固定。
category 記事のカテゴリーとタグ情報。どちらを示すのかは domain 属性で区別できる。ひとつの item 内に複数定義される。
wp:postmeta 記事のメタ情報。プラグインに関する設定などが定義される。

Markdown 変換エンジン

当初、記事の Markdown 変換は正規表現で対応する予定だった。途中までそのように設計していたのだが、table や ul、ol のように DOM の入れ子を解析して変換するのが面倒すぎて諦めた。

次に jsdom を検討した。これは Node 用の HTML DOM 解析ツールで、HTML テキストを指定すると Web ブラウザのように document オブジェクトを返してくれる。以降は querySelector なり childNodes をたどってゆくなどすればよい。

to-markdown

DOM パーサーを手に入れたが、Markdown 変換を自前で全て実装するより、既存の OSS を参考にしたほうがよいだろう。結果、to-markdown がよさそうだった

to-markdown を npm として参照するか迷ったが、このツールでは DOM 全般の改行と空白を除去してしまう。HTML の Markdown 変換という意味では適切な仕様なのだけど、WordPress の場合、SyntaxHighlighter Evolved などで構文強調している部分はプレーンテキストとしての改行と空白を維持する必要がある。

よって to-markdown の変換エンジンを移植して、wpxml2md 用にカスタマイズする方法を採用した。ありがたいことに to-markdown も Node で動作するときは jsdom を利用しているので設計思想も近い。そのため移植はかなりスムーズだった。

to-markdown エンジンの移植

to-markdown エンジンは以下で構成されている。

  • index.js
    • npm エントリー ポイント
    • 入力テキストの空白と改行の除去や jsdom 解析などを実行
    • Markdown と GitHub Flavored Markdown 変換エンジンの実行
  • md-converters.js
    • Markdown 変換エンジン
    • Array になっている
    • Array の要素は HTML タグ単位
    • DOM を列挙して一致するタグを見つけたら変換、という設計になっている
  • gfm-converters.js
    • GitHub Flavored Markdown 変換エンジン
    • Markdown 版と同じ設計
    • index.js 上では Markdown 版より優先実行される

これらを wpxml2md の構成へ沿うように再構築し、ES2015 として rewrite した。

to-markdown の設計で最も重要なのは DOM Node の処理順である。これを適切に制御するため、事前に document.body 以下の childNodes をたどって直列化している。関数を再帰していないのも設計としてポイント高い。以下はその移植版。

export default class Converter {
  static flattenNodes( node ) {
    const inqueue  = [ node ];
    const outqueue = [];

    while( 0 < inqueue.length ) {
      const elm = inqueue.shift();
      outqueue.push( elm );

      for( let i = 0, max = elm.childNodes.length; i < max; ++i ) {
        const child = elm.childNodes[ i ];
        if( child.nodeType === NodeTypes.ELEMENT_NODE ) {
          inqueue.push( child );
        }
      }
    }

    outqueue.shift(); // Remove root
    return outqueue;
  }
}

これで全 Node をシーケンシャル リードできるようになった。そのうえ各 DOM Node の childNodes 参照も残っているため、階層を意識した処理も可能となっている。これらを処理するときは降順に読んでゆく。

export default class Converter {
  static convert( post, options = {} ) {
    // Process through nodes in reverse ( so deepest child elements are first ).
    for( let i = nodes.length - 1; 0 <= i; --i ) {
      Converter.process( nodes[ i ], converters, options );
    }  
  }
}

この設計だと litdth など入れ子構造における子となる要素は親よりも先に検出される。その際、parentNode をたどることで自身の位置を取得し、インデントなどの処理を実行できる。

変換エンジンへ渡すテキストの事前処理はショートコードなどを活かすため、以下のように ELEMENT_NODE のみを対象としている。空白除去は to-markdown と同様に collapse-whitespace を利用。

export default class Converter {
  static collapseWhitespace( nodes ) {
    nodes.forEach( ( node ) => {
      if( node.nodeType === NodeTypes.ELEMENT_NODE ) {
        CollapseWhitespace( node, Util.isBlockElement );
      }
    } );
  }
}

WordPress ではテキスト間に空行を入れることで、自動的に p タグに包まれて段落化される。そのため大半の文章は HTML タグを利用せず TEXT_NODE になる。よってこのように処理することで空白と改行を維持できる。

他にも工夫はあるのだが、to-markdown と wpxml2md 的に重要なのはこの 2 点である。

あと、Markdown 的に変更したところがあった。to-markdown だと ulol 内の li*. になるのだが、シンボルとテキスト間の空白が多すぎるので 1 文字に詰めて .* にした。Markdown のリスト記法では、こちらで説明されていることが多いし GitHub の Basic writing and formatting syntax – User Documentation でもそうなので、これで良しとする。

その他

前に icon-gen という npm を開発したときは CLI 部分をユニット テストしていなかった。そのためオプション名の変更をミスして何度か手戻りがあり、これは自動テストするべきだと感じていた。

そこで今回は CLI 部分もユニット テストしてみた。CLI オプション解析とヘルプ、バージョン出力を対象としている。オプションについては簡単で、単に process.argv を解析する関数を実装し、それに色々な Array を与えればテストできる。

このブログの Markdown 移行について。

wpxml2md が一段落ついたので、変換結果の反映に取り組みはじめた。構文強調とテーブル変換が成功していた場合、ほぼコピペと上書きで済むので随分と楽になった。しかしヘッダー行のないテーブルには対応しておらず、コードブロックの行強調もなくなるため、それらの対応は面倒に感じる。

ただ、過去記事の棚卸しと考えて、誤字脱字や表現の向上なども実施する予定。

Markdown 変換した記事は Quiver 上に保存している。最近、Markdown な資料はこのツールで編集 & 管理していて、Dropbox で共有している。有料だが購入してよかった。後で Quiver について独立したレビュー記事を書きたい。

WordPress に Markdown と highlight.js を導入

2016年2月20日 0 開発 , ,

ここ数年 Markdown で資料を作成する機会が増えたので、ブログもこれで書きたくなった。Markdown なら対応エディタも多くて可搬性が高い。また WordPress から別の CMS へ移行しやすくなるだろう。例えば Hugo のような静的サイト ジェネレーターなど。

このブログではプログラミングについて扱うことが多いので、ソース コードの構文強調も重要である。これまでは機能性から SyntaxHighlighter Evolve を利用していたが、ES2015 や Stylus など最近、登場する機会の増えた言語がサポートされていないので、こちらも乗り換えを検討したい。

Jetpack の Markdown 機能を利用する

WordPress を Markdown 対応させるプラグインはいくつかあるが、信頼性とサポートを考慮して Jetpack を採用することにした。

このプラグインは WordPress.com が提供しており、WordPress の管理画面でもインストールを促されるので気になっていた。既にアクセス解析で利用している WordPress.com Stats も Jetpack に含まれているらしく、追加インストールした場合は Jetpack 側の Site Stats が有効化されて継続利用できる。

インストールはプラグインの管理画面から Jetpack を検索して、そこから実行すればよい。Site Stats 機能などを利用する場合はインストール後に WordPress.com との連携を設定する。要、WordPress.com アカウント。私は Stats 用に設定済みだったので、連携を有効にするだけだった。

Jecpack は非常に多機能だが、それらは個別に有効化する必要がある。

WordPress 管理画面に Jecpack という項目が追加されているので、その中の設定を選ぶ。すると利用可能な機能一覧が表示される。ここから Markdown有効にする。

これで Markdown が使えるようになったはず。試しに適当な記事を作成し、テキスト モードで

## 見出し

段落 1

段落 2

のように書いてプレビューすると、しっかり HTML 化されることを確認できる。

Markdown と WordPress 記法の混在

これからブログを始めるなら問題ないが、既に WordPress 記法で大量の記事がある場合、Markdown との互換性が気になるだろう。Jetpack の Markdown 機能はこのような状況も考慮されており、WordPress 記法と混在可能である。

Markdown は WordPress 記法に追加されるような状態であり、

<h2> WordPress 記法 ( HTML タグ ) の見出し</h2>

## Markdown の見出し

と書いた場合、どちらも H2 として描画される。もちろん WordPress のショートコードも動作する。

またソース コードの構文強調は SyntaxHighlighter Evolve や WP Code Highlight.js などのプラグインがあれば、それらによって描画される。そのため、Markdown として構文強調をそのまま記述すればよい。

以上を踏まえ、新しい記事は Markdown で書き、時間があれば少しずつ過去記事を Markdown 化してゆく予定。

localhost で Jetpack を利用する

私は自分のブログ用に WordPress テーマやプラグインを開発しており、動作確認は Vagrant + VirtualBox + Varying-Vagrant-Vagrants/VVV を利用している。この環境も本番と同等にするため Jetpack を動かしたいのだが、Vagrant 上の VM は localhost になるため Jetpack を有効にすると XML-RPC エラーが発生する。

これを回避するには Jetpack を開発モードで動作させる。その手順は以下。

  1. Jetpack を VM 上の WordPress にインストール
  2. WordPress 管理画面のメニューからプラグインを選択
  3. プラグイン一覧の Jetpack by WordPress.com から編集を選択
  4. jetpack/jetpack.php が対象になっていることを確認
  5. ファイルの末尾行define( 'JETPACK_DEV_DEBUG', true ); を追加
  6. ファイルを更新ボタンを押して編集内容を保存

これで localhost でも Jetpack を利用可能になる。開発モードだと Stats などの連携が無効になるそうだが、この用途であれば不要なので問題ない。

WP Code Highlight.js で highlight.js を利用する

highlight.js という JavaScript 製の構文強調ライブラリがある。これを WordPress で利用する場合、テーマの function.php で指定する方法が考えられるけど、これではライブラリが更新されるたびにテーマの修正が発生して面倒だ。

もう少しスマートに運用するなら Header and Footer を利用する方法がある。このプラグインは記事のヘッダーやフッターにテキストを挿入するもので、ブログ パーツの動的な追加にも利用できる。私も以前、Zenback を埋め込むために採用していた。これで highlight.js の CDN を参照する script タグを挿入すれば、更新をここで管理できる。

しかし、どちらの方法も素の highlight.js を利用するため、記事内で構文強調する場合は HTML として書かなくてはならない。

<pre><cpde class="js">
// 構文強調するコード
</code></pre>

これは面倒だし、前述の Markdown による構文強調と組み合わせたいので WP Code Highlight.js を利用する。これは SyntaxHighlighter Evolve における SyntaxHighlighter と同様に highlight.js を WordPress 向けに wrap してくれる。

使用方法は簡単で、プラグイン管理画面から WP Code Highlight.js で検索して、その結果からインストール & 有効化すればよい。これだけで Markdown の構文強調が解析対象になる。

```js
// 構文強調するコード
```

このプラグインが提供する機能を以下にまとめる。

CDN

highlight.js の参照方法を変更できる。各種 CDN サイトとプラグイン組み込みの local から選べる。

通信量と Web ブラウザのキャッシュを考慮するなら CDN にする。私は local にした。負荷はあまり気にしておらず、組み込みで固定バージョンを利用するほうが好みなので。

Package

構文強調の対象となる言語を選択する。v0.5.9 時点では以下がリストされている。

  1. Common(about 42KB)
  2. All(about 393KB)
  3. Custome

大抵は 1 で十分、今後の言語追加に備えるなら 2。1 に対して個別に言語を追加したいなら 3。私は Stylus だけ欲しかったので 3 にした。CDN 利用なら負荷分散できるので 2 を選ぶのがよいかも。

対象言語は Common 24 + Other 123 で合計 147 もある。かなりマイナーな言語も含まれているうえ、SwiftKotlin など最近の言語が採用されていたり、JavaScript における ES2015 など新しい仕様にも対応しており、安心感がある。

Color Scheme

構文強調の外観を選べる。公式サイトの highlight.js demo にプレビューがあるので、好みを探そう。私は Tomorrow Night にした。

Highlight.js Option

構文強調の細かいオプション。いまのところ利用していない。以下の設定値がある。プラグインのページに説明が見当たらないので内容は予想で書いてる。

オプション 内容
Tab replace タブ文字の置換対象となる文字列を指定する。
Class prefix CSS クラスの接頭語を指定する。WordPress テーマ側の code タグ指定と競合を避けるための機能だろうか?
Use BR 改行コードを BR タグに変換する。
Languages 用途不明。この言語というのはプログラミング言語なのか、それとも ISO 639 のような現実世界の言語なのだろうか。

You can add some additional CSS rules for better display

構文強調の CSS を独自に拡張できる。私は標準のままにしている。

他のプラグインとの互換設定

WordPress で普及している構文強調系プラグインの構文サポート。WP Code Highlight.js に移行した場合、これらをチェックすることで既存の記事を従来の記法で構文強調できるらしい。以下の設定がある。

  • Syntax Highlighter Compatiable
  • Prettify Compatible
  • Crayon Syntax Highlighter Compatiable

Syntax Highlighter Compatiable を有効にした状態で SyntaxHighlighter Evolved を無効にしてから以下の構文強調を試したが、無視された。

[html]
<div>テスト</div>
[/html]

では SyntaxHighlighter Evolved と WP Code Highlight.js が両方とも有効ならどうか?と試したら、HTML タグの不等号などが構文強調の中で文字実体参照に変換されてしまった。

この設定を正しく動作させる方法がわからないので、利用するのはやめておく。

Enable [code]code content …[/code] support

ショートコードの code 記法を対象とする設定。SyntaxHighlighter Evolved では code + lang で書く方法もあり、これが有効になる。

[code lang="js"]
const test = "test";
assert( test );
[/code]

JavaScript などは動作するが、HTML の場合、タグなどをエスケープしないと正しく描画されない。

SyntaxHighlighter Evolved から WP Code Highlight.js への移行

WP Code Highlight.js には SyntaxHighlighter Evolved の互換設定があるものの、前述のように正しく動作させられなかった。そして SyntaxHighlighter Evolved 用の構文強調は大量にあるため、いますぐ移行するのは厳しい。

また機能面でみると行番号と行の強調が失われる。highlight.js としては、強調したい行があるならその部分だけ抜粋すればよい、という設計思想らしく、今後も変わることはないだろう。

とはいえ、対応言語と最新仕様への対応は魅力的だ。そのため以下のように移行計画を立てた。

  • 過去記事の構文強調をすべて Markdown 化する
    • この対応で SyntaxHighlighter Evolved のショートコードも Markdown 化される
    • Markdown にしても構文強調は動作する
  • 過去記事で行番号の強調に頼る記述を依存しないものへ修正する
    • 強調・ハイライトしている部分が云々、という 記述の修正
    • 構文強調は抜粋にする
  • Markdown 化が完了したら SyntaxHighlighter Evolved を無効化、WP Code Highlight.js を有効化
    • Markdown の構文強調を処理するプラグインが変更されるだけ
    • 安全に切り替えられる

このブログは記事数こそ少ないが、個々の内容は長いため気の長い作業になるだろう。さすがに手動でやるのは辛いのでエディタの置換なりスクリプトなどで処理する予定。

WordPress 4.4 のタグクラウド対応

2016年1月31日 0 開発 ,

このブログをホストしている WordPress を 4.4 に更新したら、サイドバーで表示しているタグクラウドにスタイルが反映されなくなった。

調べてみると widget_tag_cloud_args の仕様変更が原因らしい。

修正するには自前の widget_tag_cloud_args 関数で返す値を、標準値と結合する必要がある。

function mytheme_widget_tag_cloud_args( $args )
{
    $args = array(
        'unit'     => 'em',
        'number'   => 20,
        'smallest' => 0.8,
        'largest'  => 0.8
    );

    return $args;
}
add_filter( 'widget_tag_cloud_args', 'mytheme_widget_tag_cloud_args');

これを以下のように修正する。

function mytheme_widget_tag_cloud_args( $args )
{
    $args = wp_parse_args( $args, array(
        'unit'     => 'em',
        'number'   => 20,
        'smallest' => 0.8,
        'largest'  => 0.8
    ) );

    return $args;
}
add_filter( 'widget_tag_cloud_args', 'mytheme_widget_tag_cloud_args');

WordPress で動作に関わる部分の変更は滅多にないこと、記事を投稿するときにそれ自体はチェックするがトップページはあまり見ない ( 現在のテーマではトップだけサイドバー表示している ) ことから、気づくのが遅れてしまった。

この修正と、iOS Safari の慣性スクロールに対応するため -webkit-overflow-scrolling を有効にしたテーマをリリースした。

ブログには既に反映している。