From cadc02e92182576e4bad178f8c66043abf4b0f96 Mon Sep 17 00:00:00 2001 From: Johan Thelin Date: Fri, 20 May 2022 11:26:34 +0200 Subject: [PATCH] Initial support for TOC generation A two level TOC can now be generated by supplying a tocLevel function. The TOC has two levels, but pages can also be excluded from the TOC. --- README.md | 31 ++++++++++++ package.json | 2 +- src/extendCli.js | 2 + src/generatePdf.js | 114 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 134 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b455870..0dc2ba3 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,14 @@ This is a fork of @snowdog/vuepress-plugin-pdf-export. All changes are available - Applies styles to hide UI elements like navigation or sidebar - Doesn't require other runtimes like Java to operate - Designed to work well in headless environments like CI runners +- Can filter and sort pages. +- Can generate a rudimentary table of contents ## Config options - `theme` - theme name (default `@vuepress/default`) - `sorter` - function for changing pages order (default `false`) - `filter` - function for filtering the pages (default `false`) +- `tocLevel` - function returning a TOC level for the pages, i.e. zero or one (default `false`) - `outputFileName` - name of output file (default `site.pdf`) - `puppeteerLaunchOptions` - [Puppeteer launch options object](https://github.com/puppeteer/puppeteer/blob/v2.1.1/docs/api.md#puppeteerlaunchoptions) (default `{}`) - `pageOptions` - [Puppeteer page formatting options object](https://github.com/puppeteer/puppeteer/blob/v2.1.1/docs/api.md#pagepdfoptions) (default `{format: 'A4'}`) @@ -35,6 +38,30 @@ Then run: vuepress export [path/to/your/docs] ``` +#### The filter function + +The `filter` function takes a `pages` object and returns `true` or `false`. Only pages where the function returns `true` are rendered to the pdf. The function is invoked as follows: + +``` +exportPages = exportPages.filter(filter); +``` + +#### The sorter function + +The `sorter` function takes two `pages` objects and return `-1`, `0`, or `1` to indicate the sort order. The function is invoked as follows: + +``` +exportPages = exportPages.sort(sorter) +``` + +The sorting happens after the filtering, so you only have to handle the pages that pass your filter function. + +#### The tocLevel function + +The `tocLevel` function takes a `pages` object returns a TOC level, either zero (`0`, top level) or one (`1`, secondary level), or minus one (`-1`, leave out of TOC). If the entire TOC is empty, e.g. every page is on level `-1`, no TOC is rendered. + +The TOC generation is invoked after the filtering and sorting. So the list of pages can be assumed to be filtered. + ### Tips To run this plugin on Gitlab CI you may want to run Chrome with `no-sandbox` flag. [Details](https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox) @@ -49,3 +76,7 @@ module.exports = { ] } ``` + +## Known Issues + +- At the moment, pdfjs cannot inject footers on the rendered pages, and the individual pages do not know their page number, so the page numbers in the TOC relates to the page numbers in the PDF, but no page number is rendered on the actual PDF pages. diff --git a/package.json b/package.json index 52c0e14..996a1d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@e8johan/vuepress-plugin-pdf-export", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "repository": "e8johan/vuepress-plugin-pdf-export", "scripts": { diff --git a/src/extendCli.js b/src/extendCli.js index 2c42dec..46fdedb 100644 --- a/src/extendCli.js +++ b/src/extendCli.js @@ -9,6 +9,7 @@ module.exports = options => { const theme = options.theme || '@vuepress/default' const sorter = options.sorter || false const filter = options.filter || false + const tocLevel = options.tocLevel || false const outputFileName = options.outputFileName || 'site.pdf' const puppeteerLaunchOptions = options.puppeteerLaunchOptions || {} const pageOptions = options.pageOptions || {} @@ -35,6 +36,7 @@ module.exports = options => { host: nCtx.devProcess.host, sorter, filter, + tocLevel, outputFileName, puppeteerLaunchOptions, pageOptions diff --git a/src/generatePdf.js b/src/generatePdf.js index 7b05122..ee910da 100644 --- a/src/generatePdf.js +++ b/src/generatePdf.js @@ -4,11 +4,41 @@ const { join } = require('path') const { fs, logger, chalk } = require('@vuepress/shared-utils') const { yellow, gray } = chalk +function _createToc(doc, toc, tocPageCount) { + doc.text('Table of Contents', { fontSize: 20 }); + doc.text(' ', { fontSize: 8 }); + const table = doc.table({ + widths: [5 * pdf.mm, (210-85.8) * pdf.mm, 30 * pdf.mm], + padding: 0, + borderWidth: 0 + }); + let currentPage = tocPageCount; + if (currentPage == -1) + currentPage = 9998; + + toc.forEach(t => { + const row = table.row(); + if (t.tocLevel == 0) { + row.cell(t.title, {fontSize: 11, textAlign: 'left', colspan: 2}); + row.cell((currentPage+1).toString(), {fontSize: 11, textAlign: 'right'}); + } else if (t.tocLevel == 1) { + row.cell('', {fontSize: 11, textAlign: 'left'}); + row.cell(t.title, {fontSize: 11, textAlign: 'left'}); + row.cell((currentPage+1).toString(), {fontSize: 11, textAlign: 'right'}); + } + // Other toc levels mean skipping the entry + + if (tocPageCount != -1) + currentPage += t.pageCount; + }); +} + module.exports = async (ctx, { port, host, sorter, filter, + tocLevel, outputFileName, puppeteerLaunchOptions, pageOptions @@ -17,6 +47,11 @@ module.exports = async (ctx, { const tempDir = join(tempPath, 'pdf') fs.ensureDirSync(tempDir) + // Default toc level if not specified + if (typeof tocLevel !== 'function') { + tocLevel = function() { return -1; } + } + let exportPages = pages.slice(0) if (typeof filter === 'function') { @@ -32,7 +67,8 @@ module.exports = async (ctx, { url: page.path, title: page.title, location: `http://${host}:${port}${page.path}`, - path: `${tempDir}/${page.key}.pdf` + path: `${tempDir}/${page.key}.pdf`, + relativePath: page.relativePath } }) @@ -64,24 +100,74 @@ module.exports = async (ctx, { } await new Promise(resolve => { - const mergedPdf = new pdf.Document() - - exportPages - .map(({ path }) => fs.readFileSync(path)) - .forEach(file => { + // Build the TOC (collect page numbers, etc) + var toc = [] + for (let i = 0; i < exportPages.length; i++) { + const { + relativePath, + path, + title + } = exportPages[i] + const file = fs.readFileSync(path) const page = new pdf.ExternalDocument(file) - mergedPdf.addPagesOf(page) - }) + const tl = tocLevel(exportPages[i]) + if (tl == 0 || tl == 1) { + toc.push({tocLevel: tl, title: title, pageCount: page.pageCount}) + } + } - mergedPdf.asBuffer((err, data) => { + // Generate and TOC without page numbers to count pages + const tocPdf = new pdf.Document({ + paddingLeft: 25.4 * pdf.mm, + paddingRight: 25.4 * pdf.mm, + paddingTop: 25.4 * pdf.mm, + paddingBottom: 37.6 * pdf.mm, + }); + if (toc.length > 0) { + _createToc(tocPdf, toc, -1); + } + let tocPageCount = -1; + tocPdf.asBuffer((err, data) => { if (err) { - throw err + throw err; } else { - fs.writeFileSync(outputFileName, data, { encoding: 'binary' }) - logger.success(`Export ${yellow(outputFileName)} file!`) - resolve() + const tocPages = new pdf.ExternalDocument(data); + tocPageCount = tocPages.pageCount; } - }) + }).finally(x => { + // Merge the pages, but first, insert the TOC + const mergedPdf = new pdf.Document({ + paddingLeft: 25.4 * pdf.mm, + paddingRight: 25.4 * pdf.mm, + paddingTop: 25.4 * pdf.mm, + paddingBottom: 37.6 * pdf.mm, + }); + + if (toc.length > 0) { + _createToc(mergedPdf, toc, tocPageCount); + } else { + tocPageCount = 0; + } + + for (let i = 0; i < exportPages.length; i++) { + const { + path, + } = exportPages[i] + const file = fs.readFileSync(path) + const page = new pdf.ExternalDocument(file) + mergedPdf.addPagesOf(page) + } + + mergedPdf.asBuffer((err, data) => { + if (err) { + throw err + } else { + fs.writeFileSync(outputFileName, data, { encoding: 'binary' }) + logger.success(`Export ${yellow(outputFileName)} file!`) + resolve() + } + }) + }); }) await browser.close()