From 81ba8899f956616fd8e8e78224d6bb1d01b49f3d Mon Sep 17 00:00:00 2001 From: Karim Nassar Date: Tue, 13 Oct 2015 15:46:54 -0400 Subject: [PATCH 1/2] support for multi-source input also, added basic tests --- index.js | 157 +++++++++++++++++++++++++++++++++++++++----------- package.json | 8 +++ readme.md | 27 ++++++++- test/index.js | 42 ++++++++++++++ 4 files changed, 198 insertions(+), 36 deletions(-) create mode 100644 test/index.js diff --git a/index.js b/index.js index bf1ad2a..e2fe013 100644 --- a/index.js +++ b/index.js @@ -20,32 +20,13 @@ function wkhtmltopdf(input, options, callback) { var output = options.output; delete options.output; - // make sure the special keys are last - var extraKeys = []; - var keys = Object.keys(options).filter(function(key) { - if (key === 'toc' || key === 'cover' || key === 'page') { - extraKeys.push(key); - return false; - } - - return true; - }).concat(extraKeys); - - var args = [wkhtmltopdf.command, '--quiet']; - keys.forEach(function(key) { - var val = options[key]; - if (key !== 'toc' && key !== 'cover' && key !== 'page') - key = key.length === 1 ? '-' + key : '--' + slang.dasherize(key); + try { + var args = [wkhtmltopdf.command, '--quiet'].concat(processOptions(options)).concat(processInput(input)); + } + catch (err) { + emitError(err, callback, process.stderr); + } - if (val !== false) - args.push(key); - - if (typeof val !== 'boolean') - args.push(quote(val)); - }); - - var isUrl = /^(https?|file):\/\//.test(input); - args.push(isUrl ? quote(input) : '-'); // stdin if HTML given directly args.push(output ? quote(output) : '-'); // stdout if no output file if (process.platform === 'win32') { @@ -64,14 +45,7 @@ function wkhtmltopdf(input, options, callback) { function handleError(err) { child.removeAllListeners('exit'); child.kill(); - - // call the callback if there is one - if (callback) - callback(err); - - // if not, or there are listeners for errors, emit the error event - if (!callback || stream.listeners('error').length > 0) - stream.emit('error', err); + emitError(err, callback, stream); } child.once('error', handleError); @@ -80,12 +54,127 @@ function wkhtmltopdf(input, options, callback) { }); // write input to stdin if it isn't a url - if (!isUrl) + if (!Array.isArray(input) && !isURL(input)) { child.stdin.end(input); + } // return stdout stream so we can pipe return stream; } +function emitError(err, callback, stream) { + // call the callback if there is one + if (callback) + callback(err); + + // if not, or there are listeners for errors, emit the error event + if (!callback || stream.listeners('error').length > 0) + stream.emit('error', err); +} + wkhtmltopdf.command = 'wkhtmltopdf'; module.exports = wkhtmltopdf; + + +function processOptions(options) { + + // make sure the special keys are last + var extraKeys = []; + var keys = Object.keys(options).filter(function(key) { + if (key === 'toc' || key === 'cover' || key === 'page') { + extraKeys.push(key); + return false; + } + + return true; + }).concat(extraKeys); + + var opts = []; + + keys.forEach(function(key) { + var val = options[key]; + if (key !== 'toc' && key !== 'cover' && key !== 'page') + key = key.length === 1 ? '-' + key : '--' + slang.dasherize(key); + + if (val !== false) + opts.push(key); + + if (typeof val !== 'boolean') + opts.push(quote(val)); + }); + + return opts; +} + +function processInput(inputArgs) { + + var resolvedInput = []; + + if (Array.isArray(inputArgs)) { + resolvedInput = inputArgs.map(resolveInputObject).reduce(function(accum, val) { + return accum.concat(val); + }, []); + } + else if (isURL(inputArgs)) { + resolvedInput.push(quote(inputArgs)); + } + else { + resolvedInput.push('-'); // stdin + } + + return resolvedInput; +} + +function isURL(possibleURL) { + return /^(https?|file):\/\//.test(possibleURL); +} + +function resolveInputObject(input) { + + var type, url, options; + + if (typeof input == 'string') { + if (input == 'toc') { + type = input; + } + else if (isURL(input)) { + url = input; + } + } + else { + type = input.type; + url = input.url; + if (input.options) { + options = processOptions(input.options); + } + } + + if (!options) { + options = []; + } + else if (!Array.isArray(options)) { + throw Error("Invalid 'options' Array for page '" + url + "'"); + } + + if (type == 'toc' && url) { + throw Error("URL is invalid for page type 'toc' in '" + url + "'"); + } + else if (type != 'toc' && !isURL(url)) { + throw Error("Invalid 'url' for page: " + type + "'" + url + "'"); + } + + if (!type) { + type = ''; + } + + if (!url) { + url = ''; + } + else { + url = quote(url); + } + + return [type, url].concat(options); + +} + diff --git a/package.json b/package.json index 579ba22..e11e926 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,13 @@ "type": "git", "url": "https://github.com/devongovett/node-wkhtmltopdf.git" }, + "scripts": { + "test": "mocha -R spec" + }, + "devDependencies": { + "mocha": "~2.2.5" + }, + "bugs": "http://github.com/devongovett/node-wkhtmltopdf/issues" + } diff --git a/readme.md b/readme.md index c5aa66b..c4ffbb6 100644 --- a/readme.md +++ b/readme.md @@ -25,10 +25,11 @@ wkhtmltopdf('http://google.com/', { pageSize: 'letter' }, function (code, signal }); wkhtmltopdf('http://google.com/', function (code, signal) { }); + ``` -`wkhtmltopdf` is just a function, which you call with either a URL or an inline HTML string, and it returns -a stream that you can read from or pipe to wherever you like (e.g. a file, or an HTTP response). +`wkhtmltopdf` is just a function, which you call with a URL or inline HTML string, or an Array of objects (see [Multi-Source-Input](#multi-source-input)), +and it returns a stream that you can read from or pipe to wherever you like (e.g. a file, or an HTTP response). There are [many options](http://wkhtmltopdf.org/docs.html) available to wkhtmltopdf. All of the command line options are supported as documented on the page linked to above. The @@ -37,6 +38,22 @@ options are camelCased instead-of-dashed as in the command line tool. There is also an `output` option that can be used to write the output directly to a filename, instead of returning a stream. +### Multi-Source-Input + +`wkhtmltopdf` supports the ability to construct a PDF from several source documents, and can even generate a table-of-contents based on an outline inferred from the source HTML structure. To combine several documents into a single PDF, pass an array as the first argument. Each element of the array represents a single source for the resulting PDF, and must be either: + + * A string containing the source URL or the string `toc` to generate a Table of Contents + * An object conforming to the following structure: + +``` + { + type: STRING, // Optional: 'cover' or 'toc' + url: STRING, // URL to source. Omit for type: 'toc' + options: {...}, // Page-specific options. Same format as global options + } +``` + + ## Installation First, you need to install the wkhtmltopdf command line tool on your system. The easiest way to do this is to @@ -50,6 +67,12 @@ Finally, to install the node module, use `npm`: Be sure `wkhtmltopdf` is in your PATH when you're done installing. If you don't want to do this for some reason, you can change the `wkhtmltopdf.command` property to the path to the `wkhtmltopdf` command line tool. +## Testing + +``` +npm test +``` + ## License MIT diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..2f4df01 --- /dev/null +++ b/test/index.js @@ -0,0 +1,42 @@ +var wkhtmltopdf = require('../index.js'); + +describe('wkhtmltopdf', function() { + + describe('function', function() { + var options = { + headerCenter: "TEST PDF", + headerFontSize: 10, + headerSpacing: 5, + marginTop: 15, + output: "./google.pdf" + }; + + it('should generate a PDF from a single source and global options', function() { + wkhtmltopdf('http://google.com', options); + }); + + it('should support multiple pages with individual page options', function() { + options.output = 'multi-page.pdf'; + + var pages = [ + { + type: 'cover', + url: 'http://google.com' + }, + 'toc', + { + url: 'https://github.com', + options: { + enableTocBackLinks: true, + pageOffset: -2 + } + }, + 'http://wkhtmltopdf.org/usage/wkhtmltopdf.txt' + ]; + + wkhtmltopdf(pages, options); + }); + + }); + +}); From b0ee9e8f2ecb54ce42fb60c7ccc91fa3039faa85 Mon Sep 17 00:00:00 2001 From: Karim Nassar Date: Mon, 26 Oct 2015 11:31:30 -0400 Subject: [PATCH 2/2] refactored tests to confirm args without generating PDFs --- index.js | 52 ++++++++++++++++++++++++++++----------------------- package.json | 7 ++++--- test/index.js | 28 +++++++++++++++++++++------ 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/index.js b/index.js index e2fe013..bbcf336 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ function quote(val) { // escape and quote the value if it is a string and this isn't windows if (typeof val === 'string' && process.platform !== 'win32') val = '"' + val.replace(/(["\\$`])/g, '\\$1') + '"'; - + return val; } @@ -16,30 +16,36 @@ function wkhtmltopdf(input, options, callback) { callback = options; options = {}; } - + var output = options.output; delete options.output; - + try { var args = [wkhtmltopdf.command, '--quiet'].concat(processOptions(options)).concat(processInput(input)); } catch (err) { - emitError(err, callback, process.stderr); + emitError(err, callback, process.stderr); } - + args.push(output ? quote(output) : '-'); // stdout if no output file + // console.log(args); + return genPDF(input, args, callback); +} + +function genPDF(input, args, callback) { + if (process.platform === 'win32') { var child = spawn(args[0], args.slice(1)); } else { // this nasty business prevents piping problems on linux var child = spawn('/bin/sh', ['-c', args.join(' ') + ' | cat']); } - + // call the callback with null error when the process exits successfully if (callback) child.on('exit', function() { callback(null); }); - + // setup error handling var stream = child.stdout; function handleError(err) { @@ -47,17 +53,17 @@ function wkhtmltopdf(input, options, callback) { child.kill(); emitError(err, callback, stream); } - + child.once('error', handleError); child.stderr.once('data', function(err) { handleError(new Error((err || '').toString().trim())); }); - + // write input to stdin if it isn't a url if (!Array.isArray(input) && !isURL(input)) { child.stdin.end(input); } - + // return stdout stream so we can pipe return stream; } @@ -66,7 +72,7 @@ function emitError(err, callback, stream) { // call the callback if there is one if (callback) callback(err); - + // if not, or there are listeners for errors, emit the error event if (!callback || stream.listeners('error').length > 0) stream.emit('error', err); @@ -77,7 +83,7 @@ module.exports = wkhtmltopdf; function processOptions(options) { - + // make sure the special keys are last var extraKeys = []; var keys = Object.keys(options).filter(function(key) { @@ -85,20 +91,20 @@ function processOptions(options) { extraKeys.push(key); return false; } - + return true; }).concat(extraKeys); var opts = []; - + keys.forEach(function(key) { var val = options[key]; if (key !== 'toc' && key !== 'cover' && key !== 'page') key = key.length === 1 ? '-' + key : '--' + slang.dasherize(key); - + if (val !== false) opts.push(key); - + if (typeof val !== 'boolean') opts.push(quote(val)); }); @@ -121,7 +127,7 @@ function processInput(inputArgs) { else { resolvedInput.push('-'); // stdin } - + return resolvedInput; } @@ -130,9 +136,9 @@ function isURL(possibleURL) { } function resolveInputObject(input) { - + var type, url, options; - + if (typeof input == 'string') { if (input == 'toc') { type = input; @@ -155,14 +161,14 @@ function resolveInputObject(input) { else if (!Array.isArray(options)) { throw Error("Invalid 'options' Array for page '" + url + "'"); } - + if (type == 'toc' && url) { throw Error("URL is invalid for page type 'toc' in '" + url + "'"); } else if (type != 'toc' && !isURL(url)) { throw Error("Invalid 'url' for page: " + type + "'" + url + "'"); } - + if (!type) { type = ''; } @@ -173,8 +179,8 @@ function resolveInputObject(input) { else { url = quote(url); } - + return [type, url].concat(options); - + } diff --git a/package.json b/package.json index e11e926..09b9e48 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ "test": "mocha -R spec" }, "devDependencies": { - "mocha": "~2.2.5" + "mocha": "~2.2.5", + "rewire": "~2.3.4" }, - + "bugs": "http://github.com/devongovett/node-wkhtmltopdf/issues" - + } diff --git a/test/index.js b/test/index.js index 2f4df01..8ca0cbf 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,12 @@ -var wkhtmltopdf = require('../index.js'); +var rewire = require('rewire'); +var wkhtmltopdf = rewire('../index.js'); describe('wkhtmltopdf', function() { + wkhtmltopdf.__set__("genPDF", function(input, args, callback) { + return args; + }); + describe('function', function() { var options = { headerCenter: "TEST PDF", @@ -11,11 +16,17 @@ describe('wkhtmltopdf', function() { output: "./google.pdf" }; - it('should generate a PDF from a single source and global options', function() { - wkhtmltopdf('http://google.com', options); + it('should produce a well-formed command-line invocation of wkhtmltopdf from a single source and global options', function() { + var args = wkhtmltopdf('http://google.com', options); + + var expected = 'wkhtmltopdf --quiet --header-center "TEST PDF" --header-font-size 10 --header-spacing 5 --margin-top 15 "http://google.com" "./google.pdf"'; + + if (args.join(' ') != expected) { + throw new Error("generated args don't match expected"); + } }); - it('should support multiple pages with individual page options', function() { + it('should produce a well-formed command-line invocation of wkhtmltopdf from multiple pages with individual page options', function() { options.output = 'multi-page.pdf'; var pages = [ @@ -23,7 +34,7 @@ describe('wkhtmltopdf', function() { type: 'cover', url: 'http://google.com' }, - 'toc', + 'toc', { url: 'https://github.com', options: { @@ -34,7 +45,12 @@ describe('wkhtmltopdf', function() { 'http://wkhtmltopdf.org/usage/wkhtmltopdf.txt' ]; - wkhtmltopdf(pages, options); + var args = wkhtmltopdf(pages, options); + var expected = 'wkhtmltopdf --quiet --header-center "TEST PDF" --header-font-size 10 --header-spacing 5 --margin-top 15 cover "http://google.com" toc "https://github.com" --enable-toc-back-links --page-offset -2 "http://wkhtmltopdf.org/usage/wkhtmltopdf.txt" "multi-page.pdf"'; + + if (args.join(' ') != expected) { + throw new Error("generated args don't match expected"); + } }); });