From 665206866f48136e4d12dbb64004e500ee4f13bb Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Sun, 5 Jul 2020 00:08:45 +0100 Subject: [PATCH] feat: Allow setting the margin and format of PDF Adds the --format and --margin field to specify the formatting of the PDF file, and adds tests to make sure we don't break anything. Additionally, allows setting --no-sandbox on all commands, including the default docusaurus command. Fixes #18. --- README.md | 2 + bin/index.js | 113 ++++++++++++++++++++++++++--------------- src/index.ts | 31 +++++++---- tests/cli-test.spec.ts | 60 ++++++++++++++++++++++ 4 files changed, 153 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index bf00c4a..25c7e0f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ npx docusaurus-pdf from-build-config - Mandatory: `dirPath` which points to the build directory created with `docusaurus build`. - Mandatory: `firstDocPagePath` is the URL path segment (without `baseUrl`) of your first docs page you whish to have included in the PDF. - Optional: If you have a `baseUrl` configured in your `docusaurus.config.js` then pass this value as `baseUrl`. +- Optional: You can specify larger/smaller margins, e.g. `--margin "1cm 1.5cm 1cm 2cm"` (order top right bottom left, like the css margin field). +- Optional: You can change the size of the paper by using `--format`, e.g. `--format A3` - Note: There is a optional parameter to set a custom filename. You can see further details using `npx docusaurus-pdf from-build --help`. ## Docker usage diff --git a/bin/index.js b/bin/index.js index ce3c2c5..ce3bdea 100755 --- a/bin/index.js +++ b/bin/index.js @@ -10,22 +10,55 @@ const { generatePdfFromBuildWithConfig, } = require("../lib"); +function generatePdfOptions({ sandbox, margin, format, printBackground }) { + return { + puppeteerArgs: sandbox ? [] : ["--no-sandbox"], + puppeteerPdfOpts: { margin, format, printBackground }, + }; +} + +function parseMargin(marginString) { + const matches = [...marginString.match(/\d+\w{0,2}/g)]; + if (matches.length !== 4) { + throw Error( + `Was expecting exactly 4 margin specifiers, instead got ${matches.length}. Margin specifier was "${marginString}".` + ); + } + const [top, right, bottom, left] = matches; + return { top, right, bottom, left }; +} + program .version(require("../package.json").version) .name("docusaurus-pdf") .usage(" [filename]") - .description("Generate PDF from initial docs url") - .arguments(" [filename]") - .action((initialDocsUrl, filename) => { - generatePdf(initialDocsUrl, filename) - .then(() => { - console.log(chalk.green("Finish generating PDF!")); - process.exit(0); - }) - .catch((err) => { - console.error(chalk.red(err.stack)); - process.exit(1); - }); + .description("Generate a PDF from a docusaurus website") + .option("--no-sandbox", "Start puppeteer with --no-sandbox flag") + .option( + "--margin ", + "Set margins of the pdf document with units in order top, right, bottom, left (units px,in,mm,cm)", + parseMargin, + "25px 35px 25px 35px" + ) + .option( + "--format ", + "Set the size of the page, e.g. (A4, Letter)", + "A4" + ) + .option("--no-print-background", "Disable printing the page background"); + +program + .command("from-website [filename]", { + isDefault: true, + }) + .description("Generate PDF from an already hosted website") + .action(async (initialDocsUrl, filename = "docusaurus.pdf") => { + await generatePdf( + initialDocsUrl, + filename, + generatePdfOptions(program.opts()) + ); + console.log(chalk.green("Finish generating PDF!")); }); program @@ -36,34 +69,25 @@ program "Specify your file name.", "docusaurus.pdf" ) - .option("--no-sandbox", "Start puppeteer with --no-sandbox flag") - .action((dirPath, firstDocPagePath, baseUrl, options) => { - const puppeteerArgs = options.sandbox ? [] : ["--no-sandbox"]; - generatePdfFromBuildSources( + .action(async (dirPath, firstDocPagePath, baseUrl, { outputFile }) => { + await generatePdfFromBuildSources( dirPath, firstDocPagePath, baseUrl, - options.outputFile, - puppeteerArgs - ) - .then(() => { - console.log(chalk.green("Finish generating PDF!")); - process.exit(0); - }) - .catch((err) => { - console.error(chalk.red(err.stack)); - process.exit(1); - }); + outputFile, + generatePdfOptions(program.opts()) + ); + console.log(chalk.green("Finish generating PDF!")); }); program .command("from-build-config [dirPath]") .description( - "Generate PDF from a docusaurs build artifact, loading from a docusaurus.config.js file" + "Generate PDF from a docusaurus build artifact, loading from a docusaurus.config.js file" ) .option( "--site-dir ", - "The full path for the docusuarus site directory, relative to the current workspace." + + "The full path for the docusaurus site directory, relative to the current workspace." + " Equivalent to the siteDir in `npx docusaurus build`", "./" ) @@ -72,21 +96,26 @@ program "Specify your file name", "docusaurus.pdf" ) - .option("--no-sandbox", "Start puppeteer with --no-sandbox flag") - .action((dirPath = "build", { siteDir, outputFile, sandbox }) => { - const puppeteerArgs = sandbox ? [] : ["--no-sandbox"]; - generatePdfFromBuildWithConfig(siteDir, dirPath, outputFile, puppeteerArgs) - .then(() => { - console.log(chalk.green("Finish generating PDF!")); - process.exit(0); - }) - .catch((err) => { - console.error(chalk.red(err.stack)); - process.exit(1); - }); + .action(async (dirPath = "build", { siteDir, outputFile }) => { + await generatePdfFromBuildWithConfig( + siteDir, + dirPath, + outputFile, + generatePdfOptions(program.opts()) + ); + console.log(chalk.green("Finish generating PDF!")); }); -program.parse(process.argv); +async function main() { + try { + await program.parseAsync(process.argv); + } catch (error) { + console.error(chalk.red(error.stack)); + process.exit(1); + } +} + +main(); if (!process.argv.slice(2).length) { program.outputHelp(); diff --git a/src/index.ts b/src/index.ts index c0d34cd..48ed254 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,11 @@ import * as path from "path"; const generatedPdfBuffers: Array = []; +interface GeneratePdfOptions { + puppeteerArgs: Array; + puppeteerPdfOpts: puppeteer.PDFOptions; +} + async function mergePdfBuffers(pdfBuffers: Array) { const outDoc = await PDFDocument.create(); for (const pdfBuffer of pdfBuffers) { @@ -100,9 +105,11 @@ export function getPathSegment(path: string, endSlash = true): string { export async function generatePdf( initialDocsUrl: string, filename = "docusaurus.pdf", - puppeteerArgs: Array -): Promise { - const browser = await puppeteer.launch({ args: puppeteerArgs }); + generatePdfOptions: GeneratePdfOptions +): Promise { + const browser = await puppeteer.launch({ + args: generatePdfOptions.puppeteerArgs, + }); const page = await browser.newPage(); const url = new URL(initialDocsUrl); @@ -150,9 +157,7 @@ export async function generatePdf( await page.addScriptTag({ url: scriptPath }); const pdfBuffer = await page.pdf({ path: "", - format: "A4", - printBackground: true, - margin: { top: 25, right: 35, left: 35, bottom: 25 }, + ...generatePdfOptions.puppeteerPdfOpts, }); generatedPdfBuffers.push(pdfBuffer); @@ -162,7 +167,11 @@ export async function generatePdf( await browser.close(); const mergedPdfBuffer = await mergePdfBuffers(generatedPdfBuffers); - fs.writeFileSync(`${filename}`, mergedPdfBuffer); + + if (filename) { + await fs.promises.writeFile(`${filename}`, mergedPdfBuffer); + } + return Buffer.from(mergedPdfBuffer); } interface LoadedConfig { @@ -219,7 +228,7 @@ export async function generatePdfFromBuildWithConfig( siteDir: string, buildDirPath: string, filename: string, - puppeteerArgs: Array + generatePdfOptions: GeneratePdfOptions ): Promise { const { firstDocPath, baseUrl } = await loadConfig(siteDir); await generatePdfFromBuildSources( @@ -227,7 +236,7 @@ export async function generatePdfFromBuildWithConfig( firstDocPath, baseUrl, filename, - puppeteerArgs + generatePdfOptions ); } @@ -236,7 +245,7 @@ export async function generatePdfFromBuildSources( firstDocPath: string, baseUrl: string, filename: string, - puppeteerArgs: Array + generatePdfOptions: GeneratePdfOptions ): Promise { const app = express(); @@ -269,7 +278,7 @@ export async function generatePdfFromBuildSources( await generatePdf( `http://127.0.0.1:${address.port}${baseUrl}${firstDocPath}`, filename, - puppeteerArgs + generatePdfOptions ); } finally { httpServer.close(); diff --git a/tests/cli-test.spec.ts b/tests/cli-test.spec.ts index 95c9fcd..17985a0 100644 --- a/tests/cli-test.spec.ts +++ b/tests/cli-test.spec.ts @@ -12,6 +12,13 @@ const TEST_SITE_DIR = "./tests/test-website"; // third-to-last docs path so should be faster const DOCUSAURUS_TEST_LINK = "https://v2.docusaurus.io/docs/2.0.0-alpha.56/docusaurus.config.js"; +const PUPPETEER_SETTINGS = [ + "--margin", + "2cm 2cm 2cm 2cm", + "--format", + "A3", + "--no-sandbox", +]; async function runDocusaurusPdf( args: Array, @@ -72,6 +79,30 @@ describe("testing cli", () => { ).rejects.toThrow(); expect(await isFile(outputPath)).toBe(false); }); + test("should use puppeteer settings", async () => { + const outputPath = pathJoin(TEST_OUTPUT, "docusaurus-a3.pdf"); + await runDocusaurusPdf([ + DOCUSAURUS_TEST_LINK, + outputPath, + ...PUPPETEER_SETTINGS, + ]); + expect(await isFile(outputPath)).toBe(true); + }); + test("should fail with invalid margins", async () => { + const outputPath = pathJoin( + TEST_OUTPUT, + "should-fail-invalid-margins.pdf" + ); + await expect( + runDocusaurusPdf([ + DOCUSAURUS_TEST_LINK, + outputPath, + "--margin", + "2cm 2cm 2cm", + ]) + ).rejects.toThrow(); + expect(await isFile(outputPath)).toBe(false); + }); }); describe("from-build", () => { @@ -112,6 +143,22 @@ describe("testing cli", () => { ]); expect(await isFile(outputPath)).toBe(true); }); + test("should use puppeteer settings", async () => { + const outputPath = pathJoin(TEST_OUTPUT, "from-build-a3.pdf"); + const buildDir = pathJoin(TEST_SITE_DIR, "build"); + const docsPath = "/docs-path"; + const baseUrl = "/base-url/"; + await runDocusaurusPdf([ + "from-build", + buildDir, + docsPath, + baseUrl, + "--output-file", + outputPath, + ...PUPPETEER_SETTINGS, + ]); + expect(await isFile(outputPath)).toBe(true); + }); }); describe("from-build-config", () => { @@ -130,5 +177,18 @@ describe("testing cli", () => { ); expect(await isFile(outputPath)).toBe(true); }); + test("should use puppeteer settings", async () => { + const outputPath = pathJoin(TEST_OUTPUT, "from-build-config-a3.pdf"); + await runDocusaurusPdf( + [ + "from-build-config", + "--output-file", + pathResolve(outputPath), + ...PUPPETEER_SETTINGS, + ], + { cwd: TEST_SITE_DIR } + ); + expect(await isFile(outputPath)).toBe(true); + }); }); });