├── v8 └── test.js ├── tests ├── index.html ├── lint-tests.html ├── lint_test.js ├── qunit.css ├── minify_test.js └── qunit.js ├── README.md ├── package.json ├── src ├── license.txt ├── htmllint.js ├── htmlparser.js └── htmlminifier.js ├── master.css ├── master.js └── index.html /v8/test.js: -------------------------------------------------------------------------------- 1 | load('../src/htmlparser.js'); 2 | load('../src/htmlminifier.js'); 3 | 4 | var input = read('test.html'), 5 | t1 = new Date(), 6 | output = minify(input); 7 | 8 | print('minified in: ' + (new Date() - t1) + 'ms'); -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

HTML Minifier

14 |

15 |

16 |
    17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/lint-tests.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

    HTML Lint

    15 |

    16 |

    17 |
      18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [HTMLMinifier](http://kangax.github.com/html-minifier/) is a Javascript-based HTML minifier (duh), with lint-like capabilities. 2 | 3 | Installing with [npm](https://github.com/isaacs/npm): 4 | 5 |
       6 | npm install html-minifier
       7 | 
      8 | 9 | See [corresponding blog post](http://perfectionkills.com/experimenting-with-html-minifier/) for all the gory details of [how it works](http://perfectionkills.com/experimenting-with-html-minifier/#how_it_works), [description of each option](http://perfectionkills.com/experimenting-with-html-minifier/#options), [testing results](http://perfectionkills.com/experimenting-with-html-minifier/#field_testing) and [conclusions](http://perfectionkills.com/experimenting-with-html-minifier/#cost_and_benefits). 10 | 11 | [Test suite is available online](http://kangax.github.com/html-minifier/tests/index.html). -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-minifier", 3 | "description": "HTML minifier with lint-like capabilities.", 4 | "version": "0.4.5", 5 | "keywords": ["html", "minifier", "lint"], 6 | "url" : "http://github.com/kangax/html-minifier", 7 | "maintainers": [{ 8 | "name": "Juriy Zaytsev", 9 | "email": "kangax@gmail.com", 10 | "web": "http://perfectionkills.com" 11 | }], 12 | "contributors": [{ 13 | "name": "Gilmore Davidson", 14 | "web": "https://github.com/gilmoreorless" 15 | }, 16 | { 17 | "name": "Hugo Wetterberg", 18 | "email": "hugo@wetterberg.nu" 19 | }], 20 | "licenses": [{ 21 | "type": "MIT", 22 | "url": "https://github.com/kangax/html-minifier/blob/gh-pages/src/license.txt" 23 | }], 24 | "repository": "git://github.com/kangax/html-minifier", 25 | "engines": { 26 | "node": ">=0.4.8" 27 | }, 28 | "directories": { 29 | "src": "./src" 30 | }, 31 | "main": "./src/htmlminifier.js" 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Juriy "kangax" Zaytsev 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tests/lint_test.js: -------------------------------------------------------------------------------- 1 | (function(global){ 2 | 3 | var minify, QUnit, 4 | test, equal, ok, 5 | input, output, HTMLLint, lint; 6 | 7 | if (typeof require === 'function') { 8 | QUnit = require('./qunit'); 9 | minify = require('../src/htmlminifier').minify; 10 | HTMLLint = require('../src/htmllint').HTMLLint; 11 | } else { 12 | QUnit = global.QUnit; 13 | minify = global.minify; 14 | HTMLLint = global.HTMLLint; 15 | } 16 | 17 | test = QUnit.test; 18 | equal = QUnit.equal; 19 | ok = QUnit.ok; 20 | 21 | QUnit.module('', { 22 | setup: function() { 23 | lint = new HTMLLint(); 24 | } 25 | }); 26 | 27 | test('lint exists', function() { 28 | ok(typeof lint !== 'undefined'); 29 | }); 30 | 31 | test('lint is instance of HTMLLint', function() { 32 | ok(lint instanceof HTMLLint); 33 | }); 34 | 35 | test('lint API', function() { 36 | equal(0, lint.log.length, '`log` property exists'); 37 | equal("function", typeof lint.populate, '`populate` method exists'); 38 | equal("function", typeof lint.test, '`test` method exists'); 39 | equal("function", typeof lint.testElement, '`testElement` method exists'); 40 | equal("function", typeof lint.testAttribute, '`testAttribute` method exists'); 41 | }); 42 | 43 | test('deprecated element (font)', function(){ 44 | minify('foo', { lint: lint }); 45 | var log = lint.log.join(''); 46 | 47 | ok(log.indexOf('font') > -1); 48 | ok(log.indexOf('deprecated') > -1); 49 | ok(log.indexOf('element') > -1); 50 | }); 51 | 52 | }(this)); -------------------------------------------------------------------------------- /master.css: -------------------------------------------------------------------------------- 1 | body { font-family: "Cambria", Georgia, Times, "Times New Roman", serif; margin-top: 0; padding-top: 0; } 2 | textarea { height: 30em; } 3 | h1 { margin-top: 0.5em; font-size: 1.25em; } 4 | button { font-weight: bold; width: 100px; } 5 | 6 | #outer-wrapper { overflow: hidden; } 7 | #wrapper { width: 65%; float: left; } 8 | #input { width: 99%; height: 18em; } 9 | #output { width: 99%; height: 18em; margin-bottom: 2em; } 10 | #options { float: right; width: 33%; padding-left: 1em; margin-top: 3em; } 11 | #options ul { list-style: none; padding: 0.5em; overflow: hidden; background: #ffe; margin-top: 0; } 12 | #options ul li { float: left; clear: both; padding-bottom: 0.5em; } 13 | #options ul li div { margin-left: 1.75em; } 14 | #options label, #options input { float: left; } 15 | #options input { margin-right: 0.5em; } 16 | #stats { margin-bottom: 2em; overflow: hidden; margin-top: 0; } 17 | #todo { font-family: monospace; margin-bottom: 2em; } 18 | #warning { background: #fcc; padding: 0.25em; display: inline-block; margin-top: 0; font-size: 0.85em; } 19 | #lint-report { font-family: monospace; } 20 | #report { margin-bottom: 5em; } 21 | #report ul { margin: 0.5em; padding-left: 1em; list-style: none; } 22 | 23 | .success { color: green; } 24 | .failure { color: red; } 25 | .quiet { font-size: 0.85em; color: #888; } 26 | .short { display: inline-block; width: 20em; margin-top: 0.25em; } 27 | 28 | .controls span { margin-right: 0.5em; margin-left: 1em; } 29 | .controls a { margin-left: 0.1em; } 30 | .controls a:focus, .controls a:hover { text-decoration: none; } 31 | 32 | .deprecated-element, .deprecated-attribute { color: red; } 33 | .presentational-element, .presentational-attribute, .inaccessible-attribute { color: #FF8C00; } 34 | 35 | .unsafe { color: #f33; } -------------------------------------------------------------------------------- /tests/qunit.css: -------------------------------------------------------------------------------- 1 | ol#qunit-tests { 2 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 3 | margin:0; 4 | padding:0; 5 | list-style-position:inside; 6 | 7 | font-size: smaller; 8 | } 9 | ol#qunit-tests li{ 10 | padding:0.4em 0.5em 0.4em 2.5em; 11 | border-bottom:1px solid #fff; 12 | font-size:small; 13 | list-style-position:inside; 14 | } 15 | ol#qunit-tests li ol{ 16 | box-shadow: inset 0px 2px 13px #999; 17 | -moz-box-shadow: inset 0px 2px 13px #999; 18 | -webkit-box-shadow: inset 0px 2px 13px #999; 19 | margin-top:0.5em; 20 | margin-left:0; 21 | padding:0.5em; 22 | background-color:#fff; 23 | border-radius:15px; 24 | -moz-border-radius: 15px; 25 | -webkit-border-radius: 15px; 26 | } 27 | ol#qunit-tests li li{ 28 | border-bottom:none; 29 | margin:0.5em; 30 | background-color:#fff; 31 | list-style-position: inside; 32 | padding:0.4em 0.5em 0.4em 0.5em; 33 | } 34 | 35 | ol#qunit-tests li li.pass{ 36 | border-left:26px solid #C6E746; 37 | background-color:#fff; 38 | color:#5E740B; 39 | } 40 | ol#qunit-tests li li.fail{ 41 | border-left:26px solid #EE5757; 42 | background-color:#fff; 43 | color:#710909; 44 | } 45 | ol#qunit-tests li.pass{ 46 | background-color:#D2E0E6; 47 | color:#528CE0; 48 | } 49 | ol#qunit-tests li.fail{ 50 | background-color:#EE5757; 51 | color:#000; 52 | } 53 | ol#qunit-tests li strong { 54 | cursor:pointer; 55 | } 56 | h1#qunit-header{ 57 | background-color:#0d3349; 58 | margin:0; 59 | padding:0.5em 0 0.5em 1em; 60 | color:#fff; 61 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 62 | border-top-right-radius:15px; 63 | border-top-left-radius:15px; 64 | -moz-border-radius-topright:15px; 65 | -moz-border-radius-topleft:15px; 66 | -webkit-border-top-right-radius:15px; 67 | -webkit-border-top-left-radius:15px; 68 | text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; 69 | } 70 | h2#qunit-banner{ 71 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 72 | height:5px; 73 | margin:0; 74 | padding:0; 75 | } 76 | h2#qunit-banner.qunit-pass{ 77 | background-color:#C6E746; 78 | } 79 | h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { 80 | background-color:#EE5757; 81 | } 82 | #qunit-testrunner-toolbar { 83 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 84 | padding:0; 85 | /*width:80%;*/ 86 | padding:0em 0 0.5em 2em; 87 | font-size: small; 88 | } 89 | h2#qunit-userAgent { 90 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 91 | background-color:#2b81af; 92 | margin:0; 93 | padding:0; 94 | color:#fff; 95 | font-size: small; 96 | padding:0.5em 0 0.5em 2.5em; 97 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 98 | } 99 | p#qunit-testresult{ 100 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 101 | margin:0; 102 | font-size: small; 103 | color:#2b81af; 104 | border-bottom-right-radius:15px; 105 | border-bottom-left-radius:15px; 106 | -moz-border-radius-bottomright:15px; 107 | -moz-border-radius-bottomleft:15px; 108 | -webkit-border-bottom-right-radius:15px; 109 | -webkit-border-bottom-left-radius:15px; 110 | background-color:#D2E0E6; 111 | padding:0.5em 0.5em 0.5em 2.5em; 112 | } 113 | strong b.fail{ 114 | color:#710909; 115 | } 116 | strong b.pass{ 117 | color:#5E740B; 118 | } -------------------------------------------------------------------------------- /master.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | function byId(id) { 4 | return document.getElementById(id); 5 | } 6 | 7 | function escapeHTML(str) { 8 | return (str + '').replace(/&/g, '&').replace(//g, '>'); 9 | } 10 | 11 | function getOptions() { 12 | return { 13 | removeComments: byId('remove-comments').checked, 14 | removeCommentsFromCDATA: byId('remove-comments-from-cdata').checked, 15 | removeCDATASectionsFromCDATA: byId('remove-cdata-sections-from-cdata').checked, 16 | collapseWhitespace: byId('collapse-whitespace').checked, 17 | collapseBooleanAttributes: byId('collapse-boolean-attributes').checked, 18 | removeAttributeQuotes: byId('remove-attribute-quotes').checked, 19 | removeRedundantAttributes: byId('remove-redundant-attributes').checked, 20 | useShortDoctype: byId('use-short-doctype').checked, 21 | removeEmptyAttributes: byId('remove-empty-attributes').checked, 22 | removeEmptyElements: byId('remove-empty-elements').checked, 23 | removeOptionalTags: byId('remove-optional-tags').checked, 24 | removeScriptTypeAttributes: byId('remove-script-type-attributes').checked, 25 | removeStyleLinkTypeAttributes: byId('remove-style-link-type-attributes').checked, 26 | lint: byId('use-htmllint').checked ? new HTMLLint() : null 27 | }; 28 | } 29 | 30 | function commify(str) { 31 | return String(str) 32 | .split('').reverse().join('') 33 | .replace(/(...)(?!$)/g, '$1,') 34 | .split('').reverse().join(''); 35 | } 36 | 37 | byId('minify-btn').onclick = function() { 38 | try { 39 | var options = getOptions(), 40 | lint = options.lint, 41 | originalValue = byId('input').value, 42 | minifiedValue = minify(originalValue, options), 43 | diff = originalValue.length - minifiedValue.length, 44 | savings = originalValue.length ? ((100 * diff) / originalValue.length).toFixed(2) : 0; 45 | 46 | byId('output').value = minifiedValue; 47 | 48 | byId('stats').innerHTML = 49 | '' + 50 | 'Original size: ' + commify(originalValue.length) + '' + 51 | '. Minified size: ' + commify(minifiedValue.length) + '' + 52 | '. Savings: ' + commify(diff) + ' (' + savings + '%).' + 53 | ''; 54 | 55 | if (lint) { 56 | lint.populate(byId('report')); 57 | } 58 | } 59 | catch(err) { 60 | byId('output').value = ''; 61 | byId('stats').innerHTML = '' + escapeHTML(err) + ''; 62 | } 63 | }; 64 | 65 | function setCheckedAttrOnCheckboxes(attrValue) { 66 | var checkboxes = byId('options').getElementsByTagName('input'); 67 | for (var i = checkboxes.length; i--; ) { 68 | checkboxes[i].checked = attrValue; 69 | } 70 | } 71 | 72 | byId('select-all').onclick = function() { 73 | setCheckedAttrOnCheckboxes(true); 74 | return false; 75 | }; 76 | 77 | byId('select-none').onclick = function() { 78 | setCheckedAttrOnCheckboxes(false); 79 | return false; 80 | }; 81 | 82 | byId('select-safe').onclick = function() { 83 | setCheckedAttrOnCheckboxes(true); 84 | var inputEls = byId('options').getElementsByTagName('input'); 85 | inputEls[10].checked = false; 86 | inputEls[11].checked = false; 87 | return false; 88 | }; 89 | 90 | })(); 91 | 92 | var _gaq = _gaq || []; 93 | _gaq.push(['_setAccount', 'UA-1128111-22']); 94 | _gaq.push(['_trackPageview']); 95 | 96 | (function() { 97 | var ga = document.createElement('script'); 98 | ga.type = 'text/javascript'; 99 | ga.async = true; 100 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 101 | document.getElementsByTagName('head')[0].appendChild(ga); 102 | })(); -------------------------------------------------------------------------------- /src/htmllint.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * HTMLLint (to be used in conjunction with HTMLMinifier) 3 | * 4 | * Copyright (c) 2010 Juriy "kangax" Zaytsev 5 | * Licensed under the MIT license. 6 | * 7 | */ 8 | 9 | (function(global){ 10 | 11 | function isPresentationalElement(tag) { 12 | return (/^(?:b|i|big|small|hr|blink|marquee)$/).test(tag); 13 | } 14 | function isDeprecatedElement(tag) { 15 | return (/^(?:applet|basefont|center|dir|font|isindex|menu|s|strike|u)$/).test(tag); 16 | } 17 | function isEventAttribute(attrName) { 18 | return (/^on[a-z]+/).test(attrName); 19 | } 20 | function isStyleAttribute(attrName) { 21 | return ('style' === attrName.toLowerCase()); 22 | } 23 | function isDeprecatedAttribute(tag, attrName) { 24 | return ( 25 | (attrName === 'align' && 26 | (/^(?:caption|applet|iframe|img|imput|object|legend|table|hr|div|h[1-6]|p)$/).test(tag)) || 27 | (attrName === 'alink' && tag === 'body') || 28 | (attrName === 'alt' && tag === 'applet') || 29 | (attrName === 'archive' && tag === 'applet') || 30 | (attrName === 'background' && tag === 'body') || 31 | (attrName === 'bgcolor' && (/^(?:table|t[rdh]|body)$/).test(tag)) || 32 | (attrName === 'border' && (/^(?:img|object)$/).test(tag)) || 33 | (attrName === 'clear' && tag === 'br') || 34 | (attrName === 'code' && tag === 'applet') || 35 | (attrName === 'codebase' && tag === 'applet') || 36 | (attrName === 'color' && (/^(?:base(?:font)?)$/).test(tag)) || 37 | (attrName === 'compact' && (/^(?:dir|[dou]l|menu)$/).test(tag)) || 38 | (attrName === 'face' && (/^base(?:font)?$/).test(tag)) || 39 | (attrName === 'height' && (/^(?:t[dh]|applet)$/).test(tag)) || 40 | (attrName === 'hspace' && (/^(?:applet|img|object)$/).test(tag)) || 41 | (attrName === 'language' && tag === 'script') || 42 | (attrName === 'link' && tag === 'body') || 43 | (attrName === 'name' && tag === 'applet') || 44 | (attrName === 'noshade' && tag === 'hr') || 45 | (attrName === 'nowrap' && (/^t[dh]$/).test(tag)) || 46 | (attrName === 'object' && tag === 'applet') || 47 | (attrName === 'prompt' && tag === 'isindex') || 48 | (attrName === 'size' && (/^(?:hr|font|basefont)$/).test(tag)) || 49 | (attrName === 'start' && tag === 'ol') || 50 | (attrName === 'text' && tag === 'body') || 51 | (attrName === 'type' && (/^(?:li|ol|ul)$/).test(tag)) || 52 | (attrName === 'value' && tag === 'li') || 53 | (attrName === 'version' && tag === 'html') || 54 | (attrName === 'vlink' && tag === 'body') || 55 | (attrName === 'vspace' && (/^(?:applet|img|object)$/).test(tag)) || 56 | (attrName === 'width' && (/^(?:hr|td|th|applet|pre)$/).test(tag)) 57 | ); 58 | } 59 | function isInaccessibleAttribute(attrName, attrValue) { 60 | return ( 61 | attrName === 'href' && 62 | (/^\s*javascript\s*:\s*void\s*(\s+0|\(\s*0\s*\))\s*$/i).test(attrValue) 63 | ); 64 | } 65 | 66 | function Lint() { 67 | this.log = [ ]; 68 | this._lastElement = null; 69 | this._isElementRepeated = false; 70 | } 71 | 72 | Lint.prototype.testElement = function(tag) { 73 | if (isDeprecatedElement(tag)) { 74 | this.log.push( 75 | '
    1. Found deprecated <' + 76 | tag + '> element
    2. '); 77 | } 78 | else if (isPresentationalElement(tag)) { 79 | this.log.push( 80 | '
    3. Found presentational <' + 81 | tag + '> element
    4. '); 82 | } 83 | else { 84 | this.checkRepeatingElement(tag); 85 | } 86 | }; 87 | 88 | Lint.prototype.checkRepeatingElement = function(tag) { 89 | if (tag === 'br' && this._lastElement === 'br') { 90 | this._isElementRepeated = true; 91 | } 92 | else if (this._isElementRepeated) { 93 | this._reportRepeatingElement(); 94 | this._isElementRepeated = false; 95 | } 96 | this._lastElement = tag; 97 | }; 98 | 99 | Lint.prototype._reportRepeatingElement = function() { 100 | this.log.push('
    5. Found <br> sequence. Try replacing it with styling.
    6. '); 101 | }; 102 | 103 | Lint.prototype.testAttribute = function(tag, attrName, attrValue) { 104 | if (isEventAttribute(attrName)) { 105 | this.log.push( 106 | '
    7. Found event attribute (', 107 | attrName, ') on <' + tag + '> element
    8. '); 108 | } 109 | else if (isDeprecatedAttribute(tag, attrName)) { 110 | this.log.push( 111 | '
    9. Found deprecated ' + 112 | attrName + ' attribute on <', tag, '> element
    10. '); 113 | } 114 | else if (isStyleAttribute(attrName)) { 115 | this.log.push( 116 | '
    11. Found style attribute on <', tag, '> element
    12. '); 117 | } 118 | else if (isInaccessibleAttribute(attrName, attrValue)) { 119 | this.log.push( 120 | '
    13. Found inaccessible attribute '+ 121 | '(on <', tag, '> element)
    14. '); 122 | } 123 | }; 124 | 125 | Lint.prototype.testChars = function(chars) { 126 | this._lastElement = ''; 127 | if (/( \s*){2,}/.test(chars)) { 128 | this.log.push('
    15. Found repeating &nbsp; sequence. Try replacing it with styling.
    16. '); 129 | } 130 | }; 131 | 132 | Lint.prototype.test = function(tag, attrName, attrValue) { 133 | this.testElement(tag); 134 | this.testAttribute(tag, attrName, attrValue); 135 | }; 136 | 137 | Lint.prototype.populate = function(writeToElement) { 138 | if (this._isElementRepeated) { 139 | this._reportRepeatingElement(); 140 | } 141 | var report; 142 | if (this.log.length && writeToElement) { 143 | report = '
        ' + this.log.join('') + '
      '; 144 | writeToElement.innerHTML = report; 145 | } 146 | }; 147 | 148 | global.HTMLLint = Lint; 149 | 150 | })(typeof exports === 'undefined' ? this : exports); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | HTML minifier 7 | 8 | 9 | 10 |
      11 |
      12 |
      13 |

      HTML Minifier (ver. 0.43)

      14 |

      15 | Minifier is very draft and is not yet thoroughly tested. Use at your own risk. 16 |

      17 | 18 |

      19 | 20 |

      21 | 22 |
      23 |
      24 |
        25 |
      • 26 | 27 | 28 | 29 | Conditional comments are left intact, but their inner (insignificant) whitespace is removed. 30 | 31 |
      • 32 |
      • 33 | 34 | 35 |
      • 36 |
      • 37 | 38 | 39 |
      • 40 |
      • 41 | 42 | 43 |
      • 44 |
      • 45 | 46 | 53 |
      • 54 |
      • 55 | 56 | 63 |
      • 64 |
      • 65 | 66 | 67 |
        68 | <script language="Javascript" ...>
        69 | <form method="get" ...>
        70 | <input type="text" ...>
        71 | <script src="..." charset="...">
        72 | <a id="..." name="...">
        73 | <... onclick="javascript:..." ...> 74 |
        75 |
      • 76 |
      • 77 | 78 | 81 |
      • 82 |
      • 83 | 84 | 91 |
      • 92 |
      • 93 | 94 | 110 |
      • 111 |
      • 112 | 113 | 120 |
      • 121 |
      • 122 | 123 | 130 |
      • 131 |
      • 132 | 133 | 140 |
      • 141 |
      • 142 | 143 | 146 |
      • 147 |
      148 |
      149 | Select: 150 | All, 151 | None, 152 | Safe 153 |
      154 |
      155 |
      156 |

      157 |
      158 | LINT REPORT: 159 |
      160 |
      161 |

      162 | HTMLMinifier is made by kangax, 163 | using tweaked version of HTML parser by John Resig 164 | (which, in its turn, is based on work of Erik Arvidsson). 165 | Source and bugtracker are hosted on Github. 166 |

      167 |
      168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/htmlparser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HTML Parser By John Resig (ejohn.org) 3 | * Modified by Juriy "kangax" Zaytsev 4 | * Original code by Erik Arvidsson, Mozilla Public License 5 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 6 | * 7 | * // Use like so: 8 | * HTMLParser(htmlString, { 9 | * start: function(tag, attrs, unary) {}, 10 | * end: function(tag) {}, 11 | * chars: function(text) {}, 12 | * comment: function(text) {} 13 | * }); 14 | * 15 | * // or to get an XML string: 16 | * HTMLtoXML(htmlString); 17 | * 18 | * // or to get an XML DOM Document 19 | * HTMLtoDOM(htmlString); 20 | * 21 | * // or to inject into an existing document/DOM node 22 | * HTMLtoDOM(htmlString, document); 23 | * HTMLtoDOM(htmlString, document.body); 24 | * 25 | */ 26 | 27 | (function(global){ 28 | 29 | // Regular Expressions for parsing tags and attributes 30 | var startTag = /^<(\w+)((?:\s*[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, 31 | endTag = /^<\/(\w+)[^>]*>/, 32 | attr = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g, 33 | doctype = /^]+>/i; 34 | 35 | // Empty Elements - HTML 4.01 36 | var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed"); 37 | 38 | // Block Elements - HTML 4.01 39 | var block = makeMap("address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul"); 40 | 41 | // Inline Elements - HTML 4.01 42 | var inline = makeMap("a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var"); 43 | 44 | // Elements that you can, intentionally, leave open 45 | // (and which close themselves) 46 | var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr"); 47 | 48 | // Attributes that have their values filled in disabled="disabled" 49 | var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected"); 50 | 51 | // Special Elements (can contain anything) 52 | var special = makeMap("script,style"); 53 | 54 | var reCache = { }, stackedTag, re; 55 | 56 | var HTMLParser = global.HTMLParser = function( html, handler ) { 57 | var index, chars, match, stack = [], last = html; 58 | stack.last = function(){ 59 | return this[ this.length - 1 ]; 60 | }; 61 | 62 | while ( html ) { 63 | chars = true; 64 | 65 | // Make sure we're not in a script or style element 66 | if ( !stack.last() || !special[ stack.last() ] ) { 67 | 68 | // Comment 69 | if ( html.indexOf(""); 71 | 72 | if ( index >= 0 ) { 73 | if ( handler.comment ) 74 | handler.comment( html.substring( 4, index ) ); 75 | html = html.substring( index + 3 ); 76 | chars = false; 77 | } 78 | } 79 | else if ( match = doctype.exec( html )) { 80 | if ( handler.doctype ) 81 | handler.doctype( match[0] ); 82 | html = html.substring( match[0].length ); 83 | chars = false; 84 | 85 | // end tag 86 | } else if ( html.indexOf("]*>", "i")); 120 | 121 | html = html.replace(reStackedTag, function(all, text) { 122 | if (stackedTag !== 'script' && stackedTag !== 'style') { 123 | text = text 124 | .replace(//g, "$1") 125 | .replace(//g, "$1"); 126 | } 127 | 128 | if ( handler.chars ) 129 | handler.chars( text ); 130 | 131 | return ""; 132 | }); 133 | 134 | parseEndTag( "", stackedTag ); 135 | } 136 | 137 | if ( html == last ) 138 | throw "Parse Error: " + html; 139 | last = html; 140 | } 141 | 142 | // Clean up any remaining tags 143 | parseEndTag(); 144 | 145 | function parseStartTag( tag, tagName, rest, unary ) { 146 | if ( block[ tagName ] ) { 147 | while ( stack.last() && inline[ stack.last() ] ) { 148 | parseEndTag( "", stack.last() ); 149 | } 150 | } 151 | 152 | if ( closeSelf[ tagName ] && stack.last() == tagName ) { 153 | parseEndTag( "", tagName ); 154 | } 155 | 156 | unary = empty[ tagName ] || !!unary; 157 | 158 | if ( !unary ) 159 | stack.push( tagName ); 160 | 161 | if ( handler.start ) { 162 | var attrs = []; 163 | 164 | rest.replace(attr, function(match, name) { 165 | var value = arguments[2] ? arguments[2] : 166 | arguments[3] ? arguments[3] : 167 | arguments[4] ? arguments[4] : 168 | fillAttrs[name] ? name : ""; 169 | attrs.push({ 170 | name: name, 171 | value: value, 172 | escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" 173 | }); 174 | }); 175 | 176 | if ( handler.start ) 177 | handler.start( tagName, attrs, unary ); 178 | } 179 | } 180 | 181 | function parseEndTag( tag, tagName ) { 182 | // If no tag name is provided, clean shop 183 | if ( !tagName ) 184 | var pos = 0; 185 | 186 | // Find the closest opened tag of the same type 187 | else 188 | for ( var pos = stack.length - 1; pos >= 0; pos-- ) 189 | if ( stack[ pos ] == tagName ) 190 | break; 191 | 192 | if ( pos >= 0 ) { 193 | // Close all the open elements, up the stack 194 | for ( var i = stack.length - 1; i >= pos; i-- ) 195 | if ( handler.end ) 196 | handler.end( stack[ i ] ); 197 | 198 | // Remove the open elements from the stack 199 | stack.length = pos; 200 | } 201 | } 202 | }; 203 | 204 | global.HTMLtoXML = function( html ) { 205 | var results = ""; 206 | 207 | HTMLParser(html, { 208 | start: function( tag, attrs, unary ) { 209 | results += "<" + tag; 210 | 211 | for ( var i = 0; i < attrs.length; i++ ) 212 | results += " " + attrs[i].name + '="' + attrs[i].escaped + '"'; 213 | 214 | results += (unary ? "/" : "") + ">"; 215 | }, 216 | end: function( tag ) { 217 | results += ""; 218 | }, 219 | chars: function( text ) { 220 | results += text; 221 | }, 222 | comment: function( text ) { 223 | results += ""; 224 | } 225 | }); 226 | 227 | return results; 228 | }; 229 | 230 | global.HTMLtoDOM = function( html, doc ) { 231 | // There can be only one of these elements 232 | var one = makeMap("html,head,body,title"); 233 | 234 | // Enforce a structure for the document 235 | var structure = { 236 | link: "head", 237 | base: "head" 238 | }; 239 | 240 | if ( !doc ) { 241 | if ( typeof DOMDocument != "undefined" ) 242 | doc = new DOMDocument(); 243 | else if ( typeof document != "undefined" && document.implementation && document.implementation.createDocument ) 244 | doc = document.implementation.createDocument("", "", null); 245 | else if ( typeof ActiveX != "undefined" ) 246 | doc = new ActiveXObject("Msxml.DOMDocument"); 247 | 248 | } else 249 | doc = doc.ownerDocument || 250 | doc.getOwnerDocument && doc.getOwnerDocument() || 251 | doc; 252 | 253 | var elems = [], 254 | documentElement = doc.documentElement || 255 | doc.getDocumentElement && doc.getDocumentElement(); 256 | 257 | // If we're dealing with an empty document then we 258 | // need to pre-populate it with the HTML document structure 259 | if ( !documentElement && doc.createElement ) (function(){ 260 | var html = doc.createElement("html"); 261 | var head = doc.createElement("head"); 262 | head.appendChild( doc.createElement("title") ); 263 | html.appendChild( head ); 264 | html.appendChild( doc.createElement("body") ); 265 | doc.appendChild( html ); 266 | })(); 267 | 268 | // Find all the unique elements 269 | if ( doc.getElementsByTagName ) 270 | for ( var i in one ) 271 | one[ i ] = doc.getElementsByTagName( i )[0]; 272 | 273 | // If we're working with a document, inject contents into 274 | // the body element 275 | var curParentNode = one.body; 276 | 277 | HTMLParser( html, { 278 | start: function( tagName, attrs, unary ) { 279 | // If it's a pre-built element, then we can ignore 280 | // its construction 281 | if ( one[ tagName ] ) { 282 | curParentNode = one[ tagName ]; 283 | return; 284 | } 285 | 286 | var elem = doc.createElement( tagName ); 287 | 288 | for ( var attr in attrs ) 289 | elem.setAttribute( attrs[ attr ].name, attrs[ attr ].value ); 290 | 291 | if ( structure[ tagName ] && typeof one[ structure[ tagName ] ] != "boolean" ) 292 | one[ structure[ tagName ] ].appendChild( elem ); 293 | 294 | else if ( curParentNode && curParentNode.appendChild ) 295 | curParentNode.appendChild( elem ); 296 | 297 | if ( !unary ) { 298 | elems.push( elem ); 299 | curParentNode = elem; 300 | } 301 | }, 302 | end: function( tag ) { 303 | elems.length -= 1; 304 | 305 | // Init the new parentNode 306 | curParentNode = elems[ elems.length - 1 ]; 307 | }, 308 | chars: function( text ) { 309 | curParentNode.appendChild( doc.createTextNode( text ) ); 310 | }, 311 | comment: function( text ) { 312 | // create comment node 313 | } 314 | }); 315 | 316 | return doc; 317 | }; 318 | 319 | function makeMap(str){ 320 | var obj = {}, items = str.split(","); 321 | for ( var i = 0; i < items.length; i++ ) { 322 | obj[ items[i] ] = true; 323 | obj[ items[i].toUpperCase() ] = true; 324 | } 325 | return obj; 326 | } 327 | })(typeof exports === 'undefined' ? this : exports); -------------------------------------------------------------------------------- /src/htmlminifier.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * HTMLMinifier v0.4.4 3 | * http://kangax.github.com/html-minifier/ 4 | * 5 | * Copyright (c) 2010 Juriy "kangax" Zaytsev 6 | * Licensed under the MIT license. 7 | * 8 | */ 9 | 10 | (function(global){ 11 | 12 | var log, HTMLParser; 13 | if (global.console && global.console.log) { 14 | log = function(message) { 15 | // "preserving" `this` 16 | global.console.log(message); 17 | }; 18 | } 19 | else { 20 | log = function(){ }; 21 | } 22 | 23 | if (global.HTMLParser) { 24 | HTMLParser = global.HTMLParser; 25 | } 26 | else if (typeof require === 'function') { 27 | HTMLParser = require('./htmlparser').HTMLParser; 28 | } 29 | 30 | function trimWhitespace(str) { 31 | return str.replace(/^\s+/, '').replace(/\s+$/, ''); 32 | } 33 | if (String.prototype.trim) { 34 | trimWhitespace = function(str) { 35 | return str.trim(); 36 | }; 37 | } 38 | 39 | function collapseWhitespace(str) { 40 | return str.replace(/\s+/g, ' '); 41 | } 42 | 43 | function isConditionalComment(text) { 44 | return (/\[if[^\]]+\]/).test(text); 45 | } 46 | 47 | function isEventAttribute(attrName) { 48 | return (/^on[a-z]+/).test(attrName); 49 | } 50 | 51 | function canRemoveAttributeQuotes(value) { 52 | // http://www.w3.org/TR/html4/intro/sgmltut.html#attributes 53 | // avoid \w, which could match unicode in some implementations 54 | return (/^[a-zA-Z0-9-._:]+$/).test(value); 55 | } 56 | 57 | function attributesInclude(attributes, attribute) { 58 | for (var i = attributes.length; i--; ) { 59 | if (attributes[i].name.toLowerCase() === attribute) { 60 | return true; 61 | } 62 | } 63 | return false; 64 | } 65 | 66 | function isAttributeRedundant(tag, attrName, attrValue, attrs) { 67 | attrValue = trimWhitespace(attrValue.toLowerCase()); 68 | return ( 69 | (tag === 'script' && 70 | attrName === 'language' && 71 | attrValue === 'javascript') || 72 | 73 | (tag === 'form' && 74 | attrName === 'method' && 75 | attrValue === 'get') || 76 | 77 | (tag === 'input' && 78 | attrName === 'type' && 79 | attrValue === 'text') || 80 | 81 | (tag === 'script' && 82 | attrName === 'charset' && 83 | !attributesInclude(attrs, 'src')) || 84 | 85 | (tag === 'a' && 86 | attrName === 'name' && 87 | attributesInclude(attrs, 'id')) || 88 | 89 | (tag === 'area' && 90 | attrName === 'shape' && 91 | attrValue === 'rect') 92 | ); 93 | } 94 | 95 | function isScriptTypeAttribute(tag, attrName, attrValue) { 96 | return ( 97 | tag === 'script' && 98 | attrName === 'type' && 99 | trimWhitespace(attrValue.toLowerCase()) === 'text/javascript' 100 | ); 101 | } 102 | 103 | function isStyleLinkTypeAttribute(tag, attrName, attrValue) { 104 | return ( 105 | (tag === 'style' || tag === 'link') && 106 | attrName === 'type' && 107 | trimWhitespace(attrValue.toLowerCase()) === 'text/css' 108 | ); 109 | } 110 | 111 | function isBooleanAttribute(attrName) { 112 | return (/^(?:checked|disabled|selected|readonly)$/).test(attrName); 113 | } 114 | 115 | function isUriTypeAttribute(attrName, tag) { 116 | return ( 117 | ((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') || 118 | (tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) || 119 | (tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) || 120 | (tag === 'q' && attrName === 'cite') || 121 | (tag === 'blockquote' && attrName === 'cite') || 122 | ((tag === 'ins' || tag === 'del') && attrName === 'cite') || 123 | (tag === 'form' && attrName === 'action') || 124 | (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) || 125 | (tag === 'head' && attrName === 'profile') || 126 | (tag === 'script' && (attrName === 'src' || attrName === 'for')) 127 | ); 128 | } 129 | 130 | function isNumberTypeAttribute(attrName, tag) { 131 | return ( 132 | ((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') || 133 | (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) || 134 | (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) || 135 | (tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) || 136 | (tag === 'colgroup' && attrName === 'span') || 137 | (tag === 'col' && attrName === 'span') || 138 | ((tag === 'th' || tag == 'td') && (attrName === 'rowspan' || attrName === 'colspan')) 139 | ); 140 | } 141 | 142 | function cleanAttributeValue(tag, attrName, attrValue) { 143 | if (isEventAttribute(attrName)) { 144 | return trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, ''); 145 | } 146 | else if (attrName === 'class') { 147 | return collapseWhitespace(trimWhitespace(attrValue)); 148 | } 149 | else if (isUriTypeAttribute(attrName, tag) || isNumberTypeAttribute(attrName, tag)) { 150 | return trimWhitespace(attrValue); 151 | } 152 | else if (attrName === 'style') { 153 | return trimWhitespace(attrValue).replace(/\s*;\s*$/, ''); 154 | } 155 | return attrValue; 156 | } 157 | 158 | function cleanConditionalComment(comment) { 159 | return comment 160 | .replace(/^(\[[^\]]+\]>)\s*/, '$1') 161 | .replace(/\s*( */" or "// ]]>" 169 | .replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, ''); 170 | } 171 | 172 | var reStartDelimiter = { 173 | // account for js + html comments (e.g.: //\s*$/, 179 | 'style': /\s*-->\s*$/ 180 | }; 181 | function removeComments(text, tag) { 182 | return text.replace(reStartDelimiter[tag], '').replace(reEndDelimiter[tag], ''); 183 | } 184 | 185 | function isOptionalTag(tag) { 186 | return (/^(?:html|t?body|t?head|tfoot|tr|option)$/).test(tag); 187 | } 188 | 189 | var reEmptyAttribute = new RegExp( 190 | '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' + 191 | '?:down|up|over|move|out)|key(?:press|down|up)))$'); 192 | 193 | function canDeleteEmptyAttribute(tag, attrName, attrValue) { 194 | var isValueEmpty = /^(["'])?\s*\1$/.test(attrValue); 195 | if (isValueEmpty) { 196 | return ( 197 | (tag === 'input' && attrName === 'value') || 198 | reEmptyAttribute.test(attrName)); 199 | } 200 | return false; 201 | } 202 | 203 | function canRemoveElement(tag) { 204 | return tag !== 'textarea'; 205 | } 206 | 207 | function canCollapseWhitespace(tag) { 208 | return !(/^(?:script|style|pre|textarea)$/.test(tag)); 209 | } 210 | 211 | function canTrimWhitespace(tag) { 212 | return !(/^(?:pre|textarea)$/.test(tag)); 213 | } 214 | 215 | function normalizeAttribute(attr, attrs, tag, options) { 216 | 217 | var attrName = attr.name.toLowerCase(), 218 | attrValue = attr.escaped, 219 | attrFragment; 220 | 221 | if ((options.removeRedundantAttributes && 222 | isAttributeRedundant(tag, attrName, attrValue, attrs)) 223 | || 224 | (options.removeScriptTypeAttributes && 225 | isScriptTypeAttribute(tag, attrName, attrValue)) 226 | || 227 | (options.removeStyleLinkTypeAttributes && 228 | isStyleLinkTypeAttribute(tag, attrName, attrValue))) { 229 | return ''; 230 | } 231 | 232 | attrValue = cleanAttributeValue(tag, attrName, attrValue); 233 | 234 | if (!options.removeAttributeQuotes || 235 | !canRemoveAttributeQuotes(attrValue)) { 236 | attrValue = '"' + attrValue + '"'; 237 | } 238 | 239 | if (options.removeEmptyAttributes && 240 | canDeleteEmptyAttribute(tag, attrName, attrValue)) { 241 | return ''; 242 | } 243 | 244 | if (options.collapseBooleanAttributes && 245 | isBooleanAttribute(attrName)) { 246 | attrFragment = attrName; 247 | } 248 | else { 249 | attrFragment = attrName + '=' + attrValue; 250 | } 251 | 252 | return (' ' + attrFragment); 253 | } 254 | 255 | 256 | function setDefaultTesters(options) { 257 | 258 | var defaultTesters = ['canCollapseWhitespace','canTrimWhitespace']; 259 | 260 | for (var i = 0, len = defaultTesters.length; i < len; i++) { 261 | if (!options[defaultTesters[i]]) { 262 | options[defaultTesters[i]] = function() { 263 | return false; 264 | } 265 | } 266 | } 267 | } 268 | 269 | function minify(value, options) { 270 | 271 | options = options || { }; 272 | value = trimWhitespace(value); 273 | setDefaultTesters(options); 274 | 275 | var results = [ ], 276 | buffer = [ ], 277 | currentChars = '', 278 | currentTag = '', 279 | currentAttrs = [], 280 | stackNoTrimWhitespace = [], 281 | stackNoCollapseWhitespace = [], 282 | lint = options.lint, 283 | t = new Date() 284 | 285 | function _canCollapseWhitespace(tag, attrs) { 286 | return canCollapseWhitespace(tag) || options.canTrimWhitespace(tag, attrs); 287 | } 288 | 289 | function _canTrimWhitespace(tag, attrs) { 290 | return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs); 291 | } 292 | 293 | HTMLParser(value, { 294 | start: function( tag, attrs, unary ) { 295 | tag = tag.toLowerCase(); 296 | currentTag = tag; 297 | currentChars = ''; 298 | currentAttrs = attrs; 299 | 300 | // set whitespace flags for nested tags (eg. within a
      )
      301 |         if (options.collapseWhitespace) {
      302 |           if (!_canTrimWhitespace(tag, attrs)) {
      303 |             stackNoTrimWhitespace.push(tag);
      304 |           }
      305 |           if (!_canCollapseWhitespace(tag, attrs)) {
      306 |             stackNoCollapseWhitespace.push(tag);
      307 |           }
      308 |         }
      309 |         
      310 |         buffer.push('<', tag);
      311 |         
      312 |         lint && lint.testElement(tag);
      313 |         
      314 |         for ( var i = 0, len = attrs.length; i < len; i++ ) {
      315 |           lint && lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
      316 |           buffer.push(normalizeAttribute(attrs[i], attrs, tag, options));
      317 |         }
      318 |         
      319 |         buffer.push('>');
      320 |       },
      321 |       end: function( tag ) {
      322 |         // check if current tag is in a whitespace stack
      323 |         if (options.collapseWhitespace) {
      324 |           if (stackNoTrimWhitespace.length &&
      325 |             tag == stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
      326 |             stackNoTrimWhitespace.pop();
      327 |           }
      328 |           if (stackNoCollapseWhitespace.length &&
      329 |             tag == stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
      330 |             stackNoCollapseWhitespace.pop();
      331 |           }
      332 |         }
      333 |         
      334 |         var isElementEmpty = currentChars === '' && tag === currentTag;
      335 |         if ((options.removeEmptyElements && isElementEmpty && canRemoveElement(tag))) {
      336 |           // remove last "element" from buffer, return
      337 |           buffer.splice(buffer.lastIndexOf('<'));
      338 |           return;
      339 |         }
      340 |         else if (options.removeOptionalTags && isOptionalTag(tag)) {
      341 |           // noop, leave start tag in buffer
      342 |           return;
      343 |         }
      344 |         else {
      345 |           // push end tag to buffer
      346 |           buffer.push('');
      347 |           results.push.apply(results, buffer);
      348 |         }
      349 |         // flush buffer
      350 |         buffer.length = 0;
      351 |         currentChars = '';
      352 |       },
      353 |       chars: function( text ) {
      354 |         if (currentTag === 'script' || currentTag === 'style') {
      355 |           if (options.removeCommentsFromCDATA) {
      356 |             text = removeComments(text, currentTag);
      357 |           }
      358 |           if (options.removeCDATASectionsFromCDATA) {
      359 |             text = removeCDATASections(text);
      360 |           }
      361 |         }
      362 |         if (options.collapseWhitespace) {
      363 |           if (!stackNoTrimWhitespace.length && _canTrimWhitespace(currentTag, currentAttrs)) {
      364 |             text = trimWhitespace(text);
      365 |           }
      366 |           if (!stackNoCollapseWhitespace.length && _canCollapseWhitespace(currentTag, currentAttrs)) {
      367 |             text = collapseWhitespace(text);
      368 |           }
      369 |         }
      370 |         currentChars = text;
      371 |         lint && lint.testChars(text);
      372 |         buffer.push(text);
      373 |       },
      374 |       comment: function( text ) {
      375 |         if (options.removeComments) {
      376 |           if (isConditionalComment(text)) {
      377 |             text = '';
      378 |           }
      379 |           else {
      380 |             text = '';
      381 |           }
      382 |         }
      383 |         else {
      384 |           text = '';
      385 |         }
      386 |         buffer.push(text);
      387 |       },
      388 |       doctype: function(doctype) {
      389 |         buffer.push(options.useShortDoctype ? '' : collapseWhitespace(doctype));
      390 |       }
      391 |     });  
      392 |     
      393 |     results.push.apply(results, buffer)    
      394 |     var str = results.join('');
      395 |     log('minified in: ' + (new Date() - t) + 'ms');
      396 |     return str;
      397 |   }
      398 | 
      399 |   // for CommonJS enviroments, export everything
      400 |   if ( typeof exports !== "undefined" ) {
      401 |     exports.minify = minify;
      402 |   } else {
      403 |     global.minify = minify;
      404 |   }
      405 | 
      406 | }(this));
      
      
      --------------------------------------------------------------------------------
      /tests/minify_test.js:
      --------------------------------------------------------------------------------
        1 | (function(global){
        2 |   
        3 |   var minify, QUnit, 
        4 |     test, equal, ok,
        5 |     input, output;
        6 | 
        7 |   if (typeof require === 'function') {
        8 |     QUnit = require('./qunit');
        9 |     minify = require('../src/htmlminifier').minify;
       10 |   } else {
       11 |     QUnit = global.QUnit;
       12 |     minify = global.minify;
       13 |   }
       14 | 
       15 |   test = QUnit.test;
       16 |   equal = QUnit.equal;
       17 |   ok = QUnit.ok;
       18 | 
       19 |   test('parsing non-trivial markup', function() {
       20 |     equal(minify('

      x

      '), '

      x

      '); 21 | equal(minify('

      x

      '), '

      x

      '); 22 | equal(minify('

      x

      '), '

      x

      '); 23 | equal(minify('

      xxx

      '), '

      xxx

      '); 24 | equal(minify('

      xxx

      '), '

      xxx

      '); 25 | 26 | input = '
      '+ 27 | 'i\'m 10 levels deep'+ 28 | '
      '; 29 | 30 | equal(minify(input), input); 31 | 32 | equal(minify('