GROWI, an open-source Wiki, provides a plugin feature that can be used to display or customize your data.
This time, I created a plugin for GROWI that adds a PDF export feature. Although PDF export can seem tedious, it turned out to be easier to implement than expected.
Plugin Behavior
Markdown is written as follows, using Remark Directive:
::pdf
When this is written, a circular button appears in the upper-right corner of the screen. This button is also visible in the editor but does not function there. It only works when the page is displayed.
About the Code
The code is available at goofmint/growi-plugin-pdf-export under the MIT License.
Adding the Plugin
To use it, add it in the Plugin
section of the GROWI admin panel. The URL is https://github.com/goofmint/growi-plugin-pdf-export
.
About the Code
This plugin uses Remark Directive to convert ::pdf
into a button (actually an a
tag), and then processes PDF export within the component.
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);
// Convert Remark Directive into a button
options.remarkPlugins.push(remarkPlugin as any);
// Handle PDF export
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 Processing
Remark Directive handles the process of displaying the button at the top of the screen. A Node is added to hChildren
.
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 a floating button to the top-right
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' },
],
},
],
},
];
});
};
};
This process adds the following 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>
Component Processing
Next, the component adds click handling to a.pdf-export
.
export const pdfExport = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
return ({ children, ...props }) => {
try {
// If the class is not `pdf-export`, process as a normal `a` tag
if (!props.className.split(' ').includes('pdf-export')) {
return (<a {...props}>{children}</a>);
}
// Click event
const onClick = async(e: MouseEvent) => {
};
// Add click event and return
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>
);
};
};
Handling Button Clicks
The following is the content of the onClick
function. The implementation supports multiple pages, based on How to Capture DOM and Save It as PDF in JavaScript - Qiita.
The GROWI content is located within .wiki
. The content is converted to a dataURI, and the page title is used as the filename.
e.preventDefault();
const title = document.title.replace(/ - .*/, '');
const element = document.querySelector('.wiki');
if (!element) {
return;
}
// Convert to blob format
const blob = await toBlob(element as any as HTMLElement);
if (!blob) {
return;
}
const dataUrl = await readBlobAsDataURL(blob);
Next, variables for jsPDF are prepared.
// Create an instance of jsPDF
const pdf = new JSPDF({
orientation: 'p',
unit: 'px',
format: 'a4',
compress: true,
});
// Calculate PDF width, height, and scale to match the 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;
// Get background color
const computedStyle = window.getComputedStyle(document.body);
const backgroundColor = computedStyle.backgroundColor.match(/rgb\(([0-9]{1,3}), ([0-9]{1,3}), ([0-9]{1,3})\)/);
Finally, the PDF is generated. To maintain readability, the document's background color is retrieved and set in the PDF.
// Calculate the number of pages required
const pages = Math.ceil(scaledHeight / pdfHeight);
// Generate PDF for each page
for (let i = 0; i < pages; i++) {
// Set the page to be drawn
pdf.setPage(i + 1);
// Set background color
pdf.setFillColor(parseInt(backgroundColor![1]), parseInt(backgroundColor![2]), parseInt(backgroundColor![3]));
pdf.rect(0, 0, scaledWidth, scaledHeight, 'F');
// Draw
pdf.addImage(
dataUrl,
'JPEG',
0,
-pdfHeight * i,
scaledWidth,
scaledHeight,
);
// Add the next page
pdf.addPage();
}
// Delete the unnecessary last page
pdf.deletePage(pdf.getNumberOfPages());
// Set the file name and save (download)
pdf.save(`${title === '/' ? 'Root' : title}.pdf`);
Images and other content are embedded, and the page content is almost fully preserved in the PDF. Amazingly, jsPDF even embeds transparent text, even though the content is added as JPEG.
About the GROWI Community
If you have any questions or requests about using this plugin, feel free to reach out to the GROWI community. If feasible, I’ll try to accommodate your requests. There are also help channels available, so be sure to join!
Conclusion
With GROWI plugins, you can freely extend the display functionality. If there’s a missing feature, feel free to add it. Customize your Wiki as you like!
OSS Development Wiki Tool GROWI | Comfortable Information Sharing for Everyone
Top comments (0)