Skip to content

Commit

Permalink
feat: Allow setting the margin and format of PDF
Browse files Browse the repository at this point in the history
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 kohheepeace#18.
  • Loading branch information
aloisklink committed Jul 4, 2020
1 parent ca67f33 commit 6652068
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 53 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 71 additions & 42 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("<initialDocsUrl> [filename]")
.description("Generate PDF from initial docs url")
.arguments("<initialDocsUrl> [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 <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 <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 <initialDocsUrl> [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
Expand All @@ -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 <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`",
"./"
)
Expand All @@ -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();
Expand Down
31 changes: 20 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import * as path from "path";

const generatedPdfBuffers: Array<Buffer> = [];

interface GeneratePdfOptions {
puppeteerArgs: Array<string>;
puppeteerPdfOpts: puppeteer.PDFOptions;
}

async function mergePdfBuffers(pdfBuffers: Array<Buffer>) {
const outDoc = await PDFDocument.create();
for (const pdfBuffer of pdfBuffers) {
Expand Down Expand Up @@ -100,9 +105,11 @@ export function getPathSegment(path: string, endSlash = true): string {
export async function generatePdf(
initialDocsUrl: string,
filename = "docusaurus.pdf",
puppeteerArgs: Array<string>
): Promise<void> {
const browser = await puppeteer.launch({ args: puppeteerArgs });
generatePdfOptions: GeneratePdfOptions
): Promise<Buffer> {
const browser = await puppeteer.launch({
args: generatePdfOptions.puppeteerArgs,
});
const page = await browser.newPage();

const url = new URL(initialDocsUrl);
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -219,15 +228,15 @@ export async function generatePdfFromBuildWithConfig(
siteDir: string,
buildDirPath: string,
filename: string,
puppeteerArgs: Array<string>
generatePdfOptions: GeneratePdfOptions
): Promise<void> {
const { firstDocPath, baseUrl } = await loadConfig(siteDir);
await generatePdfFromBuildSources(
buildDirPath,
firstDocPath,
baseUrl,
filename,
puppeteerArgs
generatePdfOptions
);
}

Expand All @@ -236,7 +245,7 @@ export async function generatePdfFromBuildSources(
firstDocPath: string,
baseUrl: string,
filename: string,
puppeteerArgs: Array<string>
generatePdfOptions: GeneratePdfOptions
): Promise<void> {
const app = express();

Expand Down Expand Up @@ -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();
Expand Down
60 changes: 60 additions & 0 deletions tests/cli-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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);
});
});
});

0 comments on commit 6652068

Please sign in to comment.