GutenbergにMarkdown Blockを追加するWordPressプラグイン

本記事は、[WordPress Advent Calendar 2017](https://qiita.com/advent-calendar/2017/wordpress) 16日目の**代理投稿**です。

WordPressの新エディタ、GutenbergでMarkdownを書けるようにするブロックを作ったので、動的ブロックの作例として紹介します。

本記事のサンプルコード（zipをダウンロードすればWordPressプラグインとして使えます）は下記リポジトリで公開しています。
https://github.com/ryo-utsunomiya/markdown-block

## 2018-08-08追記

現在のGutenberg（3.4）では、普通にMarkdownを書いていくと自然にブロックに変換されるようになっています。したがって、実用上はこのプラグインの必要性はありません。。。あくまで、サーバサイドに保存したデータをPHPで動的に変換するブロックの作例として眺めていただければと思います。

## 基本設計

Markdownのレンダリングはサーバサイドで行うようにします。ライブラリとしては[cebe/markdown](https://github.com/cebe/markdown)がよさそうです。

## 開発環境

[Gutenberg公式](https://github.com/WordPress/gutenberg)の開発環境を使用します。プラグイン作成用のカスタマイズ方法は以下の記事を参考にしてください。

- [Gutenbergことはじめ](https://qiita.com/ryo511/items/81d0b3c199c3a7725443)
- [はじめてのGutenberg Custom Block Type](https://qiita.com/ryo511/items/d729f6f9695518edc5fa)

## 実装

まず、`registerBlockType`関数でブロックを登録します。今回はテキストの編集領域を出したいので、`Editable`コンポーネントも使います。

```index.js
/* 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ことはじめ](https://qiita.com/ryo511/items/81d0b3c199c3a7725443)にも書きました）。

もう1つは`save`関数で`null`を返すこと。こうすると、post_contentにはattributesだけがHTMLコメント内に保存されます。

この状態で、記事を保存して、post_contentを見てみましょう。

Markdownブロックに以下のようなMarkdownを記入してみます。

<img width="429" alt="Screen Shot 2017-12-22 at 20.08.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/33450/185cb105-22b7-b5c8-6f08-959afdafb1ef.png">

これを保存すると、以下のようにHTMLが描画されます。

<img width="450" alt="Screen Shot 2017-12-22 at 20.19.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/33450/6ae7e8b5-e6a3-774c-cdd3-291ab33a604f.png">

post_contentには以下のような文字列が書き込まれます。

```html
<!-- 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を復元するのに便利なので、このままにしておきます。

```html
<!-- 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文字列に変換できます。

```php
$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部分の全体を載せておきます：

```markdown-block.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 />`だけ考えれば良いので、以下のように簡単なコードで復元できます。

```js
/**
 * 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コンポーネントへの値の注入のところで使うのがミソです。

```js
<Editable
	onChange={ onChange }
	value={ toJSX( attributes.content ) }
	focus={ focus }
	onFocus={ setFocus }
/>
```

## まとめ

単機能のブロックなら、100行程度で作れます。データの保存形式にややクセがあるため、再編集のことも考えるとやや面倒な場面がありそうな気がします。あと、現状だとドキュメントが少ないのがつらいですね。ハマったときはGutenbergのコードも読んでいく必要があります。

## 余談：プラグインの実用性について

Markdownで記事を書きたいなら、エディタのテキストモード + JetpackプラグインのMarkdownサポートを使った方が良いです。。。ビジュアルモードはGutenbergに置き換えられる予定ですが、テキストモードは従来通り使えます。Gutenbergは[WYSIWYG](https://ja.wikipedia.org/wiki/WYSIWYG)であることに価値のあるエディタなので、変換後でないとコンテンツがプレビューできないMarkdownは、Gutenbergの思想とは相容れません。

## 2017-01-13追記

Gutenberg 2.0（もしかしたらもっと前のバージョン）で、編集領域にペーストされた文字列がプレインテキストで、かつ、Markdownとしてパースできるなら、Markdownとみなしてブロックに変換する、という機能が加わりました。

https://github.com/WordPress/gutenberg/blob/master/blocks/api/raw-handling/index.js#L52-L58

したがって、エディタにMarkdownを書く => Gutenbergにペーストする というワークフローであれば、プラグイン不要でMarkdownによる記事執筆が可能になります。

## 2018-08-08追記

現在のGutenberg（3.4）では、↑に書いたコピペすら不要で、普通にMarkdownを書いていくと自然にブロックに変換されるようになっています。