GROWIプラグインを開発する（Remarkプラグイン編）

[オープンソースのWikiであるGROWI](https://growi.org/ja/)にはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。

今回は、GROWIプラグインの開発手順について解説します。以前にYouTubeのURLを自動で埋め込み表示にするプラグインを作成しましたが、今回は同様の操作をRemarkプラグインとして作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8fdb5632-4c32-efbe-90ab-a37a77ddc7bf.png)


## コードについて

コードは[goofmint/growi-plugin-remark-youtube: GROWI plugin for embed YouTube by Remark](https://github.com/goofmint/growi-plugin-remark-youtube)にあります。見るべきファイルとしては、以下の2つになります。

- [client\-entry\.tsx](https://github.com/goofmint/growi-plugin-remark-youtube/blob/main/client-entry.tsx)
- [src/youtube.ts](https://github.com/goofmint/growi-plugin-remark-youtube/blob/main/src/youtube.ts)

## Remarkとは

RemarkはMarkdownのプロセッサです。GROWIでも、MarkdownからHTMLに変換する際のエンジンとして利用しています。

[remarkjs/remark: markdown processor powered by plugins part of the @unifiedjs collective](https://github.com/remarkjs/remark)

Remarkはプラグインによって独自の記法をMarkdown内に追加できます。この仕組みを使って、GROWIでもプラグインを追加できます。

## プラグインを追加する

今回はクライアントサイド（ブラウザ）で動作するプラグインを作成します。その場合、 `client-entry.tsx` にプラグインのエントリーポイントを記述します。

`remarkPlugins` に対して関数（後述）を追加することで、Remarkプラグインを追加できます。

```tsx
import { plugin } from './src/youtube';

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(plugin as any);
    return options;
  };
};
```

## プラグインの内容

プラグインは `src/youtube.ts` に記述しています。このファイルは、Remarkプラグインとして動作するための記述がされています。

```ts
// 読み込み必須
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';

// 足りないプロパティを追加
interface GrowiNode extends Node {
  name: string;
  type: string;
  attributes: {[key: string]: string}
  children: GrowiNode[];
  value: string;
}

// プラグインの関数を定義
export const plugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
      const n = node as unknown as GrowiNode;
      try {
        if (n.type === 'leafGrowiPluginDirective' && n.name === 'youtube') {
					// 1つ目の引数がYouTubeの動画ID
          const id = Object.keys(n.attributes)[0];
					// 付属オプション（任意）のwidth/heightを取得
          const { width, height } = n.attributes;
          n.type = 'html';
					// YouTubeの埋め込みHTMLを作成
          n.value = `<div style="width: 100%; aspect-ratio: 16/9">
            <iframe
              width="${width || '560'}"
              height="${height || '315'}"
              src="https://www.youtube.com/embed/${id}"
              title="YouTube video player"
              frameborder="0"
              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
              referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
            </iframe>
          </div>
          `;
        }
      }
      catch (e) {
        n.type = 'html';
        n.value = `<div style="color: red;">Error: ${(e as Error).message}</div>`;
      }
    });
  };
};
```

### 解説

プラグインの基本形は以下の通りです。

```tsx
export const plugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
		// この中で処理
    });
  };
};
```

`node` の中にRemarkでのAST（抽象構文木）が入っています。ASTはMarkdownをパースした結果のデータ構造です。すべてのノード情報が送られてくるので、必要なものだけを処理します。

そして、 `node.value` に対してレンダリングしたいHTMLを送れば、GROWI上に表示されます。

## 使い方

プラグインの使い方です。これでYouTube動画が埋め込み表示になります。

```
$youtube(fGWaKXWrhdY,width=100%,height=100%)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/eedc207b-c46f-ef07-7713-b4e6270ed137.png)


`width` と `height` は任意です。指定しない場合はデフォルト値が適用されます。

```
$youtube(fGWaKXWrhdY)
$youtube(fGWaKXWrhdY,width=100%)
$youtube(fGWaKXWrhdY,height=100%)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e192cc92-05ce-ec45-07ce-d72297f36d00.png)


何かエラーがあった場合は、その内容が赤文字で表示されます。

![FireShot Capture 352 - _ - GROWI - localhost 352.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/0743e63b-f0c4-8b47-78d1-2f9b55b2e98d.jpeg)


## コンポーネントベースのプラグインとの違い

以前は `https://youtube.com/watch?v=fGWaKXWrhdY` というURLを自動で埋め込み表示するプラグインを作成しました。このプラグインは、URLをMarkdownに貼り付けるだけで動作します。この場合、Reactのaタグ動作を上書きする形で動作します。

コンポーネントベースの場合、h1〜6タグやcodeタグなど、Markdown記法で使われるタグに対してのみ利用できます。つまり、divタグやpタグなどでは利用できません。

`$youtube(fGWaKXWrhdY)` のようにエディタ内に地の文として記述してもプラグインとして使えるようにするためには、Remarkプラグインを作成する必要があります。

また、Remarkプラグインの場合はオプションを渡せる点が便利です。オプションを使うことで、表示をより細かくカスタマイズできます。

## まとめ

GROWIのプラグイン機構はさらに拡張性があります（サーバーサイドで動かすものなど）。ぜひ、プラグインを開発してGROWIを自分好みにカスタマイズしてください。

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