Skip to content

Commit

Permalink
Merge pull request #5 from zeebonk/master
Browse files Browse the repository at this point in the history
Added option to disable removing of optional attribute quotes
  • Loading branch information
mankyd committed Jan 8, 2014
2 parents a3bee6b + e8759ce commit 079525c
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 21 deletions.
16 changes: 12 additions & 4 deletions htmlmin/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@

parser.add_argument('-s', '--remove-empty-space',
help=(
'''When set, this removes empty space betwen tags in certain cases.
'''When set, this removes empty space betwen tags in certain cases.
Specifically, it will remove empty space if and only if there a newline
character occurs within the space. Thus, code like
character occurs within the space. Thus, code like
'<span>x</span> <span>y</span>' will be left alone, but code such as
' ...
</head>
<body>
...'
will become '...</head><body>...'. Note that this CAN break your
will become '...</head><body>...'. Note that this CAN break your
html if you spread two inline tags over two lines. Use with caution.
'''),
Expand All @@ -85,6 +85,13 @@
avoid this problem. Only use if you are confident in the result. Whitespace is
not removed from inside of tags, thus '<span> </span>' will be left alone.
'''),
action='store_true')

parser.add_argument('--keep-optional-attribute-quotes',
help=(
'''When set, this keeps all attribute quotes, even if they are optional.
'''),
action='store_true')

Expand All @@ -107,7 +114,7 @@

parser.add_argument('-a', '--pre-attr',
help=(
'''The attribute htmlmin looks for to find blocks of HTML that it should not
'''The attribute htmlmin looks for to find blocks of HTML that it should not
minify. This attribute will be removed from the HTML unless '-k' is
specified. Defaults to 'pre'.
Expand Down Expand Up @@ -136,6 +143,7 @@ def main():
minifier = Minifier(
remove_comments=args.remove_comments,
remove_empty_space=args.remove_empty_space,
remove_optional_attribute_quotes=not args.keep_optional_attribute_quotes,
pre_tags=args.pre_tags,
keep_pre=args.keep_pre_attr,
pre_attr=args.pre_attr,
Expand Down
10 changes: 7 additions & 3 deletions htmlmin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def minify(input,
remove_empty_space=False,
remove_all_empty_space=False,
reduce_boolean_attributes=False,
remove_optional_attribute_quotes=True,
keep_pre=False,
pre_tags=parser.PRE_TAGS,
pre_attr='pre'):
Expand Down Expand Up @@ -73,7 +74,7 @@ def minify(input,
free to change this list as you see fit, but you will probably want to
include ``pre`` and ``textarea`` if you make any changes to the list. Note
that ``<script>`` and ``<style>`` tags are never minimized.
:param pre_attr: Specifies the attribute that, when found in an HTML tag,
:param pre_attr: Specifies the attribute that, when found in an HTML tag,
indicates that the content of the tag should not be minified. Defaults to
``pre``.
:return: A string containing the minified HTML.
Expand All @@ -86,6 +87,7 @@ def minify(input,
remove_empty_space=remove_empty_space,
remove_all_empty_space=remove_all_empty_space,
reduce_boolean_attributes=reduce_boolean_attributes,
remove_optional_attribute_quotes=remove_optional_attribute_quotes,
keep_pre=keep_pre,
pre_tags=pre_tags,
pre_attr=pre_attr)
Expand All @@ -96,7 +98,7 @@ def minify(input,
class Minifier(object):
"""An object that supports HTML Minification.
Options are passed into this class at initialization time and are then
Options are passed into this class at initialization time and are then
persisted across each use of the instance. If you are going to be minifying
multiple peices of HTML, this will be more efficient than using
:class:`htmlmin.minify`.
Expand All @@ -109,6 +111,7 @@ def __init__(self,
remove_empty_space=False,
remove_all_empty_space=False,
reduce_boolean_attributes=False,
remove_optional_attribute_quotes=True,
keep_pre=False,
pre_tags=parser.PRE_TAGS,
pre_attr='pre'):
Expand All @@ -121,6 +124,7 @@ def __init__(self,
remove_empty_space=remove_empty_space,
remove_all_empty_space=remove_all_empty_space,
reduce_boolean_attributes=reduce_boolean_attributes,
remove_optional_attribute_quotes=remove_optional_attribute_quotes,
keep_pre=keep_pre,
pre_tags=pre_tags,
pre_attr=pre_attr)
Expand Down Expand Up @@ -162,7 +166,7 @@ def output(self):
def finalize(self):
"""Finishes current input HTML and returns mininified result.
This method flushes any remaining input HTML and returns the minified
This method flushes any remaining input HTML and returns the minified
result. It resets the state of the internal parser in the process so that
new HTML can be minified. Be sure to call this method before you reuse
the ``Minifier`` instance on a new HTML document.
Expand Down
12 changes: 7 additions & 5 deletions htmlmin/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
'form': ('novalidate',),
'iframe': ('seamless',),
'img': ('ismap',),
'input': ('autofocus', 'checked', 'disabled', 'formnovalidate', 'multiple',
'input': ('autofocus', 'checked', 'disabled', 'formnovalidate', 'multiple',
'readonly', 'required',),
'keygen': ('autofocus', 'disabled',),
'object': ('typesmustmatch',),
Expand Down Expand Up @@ -85,6 +85,7 @@ def __init__(self,
remove_empty_space=False,
remove_all_empty_space=False,
reduce_boolean_attributes=False,
remove_optional_attribute_quotes=True,
keep_pre=False,
pre_tags=PRE_TAGS,
pre_attr='pre'):
Expand All @@ -95,6 +96,7 @@ def __init__(self,
self.remove_empty_space = remove_empty_space
self.remove_all_empty_space = remove_all_empty_space
self.reduce_boolean_attributes = reduce_boolean_attributes
self.remove_optional_attribute_quotes = remove_optional_attribute_quotes
self.pre_attr = pre_attr
self._data_buffer = []
self._in_pre_tag = 0
Expand All @@ -117,7 +119,7 @@ def build_tag(self, tag, attrs, close_tag):
k in BOOLEAN_ATTRIBUTES.get(tag,[]) or
k in BOOLEAN_ATTRIBUTES['*']):
pass
elif not any((c in v for c in ('"', "'", ' ', '<', '>'))):
elif self.remove_optional_attribute_quotes and not any((c in v for c in ('"', "'", ' ', '<', '>'))):
result += '={}'.format(escape(v, quote=True))
else:
result += '="{}"'.format(escape(v, quote=True).replace('&#x27;', "'"))
Expand All @@ -126,7 +128,7 @@ def build_tag(self, tag, attrs, close_tag):
return result + '>'

def handle_decl(self, decl):
if (len(self._data_buffer) == 1 and
if (len(self._data_buffer) == 1 and
whitespace_re.match(self._data_buffer[0])):
self._data_buffer = []
self._data_buffer.append('<!' + decl + '>\n')
Expand Down Expand Up @@ -241,7 +243,7 @@ def handle_data(self, data):
if self._in_pre_tag > 0:
self._data_buffer.append(data)
else:
# remove_all_empty_space matches everything. remove_empty_space only
# remove_all_empty_space matches everything. remove_empty_space only
# matches if there's a newline involved.
if self.remove_all_empty_space or self._in_head or self._after_doctype:
match = whitespace_re.match(data)
Expand All @@ -251,7 +253,7 @@ def handle_data(self, data):
match = whitespace_newline_re.match(data)
if match and match.end(0) == len(data):
return


# if we're in the title, remove leading and trailing whitespace
if self._tag_stack and self._tag_stack[0][0] == 'title':
Expand Down
26 changes: 17 additions & 9 deletions htmlmin/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@
'<body> this text should <!--! not --> have comments removed</body>',
'<body> this text should <!-- not --> have comments removed</body>',
),
'keep_optional_attribute_quotes': (
'<img width="100" height="50" src="#something" />',
'<img width="100" height="50" src="#something">',
),
'keep_pre_attribute': (
'<body>the <strong pre style="">pre</strong> should stay </body>',
'<body>the <strong pre style>pre</strong> should stay </body>',
Expand Down Expand Up @@ -326,6 +330,10 @@ def test_keep_comments(self):
text = self.__reference_texts__['keep_comments']
self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1])

def test_keep_optional_attribute_quotes(self):
text = self.__reference_texts__['keep_optional_attribute_quotes']
self.assertEqual(htmlmin.minify(text[0], remove_optional_attribute_quotes=False), text[1])

def test_keep_pre_attribute(self):
text = self.__reference_texts__['keep_pre_attribute']
self.assertEqual(htmlmin.minify(text[0], keep_pre=True), text[1])
Expand All @@ -344,7 +352,7 @@ def test_remove_empty(self):

def test_remove_all_empty(self):
text = self.__reference_texts__['remove_all_empty']
self.assertEqual(htmlmin.minify(text[0], remove_all_empty_space=True),
self.assertEqual(htmlmin.minify(text[0], remove_all_empty_space=True),
text[1])

def test_dont_minify_div(self):
Expand Down Expand Up @@ -401,28 +409,28 @@ def start_response(status, headers, exc_info=None):
response_headers.append(headers)
response_body = ''.join(app({'status': status,
'content': content,
'headers': headers},
'headers': headers},
start_response))
return response_status[0], response_headers[0], response_body

def test_middlware(self):
app = HTMLMinMiddleware(self.wsgi_app)
status, headers, body = self.call_app(
app, '200 OK', (('Content-Type', 'text/html'),),
app, '200 OK', (('Content-Type', 'text/html'),),
' X Y ')
self.assertEqual(body, ' X Y ')

def test_middlware_minifier_options(self):
app = HTMLMinMiddleware(self.wsgi_app, remove_comments=True)
status, headers, body = self.call_app(
app, '200 OK', (('Content-Type', 'text/html'),),
app, '200 OK', (('Content-Type', 'text/html'),),
' X Y <!-- Z -->')
self.assertEqual(body, ' X Y ')

def test_middlware_off_by_default(self):
app = HTMLMinMiddleware(self.wsgi_app, by_default=False)
status, headers, body = self.call_app(
app, '200 OK', (('Content-Type', 'text/html'),),
app, '200 OK', (('Content-Type', 'text/html'),),
' X Y ')
self.assertEqual(body, ' X Y ')

Expand All @@ -432,7 +440,7 @@ def test_middlware_on_by_header(self):
app, '200 OK', (
('Content-Type', 'text/html'),
('X-HTML-Min-Enable', 'True'),
),
),
' X Y ')
self.assertEqual(body, ' X Y ')

Expand All @@ -442,7 +450,7 @@ def test_middlware_off_by_header(self):
app, '200 OK', (
('Content-Type', 'text/html'),
('X-HTML-Min-Enable', 'False'),
),
),
' X Y ')
self.assertEqual(body, ' X Y ')

Expand All @@ -452,7 +460,7 @@ def test_middlware_remove_header(self):
app, '200 OK', (
('Content-Type', 'text/html'),
('X-HTML-Min-Enable', 'False'),
),
),
' X Y ')
self.assertFalse(any((h == 'X-HTML-Min-Enable' for h, v in headers)))

Expand All @@ -462,7 +470,7 @@ def test_middlware_keep_header(self):
app, '200 OK', [
('Content-Type', 'text/html'),
('X-HTML-Min-Enable', 'False'),
],
],
' X Y ')
self.assertTrue(any((h == 'X-HTML-Min-Enable' for h, v in headers)))

Expand Down

0 comments on commit 079525c

Please sign in to comment.