#!/usr/bin/env node /** * html-minifier-terser CLI tool * * The MIT License (MIT) * * Copyright (c) 2014-2016 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. * */ import fs from 'fs'; import path from 'path'; import { createRequire } from 'module'; import { camelCase } from 'camel-case'; import { paramCase } from 'param-case'; import { Command } from 'commander'; import { minify } from './src/htmlminifier.js'; const require = createRequire(import.meta.url); const pkg = require('./package.json'); const program = new Command(); program.name(pkg.name); program.version(pkg.version); function fatal(message) { console.error(message); process.exit(1); } /** * JSON does not support regexes, so, e.g., JSON.parse() will not create * a RegExp from the JSON value `[ "/matchString/" ]`, which is * technically just an array containing a string that begins and end with * a forward slash. To get a RegExp from a JSON string, it must be * constructed explicitly in JavaScript. * * The likelihood of actually wanting to match text that is enclosed in * forward slashes is probably quite rare, so if forward slashes were * included in an argument that requires a regex, the user most likely * thought they were part of the syntax for specifying a regex. * * In the unlikely case that forward slashes are indeed desired in the * search string, the user would need to enclose the expression in a * second set of slashes: * * --customAttrSrround "[\"//matchString//\"]" */ function parseRegExp(value) { if (value) { return new RegExp(value.replace(/^\/(.*)\/$/, '$1')); } } function parseJSON(value) { if (value) { try { return JSON.parse(value); } catch (e) { if (/^{/.test(value)) { fatal('Could not parse JSON value \'' + value + '\''); } return value; } } } function parseJSONArray(value) { if (value) { value = parseJSON(value); return Array.isArray(value) ? value : [value]; } } function parseJSONRegExpArray(value) { value = parseJSONArray(value); return value && value.map(parseRegExp); } function parseString(value) { return value; } const mainOptions = { caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)', collapseBooleanAttributes: 'Omit attribute values from boolean attributes', collapseInlineTagWhitespace: 'Collapse white space around inline tag', collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.', conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)', continueOnParseError: 'Handle parse errors instead of aborting', customAttrAssign: ['Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'
\')', parseJSONRegExpArray], customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g. /ng-class/)', parseRegExp], customAttrSurround: ['Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. )', parseJSONRegExpArray], customEventAttributes: ['Arrays of regex\'es that allow to support custom event attributes for minifyJS (e.g. ng-click)', parseJSONRegExpArray], decodeEntities: 'Use direct Unicode characters whenever possible', html5: 'Parse input according to HTML5 specifications', ignoreCustomComments: ['Array of regex\'es that allow to ignore certain comments, when matched', parseJSONRegExpArray], ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. , {{ ... }})', parseJSONRegExpArray], includeAutoGeneratedTags: 'Insert tags generated by HTML parser', keepClosingSlash: 'Keep the trailing slash on singleton elements', maxLineLength: ['Max line length', parseInt], minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON], minifyJS: ['Minify Javascript in script elements and on* attributes (uses terser)', parseJSON], minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON], noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element', preserveLineBreaks: 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break.', preventAttributesEscaping: 'Prevents the escaping of the values of attributes.', processConditionalComments: 'Process contents of conditional comments through minifier', processScripts: ['Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.)', parseJSONArray], quoteCharacter: ['Type of quote to use for attribute values (\' or ")', parseString], removeAttributeQuotes: 'Remove quotes around attributes when possible.', removeComments: 'Strip HTML comments', removeEmptyAttributes: 'Remove all attributes with whitespace-only values', removeEmptyElements: 'Remove all elements with empty contents', removeOptionalTags: 'Remove unrequired tags', removeRedundantAttributes: 'Remove attributes when value matches default.', removeScriptTypeAttributes: 'Removes the following attributes from script tags: text/javascript, text/ecmascript, text/jscript, application/javascript, application/x-javascript, application/ecmascript. Other type attribute values are left intact', removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link tags. Other type attribute values are left intact.', removeTagWhitespace: 'Remove space between attributes whenever possible', sortAttributes: 'Sort attributes by frequency', sortClassName: 'Sort style classes by frequency', trimCustomFragments: 'Trim white space around ignoreCustomFragments.', useShortDoctype: 'Replaces the doctype with the short (HTML5) doctype' }; // configure commandline flags const mainOptionKeys = Object.keys(mainOptions); mainOptionKeys.forEach(function (key) { const option = mainOptions[key]; if (Array.isArray(option)) { key = key === 'minifyURLs' ? '--minify-urls' : '--' + paramCase(key); key += option[1] === parseJSON ? ' [value]' : '