From af0a5d7be614ab2747346f82b253c8c572c862f6 Mon Sep 17 00:00:00 2001 From: tssajo Date: Thu, 15 May 2014 01:22:01 +0200 Subject: [PATCH] CLI implementation --- cli.js | 204 ++++++++++++++++++++++++++++++++++++ package.json | 8 +- sample-cli-config-file.conf | 21 ++++ 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100755 cli.js create mode 100644 sample-cli-config-file.conf diff --git a/cli.js b/cli.js new file mode 100755 index 0000000..49279b0 --- /dev/null +++ b/cli.js @@ -0,0 +1,204 @@ +#!/usr/bin/env node +/** + * html-minifier CLI tool + * + * The MIT License (MIT) + * + * Copyright (c) 2014 Zoltan Frombach + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +'use strict'; + +var cli = require('cli'); +var changeCase = require('change-case'); +var path = require('path'); +var fs = require('fs'); +var appName = require('./package.json').name; +var appVersion = require('./package.json').version; +var minify = require('./dist/htmlminifier.js').minify; +var minifyOptions = {}; +var input = null; +var output = null; + +cli.width = 100; +cli.option_width = 40; +cli.setApp(appName, appVersion); + +var usage = appName + ' [OPTIONS] [FILE(s)]\n\n'; +usage += ' If no input file(s) specified then STDIN will be used for input.\n'; +usage += ' If more than one input file specified those will be concatenated and minified together.\n\n'; +usage += ' When you specify a config file with the --config-file option (see sample-cli-config-file.conf for format)\n'; +usage += ' you can still override some of its contents by providing individual command line options, too.\n\n'; +usage += ' When you want to provide an array of strings for --ignore-custom-comments or --process-scripts options\n'; +usage += ' on the command line you must escape those such as --ignore-custom-comments "[\\"string1\\",\\"string1\\"]"\n'; + +cli.setUsage(usage); + +var mainOptions = { + 'removeComments': [false, 'Strip HTML comments', false], + 'removeCommentsFromCDATA': [false, 'Strip HTML comments from scripts and styles', false], + 'removeCDATASectionsFromCDATA': [false, 'Remove CDATA sections from script and style elements', false], + 'collapseWhitespace': [false, 'Collapse white space that contributes to text nodes in a document tree.', false], + 'conservativeCollapse': [false, 'Always collapse to 1 space (never remove it entirely)', false], + 'collapseBooleanAttributes': [false, 'Omit attribute values from boolean attributes', false], + 'removeAttributeQuotes': [false, 'Remove quotes around attributes when possible.', false], + 'removeRedundantAttributes': [false, 'Remove attributes when value matches default.', false], + 'useShortDoctype': [false, 'Replaces the doctype with the short (HTML5) doctype', false], + 'removeEmptyAttributes': [false, 'Remove all attributes with whitespace-only values', false], + 'removeOptionalTags': [false, 'Remove unrequired tags', false], + 'removeEmptyElements': [false, 'Remove all elements with empty contents', false], + 'lint': [false, 'Toggle linting', false], + 'keepClosingSlash': [false, 'Keep the trailing slash on singleton elements', false], + 'caseSensitive': [false, 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)', false], + 'minifyJS': [false, 'Minify Javascript in script elements and on* attributes (uses UglifyJS)', false], + 'minifyCSS': [false, 'Minify CSS in style elements and style attributes (uses clean-css)', false], + 'ignoreCustomComments': [false, 'Array of regex\'es that allow to ignore certain comments, when matched', 'string'], + 'processScripts': [false, 'Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.)', 'string'] +}; + +var cliOptions = { + 'version': ['v', 'Version information'], + 'output': ['o', 'Specify output file (if not specified STDOUT will be used for output)', 'file'], + 'config-file': ['c', 'Use config file', 'file'] +}; + +var mainOptionKeys = Object.keys(mainOptions); +var paramOptions = {}; +mainOptionKeys.forEach(function(key) { + var paramKey = changeCase.paramCase(key); + paramOptions[paramKey] = {name: key, type: mainOptions[key][2]}; + cliOptions[paramKey] = mainOptions[key]; +}); + +cli.parse(cliOptions); + +function saveOutput(output, results) { + try { + if (output !== null) { + fs.writeFileSync(path.resolve(output), results); + } else { + process.stdout.write(results); + } + } catch(e) { + process.stderr.write('Error: Cannot write to output'); + } +} + +cli.main(function(args, options) { + + if (options.version) { + process.stderr.write(appName + ' v' + appVersion); + cli.exit(0); + } + + if (options['config-file']) { + try { + var fileOptions = JSON.parse(fs.readFileSync(path.resolve(options['config-file']), 'utf8')); + if ((fileOptions !== null) && (typeof fileOptions === 'object')) minifyOptions = fileOptions; + } catch(e) { + process.stderr.write('Error: Cannot read the config file'); + cli.exit(1); + } + } + + var paramKeys = Object.keys(paramOptions); + paramKeys.forEach(function(key) { + var paramOpt = paramOptions[key]; + if (options[key] !== null) { + if (paramOpt.type) { + var value = options[key]; + if (value !== null) { + var jsonArray; + try { + jsonArray = JSON.parse(value); + } catch(e) {} + if (jsonArray instanceof Array) { + minifyOptions[paramOpt.name] = jsonArray; + } else { + minifyOptions[paramOpt.name] = [value]; + } + } + } else { + minifyOptions[paramOpt.name] = true; + } + } + }); + + if (args.length) input = args; + + if (options.output) output = options.output; + + var original = ''; + var status = 0; + + if (input !== null) { // Minifying one or more files specified on the CMD line + + input.forEach(function(afile) { + try { + original += fs.readFileSync(afile, 'utf8'); + } catch(e) { + status = 2; + process.stderr.write('Error: Cannot read file ' + afile); + } + }); + + } else { // Minifying input coming from STDIN + + var BUFSIZE = 4096; + var buf = new Buffer(BUFSIZE); + var bytesRead; + + while (true) { // Loop as long as stdin input is available. + bytesRead = 0; + try { + bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE); + } catch (e) { + if (e.code === 'EAGAIN') { // 'resource temporarily unavailable' + // Happens on OS X 10.8.3 (not Windows 7!), if there's no + // stdin input - typically when invoking a script without any + // input (for interactive stdin input). + // If you were to just continue, you'd create a tight loop. + process.stderr.write('ERROR: interactive stdin input not supported'); + cli.exit(2); + } else if (e.code === 'EOF') { + // Happens on Windows 7, but not OS X 10.8.3: + // simply signals the end of *piped* stdin input. + break; + } + throw e; // unexpected exception + } + if (bytesRead === 0) { + // No more stdin input available. + // OS X 10.8.3: regardless of input method, this is how the end + // of input is signaled. + // Windows 7: this is how the end of input is signaled for + // *interactive* stdin input. + break; + } + original += buf.toString('utf8', 0, bytesRead); + } + + } + + saveOutput(output, minify(original, minifyOptions)); + cli.exit(status); + +}); diff --git a/package.json b/package.json index 26cb53c..7f103d6 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ ], "contributors": [ "Gilmore Davidson (https://github.com/gilmoreorless)", - "Hugo Wetterberg " + "Hugo Wetterberg ", + "Zoltan Frombach " ], "license": { "type": "MIT", "url": "https://github.com/kangax/html-minifier/blob/gh-pages/LICENSE" }, + "bin": "./cli.js", "repository": { "type": "git", "url": "https://github.com/kangax/html-minifier" @@ -38,7 +40,9 @@ }, "dependencies": { "clean-css": "2.1.x", - "uglify-js": "2.4.x" + "uglify-js": "2.4.x", + "cli": "~0.6.2", + "change-case": "~2.1.1" }, "devDependencies": { "grunt": "0.4.x", diff --git a/sample-cli-config-file.conf b/sample-cli-config-file.conf new file mode 100644 index 0000000..1208ab0 --- /dev/null +++ b/sample-cli-config-file.conf @@ -0,0 +1,21 @@ +{ + "removeComments": false, + "removeCommentsFromCDATA": false, + "removeCDATASectionsFromCDATA": false, + "collapseWhitespace": false, + "conservativeCollapse": false, + "collapseBooleanAttributes": false, + "removeAttributeQuotes": false, + "removeRedundantAttributes": false, + "useShortDoctype": false, + "removeEmptyAttributes": false, + "removeOptionalTags": false, + "removeEmptyElements": false, + "lint": false, + "keepClosingSlash": false, + "caseSensitive": false, + "minifyJS": false, + "minifyCSS": false, + "ignoreCustomComments": [], + "processScripts": [] +} -- 2.34.1