markugen
Version:
Markdown to HTML/PDF static site generation tool
185 lines (184 loc) • 7.5 kB
JavaScript
;
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = __importDefault(require("node:path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
const url_1 = __importDefault(require("url"));
const generator_1 = __importDefault(require("./generator"));
__exportStar(require("./pdfoptions"), exports);
/**
* Generator for HTML to PDF file generation
*/
class PdfGenerator extends generator_1.default {
/**
* The options to use in generate
*/
options;
/**
* The list of files to convert
*/
files = [];
/**
* Constructs a new generator
* @param mark the instance of {@link Markugen}
*/
constructor(mark, options) {
super(mark, options);
this.options = {
input: options.input,
remove: options.remove ?? false,
extensions: options.extensions ?? ['html'],
links: options.links ?? true,
};
}
/**
* Generates the PDFs for the given {@link PdfOptions options}
* @param options the {@link PdfOptions options} to use for generation
* @returns a list of PDF files that were generated
*/
async generate() {
this.validate();
this.start();
// prepare the browser
this.log('Browser:', this.mark.options.browser);
const generated = [];
const promises = [];
// loop over and write the pdf for each file
for (const file of this.files)
promises.push(this.writePdf(file));
// wait for all promises to be settled
const results = await Promise.allSettled(promises);
for (const result of results) {
if (result.status === 'fulfilled')
generated.push(result.value);
else if (result.status === 'rejected')
throw new Error(result.reason);
}
this.finish();
return generated;
}
/**
* Creates the pdf version of the file
* @param file the path to the html file or the html string
*/
async writePdf(file) {
const parts = node_path_1.default.parse(file);
const pdf = node_path_1.default.join(parts.dir, parts.name + '.pdf');
this.log('Generating PDF:', pdf);
const browser = await puppeteer_core_1.default.launch({
executablePath: this.mark.options.browser,
args: this.mark.options.sandbox === false ? ['--no-sandbox', '--disable-setuid-sandbox'] : undefined,
});
const page = await browser.newPage();
await page.goto(url_1.default.pathToFileURL(file).toString(), { waitUntil: 'networkidle2' });
// replace all markdown relative links with the pdf equivalent
if (this.options.links)
await page.evaluate(() => {
const links = document.querySelectorAll('.markugen-md-link');
for (const link of links) {
//path.basename('foo/bar.txt');
// @ts-expect-error puppeteer types no work here
const html = link.href.split('#')[0].split('/').pop();
const pdf = html.replace(/\.html$/ig, '.pdf');
// @ts-expect-error puppeteer types no work here
link.href = link.href.replace(html, pdf);
link.innerHTML = link.innerHTML.replace(html, pdf);
}
});
// get the content box
const content = await page.$('#markugen-content');
const box = await content?.boxModel();
content?.dispose();
await page.pdf({
format: 'A4',
path: pdf,
margin: {
left: box?.content[0].x ?? '25px',
right: box?.content[3].x ?? '25px',
bottom: box?.content[0].y ?? '25px',
top: box?.content[0].y ?? '25px',
},
});
// close the browser
await browser.close();
// remove the html file if remove option given
if (this.options.remove)
fs_extra_1.default.removeSync(file);
// return the path to the pdf
return pdf;
}
/**
* Validates the given options
* @param options the {@link PdfOptions options} to validate
* @returns an array of files to generate PDFs for
*/
validate() {
// clear out the files
this.files.length = 0;
// always make it an array
if (!Array.isArray(this.options.input))
this.options.input = [this.options.input];
// resolve the paths
for (let i = 0; i < this.options.input.length; i++) {
this.options.input[i] = node_path_1.default.resolve(this.options.input[i]);
if (!fs_extra_1.default.existsSync(this.options.input[i]))
throw new Error(`Input path does not exist [${this.options.input[i]}]`);
}
// check the browser
if (!this.mark.options.browser || !fs_extra_1.default.existsSync(this.mark.options.browser))
throw new Error(`Unable to locate browser at [${this.mark.options.browser}], cannot generate PDFs`);
// handle the extensions
if (this.options.extensions.length === 0)
this.options.extensions.push('html');
let pattern = '\\.';
for (let i = 0; i < this.options.extensions.length; i++)
pattern += `${i !== 0 ? '|' : ''}(${this.options.extensions[i]})`;
pattern += '$';
const regex = new RegExp(pattern, 'i');
// collect the files
for (const file of this.options.input) {
const stat = fs_extra_1.default.lstatSync(file);
// directories need to be globbed
if (stat.isDirectory())
this.collect(file, regex);
// files are good to go
else
this.files.push(file);
}
// nothing to do if no files found
if (this.files.length === 0)
throw new Error('No files found for PDF generation');
}
/**
* Collects all html files found in the given directory
* @param dir the directory to look in
*/
collect(dir, regex) {
const paths = fs_extra_1.default.readdirSync(dir, { withFileTypes: true });
for (const p of paths) {
const full = node_path_1.default.join(p.parentPath, p.name);
if (p.isFile() && regex.test(p.name))
this.files.push(full);
else if (p.isDirectory())
this.collect(full, regex);
}
}
}
exports.default = PdfGenerator;