diff --git a/readme.md b/readme.md index 3a36159..de0ecab 100644 --- a/readme.md +++ b/readme.md @@ -81,34 +81,39 @@ md-to-pdf ./**/*.md _(You might need to enable the `globstar` option in bash for recursive globbing.)_ -Alternatively, you can pass the markdown in from `stdin`: +Alternatively, you can pass the markdown in from `stdin` and pipe its `stdout` into a target file: ```sh -cat file.md | md-to-pdf +cat file.md | md-to-pdf > path/to/output.pdf ``` -_(It's not currently possible to pipe the output into a file, it will just be written to `output.pdf` in the current working directory. However this will be implemented before the release of v3.)_ +_You can concatenate multiple files using `cat file1.md file2.md`._ The current working directory (`process.cwd()`) serves as the base directory of the file server by default. This can be adjusted with the `--basedir` flag (or equivalent config option). - #### Programmatic API -The programmatic API is very simple: it only exposes one function that accepts either the path to or content of a markdown file, and an optional config object (which can be used to specify the output file destination). +The programmatic API is very simple: it only exposes one function that accepts either a `path` to or `content` of a markdown file, and an optional config object (which can be used to specify the output file destination). ```js +const fs = require('fs'); const mdToPdf = require('md-to-pdf'); (async () => { - const pdf = await mdToPdf({ path: 'readme.md' }, { dest: 'readme.pdf' }).catch(console.error); + const pdf = await mdToPdf({ path: 'readme.md' }).catch(console.error); - if (pdf) { - console.log(pdf.filename); - } + if (pdf) { + fs.writeFileSync(pdf.filename, pdf.content); + } })(); ``` -The function throws an error if anything goes wrong, which can be handled by catching the rejected promise. +The function throws an error if anything goes wrong, which can be handled by catching the rejected promise. If you set the `dest` option in the config, the file will be written to the specified location straight away: + +```js +await mdToPdf({ content: '# Hello, World' }, { dest: 'path/to/output.pdf' }); +``` + #### Page Break diff --git a/src/index.ts b/src/index.ts index dc5eebf..f102c19 100755 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,6 @@ import { serveDirectory } from './lib/serve-dir'; /** * Convert a markdown file to PDF. - * - * @returns the path that the PDF was written to */ export const mdToPdf = async (input: { path: string } | { content: string }, config: Partial = {}) => { if (!('path' in input ? input.path : input.content)) { @@ -24,6 +22,10 @@ export const mdToPdf = async (input: { path: string } | { content: string }, con config.basedir = 'path' in input ? getDir(input.path) : process.cwd(); } + if (!config.dest) { + config.dest = ''; + } + const mergedConfig: Config = { ...defaultConfig, ...config, diff --git a/src/lib/write-output.ts b/src/lib/generate-output.ts similarity index 78% rename from src/lib/write-output.ts rename to src/lib/generate-output.ts index c057702..7ca84a9 100644 --- a/src/lib/write-output.ts +++ b/src/lib/generate-output.ts @@ -1,25 +1,15 @@ -import { writeFile as fsWriteFile } from 'fs'; -import { promisify } from 'util'; import puppeteer from 'puppeteer'; -import { isHttpUrl } from './is-http-url'; import { Config } from './config'; - -const writeFile = promisify(fsWriteFile); +import { isHttpUrl } from './is-http-url'; /** - * Write the output (either PDF or HTML) to disk. - * - * The reason that relative paths are resolved properly is that the base dir is served locally + * Generate the output (either PDF or HTML). */ -export const writeOutput = async ( +export const generateOutput = async ( html: string, relativePath: string, config: Config, ): Promise<{} | { filename: string; content: string | Buffer }> => { - if (!config.dest) { - throw new Error('No output file destination has been specified.'); - } - const browser = await puppeteer.launch({ devtools: config.devtools, ...config.launch_options }); const page = await browser.newPage(); @@ -56,8 +46,6 @@ export const writeOutput = async ( await page.emulateMediaType('screen'); outputFileContent = await page.pdf(config.pdf_options); } - - await writeFile(config.dest, outputFileContent); } await browser.close(); diff --git a/src/lib/md-to-pdf.ts b/src/lib/md-to-pdf.ts index c3ec1b8..951d751 100644 --- a/src/lib/md-to-pdf.ts +++ b/src/lib/md-to-pdf.ts @@ -1,12 +1,12 @@ +import { promises as fs } from 'fs'; import { dirname, resolve } from 'path'; -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 { generateOutput } from './generate-output'; import { getHtml } from './get-html'; -import { writeOutput } from './write-output'; +import { getOutputFilePath } from './get-output-file-path'; +import { getMarginObject } from './helpers'; +import { readFile } from './read-file'; +const grayMatter = require('gray-matter'); /** * Convert markdown to pdf. @@ -54,11 +54,8 @@ export const convertMdToPdf = async (input: { path: string } | { content: string } // set output destination - if (!config.dest) { - config.dest = - 'path' in input - ? getOutputFilePath(input.path, config.as_html ? 'html' : 'pdf') - : resolve(process.cwd(), `output.${config.as_html ? 'html' : 'pdf'}`); + if (config.dest === undefined) { + config.dest = 'path' in input ? getOutputFilePath(input.path, config.as_html ? 'html' : 'pdf') : 'stdout'; } const highlightStylesheet = resolve( @@ -74,11 +71,23 @@ export const convertMdToPdf = async (input: { path: string } | { content: string const relativePath = 'path' in input ? resolve(input.path).replace(config.basedir, '') : '/'; - const output = await writeOutput(html, relativePath, config); + const output = await generateOutput(html, relativePath, config); if (!('filename' in output)) { + if (config.devtools) { + throw new Error('No file is generated when the --devtools option is enabled.'); + } + throw new Error(`Failed to create ${config.as_html ? 'HTML' : 'PDF'}.`); } - return output as { filename: string }; + if (output.filename) { + if (output.filename === 'stdout') { + process.stdout.write(output.content); + } else { + await fs.writeFile(output.filename, output.content); + } + } + + return output; }; diff --git a/src/test/api.spec.ts b/src/test/api.spec.ts index 953d905..a7046c8 100644 --- a/src/test/api.spec.ts +++ b/src/test/api.spec.ts @@ -1,22 +1,47 @@ -import { resolve, basename } from 'path'; -import test from 'ava'; - +import test, { before } from 'ava'; +import { readFileSync, unlinkSync } from 'fs'; +import { basename, resolve } from 'path'; import { mdToPdf } from '..'; +before(() => { + const filesToDelete = [resolve(__dirname, 'basic', 'api-test.pdf'), resolve(__dirname, 'basic', 'api-test.html')]; + + for (const file of filesToDelete) { + try { + unlinkSync(file); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } +}); + test('should compile the basic example to pdf', async t => { + const pdf = await mdToPdf({ path: resolve(__dirname, 'basic', 'test.md') }); + + t.is(pdf.filename, ''); + t.truthy(pdf.content); +}); + +test('should compile the basic example to pdf and write to disk', async t => { const pdf = await mdToPdf( { path: resolve(__dirname, 'basic', 'test.md') }, { dest: resolve(__dirname, 'basic', 'api-test.pdf') }, ); t.is(basename(pdf.filename), 'api-test.pdf'); + + t.notThrows(() => readFileSync(resolve(__dirname, 'basic', 'api-test.pdf'), 'utf-8')); }); -test('should compile the basic example to html', async t => { +test('should compile the basic example to html and write to disk', async t => { const pdf = await mdToPdf( { path: resolve(__dirname, 'basic', 'test.md') }, { dest: resolve(__dirname, 'basic', 'api-test.html'), as_html: true }, ); t.is(basename(pdf.filename), 'api-test.html'); + + t.notThrows(() => readFileSync(resolve(__dirname, 'basic', 'api-test.html'), 'utf-8')); }); diff --git a/src/test/cli.spec.ts b/src/test/cli.spec.ts index ec29ec2..d9a8645 100644 --- a/src/test/cli.spec.ts +++ b/src/test/cli.spec.ts @@ -6,6 +6,7 @@ import { join, resolve } from 'path'; before(() => { const filesToDelete = [ resolve(__dirname, 'basic', 'test.pdf'), + resolve(__dirname, 'basic', 'test-stdio.pdf'), resolve(__dirname, 'nested', 'root.pdf'), resolve(__dirname, 'nested', 'level-one', 'one.pdf'), resolve(__dirname, 'nested', 'level-one', 'level-two', 'two.pdf'), @@ -22,7 +23,7 @@ before(() => { } }); -test('should compile the basic example to pdf using --basedir', async t => { +test('should compile the basic example to pdf using --basedir', t => { const cmd = [ resolve(__dirname, '..', '..', 'node_modules', '.bin', 'ts-node'), // ts-node binary resolve(__dirname, '..', 'cli'), // md-to-pdf cli script (typescript) @@ -36,6 +37,26 @@ test('should compile the basic example to pdf using --basedir', async t => { t.notThrows(() => readFileSync(resolve(__dirname, 'basic', 'test.pdf'), 'utf-8')); }); +test('should compile the basic example using stdio', t => { + const cmd = [ + 'cat', + resolve(__dirname, 'basic', 'test.md'), // file to convert + '|', + resolve(__dirname, '..', '..', 'node_modules', '.bin', 'ts-node'), // ts-node binary + resolve(__dirname, '..', 'cli'), // md-to-pdf cli script (typescript) + '--basedir', + resolve(__dirname, 'basic'), + '>', + resolve(__dirname, 'basic', 'test-stdio.pdf'), + ].join(' '); + + console.log(cmd); + + t.notThrows(() => execSync(cmd)); + + t.notThrows(() => readFileSync(resolve(__dirname, 'basic', 'test-stdio.pdf'), 'utf-8')); +}); + test('should compile the nested example to pdfs', t => { const cmd = [ resolve(__dirname, '..', '..', 'node_modules', '.bin', 'ts-node'), // ts-node binary