@alauda/doom
Version:
Doctor Doom making docs.
236 lines (235 loc) • 8.17 kB
JavaScript
import EventEmitter from 'node:events';
import path from 'node:path';
import process from 'node:process';
import { PDFDocument } from 'pdf-lib';
import { chromium } from 'playwright';
import { red } from 'yoctocolors';
import { getOutlineNodes, setOutlineNodes, } from './outline.js';
import { setMetadata } from './postprocesser.js';
export class Printer extends EventEmitter {
debug;
headless;
allowLocal;
outlineTags;
allowRemote;
initScripts;
additionalScripts;
additionalStyles;
allowedPaths;
allowedDomains;
ignoreHTTPSErrors;
browserWSEndpoint;
browserArgs;
timeout;
emulateMedia;
enableWarnings;
outlineContainerSelector;
browser;
browserContext;
page;
url;
constructor(options = {}) {
super();
this.debug = options.debug ?? false;
this.headless = options.headless ?? true;
this.allowLocal = options.allowLocal ?? false;
this.allowRemote = options.allowRemote ?? true;
this.outlineTags = options.outlineTags ?? [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
];
this.initScripts = options.initScripts ?? [];
this.additionalScripts = options.additionalScripts ?? [];
this.additionalStyles = options.additionalStyles ?? [];
this.allowedPaths = options.allowedPaths ?? [];
this.allowedDomains = options.allowedDomains ?? [];
this.ignoreHTTPSErrors = options.ignoreHTTPSErrors ?? false;
this.browserWSEndpoint = options.browserEndpoint;
this.browserArgs = options.browserArgs ?? [];
this.timeout = options.timeout ?? 0;
this.emulateMedia = options.emulateMedia ?? 'print';
this.enableWarnings = options.enableWarnings ?? false;
this.outlineContainerSelector = options.outlineContainerSelector ?? '';
if (this.debug) {
this.headless = false;
}
}
async setup(launchOptions) {
if (this.page) {
return this.page;
}
const browserOptions = {
pipe: true,
headless: this.headless,
args: ['--disable-dev-shm-usage', '--export-tagged-pdf'],
...launchOptions,
};
if (this.allowLocal) {
browserOptions.args.push('--allow-file-access-from-files');
}
if (this.browserArgs.length) {
browserOptions.args.push(...this.browserArgs);
}
if (this.browserWSEndpoint) {
this.browser = await chromium.connect(this.browserWSEndpoint, browserOptions);
}
else {
this.browser = await chromium.launch(browserOptions);
}
this.browserContext = await this.browser.newContext({
ignoreHTTPSErrors: this.ignoreHTTPSErrors,
});
for (const script of this.initScripts) {
await this.browserContext.addInitScript(script);
}
const page = await this.browserContext.newPage();
this.page = page;
await page.emulateMedia({
colorScheme: 'light',
media: this.emulateMedia,
});
if (this.needsAllowedRules()) {
await page.route('**/*', (route) => {
const uri = new URL(route.request().url());
const { host, protocol, pathname } = uri;
const local = protocol === 'file:';
if (local && !this.withinAllowedPath(pathname)) {
return route.abort();
}
if (local && !this.allowLocal) {
return route.abort();
}
if (host && !this.isAllowedDomain(host)) {
return route.abort();
}
if (host && !this.allowRemote) {
return route.abort();
}
return route.continue();
});
}
return page;
}
async render(url) {
const page = this.page || (await this.setup());
if (url === this.url) {
return page;
}
this.url = url;
try {
await page.goto(url);
for (const style of this.additionalStyles) {
await page.addStyleTag({ path: style });
}
for (const script of this.additionalScripts) {
await page.addScriptTag({ path: script });
}
await page.waitForLoadState('networkidle', { timeout: this.timeout });
return page;
}
catch (error) {
await this.closeBrowser();
throw error;
}
}
async pdf(url, options = {}, pdfOutlines) {
const page = await this.render(url);
try {
// Get metatags
const meta = await page.evaluate(() => {
const meta = {};
const title = document.querySelector('title');
if (title) {
meta.title = title.textContent.trim();
}
const lang = document.querySelector('html')?.getAttribute('lang');
if (lang) {
meta.lang = lang;
}
const metaTags = document.querySelectorAll('meta');
metaTags.forEach((tag) => {
if (tag.name) {
meta[tag.name] = tag.content;
}
});
return meta;
});
const pdfExportOptions = {
scale: !options.scale ? 1 : options.scale,
displayHeaderFooter: false,
headerTemplate: options.headerTemplate,
footerTemplate: options.footerTemplate,
preferCSSPageSize: options.preferCSSPageSize,
printBackground: options.printBackground,
width: options.width,
height: options.height,
landscape: options.landscape,
format: options.format,
};
if (options.margin) {
pdfExportOptions.margin = options.margin;
}
if (options.pageRanges) {
pdfExportOptions.pageRanges = options.pageRanges;
}
if (options.headerTemplate || options.footerTemplate) {
pdfExportOptions.displayHeaderFooter = true;
}
const pdf = await page.pdf(pdfExportOptions);
this.emit('postprocessing');
const pdfDoc = await PDFDocument.load(pdf);
setMetadata(pdfDoc, meta);
let outlineNodes = [];
if (pdfOutlines) {
outlineNodes = await getOutlineNodes(page, this.outlineTags, this.outlineContainerSelector);
setOutlineNodes(pdfDoc, outlineNodes, this.enableWarnings);
}
return {
data: await pdfDoc.save(),
outlineNodes,
};
}
catch (error) {
await this.closeBrowser();
throw error;
}
}
async closeBrowser() {
if (this.browser) {
await this.browser.close();
this.browser = undefined;
this.browserContext = undefined;
this.page = undefined;
}
else {
process.stdout.write(red('Browser instance not found'));
}
}
needsAllowedRules() {
return this.allowedPaths.length || this.allowedDomains.length;
}
withinAllowedPath(pathname) {
if (!this.allowedPaths.length) {
return true;
}
for (const parent of this.allowedPaths) {
const relative = path.relative(parent, pathname);
if (relative &&
!relative.startsWith('..') &&
!path.isAbsolute(relative)) {
return true;
}
}
return false;
}
isAllowedDomain(domain) {
if (!this.allowedDomains.length) {
return true;
}
return this.allowedDomains.includes(domain);
}
}