オープンソースのWikiであるGROWIにはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。
今回は、GROWIプラグインとしてPDFエクスポート機能を追加するプラグインを作りました。PDFエクスポートは面倒だと思っていたのですが、予想よりも手軽に実現できるようになっています。
プラグインの動作
Markdownは以下のように記述します。Remark Directiveを利用しています。
::pdf
この記述があると、画面右上に丸いボタンが表示されます。このボタンはエディタでも表示されますが、機能しません。ページ表示時のみ機能します。
コードについて
コードはgoofmint/growi-plugin-pdf-exportにて公開しています。ライセンスはMIT Licenseになります。
プラグインを追加する
利用する際には、GROWIの管理画面の プラグイン
にて追加してください。URLは https://github.com/goofmint/growi-plugin-pdf-export
です。
コードについて
このプラグインでは、Remark Directiveを使って ::pdf
をボタン(実際にはaタグ)にし、その後コンポーネントでPDFをエクスポートする処理を行っています。
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);
// Remark Directiveをボタンに変換する処理
options.remarkPlugins.push(remarkPlugin as any);
// PDFをエクスポートする処理
const { a } = options.components;
options.components.a = pdfExport(a);
return options;
};
// For preview
const originalGeneratePreviewOptions = optionsGenerators.customGeneratePreviewOptions;
optionsGenerators.customGeneratePreviewOptions = (...args) => {
const preview = originalGeneratePreviewOptions ? originalGeneratePreviewOptions(...args) : optionsGenerators.generatePreviewOptions(...args);
preview.remarkPlugins.push(remarkPlugin as any);
const { a } = preview.components;
preview.components.a = pdfExport(a);
return preview;
};
};
Remark Directive版の処理
Remark Directiveでは、画面上部にボタンを標示する処理を行います。 hChildren
にNodeを追加します。
export const remarkPlugin: Plugin = () => {
return (tree: Node) => {
visit(tree, 'leafDirective', (node: Node) => {
const n = node as unknown as GrowiNode;
if (n.name !== 'pdf') return;
const data = n.data || (n.data = {});
// add float button to the right top
data.hChildren = [
{
tagName: 'div',
type: 'element',
properties: { className: 'pdf-export-float-button' },
children: [
{
tagName: 'a',
type: 'element',
properties: { className: 'material-symbols-outlined me-1 grw-page-control-dropdown-icon pdf-export' },
children: [
{ type: 'text', value: 'cloud_download' },
],
},
],
},
];
});
};
};
つまり、この処理では以下のDOMを追加しています。
<div class="pdf-export-float-button">
<a class="material-symbols-outlined me-1 grw-page-control-dropdown-icon pdf-export">
cloud_download
</a>
</div>
コンポーネントとしての処理
続いてコンポーネントでは、 a.pdf-export
に対してクリック処理を追加します。
export const pdfExport = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
return ({ children, ...props }) => {
try {
// pdf-exportクラスがない場合は、通常のaタグとして処理
if (!props.className.split(' ').includes('pdf-export')) {
return (<a {...props}>{children}</a>);
}
// クリックイベント
const onClick = async(e: MouseEvent) => {
};
// クリックイベントを追加して返す
return (
<a {...props} onClick={onClick}>{children}</a>
);
}
catch (err) {
// console.error(err);
}
// Return the original component if an error occurs
return (
<a {...props}>{children}</a>
);
};
};
ボタンをクリックした際の処理
以下は onClick
関数の内容です。実装はDOMをキャプチャしてPDFで保存する方法 #JavaScript - Qiitaを参考にして、複数ページに対応しています。
GROWIの表示内容は .wiki
内にあるので、その内容を dataURI に変換します。また、ページのタイトルを取得して、ファイル名にします。
e.preventDefault();
const title = document.title.replace(/ - .*/, '');
const element = document.querySelector('.wiki');
if (!element) {
return;
}
// blob形式に変換
const blob = await toBlob(element as any as HTMLElement);
if (!blob) {
return;
}
const dataUrl = await readBlobAsDataURL(blob);
続いてjsPDF向けの変数を用意します。
// jsPDFのインスタンスを生成
const pdf = new JSPDF({
orientation: 'p',
unit: 'px',
format: 'a4',
compress: true,
});
// PDFの幅と高さ、DOMに合わせたスケールを計算
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const scale = pdfWidth / element.clientWidth;
const scaledWidth = element.clientWidth * scale;
const scaledHeight = element.clientHeight * scale;
// 背景色を取得
const computedStyle = window.getComputedStyle(document.body);
const backgroundColor = computedStyle.backgroundColor.match(/rgb\(([0-9]{1,3}), ([0-9]{1,3}), ([0-9]{1,3})\)/);
そして、PDFを生成します。HTMLの内容がそのままPDFになるので、PDF側でも同じ背景色にしないとテキストが読みにくくなります。そのため、ドキュメントの背景色を取得して、PDFの背景色に設定しています。
// 標示に利用するページ数を計算する
const pages = Math.ceil(scaledHeight / pdfHeight);
// ページ数分のPDFを生成
for (let i = 0; i < pages; i++) {
// 描画するページを設定
pdf.setPage(i + 1);
// 背景色を設定
pdf.setFillColor(parseInt(backgroundColor![1]), parseInt(backgroundColor![2]), parseInt(backgroundColor![3]));
pdf.rect(0, 0, scaledWidth, scaledHeight, 'F');
// 描画
pdf.addImage(
dataUrl,
'JPEG',
0,
-pdfHeight * i,
scaledWidth,
scaledHeight,
);
// 次のページを追加
pdf.addPage();
}
// 最後のページは不要なので削除
pdf.deletePage(pdf.getNumberOfPages());
// ファイル名を設定して保存(ダウンロード)
pdf.save(`${title === '/' ? 'Root' : title}.pdf`);
画像なども埋め込まれており、ページの内容がほぼそのままPDFとして取得できます。しかもJPEGとして追加しているのに、透明テキストまで埋め込まれます。jsPDF、すごい!
GROWIコミュニティについて
プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください!
まとめ
GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。