オープンソースのWikiであるGROWIにはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。
今回は、GROWIプラグインとしてGitHubのIssueを一覧表示するプラグインを作りました。プロジェクト関連の情報共有にGROWIを利用しているケースは多いので、そのIssueも一覧で表示できれば情報の一元管理が進みます。
プラグインの動作
Markdownは以下のように記述します。Remark Directiveを利用しています。
::github[issue]{repo=weseek/growi per_page=10}
将来的にIssue以外にも対応できそうなので、 github
というキーワードにしました。{}
内はオプションです。各オプションはスペースで繋ぎます。オプションは REST API endpoints for issues - GitHub Docs に準拠しています。
コードについて
コードはgoofmint/growi-plugin-githubにて公開しています。ライセンスはMIT Licenseになります。
プラグインを追加する
利用する際には、GROWIの管理画面の プラグイン
にて追加してください。URLは https://github.com/goofmint/growi-plugin-github
です。
注意点
GitHub APIを利用する際に、APIキーを通していません。そのため、GitHubのAPI制限に引っかかる可能性があります。
コードについて
このプラグインでは、以下のような手順を取っています。
- Remark Directiveをcodeタグに変換
- codeタグを取得してGitHub APIを実行、結果を表示
import { gitHub, githubPlugin } from './src/GitHub';
const activate = (): void => {
if (growiFacade == null || growiFacade.markdownRenderer == null) {
return;
}
const { optionsGenerators } = growiFacade.markdownRenderer;
const originalCustomViewOptions = optionsGenerators.customGenerateViewOptions;
optionsGenerators.customGenerateViewOptions = (...args) => {
const options = originalCustomViewOptions ? originalCustomViewOptions(...args) : optionsGenerators.generateViewOptions(...args);
const { code } = options.components;
// replace
options.components.code = gitHub(code);
options.remarkPlugins.push(githubPlugin as any);
return options;
};
// For preview
const originalGeneratePreviewOptions = optionsGenerators.customGeneratePreviewOptions;
optionsGenerators.customGeneratePreviewOptions = (...args) => {
const preview = originalGeneratePreviewOptions ? originalGeneratePreviewOptions(...args) : optionsGenerators.generatePreviewOptions(...args);
const { code } = preview.components;
preview.components.code = gitHub(code); // Wrap the default component
preview.remarkPlugins.push(githubPlugin as any);
return preview;
};
};
Remark Directiveの処理
Remark Directiveでは、渡されたオプションを解析して、 code
タグに変換します。
export const githubPlugin: Plugin = () => {
return (tree: Node) => {
visit(tree, 'leafDirective', (node: Node) => {
const n = node as unknown as GrowiNode;
if (n.name !== 'github') return;
const data = n.data || (n.data = {});
const type = n.children[0].value;
data.hName = 'code';
data.hProperties = { className: `language-github-${type}` };
data.hChildren = [{ type: 'text', value: JSON.stringify(n.attributes) }];
});
};
};
イメージとしては、以下のようになります。
::github[issue]{repo=weseek/growi per_page=10}
というMarkdownが、以下のようなHTMLになっています。
<code class="language-github-issue">
{"repo":"weseek/growi","per_page":"10"}
</code>
コンポーネントとしての処理
そして、 code
タグに対する処理を行います。この場合は、GitHubのAPIを叩いて、Issueを取得しています。
export const gitHub = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
return ({
children, ...props
}) => {
try {
// クラスの判定
if (!props.className.match(/language-github-/)) {
return <Tag {...props}>{children}</Tag>; // 対象以外は普通のコンポーネントとして扱う
}
// JSONをパース
const params = JSON.parse(children);
switch (props.className) {
case 'language-github-issue': // Issue表示
return GitHubIssue(params);
default:
return <Tag {...props}>{children}</Tag>; // 対象以外は普通のコンポーネントとして扱う
}
}
catch (err) {
// console.error(err);
}
// Return the original component if an error occurs
return (
<Tag {...props}>{children}</Tag>
);
};
};
GitHubIssueの処理
GitHubIssue
では、GitHubのAPIを叩いてIssueを取得しています。データの取得は react-async
を使い、非同期処理にて行います。そして、受け取った結果は table
タグで描画しています。
const getUrl = async({ url }: any) => {
const res = await fetch(url);
const json = await res.json();
return json;
};
const GitHubIssue = (params: GitHubIssueProps) => {
// URLの作成
const baseUrl = `https://api.github.com/repos/${params.repo}/issues`;
// クエリーの作成
const query = Object.entries(params).map(([key, value]) => {
if (key !== 'repo' && value !== null) {
return `${key}=${value}`;
}
return '';
}).join('&');
const url = `${baseUrl}?${query}`;
return (
<Async promiseFn={getUrl} url={url}>
{({ data, error, isPending }) => {
if (isPending) return 'Loading...';
if (error) return `Something went wrong: ${error.message}`;
if (data) {
return (
<>
<table className='table striped'>
<tbody>
{data.map((item: any) => (
<tr key={item.id}>
<td><a href={item.html_url} target='_blank'>
{item.title}</a><br />
🕒 {format(item.created_at)} by {item.user.login}
</td>
</tr>
))}
</tbody>
</table>
</>
);
}
return null;
}}
</Async>
);
};
GROWIコミュニティについて
プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください!
まとめ
GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。