UNPKG

@mfgames-writing/weasyprint-format

Version:

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

333 lines 14.4 kB
"use strict"; /* eslint @typescript-eslint/no-explicit-any: 0 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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 = __importStar(require("child_process")); const fs_extra_1 = require("fs-extra"); const path = __importStar(require("path")); const rimraf = __importStar(require("rimraf")); class WeasyPrintFormatter { contentCounter = 0; createdWorkDirectory; existingImageIds = []; htmlBuffer = ""; lastContent; previousStyleName; stylesheets = { count: 0 }; workDirectory; workFinalPdfPath; workPdfPaths = []; getSettings() { // We don't wrap files because we have to manipulate the resulting HTML. const 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. let 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. const contentTheme = content.theme.getContentTheme(content); const 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(async () => await 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)}`; const sourceFileName = image.href; const 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. const callback = async (img) => { await 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(async () => { await 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. const execArgs = this.workPdfPaths.splice(0); execArgs.push("cat", "output", this.workFinalPdfPath); args.logger.debug(`pdftk ${execArgs.join(" ")}`); // Create the PDFs chains. let 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) { let 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. const 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. const htmlPath = `${basePath}.html`; const pdfPath = `${basePath}.pdf`; const padPdfPath = padToRight ? `${basePath}-pad.pdf` : pdfPath; // We have to wrap the file in the styling from the theme. promise = promise.then(() => { const padContentData = { element: "default", directory: content.directory, source: content.source, }; const 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) { let 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) { const promise = new Promise((resolve) => { // Figure out the filename we'll be writing. const 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) => { const 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. const 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