├── requirements.txt ├── htmlmin ├── tests │ ├── __init__.py │ ├── test_escape.py │ └── tests.py ├── __init__.py ├── decorator.py ├── middleware.py ├── python3html │ ├── __init__.py │ ├── LICENSE │ └── parser.py ├── command.py ├── escape.py ├── main.py └── parser.py ├── MANIFEST.in ├── .gitignore ├── docs ├── tutorial.rst ├── reference.rst ├── index.rst ├── make.bat ├── Makefile ├── quickstart.rst └── conf.py ├── tox.ini ├── .travis.yml ├── README.rst ├── CHANGELOG ├── setup.py └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htmlmin/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | prune htmlmin/tests 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | build 4 | _build 5 | *.egg-info 6 | dist 7 | .tox 8 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial & Examples 2 | =================== 3 | 4 | Coming soon... 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27, py33, py34 3 | 4 | [testenv] 5 | commands=python setup.py test 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | install: "pip install -r requirements.txt" 6 | script: "python setup.py test" 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A configurable HTML Minifier with safety features. 2 | 3 | .. image:: https://travis-ci.org/mankyd/htmlmin.png?branch=master 4 | :target: http://travis-ci.org/mankyd/htmlmin 5 | 6 | Documentation: https://htmlmin.readthedocs.io/en/latest/ 7 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Main Functions 5 | -------------- 6 | .. autofunction:: htmlmin.minify 7 | 8 | .. autoclass:: htmlmin.Minifier 9 | :members: 10 | :member-order: bysource 11 | 12 | WSGI Middlware 13 | -------------- 14 | .. autoclass:: htmlmin.middleware.HTMLMinMiddleware 15 | 16 | Decorator 17 | --------- 18 | .. autofunction:: htmlmin.decorator.htmlmin 19 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.1.12 2 | ====== 3 | * Fix bug that can occur when minifying two pages in a row when the 4 | first page has a dangling tag. 5 | 6 | 0.1.11 7 | ====== 8 | * Fix XSS 9 | * Add support for a 'pre' attribute prefix 10 | * Remove redundat 'lang' tags 11 | (thanks mina86) 12 | * Fixed IndexError when data is empty 13 | (thanks mercuree) 14 | 15 | 0.1.10 16 | ====== 17 | * Fix bug in Python 3.5 where char refs were being improperly escaped 18 | (thanks tenzer) 19 | 20 | 0.1.9 21 | ===== 22 | * Fix bug introduced in 0.1.7 involving spaces in attribute values 23 | 24 | 25 | 0.1.8 26 | ===== 27 | * Fix bug introduced in 0.1.7 involving repeated ampersands. 28 | 29 | 0.1.7 30 | ===== 31 | * Improved attribute escaping. Greatly improves compression. 32 | 33 | 0.1.6 34 | ===== 35 | * Always quote attributes that end in "/". (Thanks nvie) 36 | * Use StringIO to speed up string building. (Thanks nvie) 37 | * Typo fixes in documentation. (Thanks aabrahamowicz and Namibnat) 38 | * Keep Microsoft's conditional comments. (Thanks mreinhardt) 39 | * Properly handle empty comments. (Thanks Epsirom) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | from htmlmin import __version__ 5 | 6 | here = os.path.dirname(__file__) 7 | 8 | README = open(os.path.join(here, 'README.rst')).read() 9 | LICENSE = open(os.path.join(here, 'LICENSE')).read() 10 | 11 | setup( 12 | name='htmlmin', 13 | version=__version__, 14 | license='BSD', 15 | description='An HTML Minifier', 16 | long_description=README, 17 | url='https://htmlmin.readthedocs.io/en/latest/', 18 | download_url='https://github.com/mankyd/htmlmin', 19 | author='Dave Mankoff', 20 | author_email='mankyd@gmail.com', 21 | packages=find_packages(), 22 | include_package_data=True, 23 | zip_safe=True, 24 | test_suite='htmlmin.tests.tests.suite', 25 | install_requires=[], 26 | tests_require=[], 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: BSD License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python", 33 | "Programming Language :: Python :: 2.7", 34 | "Programming Language :: Python :: 3.2", 35 | "Topic :: Text Processing :: Markup :: HTML", 36 | ], 37 | entry_points={ 38 | 'console_scripts': [ 39 | 'htmlmin = htmlmin.command:main', 40 | ], 41 | }, 42 | ) 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Dave Mankoff 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Dave Mankoff nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL DAVE MANKOFF BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /htmlmin/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013, Dave Mankoff 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Dave Mankoff nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL DAVE MANKOFF BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | from .main import minify, Minifier 29 | 30 | __version__ = '0.1.12' 31 | -------------------------------------------------------------------------------- /htmlmin/decorator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013, Dave Mankoff 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Dave Mankoff nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL DAVE MANKOFF BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | from .main import Minifier 29 | 30 | def htmlmin(*args, **kwargs): 31 | """Minifies HTML that is returned by a function. 32 | 33 | A simple decorator that minifies the HTML output of any function that it 34 | decorates. It supports all the same options that :class:`htmlmin.minify` has. 35 | With no options, it uses ``minify``'s default settings:: 36 | 37 | @htmlmin 38 | def foobar(): 39 | return ' minify me! ' 40 | 41 | or:: 42 | 43 | @htmlmin(remove_comments=True) 44 | def foobar(): 45 | return ' minify me! ' 46 | """ 47 | def _decorator(fn): 48 | minify = Minifier(**kwargs).minify 49 | def wrapper(*a, **kw): 50 | return minify(fn(*a, **kw)) 51 | return wrapper 52 | 53 | if len(args) == 1: 54 | if callable(args[0]) and not kwargs: 55 | return _decorator(args[0]) 56 | else: 57 | raise RuntimeError( 58 | 'htmlmin decorator does accept positional arguments') 59 | elif len(args) > 1: 60 | raise RuntimeError( 61 | 'htmlmin decorator does accept positional arguments') 62 | else: 63 | return _decorator 64 | 65 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. htmlmin documentation master file, created by 2 | sphinx-quickstart on Thu Feb 14 22:56:34 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | htmlmin 7 | =================================== 8 | An HTML Minifier with Seatbelts 9 | 10 | .. toctree:: 11 | 12 | quickstart 13 | tutorial 14 | reference 15 | 16 | 17 | htmlmin is an HTML minifier that just works. It comes with safe defaults and 18 | an easily configurable set options. It can turn this:: 19 | 20 | 21 | 22 | Hello, World! 23 | 24 | 25 |

How are you doing?

26 | 27 | 28 | 29 | Into this:: 30 | 31 | Hello, World!

How are you doing?

32 | 33 | When we say that htmlmin has 'seatbelts', what we mean is that it comes with 34 | features that you can use to safely minify beyond the defaults, but you have to 35 | put them in yourself. For instance, by default, htmlmin will never minimize the 36 | content between ``
``, `` '),
 64 |     ('   a b  '
 65 |      '
   x 
') 66 | ), 67 | 'with_doctype': ( 68 | '\n\n\n\n X Y ', 69 | ' X Y ' 70 | ), 71 | 'dangling_tag': ( 72 | "

first page", 73 | "

first page" 74 | ), 75 | 'dangling_tag_followup': ( 76 | "

next page", 77 | "

next page" 78 | ) 79 | } 80 | 81 | FEATURES_TEXTS = { 82 | 'remove_quotes': ( 83 | '

', 84 | '
', 85 | ), 86 | 'remove_quotes_drop_trailing_slash': ( 87 | '
', 88 | # Note: According to https://github.com/mankyd/htmlmin/pull/12 older version 89 | # of WebKit would erroneously interpret "<... y=y/>" as self-closing tag. 90 | '
', 91 | ), 92 | 'remove_quotes_keep_space_before_slash': ( 93 | '', 94 | '', # NOTE: Space added so self-closing tag is parsed as such. 95 | ), 96 | 'remove_single_quotes': ( 97 | '
', 98 | '
', 99 | ), 100 | 'keep_nested_single_quotes': ( 101 | '
', 102 | '
', 103 | ), 104 | 'remove_tag_name_whitespace': ( 105 | '
' 107 | ), 108 | 'no_reduce_empty_attributes': ( 109 | '', 110 | '', 111 | ), 112 | 'no_reduce_empty_attributes_keep_quotes': ( 113 | '', 114 | '', 115 | ), 116 | 'reduce_empty_attributes': ( 117 | '', 118 | '', 119 | ), 120 | 'keep_boolean_attributes': ( 121 | '', 122 | '', 123 | ), 124 | 'reduce_boolean_attributes': ( 125 | '', 126 | '', 127 | ), 128 | 'remove_close_tags': ( 129 | '


x
', 130 | '

x ', 131 | ), 132 | 'remove_comments': ( 133 | ' this text should have comments removed', 134 | ' this text should have comments removed', 135 | ), 136 | 'keep_comments': ( 137 | ' this text should have comments removed', 138 | ' this text should have comments removed', 139 | ), 140 | 'keep_empty_comments': ( 141 | ' this text should not have empty comments removed', 142 | ' this text should not have empty comments removed', 143 | ), 144 | 'keep_conditional_comments': ( 145 | 'keep IE conditional styles ', 146 | 'keep IE conditional styles ', 147 | ), 148 | 'remove_nonconditional_comments': ( 149 | 'remove other [if] things ', 150 | 'remove other [if] things ', 151 | ), 152 | 'keep_optional_attribute_quotes': ( 153 | '', 154 | '', 155 | ), 156 | 'remove_optional_attribute_quotes': ( 157 | ( 158 | '', 159 | '', 160 | ), 161 | ( 162 | '', 163 | '', 164 | ), 165 | ), 166 | 'keep_pre_attribute': ( 167 | 'the pre should stay ', 168 | 'the pre should stay ', 169 | ), 170 | 'custom_pre_attribute': ( 171 | 'the X Y ', 172 | 'the X Y ', 173 | ), 174 | 'keep_empty': ( 175 | '
A
B
', 176 | '
A
B
', 177 | ), 178 | 'remove_empty': ( 179 | (' \n
A
\r' 180 | '
B
\r\n
C
D
'), 181 | ('
A
' 182 | '
B
C
D
'), 183 | ), 184 | 'remove_all_empty': ( 185 | (' \n
A
\r' 186 | '
B
\r\n
C
D
'), 187 | ('
A
' 188 | '
B
C
D
'), 189 | ), 190 | 'dont_minify_div': ( 191 | '
X
', 192 | '
X
', 193 | ), 194 | 'minify_pre': ( 195 | '
   X  
', 196 | '
 X 
', 197 | ), 198 | 'remove_head_spaces': ( 199 | ' ☃X Y & Z ', 200 | '☃X Y & Z', 201 | ), 202 | 'dont_minify_scripts_or_styles': ( 203 | ' ', 204 | ' ', 205 | ), 206 | 'remove_close_from_tags': ( 207 | ('

' 208 | ' ' 209 | ' '), 210 | ('

' 211 | ' ' 212 | ' '), 213 | ), 214 | 'remove_space_from_self_closed_tags': ( 215 | ' ', 216 | ' ', 217 | ), 218 | 'remove_redundant_lang_0': ( 219 | ('

This is an example.' 220 | '

I po polsku and more English.'), 221 | ('

This is an example.' 222 | '

I po polsku and more English.'), 223 | ), 224 | 'convert_charrefs': ( 225 | '', 226 | u'', 227 | ), 228 | 'convert_charrefs_false': ( 229 | '', 230 | '', 231 | ), 232 | 'dont_convert_pre_attr': ( 233 | '', 234 | '', 235 | ), 236 | } 237 | 238 | SELF_CLOSE_TEXTS = { 239 | 'p_self_close': ( 240 | '

X

Y ', 241 | '

X

Y ', 242 | ), 243 | 'li_self_close': ( 244 | '

  • X
  • Y
  • Z
Q', 245 | '
  • X
  • Y
  • Z
Q', 246 | ), 247 | 'dt_self_close': ( 248 | '
X
Y
Z
Q', 249 | '
X
Y
Z
Q', 250 | ), 251 | 'dd_self_close': ( 252 | '
X
Y
Z
Q', 253 | '
X
Y
Z
Q', 254 | ), 255 | 'optgroup_self_close': ( 256 | (' '), 258 | (' '), 260 | ), 261 | 'option_self_close': ( 262 | (' '), 264 | (' '), 266 | ), 267 | 'colgroup_self_close': ( 268 | '
', 269 | '
', 270 | ), 271 | 'tbody_self_close': ( 272 | (' \n' 273 | '\n \n\n\n '), 274 | ('
X
Y
\n' 275 | '\n '), 276 | ), 277 | 'thead_self_close': ( 278 | ('
X
Y
' 279 | ' '), 280 | ('
X
Y
' 281 | ' '), 282 | ), 283 | 'tfoot_self_close': ( 284 | ('
X
Y
' 285 | ' '), 286 | ('
X
Y
' 287 | ' '), 288 | ), 289 | 'tr_self_close': ( 290 | ('
X
Y
' 291 | ' '), 292 | ('
X
Y
' 293 | ' '), 294 | ), 295 | 'td_self_close': ( 296 | '
X
Y
X Y ', 297 | '
X Y ', 298 | ), 299 | 'th_self_close': ( 300 | '
X Y ', 301 | '
X Y ', 302 | ), 303 | 'a_p_interaction': ( # the 'pre' functionality continues after the 304 | '

X

Y', 305 | '

X

Y', 306 | ), 307 | } 308 | 309 | SELF_OPENING_TEXTS = { 310 | 'html_closed_no_open': ( 311 | ' X ', 312 | ' X ' 313 | ), 314 | 'head_closed_no_open': ( 315 | ' X ', 316 | ' X ' # TODO: we could theoretically kill that leading 317 | # space. See HTMLMinParse.handle_endtag 318 | ), 319 | 'body_closed_no_open': ( 320 | ' X ', 321 | ' X ' 322 | ), 323 | 'colgroup_self_open': ( 324 | '
', 325 | '
', 326 | ), 327 | 'tbody_self_open': ( 328 | '
', 329 | '
', 330 | ), 331 | 'p_closed_no_open': ( # this isn't valid html, but its worth accounting for 332 | '

X

Y

', 333 | '
X

Y

', 334 | ), 335 | } 336 | 337 | class HTMLMinTestMeta(type): 338 | def __new__(cls, name, bases, dct): 339 | def make_test(text): 340 | def inner_test(self): 341 | self.assertEqual(self.minify(text[0]), text[1]) 342 | return inner_test 343 | 344 | for k, v in dct.get('__reference_texts__',{}).items(): 345 | if 'test_'+k not in dct: 346 | dct['test_'+k] = make_test(v) 347 | return type.__new__(cls, str(name), bases, dct) 348 | 349 | class HTMLMinTestCase( 350 | HTMLMinTestMeta('HTMLMinTestCase', (unittest.TestCase, ), {})): 351 | def setUp(self): 352 | self.minify = htmlmin.minify 353 | 354 | class TestMinifyFunction(HTMLMinTestCase): 355 | __reference_texts__ = MINIFY_FUNCTION_TEXTS 356 | 357 | def test_basic_minification_quality(self): 358 | import codecs 359 | with codecs.open('htmlmin/tests/large_test.html', encoding='utf-8') as inpf: 360 | inp = inpf.read() 361 | out = self.minify(inp) 362 | self.assertEqual(len(inp) - len(out), 9408) 363 | 364 | def test_high_minification_quality(self): 365 | import codecs 366 | with codecs.open('htmlmin/tests/large_test.html', encoding='utf-8') as inpf: 367 | inp = inpf.read() 368 | out = self.minify(inp, remove_all_empty_space=True, remove_comments=True) 369 | self.assertEqual(len(inp) - len(out), 12518) 370 | 371 | class TestMinifierObject(HTMLMinTestCase): 372 | __reference_texts__ = MINIFY_FUNCTION_TEXTS 373 | 374 | def setUp(self): 375 | HTMLMinTestCase.setUp(self) 376 | self.minifier = htmlmin.Minifier() 377 | self.minify = self.minifier.minify 378 | 379 | def test_reuse(self): 380 | text = self.__reference_texts__['simple_text'] 381 | self.assertEqual(self.minify(text[0]), text[1]) 382 | self.assertEqual(self.minify(text[0]), text[1]) 383 | 384 | def test_dangling_tag(self): 385 | dangling_tag = self.__reference_texts__['dangling_tag'] 386 | dangling_tag_followup = self.__reference_texts__['dangling_tag_followup'] 387 | self.assertEqual(self.minify(dangling_tag[0]), dangling_tag[1]) 388 | self.assertEqual(self.minify(dangling_tag_followup[0]), dangling_tag_followup[1]) 389 | 390 | def test_buffered_input(self): 391 | text = self.__reference_texts__['long_text'] 392 | self.minifier.input(text[0][:len(text[0]) // 2]) 393 | self.minifier.input(text[0][len(text[0]) // 2:]) 394 | self.assertEqual(self.minifier.finalize(), text[1]) 395 | 396 | 397 | class TestMinifyFeatures(HTMLMinTestCase): 398 | __reference_texts__ = FEATURES_TEXTS 399 | 400 | def test_remove_comments(self): 401 | text = self.__reference_texts__['remove_comments'] 402 | self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1]) 403 | 404 | def test_no_reduce_empty_attributes(self): 405 | text = self.__reference_texts__['no_reduce_empty_attributes'] 406 | self.assertEqual(htmlmin.minify(text[0], reduce_empty_attributes=False), text[1]) 407 | 408 | def test_no_reduce_empty_attributes_keep_quotes(self): 409 | text = self.__reference_texts__['no_reduce_empty_attributes_keep_quotes'] 410 | self.assertEqual(htmlmin.minify(text[0], reduce_empty_attributes=False, remove_optional_attribute_quotes=False), text[1]) 411 | 412 | def test_reduce_empty_attributes(self): 413 | text = self.__reference_texts__['reduce_empty_attributes'] 414 | self.assertEqual(htmlmin.minify(text[0], reduce_empty_attributes=True), text[1]) 415 | 416 | def test_reduce_boolean_attributes(self): 417 | text = self.__reference_texts__['reduce_boolean_attributes'] 418 | self.assertEqual(htmlmin.minify(text[0], reduce_boolean_attributes=True), text[1]) 419 | 420 | def test_keep_comments(self): 421 | text = self.__reference_texts__['keep_comments'] 422 | self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1]) 423 | 424 | def test_keep_empty_comments(self): 425 | text = self.__reference_texts__['keep_empty_comments'] 426 | self.assertEqual(htmlmin.minify(text[0]), text[1]) 427 | 428 | def test_keep_conditional_comments(self): 429 | text = self.__reference_texts__['keep_conditional_comments'] 430 | self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1]) 431 | 432 | def test_remove_nonconditional_comments(self): 433 | text = self.__reference_texts__['remove_nonconditional_comments'] 434 | self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1]) 435 | 436 | def test_keep_optional_attribute_quotes(self): 437 | text = self.__reference_texts__['keep_optional_attribute_quotes'] 438 | self.assertEqual(htmlmin.minify(text[0], remove_optional_attribute_quotes=False), text[1]) 439 | 440 | def test_remove_optional_attribute_quotes(self): 441 | texts = self.__reference_texts__['remove_optional_attribute_quotes'] 442 | for text in texts: 443 | self.assertEqual(htmlmin.minify(text[0], remove_optional_attribute_quotes=True), text[1]) 444 | 445 | def test_keep_pre_attribute(self): 446 | text = self.__reference_texts__['keep_pre_attribute'] 447 | self.assertEqual(htmlmin.minify(text[0], keep_pre=True), text[1]) 448 | 449 | def test_custom_pre_attribute(self): 450 | text = self.__reference_texts__['custom_pre_attribute'] 451 | self.assertEqual(htmlmin.minify(text[0], pre_attr='custom'), text[1]) 452 | 453 | def test_keep_empty(self): 454 | text = self.__reference_texts__['keep_empty'] 455 | self.assertEqual(htmlmin.minify(text[0]), text[1]) 456 | 457 | def test_remove_empty(self): 458 | text = self.__reference_texts__['remove_empty'] 459 | self.assertEqual(htmlmin.minify(text[0], remove_empty_space=True), text[1]) 460 | 461 | def test_remove_all_empty(self): 462 | text = self.__reference_texts__['remove_all_empty'] 463 | self.assertEqual(htmlmin.minify(text[0], remove_all_empty_space=True), 464 | text[1]) 465 | 466 | def test_dont_minify_div(self): 467 | text = self.__reference_texts__['dont_minify_div'] 468 | self.assertEqual(htmlmin.minify(text[0], pre_tags=('div',)), text[1]) 469 | 470 | def test_minify_pre(self): 471 | text = self.__reference_texts__['minify_pre'] 472 | self.assertEqual(htmlmin.minify(text[0], pre_tags=('div',)), text[1]) 473 | 474 | def test_remove_head_spaces(self): 475 | text = self.__reference_texts__['remove_head_spaces'] 476 | self.assertEqual(htmlmin.minify(text[0]), text[1]) 477 | 478 | def test_dont_minify_scripts_or_styles(self): 479 | text = self.__reference_texts__['dont_minify_scripts_or_styles'] 480 | self.assertEqual(htmlmin.minify(text[0], pre_tags=[]), text[1]) 481 | 482 | def test_convert_charrefs_false(self): 483 | text = self.__reference_texts__['convert_charrefs_false'] 484 | self.assertEqual(htmlmin.minify(text[0], convert_charrefs=False), text[1]) 485 | 486 | 487 | class TestSelfClosingTags(HTMLMinTestCase): 488 | __reference_texts__ = SELF_CLOSE_TEXTS 489 | 490 | class TestSelfOpeningTags(HTMLMinTestCase): 491 | __reference_texts__ = SELF_OPENING_TEXTS 492 | 493 | class TestDecorator(HTMLMinTestCase): 494 | def test_direct_decorator(self): 495 | @htmlmindecorator 496 | def directly_decorated(): 497 | return ' X Y ' 498 | 499 | self.assertEqual(' X Y ', directly_decorated()) 500 | 501 | def test_options_decorator(self): 502 | @htmlmindecorator(remove_comments=True) 503 | def directly_decorated(): 504 | return ' X Y ' 505 | 506 | self.assertEqual(' X Y ', directly_decorated()) 507 | 508 | class TestMiddleware(HTMLMinTestCase): 509 | def setUp(self): 510 | HTMLMinTestCase.setUp(self) 511 | def wsgi_app(environ, start_response): 512 | start_response(environ['status'], environ['headers']) 513 | yield environ['content'] 514 | 515 | self.wsgi_app = wsgi_app 516 | 517 | def call_app(self, app, status, headers, content): 518 | response_status = [] # these need to be mutable so that they can be changed 519 | response_headers = [] # within our inner function. 520 | def start_response(status, headers, exc_info=None): 521 | response_status.append(status) 522 | response_headers.append(headers) 523 | response_body = ''.join(app({'status': status, 524 | 'content': content, 525 | 'headers': headers}, 526 | start_response)) 527 | return response_status[0], response_headers[0], response_body 528 | 529 | def test_middlware(self): 530 | app = HTMLMinMiddleware(self.wsgi_app) 531 | status, headers, body = self.call_app( 532 | app, '200 OK', (('Content-Type', 'text/html'),), 533 | ' X Y ') 534 | self.assertEqual(body, ' X Y ') 535 | 536 | def test_middlware_minifier_options(self): 537 | app = HTMLMinMiddleware(self.wsgi_app, remove_comments=True) 538 | status, headers, body = self.call_app( 539 | app, '200 OK', (('Content-Type', 'text/html'),), 540 | ' X Y ') 541 | self.assertEqual(body, ' X Y ') 542 | 543 | def test_middlware_off_by_default(self): 544 | app = HTMLMinMiddleware(self.wsgi_app, by_default=False) 545 | status, headers, body = self.call_app( 546 | app, '200 OK', (('Content-Type', 'text/html'),), 547 | ' X Y ') 548 | self.assertEqual(body, ' X Y ') 549 | 550 | def test_middlware_on_by_header(self): 551 | app = HTMLMinMiddleware(self.wsgi_app, by_default=False) 552 | status, headers, body = self.call_app( 553 | app, '200 OK', ( 554 | ('Content-Type', 'text/html'), 555 | ('X-HTML-Min-Enable', 'True'), 556 | ), 557 | ' X Y ') 558 | self.assertEqual(body, ' X Y ') 559 | 560 | def test_middlware_off_by_header(self): 561 | app = HTMLMinMiddleware(self.wsgi_app) 562 | status, headers, body = self.call_app( 563 | app, '200 OK', ( 564 | ('Content-Type', 'text/html'), 565 | ('X-HTML-Min-Enable', 'False'), 566 | ), 567 | ' X Y ') 568 | self.assertEqual(body, ' X Y ') 569 | 570 | def test_middlware_remove_header(self): 571 | app = HTMLMinMiddleware(self.wsgi_app) 572 | status, headers, body = self.call_app( 573 | app, '200 OK', ( 574 | ('Content-Type', 'text/html'), 575 | ('X-HTML-Min-Enable', 'False'), 576 | ), 577 | ' X Y ') 578 | self.assertFalse(any((h == 'X-HTML-Min-Enable' for h, v in headers))) 579 | 580 | def test_middlware_keep_header(self): 581 | app = HTMLMinMiddleware(self.wsgi_app, keep_header=True) 582 | status, headers, body = self.call_app( 583 | app, '200 OK', [ 584 | ('Content-Type', 'text/html'), 585 | ('X-HTML-Min-Enable', 'False'), 586 | ], 587 | ' X Y ') 588 | self.assertTrue(any((h == 'X-HTML-Min-Enable' for h, v in headers))) 589 | 590 | def suite(): 591 | minify_function_suite = unittest.TestLoader().\ 592 | loadTestsFromTestCase(TestMinifyFunction) 593 | minifier_object_suite = unittest.TestLoader().\ 594 | loadTestsFromTestCase(TestMinifierObject) 595 | minify_features_suite = unittest.TestLoader().\ 596 | loadTestsFromTestCase(TestMinifyFeatures) 597 | self_closing_tags_suite = unittest.TestLoader().\ 598 | loadTestsFromTestCase(TestSelfClosingTags) 599 | self_opening_tags_suite = unittest.TestLoader().\ 600 | loadTestsFromTestCase(TestSelfOpeningTags) 601 | decorator_suite = unittest.TestLoader().\ 602 | loadTestsFromTestCase(TestDecorator) 603 | middleware_suite = unittest.TestLoader().\ 604 | loadTestsFromTestCase(TestMiddleware) 605 | return unittest.TestSuite([ 606 | minify_function_suite, 607 | minifier_object_suite, 608 | minify_features_suite, 609 | self_closing_tags_suite, 610 | self_opening_tags_suite, 611 | decorator_suite, 612 | middleware_suite, 613 | test_escape.suite(), 614 | ]) 615 | 616 | if __name__ == '__main__': 617 | unittest.main() 618 | --------------------------------------------------------------------------------