From 1f62c21622a8ea88add60530da023830bc57a231 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Tue, 19 Apr 2016 21:43:49 +0800 Subject: [PATCH] allow custom processor for minify{CSS,JS,URLs} fixes #382 closes #590 --- README.md | 6 +- src/htmlminifier.js | 174 ++++++++++++++++++++++---------------------- tests/minifier.js | 63 +++++++++++++++- 3 files changed, 150 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 112df4f..96d74e0 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Pe | `includeAutoGeneratedTags` | Insert tags generated by HTML parser | `true` | | `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` | | `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points | -| `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `false`, `Object` (options)) | -| `minifyJS` | Minify JavaScript in script elements and event attributes (uses [UglifyJS](https://github.com/mishoo/UglifyJS2)) | `false` (could be `true`, `false`, `Object` (options)) | -| `minifyURLs` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `Object` (options)) | +| `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `false`, `Object`, `Function(text, inline)`) | +| `minifyJS` | Minify JavaScript in script elements and event attributes (uses [UglifyJS](https://github.com/mishoo/UglifyJS2)) | `false` (could be `true`, `false`, `Object`, `Function(text, inline)`) | +| `minifyURLs` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `Object`, `Function(text)`) | | `preserveLineBreaks` | Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break. Must be used in conjunction with `collapseWhitespace=true` | `false` | | `preventAttributesEscaping` | Prevents the escaping of the values of attributes | `false` | | `processConditionalComments` | Process contents of conditional comments through minifier | `false` | diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 721ef40..6638ebf 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -245,17 +245,10 @@ function isSrcset(attrName, tag) { return attrName === 'srcset' && srcsetTags(tag); } -var fnPrefix = '!function(){'; -var fnSuffix = '}();'; - function cleanAttributeValue(tag, attrName, attrValue, options, attrs) { if (attrValue && isEventAttribute(attrName, options)) { - attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, ''); - if (options.minifyJS) { - var minified = minifyJS(fnPrefix + attrValue + fnSuffix, options); - return minified.slice(fnPrefix.length, -fnSuffix.length); - } - return attrValue; + attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, ''); + return options.minifyJS(attrValue, true); } else if (attrName === 'class') { attrValue = trimWhitespace(attrValue); @@ -269,10 +262,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) { } else if (isUriTypeAttribute(attrName, tag)) { attrValue = trimWhitespace(attrValue); - if (options.minifyURLs && !isCanonicalURL(tag, attrs)) { - return minifyURLs(attrValue, options); - } - return attrValue; + return isCanonicalURL(tag, attrs) ? attrValue : options.minifyURLs(attrValue); } else if (isNumberTypeAttribute(attrName, tag)) { return trimWhitespace(attrValue); @@ -282,10 +272,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) { if (attrValue && /;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) { attrValue = attrValue.replace(/\s*;$/, ''); } - if (options.minifyCSS) { - return minifyStyles(attrValue, options, true); - } - return attrValue; + return options.minifyCSS(attrValue, true); } else if (isSrcset(attrName, tag)) { // https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset @@ -301,10 +288,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) { descriptor = ' ' + num + suffix; } } - if (options.minifyURLs) { - url = minifyURLs(url, options); - } - return url + descriptor; + return options.minifyURLs(url) + descriptor; }).join(', '); } else if (isMetaViewport(tag, attrs) && attrName === 'content') { @@ -597,6 +581,13 @@ function buildAttr(normalized, hasUnarySlash, options, isLast) { return attr.customOpen + attrFragment + attr.customClose; } +function identity(value) { + return value; +} + +var fnPrefix = '!function(){'; +var fnSuffix = '}();'; + function processOptions(options) { ['html5', 'includeAutoGeneratedTags'].forEach(function(key) { if (!(key in options)) { @@ -605,7 +596,7 @@ function processOptions(options) { }); if (typeof options.log !== 'function') { - options.log = function() {}; + options.log = identity; } var defaultTesters = ['canCollapseWhitespace', 'canTrimWhitespace']; @@ -628,73 +619,85 @@ function processOptions(options) { ]; } - if (options.minifyURLs && typeof options.minifyURLs !== 'object') { - options.minifyURLs = {}; + if (typeof options.minifyURLs === 'object') { + var minifyURLs = options.minifyURLs; + options.minifyURLs = function(text) { + try { + return RelateUrl.relate(text, minifyURLs); + } + catch (err) { + options.log(err); + return text; + } + }; } - - if (options.minifyJS) { - if (typeof options.minifyJS !== 'object') { - options.minifyJS = {}; - } - options.minifyJS.fromString = true; - (options.minifyJS.output || (options.minifyJS.output = { })).inline_script = true; + else if (typeof options.minifyURLs !== 'function') { + options.minifyURLs = identity; } - if (options.minifyCSS) { - if (typeof options.minifyCSS !== 'object') { - options.minifyCSS = {}; - } - if (typeof options.minifyCSS.advanced === 'undefined') { - options.minifyCSS.advanced = false; - } + if (!options.minifyJS) { + options.minifyJS = identity; } -} - -function minifyURLs(text, options) { - try { - return RelateUrl.relate(text, options.minifyURLs); - } - catch (err) { - options.log(err); - return text; + if (typeof options.minifyJS !== 'function') { + var minifyJS = options.minifyJS; + if (typeof minifyJS !== 'object') { + minifyJS = {}; + } + minifyJS.fromString = true; + (minifyJS.output || (minifyJS.output = {})).inline_script = true; + options.minifyJS = function(text, inline) { + var start = text.match(/^\s*\s*$/, '') : text; + try { + if (inline) { + code = fnPrefix + code + fnSuffix; + } + code = UglifyJS.minify(code, minifyJS).code; + if (inline) { + code = code.slice(fnPrefix.length, -fnSuffix.length); + } + if (/;$/.test(code)) { + code = code.slice(0, -1); + } + return code; + } + catch (err) { + options.log(err); + return text; + } + }; } -} -function minifyJS(text, options) { - var start = text.match(/^\s*\s*$/, '') : text; - try { - return UglifyJS.minify(code, options.minifyJS).code; + if (!options.minifyCSS) { + options.minifyCSS = identity; } - catch (err) { - options.log(err); - return text; - } -} - -function minifyCSS(text, options, inline) { - var start = text.match(/^\s*\s*$/, '') : text; - try { - var cleanCSS = new CleanCSS(options.minifyCSS); - if (inline) { - return unwrapCSS(cleanCSS.minify(wrapCSS(style)).styles); + if (typeof options.minifyCSS !== 'function') { + var minifyCSS = options.minifyCSS; + if (typeof minifyCSS !== 'object') { + minifyCSS = {}; } - return cleanCSS.minify(style).styles; - } - catch (err) { - options.log(err); - return text; - } -} - -function minifyStyles(text, options, inline) { - if (options.minifyURLs) { - text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function(match, prefix, quote, url, suffix) { - return prefix + quote + minifyURLs(url, options) + quote + suffix; - }); + if (typeof minifyCSS.advanced === 'undefined') { + minifyCSS.advanced = false; + } + options.minifyCSS = function(text, inline) { + text = text.replace(/(url\s*\(\s*)("|'|)(.*?)\2(\s*\))/ig, function(match, prefix, quote, url, suffix) { + return prefix + quote + options.minifyURLs(url) + quote + suffix; + }); + var start = text.match(/^\s*\s*$/, '') : text; + try { + var cleanCSS = new CleanCSS(minifyCSS); + if (inline) { + return unwrapCSS(cleanCSS.minify(wrapCSS(style)).styles); + } + return cleanCSS.minify(style).styles; + } + catch (err) { + options.log(err); + return text; + } + }; } - return minifyCSS(text, options, inline); } function uniqueId(value) { @@ -1107,14 +1110,11 @@ function minify(value, options, partialMarkup) { if (options.processScripts && specialContentTags(currentTag)) { text = processScript(text, options, currentAttrs); } - if (options.minifyJS && isExecutableScript(currentTag, currentAttrs)) { - text = minifyJS(text, options); - if (/;$/.test(text)) { - text = text.slice(0, -1); - } + if (isExecutableScript(currentTag, currentAttrs)) { + text = options.minifyJS(text); } - if (options.minifyCSS && isStyleSheet(currentTag, currentAttrs)) { - text = minifyStyles(text, options); + if (isStyleSheet(currentTag, currentAttrs)) { + text = options.minifyCSS(text); } if (options.removeOptionalTags && text) { // may be omitted if first thing inside is not comment diff --git a/tests/minifier.js b/tests/minifier.js index 03a6673..d0f1642 100644 --- a/tests/minifier.js +++ b/tests/minifier.js @@ -619,6 +619,63 @@ test('remove CDATA sections from scripts/styles', function() { equal(minify(input, { minifyCSS: true }), input); }); +test('custom processors', function() { + function css(text, inline) { + return inline ? 'Inline CSS' : 'Normal CSS'; + } + + input = ''; + equal(minify(input), input); + equal(minify(input, { minifyCSS: null }), input); + equal(minify(input, { minifyCSS: false }), input); + output = ''; + equal(minify(input, { minifyCSS: css }), output); + + input = '

'; + equal(minify(input), input); + equal(minify(input, { minifyCSS: null }), input); + equal(minify(input, { minifyCSS: false }), input); + output = '

'; + equal(minify(input, { minifyCSS: css }), output); + + function js(text, inline) { + return inline ? 'Inline JS' : 'Normal JS'; + } + + input = ''; + equal(minify(input), input); + equal(minify(input, { minifyJS: null }), input); + equal(minify(input, { minifyJS: false }), input); + output = ''; + equal(minify(input, { minifyJS: js }), output); + + input = '

'; + equal(minify(input), input); + equal(minify(input, { minifyJS: null }), input); + equal(minify(input, { minifyJS: false }), input); + output = '

'; + equal(minify(input, { minifyJS: js }), output); + + function url() { + return 'URL'; + } + + input = 'bar'; + equal(minify(input), input); + equal(minify(input, { minifyURLs: null }), input); + equal(minify(input, { minifyURLs: false }), input); + output = 'bar'; + equal(minify(input, { minifyURLs: url }), output); + + input = ''; + equal(minify(input), input); + equal(minify(input, { minifyURLs: null }), input); + equal(minify(input, { minifyURLs: false }), input); + equal(minify(input, { minifyURLs: url }), input); + output = ''; + equal(minify(input, { minifyCSS: true, minifyURLs: url }), output); +}); + test('empty attributes', function() { input = '

x

'; equal(minify(input, { removeEmptyAttributes: true }), '

x

'); @@ -735,11 +792,11 @@ test('cleaning Number-based attributes', function() { test('cleaning other attributes', function() { input = 'blah'; - output = 'blah'; + output = 'blah'; equal(minify(input), output); input = '

x'; - output = '

x

'; + output = '

x

'; equal(minify(input), output); }); @@ -2324,7 +2381,7 @@ test('auto-generated tags', function() { equal(minify(input, { includeAutoGeneratedTags: true }), output); input = '

x'; - output = '

x'; + output = '

x'; equal(minify(input, { includeAutoGeneratedTags: false }), output); input = '

Well, look at me! I\'m a div!
'; -- 2.34.1