@mfgames-writing/weasyprint-format
Version:
A formatter plugin for mfgames-writing-format that creates PDF files.
305 lines • 13.7 kB
JavaScript
"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'> </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