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プラグインを追加する場合には、以下のように記述します。
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
の基本形は以下のようになります。
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)
という記述をした場合です。
{
"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プラグインを追加する場合には、以下のように記述します。
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
の基本形は以下のようになります。
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-
要素を使って判定します。
{
"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を参照してください。
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
の基本形は以下のようになります。
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を利用してください。
実際の使い方は下記記事を参照してください。
サイトを埋め込み表示するGROWIプラグインの紹介 #TypeScript - Qiita
まとめ
GROWIプラグインを作ることで、GROWIの可能性が大きく広がります。ぜひ、自分の使いたい機能をプラグインとして追加してみてください。