GROWIプラグイン開発Tips（Remark/Rehype/Reactコンポーネント）

GROWIでタグの動作を変更したり、独自のマークアップを追加する際にはスクリプトプラグインを開発します。その際、知っておきたいのがRemark/Rehype/Reactコンポーネントの使い方です。この記事では、GROWIプラグイン開発Tipsとして、これらの使い方を紹介します。

## 呼ばれる順番

GROWIのMarkdownパーサーはRemarkを使っています。RemarkはMarkdownをAST（抽象構文木）に変換するライブラリです。ASTはMarkdownの構造を表現したオブジェクトです。そして、ASTを変換するライブラリとしてRehypeがあります。RehypeはASTをHTMLに変換します。

HTMLになった内容は、そのタグの動作をReactコンポーネントで変更できます。つまり、呼ばれる順番としてはRemark → Rehype → Reactコンポーネントです。

## Remarkプラグイン

Remarkプラグインを追加する場合には、以下のように記述します。

```tsx
const activate = (): void => {
  if (growiFacade == null || growiFacade.markdownRenderer == null) {
    return;
  }
  const { optionsGenerators } = growiFacade.markdownRenderer;
  optionsGenerators.customGenerateViewOptions = (...args) => {
    const options = optionsGenerators.generateViewOptions(...args);
    options.remarkPlugins.push(remarkPlugin as any); // プラグイン登録
    return options;
  };
};
```

`remarkPlugin` の基本形は以下のようになります。

```tsx
const remarkPlugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
      console.log(JSON.stringify(node));
      try {
            // ここに処理を記述
      }
      catch (e) {
            n.type = 'html';
            n.value = `<div style="color: red;">Error: ${(e as Error).message}</div>`;
      }
    });
  };
};
```

`node` の内容は以下のようになっています。すべての情報が送られてくるので、 `type` が `leafGrowiPluginDirective` であること、 `name` がプラグインで利用するものであるかを判定して、処理を記述します。

以下は `$map(東京都新宿区西早稲田2-20-15)` という記述をした場合です。

```json
{
	"type": "leafGrowiPluginDirective",
	"name": "map",
	"attributes": {
		"東京都新宿区西早稲田2-20-15": ""
	},
	"children": [],
	"position": {
		"start": {
			"line": 5,
			"column": 1,
			"offset": 22
		},
		"end": {
			"line": 5,
			"column": 24,
			"offset": 45
		}
	},
	"data": {}
}
```

## Rehypeプラグイン

Rehypeプラグインを追加する場合には、以下のように記述します。

```tsx
const activate = (): void => {
  if (growiFacade == null || growiFacade.markdownRenderer == null) {
    return;
  }
  const { optionsGenerators } = growiFacade.markdownRenderer;
  optionsGenerators.customGenerateViewOptions = (...args) => {
    const options = optionsGenerators.generateViewOptions(...args);
    options.rehypePlugins.push(rehypePlugin as any); // プラグイン登録
    return options;
  };
};
```

`rehypePlugin` の基本形は以下のようになります。

```tsx
export const rehypePlugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
      console.log(JSON.stringify(node));
      try {
            // ここに処理を記述
      }
      catch (e) {
            n.type = 'html';
            n.value = `<div style="color: red;">Error: ${(e as Error).message}</div>`;
      }
    });
  };
};
```

`node` の内容は以下のようになっています。プラグインの処理に送る場合には `data-` 要素を使って判定します。

```json
{
	"type": "element",
	"tagName": "a",
	"properties": {
		"style": "height: 400px; width: 100%",
		"dataType": "map",
		"dataLatitude": "undefined",
		"dataLongitude": "undefined"
	},
	"children": [
		{
			"type": "text",
			"value": "\n            東京都新宿区西早稲田2-20-15\n          ",
			"position": {
				"start": {
					"line": 10,
					"column": 12,
					"offset": 188
				},
				"end": {
					"line": 12,
					"column": 11,
					"offset": 229
				}
			}
		}
	],
	"position": {
		"start": {
			"line": 5,
			"column": 1,
			"offset": 22
		},
		"end": {
			"line": 12,
			"column": 15,
			"offset": 233
		}
	}
}
```

## Reactコンポーネント

Reactコンポーネントでは、指定したタグの挙動を変更します。以下は `a` タグを変更する例です。指定できるタグは [GROWIプラグインを開発する（スクリプト編） #TypeScript - Qiita](https://qiita.com/goofmint/items/497ea7dd722528cfa0cb#client-entrytsx)を参照してください。

```tsx
const activate = (): void => {
  if (growiFacade == null || growiFacade.markdownRenderer == null) {
    return;
  }
  const { optionsGenerators } = growiFacade.markdownRenderer;
  optionsGenerators.customGenerateViewOptions = (...args) => {
    const options = optionsGenerators.generateViewOptions(...args);
    const A = options.components.a;
    options.components.a = EmbedMap(A); // replace
    return options;
  };
};
```

`EmbedMap` の基本形は以下のようになります。

```tsx
const EmbedMap = (A: React.FunctionComponent<any>): React.FunctionComponent<any> => {
  return ({ children, href, ...props }) => {
		// 指定したものでなければ終了
    if (props['data-type'] !== 'map') {
      return (
        <A
          href
          {...props}
        >
		  {children}
        </A>
      );
    }
    try {
      // タグの動作を変える処理を記述
    }
    catch {
      // エラーが出たら、最低限初期表示にする
      return (
        <A
          href
          {...props}
        >
		  {children}
        </A>
      );
    }
  };
};
```

## 注意点

Reactコンポーネントは、GROWI本体側のReactとバッティングするため、useStateなどが使えません。非同期処理を行う場合には、[async\-library/react\-async: 🍾 Flexible promise\-based React data loader](https://github.com/async-library/react-async)を利用してください。

実際の使い方は下記記事を参照してください。

[サイトを埋め込み表示するGROWIプラグインの紹介 #TypeScript - Qiita](https://qiita.com/goofmint/items/8105a2091f4e114e706f#ogp%E3%81%AE%E5%8F%96%E5%BE%97%E8%A1%A8%E7%A4%BA)

## まとめ

GROWIプラグインを作ることで、GROWIの可能性が大きく広がります。ぜひ、自分の使いたい機能をプラグインとして追加してみてください。

[OSS開発wikiツールのGROWI \| 快適な情報共有を、全ての人へ](https://growi.org/ja/)