├── LICENSE ├── Makefile ├── README └── jinja2htmlcompress.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Armin Ronacher, see AUTHORS for more details. 2 | 3 | Some 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 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | python jinja2htmlcompress.py 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- jinja2-htmlcompress 4 | 5 | a Jinja2 extension that removes whitespace between HTML tags. 6 | 7 | Example usage: 8 | 9 | env = Environment(extensions=['jinja2htmlcompress.HTMLCompress']) 10 | 11 | How does it work? It throws away all whitespace between HTML tags 12 | it can find at runtime. It will however preserve pre, textarea, style 13 | and script tags because this kinda makes sense. In order to force 14 | whitespace you can use ``{{ " " }}``. 15 | 16 | Unlike filters that work at template runtime, this remotes whitespace 17 | at compile time and does not add an overhead in template execution. 18 | 19 | What if you only want to selective strip stuff? 20 | 21 | env = Environment(extensions=['jinja2htmlcompress.SelectiveHTMLCompress']) 22 | 23 | And then mark blocks with ``{% strip %}``: 24 | 25 | {% strip %} ... {% endstrip %} 26 | 27 | -------------------------------------------------------------------------------- /jinja2htmlcompress.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | jinja2htmlcompress 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | A Jinja2 extension that eliminates useless whitespace at template 7 | compilation time without extra overhead. 8 | 9 | :copyright: (c) 2011 by Armin Ronacher. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | import re 13 | from jinja2.ext import Extension 14 | from jinja2.lexer import Token, describe_token 15 | from jinja2 import TemplateSyntaxError 16 | 17 | 18 | _tag_re = re.compile(r'(?:<(/?)([a-zA-Z0-9_-]+)\s*|(>\s*))(?s)') 19 | _ws_normalize_re = re.compile(r'[ \t\r\n]+') 20 | 21 | 22 | class StreamProcessContext(object): 23 | 24 | def __init__(self, stream): 25 | self.stream = stream 26 | self.token = None 27 | self.stack = [] 28 | 29 | def fail(self, message): 30 | raise TemplateSyntaxError(message, self.token.lineno, 31 | self.stream.name, self.stream.filename) 32 | 33 | 34 | def _make_dict_from_listing(listing): 35 | rv = {} 36 | for keys, value in listing: 37 | for key in keys: 38 | rv[key] = value 39 | return rv 40 | 41 | 42 | class HTMLCompress(Extension): 43 | isolated_elements = set(['script', 'style', 'noscript', 'textarea']) 44 | void_elements = set(['br', 'img', 'area', 'hr', 'param', 'input', 45 | 'embed', 'col']) 46 | block_elements = set(['div', 'p', 'form', 'ul', 'ol', 'li', 'table', 'tr', 47 | 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th', 'dl', 48 | 'dt', 'dd', 'blockquote', 'h1', 'h2', 'h3', 'h4', 49 | 'h5', 'h6', 'pre']) 50 | breaking_rules = _make_dict_from_listing([ 51 | (['p'], set(['#block'])), 52 | (['li'], set(['li'])), 53 | (['td', 'th'], set(['td', 'th', 'tr', 'tbody', 'thead', 'tfoot'])), 54 | (['tr'], set(['tr', 'tbody', 'thead', 'tfoot'])), 55 | (['thead', 'tbody', 'tfoot'], set(['thead', 'tbody', 'tfoot'])), 56 | (['dd', 'dt'], set(['dl', 'dt', 'dd'])) 57 | ]) 58 | 59 | def is_isolated(self, stack): 60 | for tag in reversed(stack): 61 | if tag in self.isolated_elements: 62 | return True 63 | return False 64 | 65 | def is_breaking(self, tag, other_tag): 66 | breaking = self.breaking_rules.get(other_tag) 67 | return breaking and (tag in breaking or 68 | ('#block' in breaking and tag in self.block_elements)) 69 | 70 | def enter_tag(self, tag, ctx): 71 | while ctx.stack and self.is_breaking(tag, ctx.stack[-1]): 72 | self.leave_tag(ctx.stack[-1], ctx) 73 | if tag not in self.void_elements: 74 | ctx.stack.append(tag) 75 | 76 | def leave_tag(self, tag, ctx): 77 | if not ctx.stack: 78 | ctx.fail('Tried to leave "%s" but something closed ' 79 | 'it already' % tag) 80 | if tag == ctx.stack[-1]: 81 | ctx.stack.pop() 82 | return 83 | for idx, other_tag in enumerate(reversed(ctx.stack)): 84 | if other_tag == tag: 85 | for num in xrange(idx + 1): 86 | ctx.stack.pop() 87 | elif not self.breaking_rules.get(other_tag): 88 | break 89 | 90 | def normalize(self, ctx): 91 | pos = 0 92 | buffer = [] 93 | def write_data(value): 94 | if not self.is_isolated(ctx.stack): 95 | value = _ws_normalize_re.sub(' ', value.strip()) 96 | buffer.append(value) 97 | 98 | for match in _tag_re.finditer(ctx.token.value): 99 | closes, tag, sole = match.groups() 100 | preamble = ctx.token.value[pos:match.start()] 101 | write_data(preamble) 102 | if sole: 103 | write_data(sole) 104 | else: 105 | buffer.append(match.group()) 106 | (closes and self.leave_tag or self.enter_tag)(tag, ctx) 107 | pos = match.end() 108 | 109 | write_data(ctx.token.value[pos:]) 110 | return u''.join(buffer) 111 | 112 | def filter_stream(self, stream): 113 | ctx = StreamProcessContext(stream) 114 | for token in stream: 115 | if token.type != 'data': 116 | yield token 117 | continue 118 | ctx.token = token 119 | value = self.normalize(ctx) 120 | yield Token(token.lineno, 'data', value) 121 | 122 | 123 | class SelectiveHTMLCompress(HTMLCompress): 124 | 125 | def filter_stream(self, stream): 126 | ctx = StreamProcessContext(stream) 127 | strip_depth = 0 128 | while 1: 129 | if stream.current.type == 'block_begin': 130 | if stream.look().test('name:strip') or \ 131 | stream.look().test('name:endstrip'): 132 | stream.skip() 133 | if stream.current.value == 'strip': 134 | strip_depth += 1 135 | else: 136 | strip_depth -= 1 137 | if strip_depth < 0: 138 | ctx.fail('Unexpected tag endstrip') 139 | stream.skip() 140 | if stream.current.type != 'block_end': 141 | ctx.fail('expected end of block, got %s' % 142 | describe_token(stream.current)) 143 | stream.skip() 144 | if strip_depth > 0 and stream.current.type == 'data': 145 | ctx.token = stream.current 146 | value = self.normalize(ctx) 147 | yield Token(stream.current.lineno, 'data', value) 148 | else: 149 | yield stream.current 150 | stream.next() 151 | 152 | 153 | def test(): 154 | from jinja2 import Environment 155 | env = Environment(extensions=[HTMLCompress]) 156 | tmpl = env.from_string(''' 157 | 158 |
159 |
181 | Foo
Bar
182 | Baz
183 |
184 | Moep Test Moep 185 |
186 | {% endstrip %} 187 | ''') 188 | print tmpl.render(foo=42) 189 | 190 | 191 | if __name__ == '__main__': 192 | test() 193 | --------------------------------------------------------------------------------