UNPKG

@mfgames-writing/weasyprint-format

Version:

A formatter plugin for mfgames-writing-format that creates PDF files.

305 lines 13.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WeasyPrintFormatter = void 0; const fs = require("mz/fs"); const tmp = require("tmp"); const zpad = require("zpad"); const contracts_1 = require("@mfgames-writing/contracts"); const child = require("child_process"); const fs_extra_1 = require("fs-extra"); const path = require("path"); const rimraf = require("rimraf"); class WeasyPrintFormatter { constructor() { this.contentCounter = 0; this.existingImageIds = []; this.htmlBuffer = ""; this.stylesheets = { count: 0 }; this.workPdfPaths = []; } getSettings() { // We don't wrap files because we have to manipulate the resulting HTML. var settings = new contracts_1.FormatterSettings(); settings.wrapIndividualFiles = false; return settings; } start(args) { // Start with a simple promise that makes it // easier to format our code. let promise = Promise.resolve(args); // We need a working directory for this module. promise = promise.then((args) => { // If the user provides --temp on the CLI, then use that. this.workDirectory = args.argv.temp; // If we haven't had a work directory provided, make up one. if (!this.workDirectory) { this.createdWorkDirectory = true; this.workDirectory = tmp.dirSync().name; } args.logger.debug(`Using temporary directory: ${this.workDirectory}`); // Figure out the various paths we'll be using. this.workFinalPdfPath = path.join(this.workDirectory, "output.pdf"); // Return the resulting arguments. return args; }); // Return the resulting promise. return promise; } addHtml(content) { // Keep track of the last content so we can finalize the PDF. this.lastContent = content; // Create a basic promise so we can easily insert and remove steps. var promise = Promise.resolve(content); // Figure out which stylesheet we need to process to make this work. // With the current version of WeasyPrint, we only need a different // style for segments of pages that have a different rendering for the // non-first page of each section. For example, the preface doesn't // have headers on it. let contentTheme = content.theme.getContentTheme(content); let styleName = contentTheme.styleName || "base"; // Cache the stylesheet with a consistent name in the working directory. // We use this in the HTML we'll be rendering out in blocks. promise = promise.then(() => this.loadStylesheet(content.editionArgs, styleName)); // Figure out if we have to write out the previous buffer because we // change style names or there is a page number reset. This will create // the PDF and add it to the merge list. promise = promise.then(() => __awaiter(this, void 0, void 0, function* () { return yield this.checkHtmlPdf(content, styleName, true); })); // Append the HTML content to the buffer. promise = promise.then(() => { this.htmlBuffer += content.text; return content; }); // Normalize the output and return. return promise.then((e) => content); } addImage(content, image) { // Figure out the name of the file in the working directory. const imageId = `i${zpad(this.contentCounter++, 4)}`; let sourceFileName = image.href; let workFileName = path.join(this.workDirectory, `image-${imageId}${image.extension}`); // We only want to add a single version to the zip archive. If we return // null from this function, then we already have it. if (this.existingImageIds.filter((i) => i === imageId).length > 0) { return { include: false, href: workFileName, }; } this.existingImageIds.push(imageId); // Create the promise to actually add it. let callback = (img) => __awaiter(this, void 0, void 0, function* () { yield fs.writeFile(workFileName, img.buffer); content.logger.debug(`Copied image ${sourceFileName} to ${image.imagePath}`); return content; }); // Return the resulting data. return { include: true, href: workFileName, callback: callback, }; } finish(args) { // Add in the various final steps we need. let promise = Promise.resolve(args); // Render out any pages that we might have remaining. We faked the // content by keeping track of the last one we used. We pass undefined // to force a style-based break. promise = promise.then(() => __awaiter(this, void 0, void 0, function* () { yield this.checkHtmlPdf(this.lastContent, undefined, false); return args; })); // Combine the individual PDFs into one master one. promise = promise.then(() => this.mergePdfs(args)); // Copy the files out to the build for debugging purposes. promise = promise.then(() => this.copyPdf(args)); // Clean up the working directory if we created it. if (this.createdWorkDirectory) { promise = promise.then(() => { args.logger.debug("Removing temporary directory: " + this.workDirectory); rimraf.sync(this.workDirectory); return args; }); } // Return the resulting promise. return promise; } mergePdfs(args) { // Figure out the arguments. var execArgs = this.workPdfPaths.splice(0); execArgs.push("cat", "output", this.workFinalPdfPath); args.logger.debug(`pdftk ${execArgs.join(" ")}`); // Create the PDFs chains. var promise = Promise.resolve(args); promise = promise.then(() => { child.execFileSync("pdftk", execArgs, { cwd: this.workDirectory, }); args.logger.info(`Merged into ${this.workFinalPdfPath}`); return args; }); return promise; } /** * Checks to see if we need to render the PDF because we change some * part that requires a different stylesheet. */ checkHtmlPdf(args, styleName, padToRight) { var promise = Promise.resolve(args); // If we don't have a buffer, we never have to render but we do have to // set the style to the next one. if (this.htmlBuffer.length === 0) { promise = promise.then(() => { this.previousStyleName = styleName; return args; }); args.logger.debug(args.id + ": checkHtmlPdf: empty buffer"); return promise; } // If we match the previous style and there is no explict page number // change, then we don't have to. if (!args.contentData.page && styleName === this.previousStyleName) { // Nothing changed, so we're good. args.logger.debug(args.id + ": checkHtmlPdf: same style"); return promise; } args.logger.debug(args.id + ": checkHtmlPdf: " + [ "htmlBuffer=" + this.htmlBuffer.length, "contentData.page=" + args.contentData.page, "styleName=" + styleName, "previousStyleName=" + this.previousStyleName, ].join(", ")); // If we haven't fallen out, we need to write out the current HTML as // a PDF. var basePath = path.join(this.workDirectory, `html-${this.workPdfPaths.length}`); promise = this.renderHtmlPdf(promise, args, basePath, padToRight); // Reset the buffer state after rendering and return the results. promise = promise.then(() => { this.previousStyleName = styleName; this.htmlBuffer = ""; return args; }); return promise; } renderHtmlPdf(promise, content, basePath, padToRight) { padToRight = false; // Figure out the paths. let htmlPath = `${basePath}.html`; let pdfPath = `${basePath}.pdf`; let padPdfPath = padToRight ? `${basePath}-pad.pdf` : pdfPath; // We have to wrap the file in the styling from the theme. promise = promise.then(() => { var padContentData = { element: "default", directory: content.directory, source: content.source, }; var padContent = new contracts_1.ContentArgs(content.editionArgs, padContentData); // We add in a fake page at the end of every document so we can // easily remove it using `pdftk`. padContent.text = this.htmlBuffer; if (padToRight) { padContent.text += "<div class='fake-right'>&#160;</div>"; } // Render the layout around this file. return content.theme.renderLayout(padContent); }); promise = promise.then((nc) => { this.htmlBuffer = nc.text; }); // Write the file out to the HTML. promise = promise.then(() => { fs.writeFileSync(htmlPath, this.htmlBuffer.replace(content.theme.stylesheetFileName, `${this.previousStyleName}.css`)); }); // Render the HTML as PDF. This will always have one page at the end // to make sure we have the correct start for the next. promise = promise.then(() => this.renderPdf(content.editionArgs, htmlPath, padPdfPath)); // Remove the padding page, but only if we added a pad. if (padToRight) { promise = promise.then(() => { child.execFileSync("pdftk", [padPdfPath, "cat", "1-r2", "output", pdfPath], { cwd: this.workDirectory, }); return content; }); } // Add the PDF to the list. promise = promise.then(() => { this.workPdfPaths.push(pdfPath); return content; }); // Return the resulting promise. return promise; } renderPdf(args, htmlFileName, pdfFileName) { var promise = Promise.resolve(args); promise = promise.then(() => { child.execFileSync("weasyprint", [htmlFileName, pdfFileName], { cwd: this.workDirectory, }); args.logger.info(`Rendered PDF: ${pdfFileName}`); return args; }); return promise; } writeHtml(args, fileName, html) { return fs.writeFile(fileName, html).then(() => args); } copyPdf(args) { let promise = new Promise((resolve) => { // Figure out the filename we'll be writing. let outputFileName = path.join(args.rootDirectory, args.edition.outputDirectory, args.edition.outputFilename); args.logger.info(`Copied PDF ${outputFileName}`); // Copy the file to the output. resolve((0, fs_extra_1.copy)(this.workFinalPdfPath, outputFileName).then(() => args)); }); // Return the resulting promise. return promise; } loadStylesheet(args, pageName) { // See if we already have this one loaded. If we have, then we don't // have to do anything to have it loaded. if (pageName in this.stylesheets) { return Promise.resolve(args); } this.stylesheets[pageName] = String.fromCharCode("A".charCodeAt(0) + this.stylesheets.count++); args.logger.debug(`Assigned ${pageName} stylesheet to ${this.stylesheets[pageName]}`); // Load the stylesheet from the theme. let promise = new Promise((resolve) => { let css = args.theme.renderStylesheet(args, [ `weasyprint-${pageName}`, `pdf-${pageName}`, "weasyprint", "pdf", ]); resolve(css); }); // We need a couple additional elements in our styleshet to handle // bugs with WeasyPrint. promise = promise.then((css) => { return (css + "div.fake-page { page-break-before: always; }" + "div.fake-right { page-break-before: right; }" + "div.fake-left { page-break-before: left; }"); }); // Copy the stylesheet into place. let stylePath = path.join(this.workDirectory, `${pageName}.css`); promise = promise.then((css) => fs.writeFile(stylePath, css)); // Normalize the promise output and return. promise = promise.then(() => args); return promise; } } exports.WeasyPrintFormatter = WeasyPrintFormatter; //# sourceMappingURL=WeasyPrintFormatter.js.map