Skip to content

Commit

Permalink
feat: output to stdout if input comes from stdin
Browse files Browse the repository at this point in the history
  • Loading branch information
simonhaenisch committed Feb 29, 2020
1 parent 82e1834 commit 73f8e0d
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 45 deletions.
25 changes: 15 additions & 10 deletions readme.md
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Expand Up @@ -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<Config> = {}) => {
if (!('path' in input ? input.path : input.content)) {
Expand All @@ -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,
Expand Down
18 changes: 3 additions & 15 deletions src/lib/write-output.ts → 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();
Expand Down Expand Up @@ -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();
Expand Down
35 changes: 22 additions & 13 deletions 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.
Expand Down Expand Up @@ -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(
Expand All @@ -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;
};
33 changes: 29 additions & 4 deletions 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'));
});
23 changes: 22 additions & 1 deletion src/test/cli.spec.ts
Expand Up @@ -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'),
Expand All @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit 73f8e0d

Please sign in to comment.