Skip to content

Commit

Permalink
feat: allow input from std in or shell globbing for multiple files
Browse files Browse the repository at this point in the history
  • Loading branch information
simonhaenisch committed Jan 26, 2020
1 parent 0e3faf3 commit 8051c42
Show file tree
Hide file tree
Showing 22 changed files with 685 additions and 1,193 deletions.
12 changes: 0 additions & 12 deletions lib/get-md-files-in-dir.ts

This file was deleted.

1,468 changes: 442 additions & 1,026 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions package.json
Expand Up @@ -17,13 +17,13 @@
"engines": {
"node": ">= 8.3.0"
},
"main": "index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"start": "node . --help",
"build": "tsc",
"prepare": "npm run build",
"pretest": "npm run build",
"test": "xo && nyc ava test/lib.spec.js && ava test/api.spec.js && ava test/cli.spec.js",
"test": "xo && nyc ava test/lib.spec.ts && ava test/api.spec.ts && ava test/cli.spec.ts",
"release": "npx standard-version --infile=changelog.md && git push --follow-tags origin master && npm publish"
},
"bin": {
Expand All @@ -41,7 +41,7 @@
"get-port": "5.1.1",
"get-stdin": "7.0.0",
"gray-matter": "4.0.2",
"highlight.js": "9.17.1",
"highlight.js": "9.18.0",
"iconv-lite": "0.5.1",
"listr": "0.14.3",
"marked": "0.8.0",
Expand All @@ -54,11 +54,12 @@
"@types/marked": "0.7.2",
"@types/puppeteer": "2.0.0",
"@types/serve-handler": "6.1.0",
"ava": "2.4.0",
"husky": "4.0.10",
"ava": "3.0.0",
"husky": "4.2.1",
"nyc": "15.0.0",
"prettier": "1.19.1",
"tap-xunit": "2.4.1",
"ts-node": "8.6.2",
"typescript": "3.7.5",
"xo": "0.25.3"
},
Expand All @@ -67,6 +68,14 @@
"post-merge": "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep --quiet package-lock.json && npm install"
}
},
"ava": {
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
},
"prettier": {
"printWidth": 120,
"singleQuote": true,
Expand Down
85 changes: 55 additions & 30 deletions cli.ts → src/cli.ts
Expand Up @@ -7,14 +7,13 @@ import path from 'path';
import arg from 'arg';
import chalk from 'chalk';
import Listr from 'listr';
import getStdin from 'get-stdin';
import getPort from 'get-port';
import { watch } from 'chokidar';

import { help } from './lib/help';
import { getMdFilesInDir } from './lib/get-md-files-in-dir';
import { serveDirectory } from './lib/serve-dir';
import { serveDirectory, closeServer } from './lib/serve-dir';
import { defaultConfig, Config } from './lib/config';
import { getDir } from './lib/helpers';
import { convertMdToPdf } from './lib/md-to-pdf';
import { setProcessAndTermTitle } from './lib/helpers';

Expand All @@ -24,6 +23,7 @@ import { setProcessAndTermTitle } from './lib/helpers';
const cliFlags = arg({
'--help': Boolean,
'--version': Boolean,
'--basedir': String,
'--watch': Boolean,
'--stylesheet': [String],
'--css': String,
Expand All @@ -48,7 +48,15 @@ const cliFlags = arg({
});

// --
// Main
// Run

main(cliFlags, defaultConfig).catch(error => {
console.error(error);
process.exit(1);
});

// --
// Define Main Function

async function main(args: typeof cliFlags, config: Config) {
setProcessAndTermTitle('md-to-pdf');
Expand All @@ -61,19 +69,22 @@ async function main(args: typeof cliFlags, config: Config) {
return help();
}

const [input, dest] = args._;
/**
* 1. Get input.
*/

const files = args._;

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

if (mdFiles.length === 0) {
if (files.length === 0 && !stdin) {
return help();
}

if (dest) {
config.dest = dest;
}
/**
* 2. Read config file and merge it into the config object.
*/

// merge config from config file
if (args['--config-file']) {
try {
const configFile: Partial<Config> = require(path.resolve(args['--config-file']));
Expand All @@ -92,38 +103,52 @@ async function main(args: typeof cliFlags, config: Config) {
}
}

// serve directory of first file because all files will be in the same dir
/**
* 3. Start the file server.
*/

if (args['--basedir']) {
config.basedir = args['--basedir'];
}

config.port = args['--port'] || (await getPort());
const server = await serveDirectory(getDir(mdFiles[0]), config.port);

const getListrTask = (mdFile: string) => ({
title: `generating ${args['--as-html'] ? 'HTML' : 'PDF'} from ${chalk.underline(mdFile)}`,
task: () => convertMdToPdf(mdFile, config, args),
const server = await serveDirectory(config);

/**
* 4. Either process stdin or create a Listr task for each file.
*/

if (stdin) {
await convertMdToPdf({ content: stdin }, config, args).catch(async (error: Error) => {
await closeServer(server);

console.error(error);
process.exit(1);
});

await closeServer(server);

return;
}

const getListrTask = (file: string) => ({
title: `generating ${args['--as-html'] ? 'HTML' : 'PDF'} from ${chalk.underline(file)}`,
task: () => convertMdToPdf({ path: file }, config, args),
});

// create list of tasks and run concurrently
await new Listr(mdFiles.map(getListrTask), { concurrent: true, exitOnError: false })
await new Listr(files.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: Error) => args['--debug'] && console.error(error));
watch(files).on('change', async file => {
await new Listr([getListrTask(file)]).run().catch((error: Error) => args['--debug'] && console.error(error));
});
} else {
server.close();
}
})
.catch((error: Error) => (args['--debug'] && console.error(error)) || process.exit(1));
}

// --
// Run

main(cliFlags, defaultConfig).catch(error => {
console.error(error);
process.exit(1);
});
21 changes: 14 additions & 7 deletions index.ts → src/index.ts
Expand Up @@ -4,29 +4,36 @@ import getPort from 'get-port';

import { defaultConfig, Config } from './lib/config';
import { serveDirectory } from './lib/serve-dir';
import { getDir } from './lib/helpers';
import { convertMdToPdf } from './lib/md-to-pdf';
import { getDir } from './lib/helpers';

/**
* Convert a markdown file to PDF.
*
* @returns the path that the PDF was written to
*/
export const mdToPdf = async (mdFile: string, config: Partial<Config> = {}) => {
if (typeof mdFile !== 'string') {
throw new TypeError(`mdFile has to be a string, received ${typeof mdFile}`);
export const mdToPdf = async (input: { path: string } | { content: string }, config: Partial<Config> = {}) => {
if (!('path' in input ? input.path : input.content)) {
throw new Error('Specify either content or path.');
}

if (!config.port) {
config.port = await getPort();
}

config.port = config.port || (await getPort());
const server = await serveDirectory(getDir(mdFile), config.port);
if (!config.basedir) {
config.basedir = 'path' in input ? getDir(input.path) : process.cwd();
}

const mergedConfig: Config = {
...defaultConfig,
...config,
pdf_options: { ...defaultConfig.pdf_options, ...config.pdf_options },
};

const pdf = await convertMdToPdf(mdFile, mergedConfig);
const server = await serveDirectory(mergedConfig);

const pdf = await convertMdToPdf(input, mergedConfig);

server.close();

Expand Down
6 changes: 6 additions & 0 deletions lib/config.ts → src/lib/config.ts
Expand Up @@ -3,6 +3,7 @@ import { MarkedOptions } from 'marked';
import { PDFOptions, LaunchOptions } from 'puppeteer';

export const defaultConfig: Config = {
basedir: process.cwd(),
stylesheet: [resolve(__dirname, '..', '..', 'markdown.css')],
css: '',
body_class: [],
Expand All @@ -29,6 +30,11 @@ export const defaultConfig: Config = {
* In config keys, dashes of cli flag names are replaced with underscores.
*/
export interface Config {
/**
* Base directory to be served by the file server.
*/
basedir: string;

/**
* Optional destination path for the output file (including the extension).
*/
Expand Down
File renamed without changes.
Expand Up @@ -8,9 +8,9 @@ export const getMarked = (options: MarkedOptions) => {
if (!Object.prototype.hasOwnProperty.call(renderer, 'code')) {
renderer.code = (code, language) => {
// if the given language is not available in highlight.js, fall back to plaintext
language = (getLanguage(language) && language) || 'plaintext';
const languageName = language && getLanguage(language) ? language : 'plaintext';

return `<pre><code class="hljs ${language}">${highlight(language, code).value}</code></pre>`;
return `<pre><code class="hljs ${languageName}">${highlight(languageName, code).value}</code></pre>`;
};
}

Expand Down
Expand Up @@ -4,7 +4,7 @@ import { Config } from './config';
/**
* Derive the output file path from the source markdown file.
*/
export const getOutputFilePath = (mdFilePath: string, config: Config) => {
export const getOutputFilePath = (mdFilePath: string, config: Partial<Config>) => {
const { dir, name } = parse(mdFilePath);
const extension = config.as_html ? 'html' : 'pdf';

Expand Down
1 change: 1 addition & 0 deletions lib/help.ts → src/lib/help.ts
Expand Up @@ -8,6 +8,7 @@ const helpText = `
-h, --help ${chalk.dim('...............')} Output usage information
-v, --version ${chalk.dim('............')} Output version
-w, --watch ${chalk.dim('..............')} Watch the current file(s) for changes
--basedir ${chalk.dim('................')} Base directory to be served by the file server
--stylesheet ${chalk.dim('.............')} Path to a local or remote stylesheet (can be passed multiple times)
--css ${chalk.dim('....................')} String of styles
--body-class ${chalk.dim('.............')} Classes to be added to the body tag (can be passed multiple times)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
24 changes: 18 additions & 6 deletions lib/md-to-pdf.ts → src/lib/md-to-pdf.ts
Expand Up @@ -3,17 +3,21 @@ const grayMatter = require('gray-matter');

import { Config } from './config';
import { readFile } from './read-file';
import { getMarginObject } from './helpers';
import { getOutputFilePath } from './get-output-file-path';
import { getHtml } from './get-html';
import { writeOutput } from './write-output';
import { getMarginObject } from './helpers';

/**
* Convert markdown to pdf.
*/
export const convertMdToPdf = async (mdFile: string, config: Config, args: any = {}) => {
const mdFileContent = await readFile(resolve(mdFile), args['--md-file-encoding'] || config.md_file_encoding);
export const convertMdToPdf = async (input: { path: string } | { content: string }, config: Config, args: any = {}) => {
const mdContent =
'content' in input
? input.content
: await readFile(resolve(input.path), args['--md-file-encoding'] || config.md_file_encoding);

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

// merge front-matter config
config = {
Expand Down Expand Up @@ -43,6 +47,14 @@ export const convertMdToPdf = async (mdFile: string, config: Config, args: any =
config.pdf_options.margin = getMarginObject(config.pdf_options.margin);
}

// set output destination
if (!config.dest) {
config.dest =
'path' in input
? getOutputFilePath(input.path, config)
: resolve(process.cwd(), `output.${config.as_html ? 'html' : 'pdf'}`);
}

const highlightStylesheet = resolve(
dirname(require.resolve('highlight.js')),
'..',
Expand All @@ -54,9 +66,9 @@ export const convertMdToPdf = async (mdFile: string, config: Config, args: any =

const html = getHtml(md, config);

const output = await writeOutput(mdFile, html, config);
const output = await writeOutput(mdContent, html, config);

if (!output.filename) {
if (!('filename' in output)) {
throw new Error(`Failed to create ${config.as_html ? 'HTML' : 'PDF'}.`);
}

Expand Down
File renamed without changes.
12 changes: 9 additions & 3 deletions lib/serve-dir.ts → src/lib/serve-dir.ts
@@ -1,14 +1,20 @@
import { createServer, Server } from 'http';
import serveHandler from 'serve-handler';
import { Config } from './config';

/**
* Serve a directory on a random port using a HTTP server and the Serve handler.
* Serve a directory on a random port using a HTTP server and the serve-handler package.
*
* @returns a promise that resolves with the server instance once the server is ready and listening.
*/
export const serveDirectory = async (path: string, port: number) =>
export const serveDirectory = async ({ basedir, port }: Config) =>
new Promise<Server>(resolve => {
const server = createServer((req, res) => serveHandler(req, res, { public: path }));
const server = createServer((req, res) => serveHandler(req, res, { public: basedir }));

server.listen(port, () => resolve(server));
});

/**
* Close the given server instance asynchronously.
*/
export const closeServer = async (server: Server) => new Promise(resolve => server.close(resolve));

0 comments on commit 8051c42

Please sign in to comment.