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