本記事は、WordPress Advent Calendar 2017 16日目の代理投稿です。
WordPressの新エディタ、GutenbergでMarkdownを書けるようにするブロックを作ったので、動的ブロックの作例として紹介します。
本記事のサンプルコード(zipをダウンロードすればWordPressプラグインとして使えます)は下記リポジトリで公開しています。
https://github.com/ryo-utsunomiya/markdown-block
2018-08-08追記
現在のGutenberg(3.4)では、普通にMarkdownを書いていくと自然にブロックに変換されるようになっています。したがって、実用上はこのプラグインの必要性はありません。。。あくまで、サーバサイドに保存したデータをPHPで動的に変換するブロックの作例として眺めていただければと思います。
基本設計
Markdownのレンダリングはサーバサイドで行うようにします。ライブラリとしてはcebe/markdownがよさそうです。
開発環境
Gutenberg公式の開発環境を使用します。プラグイン作成用のカスタマイズ方法は以下の記事を参考にしてください。
実装
まず、registerBlockType
関数でブロックを登録します。今回はテキストの編集領域を出したいので、Editable
コンポーネントも使います。
/* global wp */
const { registerBlockType, Editable } = wp.blocks;
registerBlockType( 'markdown-block/markdown-block', {
title: 'Markdown',
icon: 'universal-access-alt', // todo: MD icon
category: 'common',
attributes: {
content: {
type: 'text',
},
},
edit: ( { attributes, setAttributes, focus, setFocus } ) => {
const onChange = content => {
setAttributes( { content } );
};
return (
<Editable
onChange={ onChange }
value={ toJSX( attributes.content ) }
focus={ focus }
onFocus={ setFocus }
/>
);
},
save: () => null,
} );
ここでのポイントは2つ。Block typeのattributes
と、save
関数です。
attributes
では、このブロックが使用する属性を定義します。ここでは、content
プロパティを定義し、その型はtextであるとしています。このattributes
は、JSONにシリアライズされてpost_contentに埋め込まれます。(この辺の話はGutenbergことはじめにも書きました)。
もう1つはsave
関数でnull
を返すこと。こうすると、post_contentにはattributesだけがHTMLコメント内に保存されます。
この状態で、記事を保存して、post_contentを見てみましょう。
Markdownブロックに以下のようなMarkdownを記入してみます。
これを保存すると、以下のようにHTMLが描画されます。
post_contentには以下のような文字列が書き込まれます。
<!-- wp:markdown-block/markdown-block {"content":["# Hello",{"type":"br","key":"_domReact1","ref":null,"props":[],"_owner":null},{"type":"br","key":"_domReact2","ref":null,"props":[],"_owner":null},"- a",{"type":"br","key":"_domReact4","ref":null,"props":[],"_owner":null},"- b",{"type":"br","key":"_domReact6","ref":null,"props":[],"_owner":null},{"type":"br","key":"_domReact7","ref":null,"props":[],"_owner":null},"```",{"type":"br","key":"_domReact9","ref":null,"props":[],"_owner":null},"\u003c?php",{"type":"br","key":"_domReact11","ref":null,"props":[],"_owner":null},"echo 'Github Flavored Markdown.';",{"type":"br","key":"_domReact13","ref":null,"props":[],"_owner":null},"```","","","","","",""]} /-->
↑だとわかりづらいので、JSON部分を整形してみます。改行がJSXの<br />
オブジェクトとしてシリアライズされていることがわかります。これは編集時にJSXを復元するのに便利なので、このままにしておきます。
<!-- wp:markdown-block/markdown-block
{
"content": [
"# Hello",
{
"type": "br",
"key": "_domReact1",
"ref": null,
"props": [],
"_owner": null
},
{
"type": "br",
"key": "_domReact2",
"ref": null,
"props": [],
"_owner": null
},
"- a",
{
"type": "br",
"key": "_domReact4",
"ref": null,
"props": [],
"_owner": null
},
"- b",
{
"type": "br",
"key": "_domReact6",
"ref": null,
"props": [],
"_owner": null
},
{
"type": "br",
"key": "_domReact7",
"ref": null,
"props": [],
"_owner": null
},
"```",
{
"type": "br",
"key": "_domReact9",
"ref": null,
"props": [],
"_owner": null
},
"\u003c?php",
{
"type": "br",
"key": "_domReact11",
"ref": null,
"props": [],
"_owner": null
},
"echo 'Github Flavored Markdown.';",
{
"type": "br",
"key": "_domReact13",
"ref": null,
"props": [],
"_owner": null
},
"```",
"",
"",
"",
"",
"",
""
]
}
/-->
Markdownレンダリング
↑のようなデータをMarkdownとしてレンダリングするには、まずデータをMarkdown文字列に変換する必要があります。といっても話は簡単で、以下のような処理でMarkdown文字列に変換できます。
$markdown = '';
if ( isset( $attributes['content'] ) && is_array( $attributes['content'] ) ) {
foreach ( $attributes['content'] as $line ) {
if ( is_string( $line ) ) {
$markdown .= $line;
}
if ( is_array( $line ) ) {
if ( isset( $line['type'] ) && 'br' === $line['type'] ) {
$markdown .= "\n";
}
}
}
}
Markdown => HTMLへの変換はライブラリがやってくれます。プラグインのPHP部分の全体を載せておきます:
<?php
/**
* A WordPress plugin for Markdown addicted.
*
* @package WordPress
* @author Ryo Utsunomiya
* @license GPLv2
* @link https://github.com/ryo-utsunomiya/markdown-block
*/
/**
* Register Block JS file.
*/
function markdown_block_enqueue_block_editor_assets() {
wp_enqueue_script(
'markdown-block',
plugins_url( 'index.build.js', __FILE__ ),
array( 'wp-blocks', 'wp-element' )
);
}
add_action( 'enqueue_block_editor_assets', 'markdown_block_enqueue_block_editor_assets' );
/**
* Render Markdown contents.
*
* @param array $attributes Block attributes.
*
* @return string
*/
function render_markdown( $attributes ) {
$markdown = '';
if ( isset( $attributes['content'] ) && is_array( $attributes['content'] ) ) {
foreach ( $attributes['content'] as $line ) {
if ( is_string( $line ) ) {
$markdown .= $line;
}
if ( is_array( $line ) ) {
if ( isset( $line['type'] ) && 'br' === $line['type'] ) {
$markdown .= "\n";
}
}
}
}
// Use GitHub Flavored Markdown(MarkdownExtra).
$parser = new \cebe\markdown\MarkdownExtra();
return $parser->parse( $markdown );
}
register_block_type( 'markdown-block/markdown-block', array(
'render_callback' => 'render_markdown',
) );
保存した値の再編集
最後に、post_contentに保存した値を再編集できるようにします。post_contentにはJSXをシリアライズしたオブジェクトが保存されているので、これをJSXに復元します。今回は<br />
だけ考えれば良いので、以下のように簡単なコードで復元できます。
/**
* Convert content into JSX.
* @param {array} content Gutenberg content.
* @returns {array} JSX rendered Gutenberg content.
*/
const toJSX = content => {
if ( ! Array.isArray( content ) ) {
return [];
}
return content.map( line => {
if ( typeof line === 'string' ) {
return line;
}
if ( typeof line === 'object' ) {
if ( line.type === 'br' ) {
return <br />;
}
}
} );
};
これをEditableコンポーネントへの値の注入のところで使うのがミソです。
<Editable
onChange={ onChange }
value={ toJSX( attributes.content ) }
focus={ focus }
onFocus={ setFocus }
/>
まとめ
単機能のブロックなら、100行程度で作れます。データの保存形式にややクセがあるため、再編集のことも考えるとやや面倒な場面がありそうな気がします。あと、現状だとドキュメントが少ないのがつらいですね。ハマったときはGutenbergのコードも読んでいく必要があります。
余談:プラグインの実用性について
Markdownで記事を書きたいなら、エディタのテキストモード + JetpackプラグインのMarkdownサポートを使った方が良いです。。。ビジュアルモードはGutenbergに置き換えられる予定ですが、テキストモードは従来通り使えます。GutenbergはWYSIWYGであることに価値のあるエディタなので、変換後でないとコンテンツがプレビューできないMarkdownは、Gutenbergの思想とは相容れません。
2017-01-13追記
Gutenberg 2.0(もしかしたらもっと前のバージョン)で、編集領域にペーストされた文字列がプレインテキストで、かつ、Markdownとしてパースできるなら、Markdownとみなしてブロックに変換する、という機能が加わりました。
したがって、エディタにMarkdownを書く => Gutenbergにペーストする というワークフローであれば、プラグイン不要でMarkdownによる記事執筆が可能になります。
2018-08-08追記
現在のGutenberg(3.4)では、↑に書いたコピペすら不要で、普通にMarkdownを書いていくと自然にブロックに変換されるようになっています。