| /* jshint quotmark: false */ | 
| 'use strict'; | 
|   | 
| var FS = require('fs'), | 
|     PATH = require('path'), | 
|     chalk = require('chalk'), | 
|     mkdirp = require('mkdirp'), | 
|     promisify = require('util.promisify'), | 
|     readdir = promisify(FS.readdir), | 
|     readFile = promisify(FS.readFile), | 
|     writeFile = promisify(FS.writeFile), | 
|     SVGO = require('../svgo.js'), | 
|     YAML = require('js-yaml'), | 
|     PKG = require('../../package.json'), | 
|     encodeSVGDatauri = require('./tools.js').encodeSVGDatauri, | 
|     decodeSVGDatauri = require('./tools.js').decodeSVGDatauri, | 
|     checkIsDir = require('./tools.js').checkIsDir, | 
|     regSVGFile = /\.svg$/, | 
|     noop = () => {}, | 
|     svgo; | 
|   | 
| /** | 
|  * Command-Option-Argument. | 
|  * | 
|  * @see https://github.com/veged/coa | 
|  */ | 
| module.exports = require('coa').Cmd() | 
|     .helpful() | 
|     .name(PKG.name) | 
|     .title(PKG.description) | 
|     .opt() | 
|         .name('version').title('Version') | 
|         .short('v').long('version') | 
|         .only() | 
|         .flag() | 
|         .act(function() { | 
|             // output the version to stdout instead of stderr if returned | 
|             process.stdout.write(PKG.version + '\n'); | 
|             // coa will run `.toString` on the returned value and send it to stderr | 
|             return ''; | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('input').title('Input file, "-" for STDIN') | 
|         .short('i').long('input') | 
|         .arr() | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--input' must have a value."); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('string').title('Input SVG data string') | 
|         .short('s').long('string') | 
|         .end() | 
|     .opt() | 
|         .name('folder').title('Input folder, optimize and rewrite all *.svg files') | 
|         .short('f').long('folder') | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--folder' must have a value."); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('output').title('Output file or folder (by default the same as the input), "-" for STDOUT') | 
|         .short('o').long('output') | 
|         .arr() | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--output' must have a value."); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('precision').title('Set number of digits in the fractional part, overrides plugins params') | 
|         .short('p').long('precision') | 
|         .val(function(val) { | 
|             return !isNaN(val) ? val : this.reject("Option '--precision' must be an integer number"); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('config').title('Config file or JSON string to extend or replace default') | 
|         .long('config') | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--config' must have a value."); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('disable').title('Disable plugin by name, "--disable={PLUGIN1,PLUGIN2}" for multiple plugins (*nix)') | 
|         .long('disable') | 
|         .arr() | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--disable' must have a value."); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('enable').title('Enable plugin by name, "--enable={PLUGIN3,PLUGIN4}" for multiple plugins (*nix)') | 
|         .long('enable') | 
|         .arr() | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--enable' must have a value."); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('datauri').title('Output as Data URI string (base64, URI encoded or unencoded)') | 
|         .long('datauri') | 
|         .val(function(val) { | 
|             return val || this.reject("Option '--datauri' must have one of the following values: 'base64', 'enc' or 'unenc'"); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('multipass').title('Pass over SVGs multiple times to ensure all optimizations are applied') | 
|         .long('multipass') | 
|         .flag() | 
|         .end() | 
|     .opt() | 
|         .name('pretty').title('Make SVG pretty printed') | 
|         .long('pretty') | 
|         .flag() | 
|         .end() | 
|     .opt() | 
|         .name('indent').title('Indent number when pretty printing SVGs') | 
|         .long('indent') | 
|         .val(function(val) { | 
|             return !isNaN(val) ? val : this.reject("Option '--indent' must be an integer number"); | 
|         }) | 
|         .end() | 
|     .opt() | 
|         .name('recursive').title('Use with \'-f\'. Optimizes *.svg files in folders recursively.') | 
|         .short('r').long('recursive') | 
|         .flag() | 
|         .end() | 
|     .opt() | 
|         .name('quiet').title('Only output error messages, not regular status messages') | 
|         .short('q').long('quiet') | 
|         .flag() | 
|         .end() | 
|     .opt() | 
|         .name('show-plugins').title('Show available plugins and exit') | 
|         .long('show-plugins') | 
|         .flag() | 
|         .end() | 
|     .arg() | 
|         .name('input').title('Alias to --input') | 
|         .arr() | 
|         .end() | 
|     .act(function(opts, args) { | 
|         var input = opts.input || args.input, | 
|             output = opts.output, | 
|             config = {}; | 
|   | 
|         // --show-plugins | 
|         if (opts['show-plugins']) { | 
|             showAvailablePlugins(); | 
|             return; | 
|         } | 
|   | 
|         // w/o anything | 
|         if ( | 
|             (!input || input[0] === '-') && | 
|             !opts.string && | 
|             !opts.stdin && | 
|             !opts.folder && | 
|             process.stdin.isTTY === true | 
|         ) return this.usage(); | 
|   | 
|         if (typeof process == 'object' && process.versions && process.versions.node && PKG && PKG.engines.node) { | 
|             var nodeVersion = String(PKG.engines.node).match(/\d*(\.\d+)*/)[0]; | 
|             if (parseFloat(process.versions.node) < parseFloat(nodeVersion)) { | 
|                 return printErrorAndExit(`Error: ${PKG.name} requires Node.js version ${nodeVersion} or higher.`); | 
|             } | 
|         } | 
|   | 
|         // --config | 
|         if (opts.config) { | 
|             // string | 
|             if (opts.config.charAt(0) === '{') { | 
|                 try { | 
|                     config = JSON.parse(opts.config); | 
|                 } catch (e) { | 
|                     return printErrorAndExit(`Error: Couldn't parse config JSON.\n${String(e)}`); | 
|                 } | 
|             // external file | 
|             } else { | 
|                 var configPath = PATH.resolve(opts.config), | 
|                     configData; | 
|                 try { | 
|                     // require() adds some weird output on YML files | 
|                     configData = FS.readFileSync(configPath, 'utf8'); | 
|                     config = JSON.parse(configData); | 
|                 } catch (err) { | 
|                     if (err.code === 'ENOENT') { | 
|                         return printErrorAndExit(`Error: couldn't find config file '${opts.config}'.`); | 
|                     } else if (err.code === 'EISDIR') { | 
|                         return printErrorAndExit(`Error: directory '${opts.config}' is not a config file.`); | 
|                     } | 
|                     config = YAML.safeLoad(configData); | 
|                     config.__DIR = PATH.dirname(configPath); // will use it to resolve custom plugins defined via path | 
|   | 
|                     if (!config || Array.isArray(config)) { | 
|                         return printErrorAndExit(`Error: invalid config file '${opts.config}'.`); | 
|                     } | 
|                 } | 
|             } | 
|         } | 
|   | 
|         // --quiet | 
|         if (opts.quiet) { | 
|             config.quiet = opts.quiet; | 
|         } | 
|   | 
|         // --recursive | 
|         if (opts.recursive) { | 
|             config.recursive = opts.recursive; | 
|         } | 
|   | 
|         // --precision | 
|         if (opts.precision) { | 
|             var precision = Math.min(Math.max(0, parseInt(opts.precision)), 20); | 
|             if (!isNaN(precision)) { | 
|                 config.floatPrecision = precision; | 
|             } | 
|         } | 
|   | 
|         // --disable | 
|         if (opts.disable) { | 
|             changePluginsState(opts.disable, false, config); | 
|         } | 
|   | 
|         // --enable | 
|         if (opts.enable) { | 
|             changePluginsState(opts.enable, true, config); | 
|         } | 
|   | 
|         // --multipass | 
|         if (opts.multipass) { | 
|             config.multipass = true; | 
|         } | 
|   | 
|         // --pretty | 
|         if (opts.pretty) { | 
|             config.js2svg = config.js2svg || {}; | 
|             config.js2svg.pretty = true; | 
|             var indent; | 
|             if (opts.indent && !isNaN(indent = parseInt(opts.indent))) { | 
|                 config.js2svg.indent = indent; | 
|             } | 
|         } | 
|   | 
|         svgo = new SVGO(config); | 
|   | 
|         // --output | 
|         if (output) { | 
|             if (input && input[0] != '-') { | 
|                 if (output.length == 1 && checkIsDir(output[0])) { | 
|                     var dir = output[0]; | 
|                     for (var i = 0; i < input.length; i++) { | 
|                         output[i] = checkIsDir(input[i]) ? input[i] : PATH.resolve(dir, PATH.basename(input[i])); | 
|                     } | 
|                 } else if (output.length < input.length) { | 
|                     output = output.concat(input.slice(output.length)); | 
|                 } | 
|             } | 
|         } else if (input) { | 
|             output = input; | 
|         } else if (opts.string) { | 
|             output = '-'; | 
|         } | 
|   | 
|         if (opts.datauri) { | 
|             config.datauri = opts.datauri; | 
|         } | 
|   | 
|         // --folder | 
|         if (opts.folder) { | 
|             var ouputFolder = output && output[0] || opts.folder; | 
|             return optimizeFolder(config, opts.folder, ouputFolder).then(noop, printErrorAndExit); | 
|         } | 
|   | 
|         // --input | 
|         if (input) { | 
|             // STDIN | 
|             if (input[0] === '-') { | 
|                 return new Promise((resolve, reject) => { | 
|                     var data = '', | 
|                         file = output[0]; | 
|   | 
|                     process.stdin | 
|                         .on('data', chunk => data += chunk) | 
|                         .once('end', () => processSVGData(config, {input: 'string'}, data, file).then(resolve, reject)); | 
|                 }); | 
|             // file | 
|             } else { | 
|                 return Promise.all(input.map((file, n) => optimizeFile(config, file, output[n]))) | 
|                     .then(noop, printErrorAndExit); | 
|             } | 
|   | 
|         // --string | 
|         } else if (opts.string) { | 
|             var data = decodeSVGDatauri(opts.string); | 
|   | 
|             return processSVGData(config, {input: 'string'}, data, output[0]); | 
|         } | 
|     }); | 
|   | 
| /** | 
|  * Change plugins state by names array. | 
|  * | 
|  * @param {Array} names plugins names | 
|  * @param {Boolean} state active state | 
|  * @param {Object} config original config | 
|  * @return {Object} changed config | 
|  */ | 
| function changePluginsState(names, state, config) { | 
|     names.forEach(flattenPluginsCbk); | 
|   | 
|     // extend config | 
|     if (config.plugins) { | 
|         for (var name of names) { | 
|             var matched = false, | 
|                 key; | 
|   | 
|             for (var plugin of config.plugins) { | 
|                 // get plugin name | 
|                 if (typeof plugin === 'object') { | 
|                     key = Object.keys(plugin)[0]; | 
|                 } else { | 
|                     key = plugin; | 
|                 } | 
|   | 
|                 // if there is such a plugin name | 
|                 if (key === name) { | 
|                     // don't replace plugin's params with true | 
|                     if (typeof plugin[key] !== 'object' || !state) { | 
|                         plugin[key] = state; | 
|                     } | 
|                     // mark it as matched | 
|                     matched = true; | 
|                 } | 
|             } | 
|   | 
|             // if not matched and current config is not full | 
|             if (!matched && !config.full) { | 
|                 // push new plugin Object | 
|                 config.plugins.push({ [name]: state }); | 
|                 matched = true; | 
|             } | 
|         } | 
|     // just push | 
|     } else { | 
|         config.plugins = names.map(name => ({ [name]: state })); | 
|     } | 
|     return config; | 
| } | 
|   | 
| /** | 
|  * Flatten an array of plugins by invoking this callback on each element | 
|  * whose value may be a comma separated list of plugins. | 
|  * | 
|  * @param {String} name Plugin name | 
|  * @param {Number} index Plugin index | 
|  * @param {Array} names Plugins being traversed | 
|  */ | 
| function flattenPluginsCbk(name, index, names) | 
| { | 
|     var split = name.split(','); | 
|   | 
|     if(split.length > 1) { | 
|         names[index] = split.shift(); | 
|         names.push.apply(names, split); | 
|     } | 
|   | 
| } | 
|   | 
| /** | 
|  * Optimize SVG files in a directory. | 
|  * @param {Object} config options | 
|  * @param {string} dir input directory | 
|  * @param {string} output output directory | 
|  * @return {Promise} | 
|  */ | 
| function optimizeFolder(config, dir, output) { | 
|     if (!config.quiet) { | 
|         console.log(`Processing directory '${dir}':\n`); | 
|     } | 
|     return readdir(dir).then(files => processDirectory(config, dir, files, output)); | 
| } | 
|   | 
| /** | 
|  * Process given files, take only SVG. | 
|  * @param {Object} config options | 
|  * @param {string} dir input directory | 
|  * @param {Array} files list of file names in the directory | 
|  * @param {string} output output directory | 
|  * @return {Promise} | 
|  */ | 
| function processDirectory(config, dir, files, output) { | 
|     // take only *.svg files, recursively if necessary | 
|     var svgFilesDescriptions = getFilesDescriptions(config, dir, files, output); | 
|   | 
|     return svgFilesDescriptions.length ? | 
|         Promise.all(svgFilesDescriptions.map(fileDescription => optimizeFile(config, fileDescription.inputPath, fileDescription.outputPath))) : | 
|         Promise.reject(new Error(`No SVG files have been found in '${dir}' directory.`)); | 
| } | 
|   | 
| /** | 
|  * Get svg files descriptions | 
|  * @param {Object} config options | 
|  * @param {string} dir input directory | 
|  * @param {Array} files list of file names in the directory | 
|  * @param {string} output output directory | 
|  * @return {Array} | 
|  */ | 
| function getFilesDescriptions(config, dir, files, output) { | 
|     const filesInThisFolder = files | 
|         .filter(name => regSVGFile.test(name)) | 
|         .map(name => ({ | 
|             inputPath: PATH.resolve(dir, name), | 
|             outputPath: PATH.resolve(output, name), | 
|         })); | 
|   | 
|     return config.recursive ? | 
|         [].concat( | 
|             filesInThisFolder, | 
|             files | 
|                 .filter(name => checkIsDir(PATH.resolve(dir, name))) | 
|                 .map(subFolderName => { | 
|                     const subFolderPath = PATH.resolve(dir, subFolderName); | 
|                     const subFolderFiles = FS.readdirSync(subFolderPath); | 
|                     const subFolderOutput = PATH.resolve(output, subFolderName); | 
|                     return getFilesDescriptions(config, subFolderPath, subFolderFiles, subFolderOutput); | 
|                 }) | 
|                 .reduce((a, b) => [].concat(a, b), []) | 
|         ) : | 
|         filesInThisFolder; | 
| } | 
|   | 
| /** | 
|  * Read SVG file and pass to processing. | 
|  * @param {Object} config options | 
|  * @param {string} file | 
|  * @param {string} output | 
|  * @return {Promise} | 
|  */ | 
| function optimizeFile(config, file, output) { | 
|     return readFile(file, 'utf8').then( | 
|         data => processSVGData(config, {input: 'file', path: file}, data, output, file), | 
|         error => checkOptimizeFileError(config, file, output, error) | 
|     ); | 
| } | 
|   | 
| /** | 
|  * Optimize SVG data. | 
|  * @param {Object} config options | 
|  * @param {string} data SVG content to optimize | 
|  * @param {string} output where to write optimized file | 
|  * @param {string} [input] input file name (being used if output is a directory) | 
|  * @return {Promise} | 
|  */ | 
| function processSVGData(config, info, data, output, input) { | 
|     var startTime = Date.now(), | 
|         prevFileSize = Buffer.byteLength(data, 'utf8'); | 
|   | 
|     return svgo.optimize(data, info).then(function(result) { | 
|         if (config.datauri) { | 
|             result.data = encodeSVGDatauri(result.data, config.datauri); | 
|         } | 
|         var resultFileSize = Buffer.byteLength(result.data, 'utf8'), | 
|             processingTime = Date.now() - startTime; | 
|   | 
|         return writeOutput(input, output, result.data).then(function() { | 
|             if (!config.quiet && output != '-') { | 
|                 if (input) { | 
|                     console.log(`\n${PATH.basename(input)}:`); | 
|                 } | 
|                 printTimeInfo(processingTime); | 
|                 printProfitInfo(prevFileSize, resultFileSize); | 
|             } | 
|         }, | 
|         error => Promise.reject(new Error(error.code === 'ENOTDIR' ? `Error: output '${output}' is not a directory.` : error))); | 
|     }); | 
| } | 
|   | 
| /** | 
|  * Write result of an optimization. | 
|  * @param {string} input | 
|  * @param {string} output output file name. '-' for stdout | 
|  * @param {string} data data to write | 
|  * @return {Promise} | 
|  */ | 
| function writeOutput(input, output, data) { | 
|     if (output == '-') { | 
|         console.log(data); | 
|         return Promise.resolve(); | 
|     } | 
|   | 
|     mkdirp.sync(PATH.dirname(output)); | 
|   | 
|     return writeFile(output, data, 'utf8').catch(error => checkWriteFileError(input, output, data, error)); | 
| } | 
|   | 
|   | 
| /** | 
|  * Write a time taken by optimization. | 
|  * @param {number} time time in milliseconds. | 
|  */ | 
| function printTimeInfo(time) { | 
|     console.log(`Done in ${time} ms!`); | 
| } | 
|   | 
| /** | 
|  * Write optimizing information in human readable format. | 
|  * @param {number} inBytes size before optimization. | 
|  * @param {number} outBytes size after optimization. | 
|  */ | 
| function printProfitInfo(inBytes, outBytes) { | 
|     var profitPercents = 100 - outBytes * 100 / inBytes; | 
|   | 
|     console.log( | 
|         (Math.round((inBytes / 1024) * 1000) / 1000) + ' KiB' + | 
|         (profitPercents < 0 ? ' + ' : ' - ') + | 
|         chalk.green(Math.abs((Math.round(profitPercents * 10) / 10)) + '%') + ' = ' + | 
|         (Math.round((outBytes / 1024) * 1000) / 1000) + ' KiB' | 
|     ); | 
| } | 
|   | 
| /** | 
|  * Check for errors, if it's a dir optimize the dir. | 
|  * @param {Object} config | 
|  * @param {string} input | 
|  * @param {string} output | 
|  * @param {Error} error | 
|  * @return {Promise} | 
|  */ | 
| function checkOptimizeFileError(config, input, output, error) { | 
|     if (error.code == 'EISDIR') { | 
|         return optimizeFolder(config, input, output); | 
|     } else if (error.code == 'ENOENT') { | 
|         return Promise.reject(new Error(`Error: no such file or directory '${error.path}'.`)); | 
|     } | 
|     return Promise.reject(error); | 
| } | 
|   | 
| /** | 
|  * Check for saving file error. If the output is a dir, then write file there. | 
|  * @param {string} input | 
|  * @param {string} output | 
|  * @param {string} data | 
|  * @param {Error} error | 
|  * @return {Promise} | 
|  */ | 
| function checkWriteFileError(input, output, data, error) { | 
|     if (error.code == 'EISDIR' && input) { | 
|         return writeFile(PATH.resolve(output, PATH.basename(input)), data, 'utf8'); | 
|     } else { | 
|         return Promise.reject(error); | 
|     } | 
| } | 
|   | 
| /** | 
|  * Show list of available plugins with short description. | 
|  */ | 
| function showAvailablePlugins() { | 
|     console.log('Currently available plugins:'); | 
|   | 
|     // Flatten an array of plugins grouped per type, sort and write output | 
|     var list = [].concat.apply([], new SVGO().config.plugins) | 
|         .sort((a, b) => a.name.localeCompare(b.name)) | 
|         .map(plugin => ` [ ${chalk.green(plugin.name)} ] ${plugin.description}`) | 
|         .join('\n'); | 
|     console.log(list); | 
| } | 
|   | 
| /** | 
|  * Write an error and exit. | 
|  * @param {Error} error | 
|  * @return {Promise} a promise for running tests | 
|  */ | 
| function printErrorAndExit(error) { | 
|     console.error(chalk.red(error)); | 
|     process.exit(1); | 
|     return Promise.reject(error); // for tests | 
| } |