Skip to content

Commit

Permalink
feat: expose programmatic api
Browse files Browse the repository at this point in the history
This refactors the main convertToPdf function and exposes it from `util/md-to-pdf.js`, so it can be shared between the new programmatic API and the CLI. The CLI has been moved to `cli.js` and `index.js` exposes a wrapper around `mdToPdf` instead.

Closes #25
  • Loading branch information
simonhaenisch committed Jan 20, 2019
1 parent e5cdfa9 commit 8c86807
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 178 deletions.
130 changes: 130 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env node

// --
// Packages

const path = require('path');
const arg = require('arg');
const chalk = require('chalk').default;
const Listr = require('listr');
const getPort = require('get-port');
const { watch } = require('chokidar');

// --
// Utils

const help = require('./util/help');
const getMdFilesInDir = require('./util/get-md-files-in-dir');
const serveDirectory = require('./util/serve-dir');
const config = require('./util/config');
const { getDir } = require('./util/helpers');
const mdToPdf = require('./util/md-to-pdf');

// --
// Configure CLI Arguments

const args = arg({
'--help': Boolean,
'--version': Boolean,
'--watch': Boolean,
'--stylesheet': [String],
'--css': String,
'--body-class': [String],
'--highlight-style': String,
'--marked-options': String,
'--html-pdf-options': String,
'--pdf-options': String,
'--launch-options': String,
'--md-file-encoding': String,
'--stylesheet-encoding': String,
'--config-file': String,
'--devtools': Boolean,
'--debug': Boolean,

// aliases
'-h': '--help',
'-v': '--version',
'-w': '--watch',
});

// --
// Main

async function main(args, config) {
const input = args._[0];
const dest = args._[1];

if (args['--version']) {
return console.log(require('./package').version);
}

if (args['--help']) {
return help();
}

/**
* throw warning when using --html-pdf-options flag
* @todo remove in a future version
*/
if (args['--html-pdf-options']) {
console.warn(
[
chalk.red(`--html-pdf-options is not a valid argument anymore. Use --pdf-options instead.`),
chalk.gray(`valid options: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions`),
].join('\n'),
);
}

const mdFiles = input ? [input] : await getMdFilesInDir('.');

if (mdFiles.length === 0) {
return help();
}

if (dest) {
config.dest = dest;
}

// merge config from config file
if (args['--config-file']) {
try {
config = { ...config, ...require(path.resolve(args['--config-file'])) };
} catch (error) {
console.warn(chalk.red(`Warning: couldn't read config file: ${args['--config-file']}`));

if (args['--debug']) {
console.error(error);
}
}
}

// serve directory of first file because all files will be in the same dir
const port = await getPort();
const server = await serveDirectory(getDir(mdFiles[0]), port);

const getListrTask = mdFile => ({
title: `generating PDF from ${chalk.underline(mdFile)}`,
task: () => mdToPdf(mdFile, config, port, args),
});

// create list of tasks and run concurrently
await new Listr(mdFiles.map(getListrTask), { concurrent: true, exitOnError: false })
.run()
.then(() => {
if (args['--watch']) {
console.log(chalk.bgBlue('\n watching for changes \n'));

watch(mdFiles).on('change', async mdFile => {
await new Listr([getListrTask(mdFile)]).run().catch(error => args['--debug'] && console.error(error));
});
} else {
server.close();
}
})
.catch(error => (args['--debug'] && console.error(error)) || process.exit(1));
}

// --
// Run

main(args, config).catch(error => console.error(error) || process.exit(1));
187 changes: 19 additions & 168 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,176 +1,27 @@
#!/usr/bin/env node

// --
// Packages

const path = require('path');
const arg = require('arg');
const chalk = require('chalk').default;
const Listr = require('listr');
const grayMatter = require('gray-matter');
const getPort = require('get-port');
const { watch } = require('chokidar');

// --
// Utils

const help = require('./util/help');
const getMdFilesInDir = require('./util/get-md-files-in-dir');
const readFile = require('./util/read-file');
const defaultConfig = require('./util/config');
const serveDirectory = require('./util/serve-dir');
const getHtml = require('./util/get-html');
const writePdf = require('./util/write-pdf');
const config = require('./util/config');
const { getMarginObject, getDir } = require('./util/helpers');

// --
// Configure CLI Arguments

const args = arg({
'--help': Boolean,
'--version': Boolean,
'--watch': Boolean,
'--stylesheet': [String],
'--css': String,
'--body-class': [String],
'--highlight-style': String,
'--marked-options': String,
'--html-pdf-options': String,
'--pdf-options': String,
'--launch-options': String,
'--md-file-encoding': String,
'--stylesheet-encoding': String,
'--config-file': String,
'--devtools': Boolean,
'--debug': Boolean,

// aliases
'-h': '--help',
'-v': '--version',
'-w': '--watch',
});

// --
// Main

async function main(args, config) {
const input = args._[0];
const output = args._[1];

if (args['--version']) {
return console.log(require('./package').version);
}

if (args['--help']) {
return help();
}

/**
* throw warning when using --html-pdf-options flag
* @todo remove in a future version
*/
if (args['--html-pdf-options']) {
console.warn(
[
chalk.red(`--html-pdf-options is not a valid argument anymore. Use --pdf-options instead.`),
chalk.gray(`valid options: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagepdfoptions`),
].join('\n'),
);
}

const mdFiles = input ? [input] : await getMdFilesInDir('.');

if (mdFiles.length === 0) {
return help();
}

// merge config from config file
if (args['--config-file']) {
try {
config = { ...config, ...require(path.resolve(args['--config-file'])) };
} catch (error) {
console.warn(chalk.red(`Warning: couldn't read config file: ${args['--config-file']}`));

if (args['--debug']) {
console.error(error);
}
}
}

// serve directory of first file because all files will be in the same dir
const { getDir } = require('./util/helpers');
const mdToPdf = require('./util/md-to-pdf');

/**
* Convert a markdown file to PDF.
*
* @param {string} mdFile path to markdown file
* @param {*} config config object
*
* @returns the path that the PDF was written to
*/
module.exports = async (mdFile, config) => {
const port = await getPort();
const server = await serveDirectory(getDir(mdFiles[0]), port);

const getListrTask = mdFile => ({
title: `generating PDF from ${chalk.underline(mdFile)}`,
task: () => convertToPdf(mdFile),
});

// create list of tasks and run concurrently
await new Listr(mdFiles.map(getListrTask), { concurrent: true, exitOnError: false })
.run()
.then(() => {
if (args['--watch']) {
console.log(chalk.bgBlue('\n watching for changes \n'));

watch(mdFiles).on('change', async mdFile => {
await new Listr([getListrTask(mdFile)]).run().catch(error => args['--debug'] && console.error(error));
});
} else {
server.close();
}
})
.catch(error => (args['--debug'] && console.error(error)) || process.exit(1));

// this is the actual function to convert a file
async function convertToPdf(mdFile) {
const mdFileContent = await readFile(path.resolve(mdFile), args['--md-file-encoding'] || config.md_file_encoding);

const { content: md, data: frontMatterConfig } = grayMatter(mdFileContent);

// merge front-matter config
config = { ...config, ...frontMatterConfig };

// sanitize array cli arguments
for (const option of ['stylesheet', 'body_class']) {
if (!Array.isArray(config[option])) {
config[option] = [config[option]].filter(value => Boolean(value));
}
}

// merge cli args into config
const jsonArgs = ['--marked-options', '--pdf-options', '--launch-options'];
for (const arg of Object.entries(args)) {
const [argKey, argValue] = arg;
const key = argKey.substring(2).replace(/-/g, '_');
config[key] = jsonArgs.includes(argKey) ? JSON.parse(argValue) : argValue;
}

// sanitize the margin in pdf_options
if (typeof config.pdf_options.margin === 'string') {
config.pdf_options.margin = getMarginObject(config.pdf_options.margin);
}

const highlightStylesheet = path.resolve(
path.dirname(require.resolve('highlight.js')),
'..',
'styles',
`${config.highlight_style}.css`,
);

config.stylesheet = [...new Set([...config.stylesheet, highlightStylesheet])];

const html = getHtml(md, config);
const server = await serveDirectory(getDir(mdFile), port);

const pdf = await writePdf(mdFile, output, html, { ...config, port });
config = { ...defaultConfig, ...config };

if (!pdf.filename) {
throw new Error(`Failed to create PDF`);
}
}
}
const pdf = await mdToPdf(mdFile, config, port);

// --
// Run
server.close();

main(args, config).catch(error => console.error(error) || process.exit(1));
return pdf;
};
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"release": "np --standard-version --changelog-file=changelog.md"
},
"bin": {
"md-to-pdf": "./index.js",
"md2pdf": "./index.js"
"md-to-pdf": "./cli.js",
"md2pdf": "./cli.js"
},
"author": "Simon Haenisch (https://simonhaenisch.com)",
"license": "Unlicense",
Expand Down
5 changes: 5 additions & 0 deletions util/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ const path = require('path');
* In config keys, dashes of cli flag names are replaced with underscores.
*/
module.exports = {
/**
* Optional destination path for the output file (should use `.pdf` extension).
*/
dest: null,

/**
* List of css files to use for styling.
*/
Expand Down
Loading

0 comments on commit 8c86807

Please sign in to comment.