From 8a675e4a2cdf2c2511750e76a0c26072eb040425 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 16:42:48 +1200 Subject: [PATCH 01/16] Added tests for async style and script minification using callbacks. --- tests/minifier.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/minifier.js b/tests/minifier.js index 4f71d662..9f461cc7 100644 --- a/tests/minifier.js +++ b/tests/minifier.js @@ -3397,3 +3397,43 @@ QUnit.test('canCollapseWhitespace and canTrimWhitespace hooks', function(assert) canCollapseWhitespace: canCollapseAndTrimWhitespace }), output); }); + +QUnit.test('style minification with callback', function(assert) { + var input = ''; + var output = ''; + var done = assert.async(); + assert.notOk(minify( + input, + { + minifyCSS: function(css, cb) { + setTimeout(function() { + cb(css + ' callback!'); + }, 0); + } + }, + function(result) { + assert.equal(result, output); + done(); + } + )); +}); + +QUnit.test('script minification with callback', function(assert) { + var input = ''; + var output = ''; + var done = assert.async(); + assert.notOk(minify( + input, + { + minifyJS: function(js, inline, cb) { + setTimeout(function() { + cb(js + '(function(){ console.log("World"); })()'); + }, 0); + } + }, + function(result) { + assert.equal(result, output); + done(); + } + )); +}); From 7f1dc9ca8513a8f9b31da2eb2369aef089cc75e3 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 16:43:42 +1200 Subject: [PATCH 02/16] Added more tests for async style and script minification using callbacks. --- tests/minifier.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/tests/minifier.js b/tests/minifier.js index 9f461cc7..8d5abc22 100644 --- a/tests/minifier.js +++ b/tests/minifier.js @@ -3437,3 +3437,149 @@ QUnit.test('script minification with callback', function(assert) { } )); }); + +QUnit.test('style async minification with script sync minification', function(assert) { + var input = ''; + var output = ''; + var done = assert.async(); + assert.notOk(minify( + input, + { + minifyCSS: function(css, cb) { + setTimeout(function() { + cb(css + ' callback!'); + }, 0); + }, + minifyJS: true + }, + function(result) { + assert.equal(result, output); + done(); + } + )); +}); + +QUnit.test('style sync minification with script async minification', function(assert) { + var input = ''; + var output = ''; + var done = assert.async(); + assert.notOk(minify( + input, + { + minifyCSS: true, + minifyJS: function(js, inline, cb) { + setTimeout(function() { + cb(js + ' callback!'); + }, 0); + } + }, + function(result) { + assert.equal(result, output); + done(); + } + )); +}); + +QUnit.test('style async minification with script async minification', function(assert) { + var input = ''; + var output = ''; + var done = assert.async(); + assert.notOk(minify( + input, + { + minifyCSS: function(css, cb) { + setTimeout(function() { + cb(css + ' callback!'); + }, 0); + }, + minifyJS: function(js, inline, cb) { + setTimeout(function() { + cb(js + ' callback!'); + }, 0); + } + }, + function(result) { + assert.equal(result, output); + done(); + } + )); +}); + + +QUnit.test('async minification along side sync minification', function(assert) { + var input = ''; + var syncOutput = ''; + var asyncOutput = ''; + var done = assert.async(); + + assert.notOk(minify( + input, + { + minifyCSS: function(css, cb) { + setTimeout(function() { + cb(css + ' callback!'); + }, 0); + }, + minifyJS: function(js, inline, cb) { + setTimeout(function() { + cb(js + ' callback!'); + }, 0); + } + }, + function(result) { + assert.equal(result, asyncOutput); + done(); + } + )); + + assert.equal(minify(input, { + minifyCSS: true, + minifyJS: true + }), syncOutput); +}); + +QUnit.test('multiple async minifications', function(assert) { + var input = ''; + var output = ''; + var done = assert.async(2); + + assert.notOk(minify( + input, + { + minifyCSS: function(css, cb) { + setTimeout(function() { + cb(css + ' callback!'); + }, 0); + }, + minifyJS: function(js, inline, cb) { + setTimeout(function() { + cb(js + ' callback!'); + }, 0); + } + }, + function(result) { + assert.equal(result, output); + done(); + } + )); + + assert.notOk(minify( + input, + { + minifyCSS: function(css, cb) { + setTimeout(function() { + cb(css + ' callback!'); + }, 0); + }, + minifyJS: function(js, inline, cb) { + setTimeout(function() { + cb(js + ' callback!'); + }, 0); + } + }, + function(result) { + assert.equal(result, output); + done(); + } + )); +}); From 8a3105ac5fee4747ae4a639078ef7040b27d64d6 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 16:51:18 +1200 Subject: [PATCH 03/16] Added a test to ensure that the minify function still results a result when a callback is given, if it finished synchronously. --- tests/minifier.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/minifier.js b/tests/minifier.js index 8d5abc22..54643126 100644 --- a/tests/minifier.js +++ b/tests/minifier.js @@ -3583,3 +3583,17 @@ QUnit.test('multiple async minifications', function(assert) { } )); }); + +QUnit.test('sync minify with callback', function(assert) { + var input = 'TestHello World'; + var output = input; + var done = assert.async(); + assert.equal(minify( + input, + {}, + function(result) { + assert.equal(result, output); + done(); + } + ), output); +}); From a68ee19317fb2fb05b9cef9c6b3a3ed77490414b Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 16:55:55 +1200 Subject: [PATCH 04/16] Added callback parameter to minify. --- src/htmlminifier.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 7748f1c6..1ab95f8b 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -803,7 +803,7 @@ function createSortFns(value, options, uidIgnore, uidAttr) { } } -function minify(value, options, partialMarkup) { +function minify(value, options, partialMarkup, cb) { options = options || {}; var optionsStack = []; processOptions(options); @@ -1300,6 +1300,6 @@ function joinResultSegments(results, options) { return options.collapseWhitespace ? collapseWhitespace(str, options, true, true) : str; } -exports.minify = function(value, options) { - return minify(value, options); +exports.minify = function(value, options, cb) { + return minify(value, options, null, cb); }; From 892ca4b6a56d9428d71078e6b0b8b38d1ab1be59 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 16:57:07 +1200 Subject: [PATCH 05/16] Added callback parameter to minifyCSS and minifyJS. --- src/htmlminifier.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 1ab95f8b..ea298d5f 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -867,14 +867,14 @@ function minify(value, options, partialMarkup, cb) { uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)(\\s*)', 'g'); var minifyCSS = options.minifyCSS; if (minifyCSS) { - options.minifyCSS = function(text) { - return minifyCSS(escapeFragments(text)); + options.minifyCSS = function(text, cb) { + return minifyCSS(escapeFragments(text), cb); }; } var minifyJS = options.minifyJS; if (minifyJS) { - options.minifyJS = function(text, inline) { - return minifyJS(escapeFragments(text), inline); + options.minifyJS = function(text, inline, cb) { + return minifyJS(escapeFragments(text), inline, cb); }; } } From 77f378691f1f19e2c2b66742a2192fe166371b3e Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 17:00:11 +1200 Subject: [PATCH 06/16] Added an onComplete function to the the HTMLParser class. --- src/htmlparser.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/htmlparser.js b/src/htmlparser.js index dc1eb53c..1a35d98d 100644 --- a/src/htmlparser.js +++ b/src/htmlparser.js @@ -398,6 +398,20 @@ function HTMLParser(html, handler) { } } +/** + * Called when the HTMLParser has finished. + * + * @param {Function} cb - Callback function. + */ +HTMLParser.prototype.onComplete = function(cb) { + if (this.finished) { + cb(); + } + else { + this.onCompleteCallback = cb; + } +}; + exports.HTMLParser = HTMLParser; exports.HTMLtoXML = function(html) { var results = ''; From 127669dbb9f37b90f8af71d47e30a05fcb1a8e54 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 17:25:27 +1200 Subject: [PATCH 07/16] Switch out the iterative loop for a recursive loop that supports callbacks. --- src/htmlparser.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/htmlparser.js b/src/htmlparser.js index 1a35d98d..d6a8c824 100644 --- a/src/htmlparser.js +++ b/src/htmlparser.js @@ -120,7 +120,12 @@ function HTMLParser(html, handler) { var stack = [], lastTag; var attribute = attrForHandler(handler); var last, prevTag, nextTag; - while (html) { + + (function iterate(cb) { + if (!html) { + return cb(); + } + last = html; // Make sure we're not in a script or style element if (!lastTag || !special(lastTag)) { @@ -136,7 +141,7 @@ function HTMLParser(html, handler) { } html = html.substring(commentEnd + 3); prevTag = ''; - continue; + return iterate(cb); } } @@ -150,7 +155,7 @@ function HTMLParser(html, handler) { } html = html.substring(conditionalEnd + 2); prevTag = ''; - continue; + return iterate(cb); } } @@ -162,7 +167,7 @@ function HTMLParser(html, handler) { } html = html.substring(doctypeMatch[0].length); prevTag = ''; - continue; + return iterate(cb); } // End tag: @@ -171,7 +176,7 @@ function HTMLParser(html, handler) { html = html.substring(endTagMatch[0].length); endTagMatch[0].replace(endTag, parseEndTag); prevTag = '/' + endTagMatch[1].toLowerCase(); - continue; + return iterate(cb); } // Start tag: @@ -180,7 +185,7 @@ function HTMLParser(html, handler) { html = startTagMatch.rest; handleStartTag(startTagMatch); prevTag = startTagMatch.tagName.toLowerCase(); - continue; + return iterate(cb); } } @@ -239,12 +244,20 @@ function HTMLParser(html, handler) { if (html === last) { throw new Error('Parse Error: ' + html); } - } - if (!handler.partialMarkup) { - // Clean up any remaining tags - parseEndTag(); - } + + return iterate(cb); + })(function() { + if (!handler.partialMarkup) { + // Clean up any remaining tags + parseEndTag(); + } + + this.finished = true; + if (this.onCompleteCallback) { + this.onCompleteCallback(); + } + }.bind(this)); function parseStartTag(input) { var start = input.match(startTagOpen); From 16c9ab7df99e0caee6b4c033bd18ba400df5ea3f Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 17:37:07 +1200 Subject: [PATCH 08/16] Wrapped the parse error check up into a function so its execution can be postponed. --- src/htmlparser.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/htmlparser.js b/src/htmlparser.js index d6a8c824..bfcf5e55 100644 --- a/src/htmlparser.js +++ b/src/htmlparser.js @@ -126,6 +126,8 @@ function HTMLParser(html, handler) { return cb(); } + var waitingForCallback = false; + last = html; // Make sure we're not in a script or style element if (!lastTag || !special(lastTag)) { @@ -239,14 +241,18 @@ function HTMLParser(html, handler) { }); parseEndTag('', stackedTag); - } - if (html === last) { - throw new Error('Parse Error: ' + html); + if (!waitingForCallback) { + return checkForParseError(cb); } + function checkForParseError(cb) { + if (html === last) { + throw new Error('Parse Error: ' + html); + } - return iterate(cb); + return iterate(cb); + } })(function() { if (!handler.partialMarkup) { // Clean up any remaining tags From 9c447fda585f499d253079d08d86cf170a822352 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 17:47:12 +1200 Subject: [PATCH 09/16] Refactored HTMLParser's parse loop. --- src/htmlparser.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/htmlparser.js b/src/htmlparser.js index bfcf5e55..2d0e241a 100644 --- a/src/htmlparser.js +++ b/src/htmlparser.js @@ -117,13 +117,14 @@ function joinSingleAttrAssigns(handler) { } function HTMLParser(html, handler) { + var $this = this; var stack = [], lastTag; var attribute = attrForHandler(handler); var last, prevTag, nextTag; - (function iterate(cb) { + function parse() { if (!html) { - return cb(); + return parseComplete(); } var waitingForCallback = false; @@ -143,7 +144,7 @@ function HTMLParser(html, handler) { } html = html.substring(commentEnd + 3); prevTag = ''; - return iterate(cb); + return parse(); } } @@ -157,7 +158,7 @@ function HTMLParser(html, handler) { } html = html.substring(conditionalEnd + 2); prevTag = ''; - return iterate(cb); + return parse(); } } @@ -169,7 +170,7 @@ function HTMLParser(html, handler) { } html = html.substring(doctypeMatch[0].length); prevTag = ''; - return iterate(cb); + return parse(); } // End tag: @@ -178,7 +179,7 @@ function HTMLParser(html, handler) { html = html.substring(endTagMatch[0].length); endTagMatch[0].replace(endTag, parseEndTag); prevTag = '/' + endTagMatch[1].toLowerCase(); - return iterate(cb); + return parse(); } // Start tag: @@ -187,7 +188,7 @@ function HTMLParser(html, handler) { html = startTagMatch.rest; handleStartTag(startTagMatch); prevTag = startTagMatch.tagName.toLowerCase(); - return iterate(cb); + return parse(); } } @@ -243,27 +244,31 @@ function HTMLParser(html, handler) { parseEndTag('', stackedTag); if (!waitingForCallback) { - return checkForParseError(cb); + return checkForParseError(); } - function checkForParseError(cb) { + function checkForParseError() { if (html === last) { throw new Error('Parse Error: ' + html); } - return iterate(cb); + return parse(); } - })(function() { + } + + parse(); + + function parseComplete() { if (!handler.partialMarkup) { // Clean up any remaining tags parseEndTag(); } - this.finished = true; - if (this.onCompleteCallback) { - this.onCompleteCallback(); + $this.finished = true; + if ($this.onCompleteCallback) { + $this.onCompleteCallback(); } - }.bind(this)); + } function parseStartTag(input) { var start = input.match(startTagOpen); From bac6a0e0fc1d0e6eba73a5b7ddad44e80c8c5c94 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 17:52:51 +1200 Subject: [PATCH 10/16] Code at the end of the minify function now waits until the HTMLParser has finished parsing the code before continuing. --- src/htmlminifier.js | 82 ++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index ea298d5f..c3aed0ba 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -941,6 +941,7 @@ function minify(value, options, partialMarkup, cb) { trimTrailingWhitespace(charsIndex, nextTag); } + var str; new HTMLParser(value, { partialMarkup: partialMarkup, html5: options.html5, @@ -1226,50 +1227,55 @@ function minify(value, options, partialMarkup, cb) { }, customAttrAssign: options.customAttrAssign, customAttrSurround: options.customAttrSurround - }); - - if (options.removeOptionalTags) { - // may be omitted if first thing inside is not comment - // or may be omitted if empty - if (topLevelTags(optionalStartTag)) { - removeStartTag(); + }).onComplete(function() { + if (options.removeOptionalTags) { + // may be omitted if first thing inside is not comment + // or may be omitted if empty + if (topLevelTags(optionalStartTag)) { + removeStartTag(); + } + // except for or , end tags may be omitted if no more content in parent element + if (optionalEndTag && !trailingTags(optionalEndTag)) { + removeEndTag(); + } } - // except for or , end tags may be omitted if no more content in parent element - if (optionalEndTag && !trailingTags(optionalEndTag)) { - removeEndTag(); + if (options.collapseWhitespace) { + squashTrailingWhitespace('br'); } - } - if (options.collapseWhitespace) { - squashTrailingWhitespace('br'); - } - var str = joinResultSegments(buffer, options); + str = joinResultSegments(buffer, options); - if (uidPattern) { - str = str.replace(uidPattern, function(match, prefix, index, suffix) { - var chunk = ignoredCustomMarkupChunks[+index][0]; - if (options.collapseWhitespace) { - if (prefix !== '\t') { - chunk = prefix + chunk; - } - if (suffix !== '\t') { - chunk += suffix; + if (uidPattern) { + str = str.replace(uidPattern, function(match, prefix, index, suffix) { + var chunk = ignoredCustomMarkupChunks[+index][0]; + if (options.collapseWhitespace) { + if (prefix !== '\t') { + chunk = prefix + chunk; + } + if (suffix !== '\t') { + chunk += suffix; + } + return collapseWhitespace(chunk, { + preserveLineBreaks: options.preserveLineBreaks, + conservativeCollapse: !options.trimCustomFragments + }, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk)); } - return collapseWhitespace(chunk, { - preserveLineBreaks: options.preserveLineBreaks, - conservativeCollapse: !options.trimCustomFragments - }, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk)); - } - return chunk; - }); - } - if (uidIgnore) { - str = str.replace(new RegExp('', 'g'), function(match, index) { - return ignoredMarkupChunks[+index]; - }); - } + return chunk; + }); + } + if (uidIgnore) { + str = str.replace(new RegExp('', 'g'), function(match, index) { + return ignoredMarkupChunks[+index]; + }); + } + + options.log('minified in: ' + (Date.now() - t) + 'ms'); + + if (typeof cb === 'function') { + cb(str); + } + }); - options.log('minified in: ' + (Date.now() - t) + 'ms'); return str; } From b2626ce758f75394f8ca404141bfa01ed8dc4c34 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 11 Apr 2018 17:56:21 +1200 Subject: [PATCH 11/16] Added callback to chars function which will always be called once it completes. minifyJS and minifyCSS function are now passed a callback function. --- src/htmlminifier.js | 93 ++++++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index c3aed0ba..3e1c41cd 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -1107,7 +1107,7 @@ function minify(value, options, partialMarkup, cb) { } } }, - chars: function(text, prevTag, nextTag) { + chars: function(text, prevTag, nextTag, cb) { prevTag = prevTag === '' ? 'comment' : prevTag; nextTag = nextTag === '' ? 'comment' : nextTag; if (options.decodeEntities && text && !specialContentTags(currentTag)) { @@ -1161,42 +1161,75 @@ function minify(value, options, partialMarkup, cb) { if (options.processScripts && specialContentTags(currentTag)) { text = processScript(text, options, currentAttrs); } + + var tasksWaitingFor = 0, tasksComplete = 0; + + function onTaskFinished(result) { + text = result; + if (tasksWaitingFor === ++tasksComplete) { + afterTasksFinish(); + } + } + if (isExecutableScript(currentTag, currentAttrs)) { - text = options.minifyJS(text); + tasksWaitingFor++; + var minifyJSResult = options.minifyJS(text, null, onTaskFinished); + + // If the result is defined then minifyJSResult completed synchronously. + // eslint-disable-next-line no-undefined + if (minifyJSResult !== undefined) { + onTaskFinished(minifyJSResult); + } } + if (isStyleSheet(currentTag, currentAttrs)) { - text = options.minifyCSS(text); - } - if (options.removeOptionalTags && text) { - // may be omitted if first thing inside is not comment - // may be omitted if first thing inside is not space, comment, , ,