diff --git a/index.js b/index.js index bf1ad2a..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,76 +16,171 @@ function wkhtmltopdf(input, options, callback) { callback = options; options = {}; } - + 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); - - 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 + + try { + var args = [wkhtmltopdf.command, '--quiet'].concat(processOptions(options)).concat(processInput(input)); + } + catch (err) { + 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) { 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); child.stderr.once('data', function(err) { handleError(new Error((err || '').toString().trim())); }); - + // 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..09b9e48 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,14 @@ "type": "git", "url": "https://github.com/devongovett/node-wkhtmltopdf.git" }, + "scripts": { + "test": "mocha -R spec" + }, + "devDependencies": { + "mocha": "~2.2.5", + "rewire": "~2.3.4" + }, + "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..8ca0cbf --- /dev/null +++ b/test/index.js @@ -0,0 +1,58 @@ +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", + headerFontSize: 10, + headerSpacing: 5, + marginTop: 15, + output: "./google.pdf" + }; + + 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 produce a well-formed command-line invocation of wkhtmltopdf from 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' + ]; + + 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"); + } + }); + + }); + +});