mdpdf_jm
Version:
Markdown to PDF command line converter
195 lines (155 loc) • 5.22 kB
JavaScript
const fs = require('fs');
const path = require('path');
const Promise = require('bluebird');
const showdown = require('showdown');
const showdownEmoji = require('showdown-emoji');
const showdownHighlight = require('showdown-highlight');
const puppeteer = require('puppeteer');
const Handlebars = require('handlebars');
const loophole = require('loophole');
const utils = require('./utils');
const puppeteerHelper = require('./puppeteer-helper');
const readFile = Promise.promisify(fs.readFile);
const writeFile = Promise.promisify(fs.writeFile);
// Main layout template
const layoutPath = path.join(__dirname, '/layouts/doc-body.hbs');
const headerLayoutPath = path.join(__dirname, '/layouts/header.hbs');
const footerLayoutPath = path.join(__dirname, '/layouts/footer.hbs');
function getAllStyles(options) {
const cssStyleSheets = [];
// GitHub Markdown Style
if (options.ghStyle) {
cssStyleSheets.push(
path.join(__dirname, '/assets/github-markdown-css.css')
);
}
// Highlight CSS
cssStyleSheets.push(
path.join(__dirname, '/assets/highlight/styles/github.css')
);
// Some additional defaults such as margins
if (options.defaultStyle) {
cssStyleSheets.push(path.join(__dirname, '/assets/default.css'));
}
// Optional user given CSS
if (options.styles) {
cssStyleSheets.push(options.styles);
}
return {
styles: utils.getStyles(cssStyleSheets),
styleBlock: utils.getStyleBlock(cssStyleSheets),
};
}
function parseMarkdownToHtml(markdown, convertEmojis, enableHighlight) {
showdown.setFlavor('github');
const options = {
prefixHeaderId: false,
ghCompatibleHeaderId: true,
tables: true,
tasklists: true,
extensions: []
};
// Sometimes emojis can mess with time representations
// such as "00:00:00"
if (convertEmojis) {
options.extensions.push(showdownEmoji);
}
if (enableHighlight) {
options.extensions.push(showdownHighlight)
}
const converter = new showdown.Converter(options);
return converter.makeHtml(markdown);
}
async function convert(options) {
options = options || {};
if (!options.source) {
throw new Error('Source path must be provided');
}
if (!options.destination) {
throw new Error('Destination path must be provided');
}
options.assetDir = path.dirname(path.resolve(options.source));
const styles = getAllStyles(options);
let css = new Handlebars.SafeString(styles.styleBlock);
const local = {
css: css,
};
let source, template;
// Asynchronously convert
const promises = [
template = readFile(layoutPath, 'utf8').then(Handlebars.compile),
source = readFile(options.source, 'utf8'),
prepareHeader(options, styles.styles).then(v => options.header = v),
prepareFooter(options).then(v => options.footer = v),
];
let content = parseMarkdownToHtml(await source, !options.noEmoji, !options.noHighlight);
// This step awaits so options is valid
await Promise.all(promises);
template = await template;
content = utils.qualifyImgSources(content, options);
local.body = new Handlebars.SafeString(content);
// Use loophole for this body template to avoid issues with editor extensions
const html = loophole.allowUnsafeNewFunction(() => template(local));
return createPdf(html, options);
}
function prepareHeader(options, css) {
if (options.header) {
let headerTemplate;
// Get the hbs layout
return readFile(headerLayoutPath, 'utf8')
.then(headerLayout => {
headerTemplate = Handlebars.compile(headerLayout);
// Get the header html
return readFile(options.header, 'utf8');
})
.then(headerContent => {
const preparedHeader = headerContent;
// Compile the header template
const headerHtml = headerTemplate({
content: new Handlebars.SafeString(preparedHeader),
css: new Handlebars.SafeString(css.replace(/"/gm, "'")),
});
return headerHtml;
});
} else {
return Promise.resolve();
}
}
function prepareFooter(options) {
if (options.footer) {
return readFile(options.footer, 'utf8').then(footerContent => {
const preparedFooter = utils.qualifyImgSources(footerContent, options);
return preparedFooter;
});
} else {
return Promise.resolve();
}
}
function createPdf(html, options) {
// Write html to a temp file
let browser;
let page;
const tempHtmlPath = path.resolve(
path.dirname(options.destination),
'_temp.html'
);
return writeFile(tempHtmlPath, html)
.then(async () => {
const browser = await puppeteer.launch({ headless: true , args: ['--no-sandbox', '--disable-setuid-sandbox'] })
const page = (await browser.pages())[0];
await page.goto('file:' + tempHtmlPath, { waitUntil: options.waitUntil ?? 'networkidle0' });
const puppetOptions = puppeteerHelper.getOptions(options);
await page.pdf(puppetOptions);
return browser.close();
})
.then(() => {
if (options.debug) {
fs.copyFileSync(tempHtmlPath, options.debug);
}
fs.unlinkSync(tempHtmlPath);
return options.destination;
});
}
module.exports = {
convert,
};