├── Commands.sublime-commands ├── Context.sublime-menu ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Main.sublime-menu ├── Minfier.py ├── README.md ├── Side Bar.sublime-menu └── modules ├── csscompressor ├── __init__.py ├── __main__.py └── tests │ ├── __init__.py │ ├── base.py │ ├── test_compress.py │ ├── test_other.py │ ├── test_partition.py │ └── test_yui.py ├── htmlmin ├── __init__.py ├── command.py ├── decorator.py ├── main.py ├── middleware.py ├── parser.py └── tests │ ├── __init__.py │ ├── large_test.html │ └── tests.py └── jsmin ├── __init__.py ├── __main__.py └── test.py /Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "HTML Minfier: Minify File (create new)", 4 | "command": "minifier" 5 | }, 6 | { 7 | "caption": "HTML Minfier: Minify File (modify existing)", 8 | "command": "minifier2" 9 | } 10 | ] -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Minify HTML5 Code", 4 | "command": "minifier" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ 4 | "ctrl+shift+m" 5 | ], 6 | "command": "minifier" 7 | }, 8 | { 9 | "keys": [ 10 | "ctrl+shift+alt+m" 11 | ], 12 | "command": "minifier2" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ 4 | "ctrl+shift+m" 5 | ], 6 | "command": "minifier" 7 | }, 8 | { 9 | "keys": [ 10 | "ctrl+shift+alt+m" 11 | ], 12 | "command": "minifier2" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ 4 | "ctrl+shift+m" 5 | ], 6 | "command": "minifier" 7 | }, 8 | { 9 | "keys": [ 10 | "ctrl+shift+alt+m" 11 | ], 12 | "command": "minifier2" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": [ 5 | { 6 | "caption": "Minify Current HTML5 (create new)", 7 | "command": "minifier" 8 | }, 9 | { 10 | "caption": "Minify Current HTML5 (modify existing)", 11 | "command": "minifier2" 12 | } 13 | ] 14 | } 15 | ] -------------------------------------------------------------------------------- /Minfier.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import os 3 | import sys 4 | import platform 5 | import sublime 6 | import sublime_plugin 7 | sys.path.append(os.path.join(os.path.dirname(__file__), "modules")) 8 | from htmlmin import minify as htmlminify 9 | from jsmin import jsmin as jsminify 10 | from csscompressor import compress as cssminify 11 | 12 | extensions = ['js', 'css', 'html', 'htm'] 13 | IS_WINDOWS = platform.system() == "Windows" 14 | 15 | 16 | def return_minified(data, extension): 17 | if extension == 'css': 18 | return cssminify(data) 19 | elif extension == 'js': 20 | return jsminify(data) 21 | else: 22 | return htmlminify(data) 23 | 24 | 25 | class MinifierCommand(sublime_plugin.TextCommand): 26 | 27 | def get_content(self): 28 | allcontent = sublime.Region(0, self.view.size()) 29 | content = self.view.substr(allcontent) 30 | return content 31 | 32 | def location_params(self): 33 | self.location = self.view.file_name() 34 | if IS_WINDOWS: 35 | self.name = self.location.split('\\')[-1] 36 | else: 37 | self.name = self.location.split('/')[-1] 38 | 39 | self.folder = self.location.replace(self.name, '') 40 | self.extension = self.name.split('.')[-1] 41 | self.n = self.name.replace(self.extension, '') 42 | self.path = self.folder + self.n + 'min.' + self.extension 43 | 44 | return [self.location, self.name, self.folder, self.extension, self.n, self.path] 45 | 46 | def run(self, edit): 47 | td = threading.Thread(target=self.main) 48 | td.start() 49 | 50 | def main(self): 51 | self.content = self.get_content() 52 | self.locations = self.location_params() 53 | self.minifiedLocation = self.locations[-1] 54 | self.minifiedExtension = self.locations[3] 55 | t = threading.Thread(target=self.write_minified) 56 | t.start() 57 | 58 | def write_minified(self): 59 | if self.minifiedExtension in extensions: 60 | if os.path.isfile(self.minifiedLocation): 61 | content = return_minified(self.content, self.minifiedExtension) 62 | with open(self.minifiedLocation, 'w') as file: 63 | file.write(content) 64 | else: 65 | content = return_minified(self.content, self.minifiedExtension) 66 | open(self.minifiedLocation, 'w').close() 67 | with open(self.minifiedLocation, 'w') as file: 68 | file.write(content) 69 | 70 | window = sublime.active_window() 71 | self.view.window().open_file(self.minifiedLocation) 72 | 73 | class Minifier2Command(sublime_plugin.TextCommand): 74 | 75 | def get_content(self): 76 | allcontent = sublime.Region(0, self.view.size()) 77 | content = self.view.substr(allcontent) 78 | return content 79 | 80 | def extension(self): 81 | self.location = self.view.file_name() 82 | print (self.location) 83 | if IS_WINDOWS: 84 | self.name = self.location.split('\\')[-1] 85 | else: 86 | self.name = self.location.split('/')[-1] 87 | 88 | self.folder = self.location.replace(self.name, '') 89 | extension = self.name.split('.')[-1] 90 | return extension 91 | 92 | def run(self, edit): 93 | t = threading.Thread(target=self.main) 94 | t.start() 95 | 96 | def main(self): 97 | self.content = self.get_content() 98 | self.write() 99 | 100 | def write(self): 101 | self.content = return_minified(self.content, self.extension()) 102 | sublime.active_window().run_command("writeminify", {"content":self.content}) 103 | 104 | class writeminifyCommand(sublime_plugin.TextCommand): 105 | def run(self, edit, content): 106 | self.view.replace(edit,sublime.Region(0, self.view.size()),content) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sublime HTML5 Minifier 2 | 3 | 4 | 5 | This is a Sublime Text 3 Plugin for reducing the code size of HTML5, CSS3 and Javascript files. 6 | 7 | ## Installation 8 | 9 | This plugin can be installed by searching for HTML Minifier on Package Control in Sublime Text 3. 10 | 11 | You can install Package Control by following these steps: [https://sublime.wbond.net/installation](https://sublime.wbond.net/installation) 12 | 13 | ## Usage 14 | 15 | In order to minify code, Click on the Tools menu and click Minify Current File. A .min file be added onto the current directory where you are working. 16 | 17 | Alternatively, Right Click on the Editor area or the sidebar and click 'Minify HTML5 File'. 18 | 19 | For example, if you are working on a file 'main.css' in the location 'C:\Projects\' then a minified file will be created at 'C:\Projects\' with the name main.min.css making the full path 'C:\Projects\main.min.css'. 20 | 21 | This is done to keep two versions of the codebase, one minified and the other development version. 22 | 23 | If you want to overwrite your file, such that the minified version of say `main.html` should be written to `main.html` inplace of `main.min.html`, click on Tools - Minify HTML5 File (modify existing) 24 | 25 | #### Command Palllete 26 | 27 | Press Ctrl+Shift+P and then type `Minify File`. You have two options: 28 | If you want to create a new file, then click on "HTML Minfier: Minify File (create new)" or if you want to write to the current file, then click on "HTML Minfier: Minify File (modify existing)" 29 | 30 | ## Change Log 31 | 32 | Version 1.1 contains the following changes in the plugin: 33 | 34 | - Minifying Process runs in a separate thread 35 | 36 | - Source has been streamlined on the principles of OOP 37 | 38 | - Already Minified versions of the file will open up in the Sublime Window, instead of no output being shown. 39 | 40 | - Performance Improvements 41 | 42 | - Javascript Not Opening Bug Fixed 43 | 44 | 45 | Version 1.2 46 | 47 | - Performance Optimisations 48 | - Bug Fixing 49 | - Module changing for better support 50 | 51 | Version 1.3 52 | 53 | - Mac OS X Bug fixes 54 | - Code uses PEP8 55 | - Better Linux Support 56 | 57 | Version 1.4 58 | 59 | - Ability to Minify Files in the same file instead of creating a new file 60 | - Commands for Command Pallete 61 | - Switch Menu to Tools 62 | 63 | ##About 64 | 65 | This Plugin uses Closure Compiler for Javascript compilation, CSS Minifier for CSS and HTML5Minifier for HTML. 66 | 67 | Created By Pradipta aka GeekPradd 68 | -------------------------------------------------------------------------------- /Side Bar.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | 4 | "caption":"Minify HTML5 Code", 5 | "command" : "minifier", 6 | 7 | 8 | 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /modules/csscompressor/__init__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Portions Copyright (c) 2013 Sprymix Inc. 3 | # Author of python port: Yury Selivanov - http://sprymix.com 4 | # 5 | # Author: Julien Lecomte - http://www.julienlecomte.net/ 6 | # Author: Isaac Schlueter - http://foohack.com/ 7 | # Author: Stoyan Stefanov - http://phpied.com/ 8 | # Contributor: Dan Beam - http://danbeam.org/ 9 | # Portions Copyright (c) 2011-2013 Yahoo! Inc. All rights reserved. 10 | # LICENSE: BSD (revised) 11 | ## 12 | 13 | 14 | __all__ = ('compress', 'compress_partitioned') 15 | __version__ = '0.9.3' 16 | 17 | 18 | import re 19 | 20 | 21 | _url_re = re.compile(r'''(url)\s*\(\s*(['"]?)data\:''', re.I) 22 | _calc_re = re.compile(r'(calc)\s*\(', re.I) 23 | _hsl_re = re.compile(r'(hsl|hsla)\s*\(', re.I) 24 | _ws_re = re.compile(r'\s+') 25 | _str_re = re.compile(r'''("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')''') 26 | _yui_comment_re = re.compile(r'___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_(?P\d+)___') 27 | _ms_filter_re = re.compile(r'progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity=', re.I) 28 | _spaces1_re = re.compile(r'(^|\})(([^\{:])+:)+([^\{]*\{)') 29 | _spaces2_re = re.compile(r'\s+([!{};:>+\(\)\],])') 30 | _ie6special_re = re.compile(r':first-(line|letter)(\{|,)', re.I) 31 | _charset1_re = re.compile(r'^(.*)(@charset)\s+("[^"]*";)', re.I) 32 | _charset2_re = re.compile(r'^((\s*)(@charset)\s+([^;]+;\s*))+', re.I) 33 | _dirs_re = re.compile(r'''@(font-face|import| 34 | (?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)? 35 | keyframe|media|page|namespace)''', 36 | re.I | re.X) 37 | 38 | _pseudo_re = re.compile(r''':(active|after|before|checked|disabled|empty|enabled| 39 | first-(?:child|of-type)|focus|hover|last-(?:child|of-type)| 40 | link|only-(?:child|of-type)|root|:selection|target|visited)''', 41 | re.I | re.X) 42 | 43 | _common_funcs_re = re.compile(r''':(lang|not|nth-child|nth-last-child|nth-last-of-type| 44 | nth-of-type|(?:-(?:moz|webkit)-)?any)\(''', re.I | re.X) 45 | 46 | _common_val_funcs_re = re.compile(r'''([:,\(\s]\s*)(attr|color-stop|from|rgba|to|url| 47 | (?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)? 48 | (?:calc|max|min|(?:repeating-)? 49 | (?:linear|radial)-gradient)|-webkit-gradient)''', 50 | re.I | re.X) 51 | 52 | _space_and_re = re.compile(r'\band\(', re.I) 53 | 54 | _space_after_re = re.compile(r'([!{}:;>+\(\[,])\s+') 55 | 56 | _semi_re = re.compile(r';+}') 57 | 58 | _zero_fmt_spec_re = re.compile(r'''(\s|:|\(|,)(?:0?\.)?0 59 | (?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)''', 60 | re.I | re.X) 61 | 62 | _bg_pos_re = re.compile(r'''(background-position|webkit-mask-position|transform-origin| 63 | webkit-transform-origin|moz-transform-origin|o-transform-origin| 64 | ms-transform-origin):0(;|})''', re.I | re.X) 65 | 66 | _quad_0_re = re.compile(r':0 0 0 0(;|})') 67 | _trip_0_re = re.compile(r':0 0 0(;|})') 68 | _coup_0_re = re.compile(r':0 0(;|})') 69 | 70 | _point_float_re = re.compile(r'(:|\s)0+\.(\d+)') 71 | 72 | _border_re = re.compile(r'''(border|border-top|border-right|border-bottom| 73 | border-left|outline|background):none(;|})''', re.I | re.X) 74 | 75 | _o_px_ratio_re = re.compile(r'\(([\-A-Za-z]+):([0-9]+)\/([0-9]+)\)') 76 | 77 | _empty_rules_re = re.compile(r'[^\}\{/;]+\{\}') 78 | 79 | _many_semi_re = re.compile(';;+') 80 | 81 | _rgb_re = re.compile(r'rgb\s*\(\s*([0-9,\s]+)\s*\)') 82 | 83 | _hex_color_re = re.compile(r'''(\=\s*?["']?)? 84 | \#([0-9a-fA-F])([0-9a-fA-F]) 85 | ([0-9a-fA-F])([0-9a-fA-F]) 86 | ([0-9a-fA-F])([0-9a-fA-F]) 87 | (\}|[^0-9a-fA-F{][^{]*?\})''', re.X) 88 | 89 | _ie_matrix_re = re.compile(r'\s*filter:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^\)]+)\);') 90 | 91 | _colors_map = { 92 | 'f00': 'red', 93 | '000080': 'navy', 94 | '808080': 'gray', 95 | '808000': 'olive', 96 | '800080': 'purple', 97 | 'c0c0c0': 'silver', 98 | '008080': 'teal', 99 | 'ffa500': 'orange', 100 | '800000': 'maroon' 101 | } 102 | 103 | _colors_re = re.compile(r'(:|\s)' + '(\\#(' + '|'.join(_colors_map.keys()) + '))' + r'(;|})', re.I) 104 | 105 | 106 | def _preserve_call_tokens(css, regexp, preserved_tokens, remove_ws=False): 107 | max_idx = len(css) - 1 108 | append_idx = 0 109 | sb = [] 110 | 111 | for match in regexp.finditer(css): 112 | name = match.group(1) 113 | start_idx = match.start(0) + len(name) + 1 # "len" of "url(" 114 | 115 | term = match.group(2) if match.lastindex > 1 else None 116 | if not term: 117 | term = ')' 118 | 119 | found_term = False 120 | end_idx = match.end(0) - 1 121 | while not found_term and (end_idx + 1) <= max_idx: 122 | end_idx = css.find(term, end_idx + 1) 123 | 124 | if end_idx > 0: 125 | if css[end_idx - 1] != '\\': 126 | found_term = True 127 | if term != ')': 128 | end_idx = css.find(')', end_idx) 129 | else: 130 | raise ValueError('malformed css') 131 | 132 | sb.append(css[append_idx:match.start(0)]) 133 | 134 | assert found_term 135 | 136 | token = css[start_idx:end_idx] 137 | 138 | if remove_ws: 139 | token = _ws_re.sub('', token) 140 | 141 | preserver = ('{0}(___YUICSSMIN_PRESERVED_TOKEN_{1}___)' 142 | .format(name, len(preserved_tokens))) 143 | 144 | preserved_tokens.append(token) 145 | sb.append(preserver) 146 | 147 | append_idx = end_idx + 1 148 | 149 | sb.append(css[append_idx:]) 150 | 151 | return ''.join(sb) 152 | 153 | 154 | def _compress_rgb_calls(css): 155 | # Shorten colors from rgb(51,102,153) to #336699 156 | # This makes it more likely that it'll get further compressed in the next step. 157 | def _replace(match): 158 | rgb_colors = match.group(1).split(',') 159 | result = '#' 160 | for comp in rgb_colors: 161 | comp = int(comp) 162 | if comp < 16: 163 | result += '0' 164 | if comp > 255: 165 | comp = 255 166 | result += hex(comp)[2:].lower() 167 | return result 168 | return _rgb_re.sub(_replace, css) 169 | 170 | 171 | def _compress_hex_colors(css): 172 | # Shorten colors from #AABBCC to #ABC. Note that we want to make sure 173 | # the color is not preceded by either ", " or =. Indeed, the property 174 | # filter: chroma(color="#FFFFFF"); 175 | # would become 176 | # filter: chroma(color="#FFF"); 177 | # which makes the filter break in IE. 178 | # We also want to make sure we're only compressing #AABBCC patterns inside { }, 179 | # not id selectors ( #FAABAC {} ) 180 | # We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD) 181 | 182 | buf = [] 183 | 184 | index = 0 185 | while True: 186 | match = _hex_color_re.search(css, index) 187 | if not match: 188 | break 189 | 190 | buf.append(css[index:match.start(0)]) 191 | 192 | 193 | if match.group(1): 194 | # Restore, as is. Compression will break filters 195 | buf.append(match.group(1) + ('#' + match.group(2) + match.group(3) + match.group(4) + 196 | match.group(5) + match.group(6) + match.group(7))) 197 | 198 | else: 199 | if (match.group(2).lower() == match.group(3).lower() and 200 | match.group(4).lower() == match.group(5).lower() and 201 | match.group(6).lower() == match.group(7).lower()): 202 | 203 | buf.append('#' + (match.group(2) + match.group(4) + match.group(6)).lower()) 204 | 205 | else: 206 | buf.append('#' + (match.group(2) + match.group(3) + match.group(4) + 207 | match.group(5) + match.group(6) + match.group(7)).lower()) 208 | 209 | index = match.end(7) 210 | 211 | buf.append(css[index:]) 212 | 213 | return ''.join(buf) 214 | 215 | 216 | def _compress(css, max_linelen=0): 217 | start_idx = end_idx = 0 218 | total_len = len(css) 219 | 220 | preserved_tokens = [] 221 | css = _preserve_call_tokens(css, _url_re, preserved_tokens, remove_ws=True) 222 | css = _preserve_call_tokens(css, _calc_re, preserved_tokens, remove_ws=False) 223 | css = _preserve_call_tokens(css, _hsl_re, preserved_tokens, remove_ws=True) 224 | 225 | # Collect all comments blocks... 226 | comments = [] 227 | while True: 228 | start_idx = css.find('/*', start_idx) 229 | if start_idx < 0: 230 | break 231 | 232 | suffix = '' 233 | end_idx = css.find('*/', start_idx + 2) 234 | if end_idx < 0: 235 | end_idx = total_len 236 | suffix = '*/' 237 | 238 | token = css[start_idx + 2:end_idx] 239 | comments.append(token) 240 | 241 | css = (css[:start_idx + 2] + 242 | '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_{0}___'.format(len(comments)-1) + 243 | css[end_idx:] + suffix) 244 | 245 | start_idx += 2 246 | 247 | # preserve strings so their content doesn't get accidentally minified 248 | def _replace(match): 249 | token = match.group(0) 250 | quote = token[0] 251 | token = token[1:-1] 252 | 253 | 254 | # maybe the string contains a comment-like substring? 255 | # one, maybe more? put'em back then 256 | if '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_' in token: 257 | token = _yui_comment_re.sub(lambda match: comments[int(match.group('num'))], token) 258 | 259 | token = _ms_filter_re.sub('alpha(opacity=', token) 260 | 261 | preserved_tokens.append(token) 262 | return (quote + 263 | '___YUICSSMIN_PRESERVED_TOKEN_{0}___'.format(len(preserved_tokens)-1) + 264 | quote) 265 | 266 | css = _str_re.sub(_replace, css) 267 | 268 | # strings are safe, now wrestle the comments 269 | comments_iter = iter(comments) 270 | for i, token in enumerate(comments_iter): 271 | placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_{0}___".format(i) 272 | 273 | # ! in the first position of the comment means preserve 274 | # so push to the preserved tokens while stripping the ! 275 | if token.startswith('!'): 276 | preserved_tokens.append(token) 277 | css = css.replace(placeholder, '___YUICSSMIN_PRESERVED_TOKEN_{0}___'. 278 | format(len(preserved_tokens)-1)) 279 | continue 280 | 281 | # \ in the last position looks like hack for Mac/IE5 282 | # shorten that to /*\*/ and the next one to /**/ 283 | if token.endswith('\\'): 284 | preserved_tokens.append('\\') 285 | css = css.replace(placeholder, 286 | '___YUICSSMIN_PRESERVED_TOKEN_{0}___'.format(len(preserved_tokens)-1)) 287 | 288 | # attn: advancing the loop 289 | next(comments_iter) 290 | 291 | preserved_tokens.append('') 292 | css = css.replace('___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_{0}___'.format(i+1), 293 | '___YUICSSMIN_PRESERVED_TOKEN_{0}___'.format(len(preserved_tokens)-1)) 294 | 295 | continue 296 | 297 | # keep empty comments after child selectors (IE7 hack) 298 | # e.g. html >/**/ body 299 | if not token: 300 | start_idx = css.find(placeholder) 301 | if start_idx > 2: 302 | if css[start_idx-3] == '>': 303 | preserved_tokens.append('') 304 | css = css.replace(placeholder, 305 | '___YUICSSMIN_PRESERVED_TOKEN_{0}___'. 306 | format(len(preserved_tokens)-1)) 307 | 308 | # in all other cases kill the comment 309 | css = css.replace('/*{0}*/'.format(placeholder), '') 310 | 311 | # Normalize all whitespace strings to single spaces. Easier to work with that way. 312 | css = _ws_re.sub(' ', css) 313 | 314 | def _replace(match): 315 | token = match.group(1) 316 | preserved_tokens.append(token); 317 | return ('filter:progid:DXImageTransform.Microsoft.Matrix(' + 318 | '___YUICSSMIN_PRESERVED_TOKEN_{0}___);'.format(len(preserved_tokens)-1)) 319 | css = _ie_matrix_re.sub(_replace, css) 320 | 321 | # Remove the spaces before the things that should not have spaces before them. 322 | # But, be careful not to turn "p :link {...}" into "p:link{...}" 323 | # Swap out any pseudo-class colons with the token, and then swap back. 324 | css = _spaces1_re.sub(lambda match: match.group(0) \ 325 | .replace(':', '___YUICSSMIN_PSEUDOCLASSCOLON___'), css) 326 | 327 | css = _spaces2_re.sub(lambda match: match.group(1), css) 328 | 329 | # Restore spaces for !important 330 | css = css.replace('!important', ' !important'); 331 | 332 | # bring back the colon 333 | css = css.replace('___YUICSSMIN_PSEUDOCLASSCOLON___', ':') 334 | 335 | # retain space for special IE6 cases 336 | css = _ie6special_re.sub( 337 | lambda match: ':first-{0} {1}'.format(match.group(1).lower(), match.group(2)), 338 | css) 339 | 340 | # no space after the end of a preserved comment 341 | css = css.replace('*/ ', '*/') 342 | 343 | # If there are multiple @charset directives, push them to the top of the file. 344 | css = _charset1_re.sub(lambda match: match.group(2).lower() + \ 345 | ' ' + match.group(3) + match.group(1), 346 | css) 347 | 348 | # When all @charset are at the top, remove the second and after (as they are completely ignored) 349 | css = _charset2_re.sub(lambda match: match.group(2) + \ 350 | match.group(3).lower() + ' ' + match.group(4), 351 | css) 352 | 353 | # lowercase some popular @directives (@charset is done right above) 354 | css = _dirs_re.sub(lambda match: '@' + match.group(1).lower(), css) 355 | 356 | # lowercase some more common pseudo-elements 357 | css = _pseudo_re.sub(lambda match: ':' + match.group(1).lower(), css) 358 | 359 | # lowercase some more common functions 360 | css = _common_funcs_re.sub(lambda match: ':' + match.group(1).lower() + '(', css) 361 | 362 | # lower case some common function that can be values 363 | # NOTE: rgb() isn't useful as we replace with #hex later, as well as and() 364 | # is already done for us right after this 365 | css = _common_val_funcs_re.sub(lambda match: match.group(1) + match.group(2).lower(), css) 366 | 367 | # Put the space back in some cases, to support stuff like 368 | # @media screen and (-webkit-min-device-pixel-ratio:0){ 369 | css = _space_and_re.sub('and (', css) 370 | 371 | # Remove the spaces after the things that should not have spaces after them. 372 | css = _space_after_re.sub(r'\1', css) 373 | 374 | # remove unnecessary semicolons 375 | css = _semi_re.sub('}', css) 376 | 377 | # Replace 0(px,em,%) with 0. 378 | css = _zero_fmt_spec_re.sub(lambda match: match.group(1) + '0', css) 379 | 380 | # Replace 0 0 0 0; with 0. 381 | css = _quad_0_re.sub(r':0\1', css) 382 | css = _trip_0_re.sub(r':0\1', css) 383 | css = _coup_0_re.sub(r':0\1', css) 384 | 385 | # Replace background-position:0; with background-position:0 0; 386 | # same for transform-origin 387 | css = _bg_pos_re.sub(lambda match: match.group(1).lower() + ':0 0' + match.group(2), css) 388 | 389 | # Replace 0.6 to .6, but only when preceded by : or a white-space 390 | css = _point_float_re.sub(r'\1.\2', css) 391 | 392 | css = _compress_rgb_calls(css) 393 | css = _compress_hex_colors(css) 394 | 395 | # Replace #f00 -> red; other short color keywords 396 | css = _colors_re.sub(lambda match: match.group(1) + _colors_map[match.group(3).lower()] + 397 | match.group(4), 398 | css) 399 | 400 | # border: none -> border:0 401 | css = _border_re.sub(lambda match: match.group(1).lower() + ':0' + match.group(2), css) 402 | 403 | # shorter opacity IE filter 404 | css = _ms_filter_re.sub('alpha(opacity=', css) 405 | 406 | # Find a fraction that is used for Opera's -o-device-pixel-ratio query 407 | # Add token to add the "\" back in later 408 | css = _o_px_ratio_re.sub(r'\1:\2___YUI_QUERY_FRACTION___\3', css) 409 | 410 | # Remove empty rules. 411 | css = _empty_rules_re.sub('', css) 412 | 413 | # Add "\" back to fix Opera -o-device-pixel-ratio query 414 | css = css.replace('___YUI_QUERY_FRACTION___', '/') 415 | 416 | if max_linelen and len(css) > max_linelen: 417 | buf = [] 418 | start_pos = 0 419 | while True: 420 | buf.append(css[start_pos:start_pos + max_linelen]) 421 | start_pos += max_linelen 422 | while start_pos < len(css): 423 | if css[start_pos] == '}': 424 | buf.append('}\n') 425 | start_pos += 1 426 | break 427 | else: 428 | buf.append(css[start_pos]) 429 | start_pos += 1 430 | if start_pos >= len(css): 431 | break 432 | css = ''.join(buf) 433 | 434 | # Replace multiple semi-colons in a row by a single one 435 | # See SF bug #1980989 436 | css = _many_semi_re.sub(';', css) 437 | 438 | return css, preserved_tokens 439 | 440 | 441 | def _apply_preserved(css, preserved_tokens): 442 | # restore preserved comments and strings 443 | for i, token in reversed(tuple(enumerate(preserved_tokens))): 444 | css = css.replace('___YUICSSMIN_PRESERVED_TOKEN_{0}___'.format(i), token) 445 | 446 | css = css.strip() 447 | return css 448 | 449 | 450 | def compress(css, max_linelen=0): 451 | """Compress given CSS stylesheet. 452 | 453 | Parameters: 454 | 455 | - css : str 456 | An str with CSS rules. 457 | 458 | - max_linelen : int = 0 459 | Some source control tools don't like it when files containing lines longer 460 | than, say 8000 characters, are checked in. This option is used in 461 | that case to split long lines after a specific column. 462 | 463 | Returns a ``str`` object with compressed CSS. 464 | """ 465 | 466 | css, preserved_tokens = _compress(css, max_linelen=max_linelen) 467 | css = _apply_preserved(css, preserved_tokens) 468 | return css 469 | 470 | 471 | def compress_partitioned(css, 472 | max_linelen=0, 473 | max_rules_per_file=4000): 474 | """Compress given CSS stylesheet into a set of files. 475 | 476 | Parameters: 477 | 478 | - max_linelen : int = 0 479 | Has the same meaning as for "compress()" function. 480 | 481 | - max_rules_per_file : int = 0 482 | Internet Explorers <= 9 have an artificial max number of rules per CSS 483 | file (4096; http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx) 484 | When ``max_rules_per_file`` is a positive number, the function *always* returns 485 | a list of ``str`` objects, each limited to contain less than the passed number 486 | of rules. 487 | 488 | Always returns a ``list`` of ``str`` objects with compressed CSS. 489 | """ 490 | 491 | assert max_rules_per_file > 0 492 | 493 | css, preserved_tokens = _compress(css, max_linelen=max_linelen) 494 | css = css.strip() 495 | 496 | bufs = [] 497 | buf = [] 498 | rules = 0 499 | while css: 500 | if rules >= max_rules_per_file: 501 | bufs.append(''.join(buf)) 502 | rules = 0 503 | buf = [] 504 | 505 | nested = 0 506 | while True: 507 | op_idx = css.find('{') 508 | cl_idx = css.find('}') 509 | 510 | if cl_idx < 0: 511 | raise ValueError('malformed CSS: non-balanced curly-braces') 512 | 513 | if op_idx < 0 or cl_idx < op_idx: # ... } ... { ... 514 | nested -= 1 515 | 516 | if nested < 0: 517 | raise ValueError('malformed CSS: non-balanced curly-braces') 518 | 519 | buf.append(css[:cl_idx+1]) 520 | css = css[cl_idx+1:] 521 | 522 | if not nested: # closing rules 523 | break 524 | 525 | else: # ... { ... } ... 526 | nested += 1 527 | 528 | rule_line = css[:op_idx+1] 529 | buf.append(rule_line) 530 | css = css[op_idx+1:] 531 | 532 | rules += rule_line.count(',') + 1 533 | 534 | bufs.append(''.join(buf)) 535 | 536 | bufs = [_apply_preserved(buf, preserved_tokens) for buf in bufs] 537 | 538 | return bufs 539 | -------------------------------------------------------------------------------- /modules/csscompressor/__main__.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013 Sprymix Inc. 3 | # All rights reserved. 4 | # 5 | # See LICENSE for details. 6 | ## 7 | 8 | 9 | import argparse 10 | import csscompressor 11 | 12 | 13 | def _get_args(): 14 | parser = argparse.ArgumentParser( 15 | description='CSS Compressor {}'.format(csscompressor.__version__)) 16 | 17 | parser.add_argument('input', nargs='+', type=str, 18 | help='File(s) to compress') 19 | parser.add_argument('--line-break', type=int, metavar='', 20 | help='Insert a line break after the specified column number') 21 | parser.add_argument('-o', '--output', type=str, metavar='', 22 | help='Place the output into . Defaults to stdout') 23 | 24 | args = parser.parse_args() 25 | return args 26 | 27 | 28 | def main(): 29 | args = _get_args() 30 | 31 | buffer = [] 32 | for name in args.input: 33 | with open(name, 'rt') as f: 34 | buffer.append(f.read()) 35 | buffer = '\n\n'.join(buffer) 36 | 37 | line_break = 0 38 | if args.line_break is not None: 39 | line_break = args.line_break 40 | 41 | output = csscompressor.compress(buffer, max_linelen=line_break) 42 | 43 | if args.output: 44 | with open(args.output, 'wt') as f: 45 | f.write(output) 46 | f.write('\n') 47 | else: 48 | print(output) 49 | 50 | 51 | main() 52 | -------------------------------------------------------------------------------- /modules/csscompressor/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekpradd/sublime-html5-minifier/11b7fccd3978a36c52d864faf406351121e8fe27/modules/csscompressor/tests/__init__.py -------------------------------------------------------------------------------- /modules/csscompressor/tests/base.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013 Sprymix Inc. 3 | # All rights reserved. 4 | # 5 | # See LICENSE for details. 6 | ## 7 | 8 | 9 | from csscompressor import compress 10 | 11 | 12 | class BaseTest: 13 | def _test(self, input, output): 14 | result = compress(input) 15 | if result != output.strip(): 16 | print() 17 | print('CSM', repr(result)) 18 | print() 19 | print('YUI', repr(output)) 20 | print() 21 | 22 | # import difflib 23 | # d = difflib.Differ() 24 | # diff = list(d.compare(result, output.strip())) 25 | # from pprint import pprint 26 | # pprint(diff) 27 | 28 | assert False 29 | -------------------------------------------------------------------------------- /modules/csscompressor/tests/test_compress.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013 Sprymix Inc. 3 | # All rights reserved. 4 | # 5 | # See LICENSE for details. 6 | ## 7 | 8 | 9 | from csscompressor.tests.base import BaseTest 10 | from csscompressor import compress 11 | 12 | import unittest 13 | 14 | 15 | class Tests(unittest.TestCase): 16 | def test_linelen_1(self): 17 | input = ''' 18 | a {content: '}}'} 19 | b {content: '}'} 20 | c {content: '{'} 21 | ''' 22 | output = compress(input, max_linelen=2) 23 | assert output == "a{content:'}}'}\nb{content:'}'}\nc{content:'{'}" 24 | 25 | def test_linelen_2(self): 26 | input = '' 27 | output = compress(input, max_linelen=2) 28 | assert output == "" 29 | 30 | def test_linelen_3(self): 31 | input = ''' 32 | a {content: '}}'} 33 | b {content: '}'} 34 | c {content: '{'} 35 | d {content: '{'} 36 | ''' 37 | output = compress(input, max_linelen=100) 38 | assert output == "a{content:'}}'}b{content:'}'}c{content:'{'}\nd{content:'{'}" 39 | 40 | def test_compress_1(self): 41 | input = ''' 42 | a {content: '}}'} /* 43 | b {content: '}'} 44 | c {content: '{'} 45 | d {content: '{'} 46 | ''' 47 | output = compress(input) 48 | assert output == "a{content:'}}'}" 49 | 50 | def test_compress_2(self): 51 | input = ''' 52 | a {content: calc(10px-10%} 53 | ''' 54 | self.assertRaises(ValueError, compress, input) 55 | -------------------------------------------------------------------------------- /modules/csscompressor/tests/test_other.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013 Sprymix Inc. 3 | # All rights reserved. 4 | # 5 | # See LICENSE for details. 6 | ## 7 | 8 | 9 | from csscompressor.tests.base import BaseTest 10 | 11 | 12 | class Tests(BaseTest): 13 | def test_issue_108(self): 14 | # https://github.com/yui/yuicompressor/issues/108 15 | 16 | input = ''' 17 | table thead tr td { 18 | color: #CEDB00; 19 | padding: 0.5em 0 1.0em 0; 20 | text-transform: uppercase; 21 | vertical-align: bottom; 22 | } 23 | ''' 24 | 25 | output = '''table thead tr td{color:#cedb00;padding:.5em 0 1.0em 0;text-transform:uppercase;vertical-align:bottom}''' 26 | 27 | self._test(input, output) 28 | 29 | def test_issue_59(self): 30 | # https://github.com/yui/yuicompressor/issues/59 31 | 32 | input = ''' 33 | .issue-59 { 34 | width:100%; 35 | width: -webkit-calc(100% + 30px); 36 | width: -moz-calc(100% + 30px); 37 | width: calc(100% + 30px); 38 | } 39 | ''' 40 | 41 | output = '''.issue-59{width:100%;width:-webkit-calc(100% + 30px);width:-moz-calc(100% + 30px);width:calc(100% + 30px)}''' 42 | 43 | self._test(input, output) 44 | 45 | def test_issue_81(self): 46 | # https://github.com/yui/yuicompressor/issues/81 47 | 48 | input = ''' 49 | .SB-messages .SB-message a { 50 | color: rgb(185, 99, 117); 51 | border-bottom: 1px dotted text-shadow: 0 1px 0 hsl(0, 0%, 0%); 52 | text-shadow: 0 1px 0 hsla(0, 0%, 0%, 1); 53 | } 54 | ''' 55 | 56 | output = '.SB-messages .SB-message a{color:#b96375;border-bottom:1px dotted text-shadow:0 1px 0 hsl(0,0%,0%);text-shadow:0 1px 0 hsla(0,0%,0%,1)}' 57 | 58 | self._test(input, output) 59 | -------------------------------------------------------------------------------- /modules/csscompressor/tests/test_partition.py: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright (c) 2013 Sprymix Inc. 3 | # All rights reserved. 4 | # 5 | # See LICENSE for details. 6 | ## 7 | 8 | 9 | from csscompressor.tests.base import BaseTest 10 | from csscompressor import compress_partitioned 11 | 12 | import unittest 13 | 14 | 15 | class Tests(unittest.TestCase): 16 | def test_partition_1(self): 17 | input = '' 18 | output = compress_partitioned(input, max_rules_per_file=2) 19 | assert output == [''] 20 | 21 | def test_partition_2(self): 22 | input = ''' 23 | a {content: '}}'} 24 | b {content: '}'} 25 | c {content: '{'} 26 | ''' 27 | 28 | output = compress_partitioned(input, max_rules_per_file=2) 29 | assert output == ["a{content:'}}'}b{content:'}'}", "c{content:'{'}"] 30 | 31 | def test_partition_3(self): 32 | input = ''' 33 | @media{ 34 | a {p: 1} 35 | b {p: 2} 36 | x {p: 2} 37 | } 38 | @media{ 39 | c {p: 1} 40 | d {p: 2} 41 | y {p: 2} 42 | } 43 | @media{ 44 | e {p: 1} 45 | f {p: 2} 46 | z {p: 2} 47 | } 48 | ''' 49 | 50 | output = compress_partitioned(input, max_rules_per_file=2) 51 | assert output == ['@media{a{p:1}b{p:2}x{p:2}}', 52 | '@media{c{p:1}d{p:2}y{p:2}}', 53 | '@media{e{p:1}f{p:2}z{p:2}}'] 54 | 55 | def test_partition_4(self): 56 | input = ''' 57 | @media{ 58 | a {p: 1} 59 | b {p: 2} 60 | x {p: 2} 61 | ''' 62 | 63 | self.assertRaises(ValueError, compress_partitioned, 64 | input, max_rules_per_file=2) 65 | 66 | def test_partition_5(self): 67 | input = ''' 68 | @media{ 69 | a {p: 1} 70 | b {p: 2} 71 | x {p: 2} 72 | 73 | @media{ 74 | c {p: 1} 75 | d {p: 2} 76 | y {p: 2} 77 | } 78 | @media{ 79 | e {p: 1} 80 | f {p: 2} 81 | z {p: 2} 82 | } 83 | ''' 84 | 85 | self.assertRaises(ValueError, compress_partitioned, 86 | input, max_rules_per_file=2) 87 | 88 | def test_partition_6(self): 89 | input = ''' 90 | @media{}} 91 | 92 | a {p: 1} 93 | b {p: 2} 94 | x {p: 2} 95 | ''' 96 | 97 | self.assertRaises(ValueError, compress_partitioned, 98 | input, max_rules_per_file=2) 99 | 100 | def test_partition_7(self): 101 | input = ''' 102 | a, a1, a2 {color: red} 103 | b, b2, b3 {color: red} 104 | c, c3, c4, c5 {color: red} 105 | d {color: red} 106 | ''' 107 | 108 | output = compress_partitioned(input, max_rules_per_file=2) 109 | assert output == ['a,a1,a2{color:red}', 'b,b2,b3{color:red}', 110 | 'c,c3,c4,c5{color:red}', 'd{color:red}'] 111 | 112 | def test_partition_8(self): 113 | input = ''' 114 | @media{ 115 | a {p: 1} 116 | b {p: 2} 117 | x {p: 2} 118 | } 119 | @media{ 120 | c {p: 1} 121 | d {p: 2} 122 | y {p: 2} 123 | } 124 | @media{ 125 | e {p: 1} 126 | f {p: 2} 127 | z {p: 2} 128 | } 129 | z {p: 2} 130 | ''' 131 | 132 | # carefully pick 'max_linelen' to have a trailing '\n' after 133 | # '_compress' call 134 | output = compress_partitioned(input, max_rules_per_file=2, max_linelen=6) 135 | assert output == ['@media{a{p:1}\nb{p:2}x{p:2}\n}', 136 | '@media{c{p:1}\nd{p:2}y{p:2}\n}', 137 | '@media{e{p:1}\nf{p:2}z{p:2}\n}', 138 | 'z{p:2}'] 139 | -------------------------------------------------------------------------------- /modules/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.4' 31 | -------------------------------------------------------------------------------- /modules/htmlmin/command.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 | #!/usr/bin/env python 29 | 30 | import argparse 31 | import codecs 32 | import sys 33 | 34 | #import htmlmin 35 | from . import Minifier 36 | 37 | parser = argparse.ArgumentParser( 38 | description='Minify HTML', 39 | formatter_class=argparse.RawTextHelpFormatter 40 | ) 41 | 42 | parser.add_argument('input_file', 43 | nargs='?', 44 | metavar='INPUT', 45 | help='File path to html file to minify. Defaults to stdin.', 46 | ) 47 | 48 | parser.add_argument('output_file', 49 | nargs='?', 50 | metavar='OUTPUT', 51 | help="File path to output to. Defaults to stdout.", 52 | ) 53 | 54 | parser.add_argument('-c', '--remove-comments', 55 | help=( 56 | '''When set, comments will be removed. They can be kept on an individual basis 57 | by starting them with a '!': . The '!' will be removed from 58 | the final output. If you want a '!' as the leading character of your comment, 59 | put two of them: . 60 | 61 | '''), 62 | action='store_true') 63 | 64 | parser.add_argument('-s', '--remove-empty-space', 65 | help=( 66 | '''When set, this removes empty space betwen tags in certain cases. 67 | Specifically, it will remove empty space if and only if there a newline 68 | character occurs within the space. Thus, code like 69 | 'x y' will be left alone, but code such as 70 | ' ... 71 | 72 | 73 | ...' 74 | will become '......'. Note that this CAN break your 75 | html if you spread two inline tags over two lines. Use with caution. 76 | 77 | '''), 78 | action='store_true') 79 | 80 | parser.add_argument('--remove-all-empty-space', 81 | help=( 82 | '''When set, this removes ALL empty space betwen tags. WARNING: this can and 83 | likely will cause unintended consequences. For instance, 'X Y' 84 | will become 'XY'. Putting whitespace along with other text will 85 | avoid this problem. Only use if you are confident in the result. Whitespace is 86 | not removed from inside of tags, thus ' ' will be left alone. 87 | 88 | '''), 89 | action='store_true') 90 | 91 | parser.add_argument('-H', '--in-head', 92 | help=( 93 | '''If you are parsing only a fragment of HTML, and the fragment occurs in the 94 | head of the document, setting this will remove some extra whitespace. 95 | 96 | '''), 97 | action='store_true') 98 | 99 | parser.add_argument('-k', '--keep-pre-attr', 100 | help=( 101 | '''HTMLMin supports the propietary attribute 'pre' that can be added to elements 102 | to prevent minification. This attribute is removed by default. Set this flag to 103 | keep the 'pre' attributes in place. 104 | 105 | '''), 106 | action='store_true') 107 | 108 | parser.add_argument('-a', '--pre-attr', 109 | help=( 110 | '''The attribute htmlmin looks for to find blocks of HTML that it should not 111 | minify. This attribute will be removed from the HTML unless '-k' is 112 | specified. Defaults to 'pre'. 113 | 114 | '''), 115 | default='pre') 116 | 117 | 118 | parser.add_argument('-p', '--pre-tags', 119 | metavar='TAG', 120 | help=( 121 | '''By default, the contents of 'pre', and 'textarea' tags are left unminified. 122 | You can specify different tags using the --pre-tags option. 'script' and 'style' 123 | tags are always left unmininfied. 124 | 125 | '''), 126 | nargs='*', 127 | default=['pre', 'textarea']) 128 | parser.add_argument('-e', '--encoding', 129 | 130 | help=("Encoding to read and write with. Default 'utf-8'.\n\n"), 131 | default='utf-8', 132 | ) 133 | 134 | def main(): 135 | args = parser.parse_args() 136 | minifier = Minifier( 137 | remove_comments=args.remove_comments, 138 | remove_empty_space=args.remove_empty_space, 139 | pre_tags=args.pre_tags, 140 | keep_pre=args.keep_pre_attr, 141 | pre_attr=args.pre_attr, 142 | ) 143 | if args.input_file: 144 | inp = codecs.open(args.input_file, encoding=args.encoding) 145 | else: 146 | inp = sys.stdin 147 | 148 | for line in inp.readlines(): 149 | minifier.input(line) 150 | 151 | if args.output_file: 152 | codecs.open(args.output_file, 'w', encoding=args.encoding).write(minifier.output) 153 | else: 154 | print(minifier.output) 155 | 156 | if __name__ == '__main__': 157 | main() 158 | 159 | -------------------------------------------------------------------------------- /modules/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 | -------------------------------------------------------------------------------- /modules/htmlmin/main.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 | import cgi 29 | import re 30 | 31 | from . import parser 32 | 33 | def minify(input, 34 | remove_comments=False, 35 | remove_empty_space=False, 36 | remove_all_empty_space=False, 37 | reduce_boolean_attributes=False, 38 | keep_pre=False, 39 | pre_tags=parser.PRE_TAGS, 40 | pre_attr='pre'): 41 | """Minifies HTML in one shot. 42 | 43 | :param input: A string containing the HTML to be minified. 44 | :param remove_comments: Remove comments found in HTML. Individual comments can 45 | be maintained by putting a ``!`` as the first character inside the comment. 46 | Thus:: 47 | 48 | 49 | 50 | Will become simply:: 51 | 52 | 53 | 54 | The added exclamation is removed. 55 | :param remove_empty_space: Remove empty space found in HTML between an opening 56 | and a closing tag and when it contains a newline or carriage return. If 57 | whitespace is found that is only spaces and/or tabs, it will be turned into 58 | a single space. Be careful, this can have unintended consequences. 59 | :param remove_all_empty_space: A more extreme version of 60 | ``remove_empty_space``, this removes all empty whitespace found between 61 | tags. This is almost gauranteed to break your HTML unless you are very 62 | careful. 63 | nothing 64 | :param reduce_boolean_attributes: Where allowed by the HTML5 specification, 65 | attributes such as 'disabled' and 'readonly' will have their value removed, 66 | so 'disabled="true"' will simply become 'disabled'. This is generally a 67 | good option to turn on except when JavaScript relies on the values. 68 | :param keep_pre: By default, htmlmin uses the special attribute ``pre`` to 69 | allow you to demarcate areas of HTML that should not be minified. It removes 70 | this attribute as it finds it. Setting this value to ``True`` tells htmlmin 71 | to leave the attribute in the output. 72 | :param pre_tags: A list of tag names that should never be minified. You are 73 | free to change this list as you see fit, but you will probably want to 74 | include ``pre`` and ``textarea`` if you make any changes to the list. Note 75 | that `` ', 134 | ' ', 135 | ), 136 | 'remove_close_from_tags': ( 137 | ('

' 138 | ' ' 139 | ' '), 140 | ('

' 141 | ' ' 142 | ' '), 143 | ), 144 | 'remove_space_from_self_closed_tags': ( 145 | ' ', 146 | ' ', 147 | ), 148 | } 149 | 150 | SELF_CLOSE_TEXTS = { 151 | 'p_self_close': ( 152 | '

X

Y ', 153 | '

X

Y ', 154 | ), 155 | 'li_self_close': ( 156 | '

  • X
  • Y
  • Z
Q', 157 | '
  • X
  • Y
  • Z
Q', 158 | ), 159 | 'dt_self_close': ( 160 | '
X
Y
Z
Q', 161 | '
X
Y
Z
Q', 162 | ), 163 | 'dd_self_close': ( 164 | '
X
Y
Z
Q', 165 | '
X
Y
Z
Q', 166 | ), 167 | 'optgroup_self_close': ( 168 | (' '), 170 | (' '), 172 | ), 173 | 'option_self_close': ( 174 | (' '), 176 | (' '), 178 | ), 179 | 'colgroup_self_close': ( 180 | '
', 181 | '
', 182 | ), 183 | 'tbody_self_close': ( 184 | (' \n' 185 | '\n \n\n\n '), 186 | ('
X
Y
\n' 187 | '\n '), 188 | ), 189 | 'thead_self_close': ( 190 | ('
X
Y
' 191 | ' '), 192 | ('
X
Y
' 193 | ' '), 194 | ), 195 | 'tfoot_self_close': ( 196 | ('
X
Y
' 197 | ' '), 198 | ('
X
Y
' 199 | ' '), 200 | ), 201 | 'tr_self_close': ( 202 | ('
X
Y
' 203 | ' '), 204 | ('
X
Y
' 205 | ' '), 206 | ), 207 | 'td_self_close': ( 208 | '
X
Y
X Y ', 209 | '
X Y ', 210 | ), 211 | 'th_self_close': ( 212 | '
X Y ', 213 | '
X Y ', 214 | ), 215 | 'a_p_interaction': ( # the 'pre' functionality continues after the 216 | '

X

Y', 217 | '

X

Y', 218 | ), 219 | } 220 | 221 | SELF_OPENING_TEXTS = { 222 | 'html_closed_no_open': ( 223 | ' X ', 224 | ' X ' 225 | ), 226 | 'head_closed_no_open': ( 227 | ' X ', 228 | ' X ' # TODO: we could theoretically kill that leading 229 | # space. See HTMLMinParse.handle_endtag 230 | ), 231 | 'body_closed_no_open': ( 232 | ' X ', 233 | ' X ' 234 | ), 235 | 'colgroup_self_open': ( 236 | '
', 237 | '
', 238 | ), 239 | 'tbody_self_open': ( 240 | '
', 241 | '
', 242 | ), 243 | 'p_closed_no_open': ( # this isn't valid html, but its worth accounting for 244 | '

X

Y

', 245 | '
X

Y

', 246 | ), 247 | } 248 | 249 | class HTMLMinTestMeta(type): 250 | def __new__(cls, name, bases, dct): 251 | def make_test(text): 252 | def inner_test(self): 253 | self.assertEqual(self.minify(text[0]), text[1]) 254 | return inner_test 255 | 256 | for k, v in dct.get('__reference_texts__',{}).items(): 257 | if 'test_'+k not in dct: 258 | dct['test_'+k] = make_test(v) 259 | return type.__new__(cls, str(name), bases, dct) 260 | 261 | class HTMLMinTestCase( 262 | HTMLMinTestMeta('HTMLMinTestCase', (unittest.TestCase, ), {})): 263 | def setUp(self): 264 | self.minify = htmlmin.minify 265 | 266 | class TestMinifyFunction(HTMLMinTestCase): 267 | __reference_texts__ = MINIFY_FUNCTION_TEXTS 268 | 269 | def test_basic_minification_quality(self): 270 | import codecs 271 | with codecs.open('htmlmin/tests/large_test.html', encoding='utf-8') as inpf: 272 | inp = inpf.read() 273 | out = self.minify(inp) 274 | self.assertEqual(len(inp) - len(out), 8806) 275 | 276 | def test_high_minification_quality(self): 277 | import codecs 278 | with codecs.open('htmlmin/tests/large_test.html', encoding='utf-8') as inpf: 279 | inp = inpf.read() 280 | out = self.minify(inp, remove_all_empty_space=True, remove_comments=True) 281 | self.assertEqual(len(inp) - len(out), 12043) 282 | 283 | class TestMinifierObject(HTMLMinTestCase): 284 | __reference_texts__ = MINIFY_FUNCTION_TEXTS 285 | 286 | def setUp(self): 287 | HTMLMinTestCase.setUp(self) 288 | self.minifier = htmlmin.Minifier() 289 | self.minify = self.minifier.minify 290 | 291 | def test_reuse(self): 292 | text = self.__reference_texts__['simple_text'] 293 | self.assertEqual(self.minify(text[0]), text[1]) 294 | self.assertEqual(self.minify(text[0]), text[1]) 295 | 296 | def test_buffered_input(self): 297 | text = self.__reference_texts__['long_text'] 298 | self.minifier.input(text[0][:len(text[0]) // 2]) 299 | self.minifier.input(text[0][len(text[0]) // 2:]) 300 | self.assertEqual(self.minifier.finalize(), text[1]) 301 | 302 | class TestMinifyFeatures(HTMLMinTestCase): 303 | __reference_texts__ = FEATURES_TEXTS 304 | 305 | def test_remove_comments(self): 306 | text = self.__reference_texts__['remove_comments'] 307 | self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1]) 308 | 309 | def test_reduce_boolean_attributes(self): 310 | text = self.__reference_texts__['reduce_boolean_attributes'] 311 | self.assertEqual(htmlmin.minify(text[0], reduce_boolean_attributes=True), text[1]) 312 | 313 | def test_keep_comments(self): 314 | text = self.__reference_texts__['keep_comments'] 315 | self.assertEqual(htmlmin.minify(text[0], remove_comments=True), text[1]) 316 | 317 | def test_keep_pre_attribute(self): 318 | text = self.__reference_texts__['keep_pre_attribute'] 319 | self.assertEqual(htmlmin.minify(text[0], keep_pre=True), text[1]) 320 | 321 | def test_custom_pre_attribute(self): 322 | text = self.__reference_texts__['custom_pre_attribute'] 323 | self.assertEqual(htmlmin.minify(text[0], pre_attr='custom'), text[1]) 324 | 325 | def test_keep_empty(self): 326 | text = self.__reference_texts__['keep_empty'] 327 | self.assertEqual(htmlmin.minify(text[0]), text[1]) 328 | 329 | def test_remove_empty(self): 330 | text = self.__reference_texts__['remove_empty'] 331 | self.assertEqual(htmlmin.minify(text[0], remove_empty_space=True), text[1]) 332 | 333 | def test_remove_all_empty(self): 334 | text = self.__reference_texts__['remove_all_empty'] 335 | self.assertEqual(htmlmin.minify(text[0], remove_all_empty_space=True), 336 | text[1]) 337 | 338 | def test_dont_minify_div(self): 339 | text = self.__reference_texts__['dont_minify_div'] 340 | self.assertEqual(htmlmin.minify(text[0], pre_tags=('div',)), text[1]) 341 | 342 | def test_minify_pre(self): 343 | text = self.__reference_texts__['minify_pre'] 344 | self.assertEqual(htmlmin.minify(text[0], pre_tags=('div',)), text[1]) 345 | 346 | def test_remove_head_spaces(self): 347 | text = self.__reference_texts__['remove_head_spaces'] 348 | self.assertEqual(htmlmin.minify(text[0]), text[1]) 349 | 350 | def test_dont_minify_scripts_or_styles(self): 351 | text = self.__reference_texts__['dont_minify_scripts_or_styles'] 352 | self.assertEqual(htmlmin.minify(text[0], pre_tags=[]), text[1]) 353 | 354 | class TestSelfClosingTags(HTMLMinTestCase): 355 | __reference_texts__ = SELF_CLOSE_TEXTS 356 | 357 | class TestSelfOpeningTags(HTMLMinTestCase): 358 | __reference_texts__ = SELF_OPENING_TEXTS 359 | 360 | class TestDecorator(HTMLMinTestCase): 361 | def test_direct_decorator(self): 362 | @htmlmindecorator 363 | def directly_decorated(): 364 | return ' X Y ' 365 | 366 | self.assertEqual(' X Y ', directly_decorated()) 367 | 368 | def test_options_decorator(self): 369 | @htmlmindecorator(remove_comments=True) 370 | def directly_decorated(): 371 | return ' X Y ' 372 | 373 | self.assertEqual(' X Y ', directly_decorated()) 374 | 375 | class TestMiddleware(HTMLMinTestCase): 376 | def setUp(self): 377 | HTMLMinTestCase.setUp(self) 378 | def wsgi_app(environ, start_response): 379 | start_response(environ['status'], environ['headers']) 380 | yield environ['content'] 381 | 382 | self.wsgi_app = wsgi_app 383 | 384 | def call_app(self, app, status, headers, content): 385 | response_status = [] # these need to be mutable so that they can be changed 386 | response_headers = [] # within our inner function. 387 | def start_response(status, headers, exc_info=None): 388 | response_status.append(status) 389 | response_headers.append(headers) 390 | response_body = ''.join(app({'status': status, 391 | 'content': content, 392 | 'headers': headers}, 393 | start_response)) 394 | return response_status[0], response_headers[0], response_body 395 | 396 | def test_middlware(self): 397 | app = HTMLMinMiddleware(self.wsgi_app) 398 | status, headers, body = self.call_app( 399 | app, '200 OK', (('Content-Type', 'text/html'),), 400 | ' X Y ') 401 | self.assertEqual(body, ' X Y ') 402 | 403 | def test_middlware_minifier_options(self): 404 | app = HTMLMinMiddleware(self.wsgi_app, remove_comments=True) 405 | status, headers, body = self.call_app( 406 | app, '200 OK', (('Content-Type', 'text/html'),), 407 | ' X Y ') 408 | self.assertEqual(body, ' X Y ') 409 | 410 | def test_middlware_off_by_default(self): 411 | app = HTMLMinMiddleware(self.wsgi_app, by_default=False) 412 | status, headers, body = self.call_app( 413 | app, '200 OK', (('Content-Type', 'text/html'),), 414 | ' X Y ') 415 | self.assertEqual(body, ' X Y ') 416 | 417 | def test_middlware_on_by_header(self): 418 | app = HTMLMinMiddleware(self.wsgi_app, by_default=False) 419 | status, headers, body = self.call_app( 420 | app, '200 OK', ( 421 | ('Content-Type', 'text/html'), 422 | ('X-HTML-Min-Enable', 'True'), 423 | ), 424 | ' X Y ') 425 | self.assertEqual(body, ' X Y ') 426 | 427 | def test_middlware_off_by_header(self): 428 | app = HTMLMinMiddleware(self.wsgi_app) 429 | status, headers, body = self.call_app( 430 | app, '200 OK', ( 431 | ('Content-Type', 'text/html'), 432 | ('X-HTML-Min-Enable', 'False'), 433 | ), 434 | ' X Y ') 435 | self.assertEqual(body, ' X Y ') 436 | 437 | def test_middlware_remove_header(self): 438 | app = HTMLMinMiddleware(self.wsgi_app) 439 | status, headers, body = self.call_app( 440 | app, '200 OK', ( 441 | ('Content-Type', 'text/html'), 442 | ('X-HTML-Min-Enable', 'False'), 443 | ), 444 | ' X Y ') 445 | self.assertFalse(any((h == 'X-HTML-Min-Enable' for h, v in headers))) 446 | 447 | def test_middlware_keep_header(self): 448 | app = HTMLMinMiddleware(self.wsgi_app, keep_header=True) 449 | status, headers, body = self.call_app( 450 | app, '200 OK', [ 451 | ('Content-Type', 'text/html'), 452 | ('X-HTML-Min-Enable', 'False'), 453 | ], 454 | ' X Y ') 455 | self.assertTrue(any((h == 'X-HTML-Min-Enable' for h, v in headers))) 456 | 457 | def suite(): 458 | minify_function_suite = unittest.TestLoader().\ 459 | loadTestsFromTestCase(TestMinifyFunction) 460 | minifier_object_suite = unittest.TestLoader().\ 461 | loadTestsFromTestCase(TestMinifierObject) 462 | minify_features_suite = unittest.TestLoader().\ 463 | loadTestsFromTestCase(TestMinifyFeatures) 464 | self_closing_tags_suite = unittest.TestLoader().\ 465 | loadTestsFromTestCase(TestSelfClosingTags) 466 | self_opening_tags_suite = unittest.TestLoader().\ 467 | loadTestsFromTestCase(TestSelfOpeningTags) 468 | decorator_suite = unittest.TestLoader().\ 469 | loadTestsFromTestCase(TestDecorator) 470 | middleware_suite = unittest.TestLoader().\ 471 | loadTestsFromTestCase(TestMiddleware) 472 | return unittest.TestSuite([ 473 | minify_function_suite, 474 | minifier_object_suite, 475 | minify_features_suite, 476 | self_closing_tags_suite, 477 | self_opening_tags_suite, 478 | decorator_suite, 479 | middleware_suite, 480 | ]) 481 | 482 | if __name__ == '__main__': 483 | unittest.main() 484 | -------------------------------------------------------------------------------- /modules/jsmin/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is original from jsmin by Douglas Crockford, it was translated to 2 | # Python by Baruch Even. It was rewritten by Dave St.Germain for speed. 3 | # 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2013 Dave St.Germain 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | 27 | import sys 28 | is_3 = sys.version_info >= (3, 0) 29 | if is_3: 30 | import io 31 | else: 32 | import StringIO 33 | try: 34 | import cStringIO 35 | except ImportError: 36 | cStringIO = None 37 | 38 | 39 | __all__ = ['jsmin', 'JavascriptMinify'] 40 | __version__ = '2.0.11' 41 | 42 | 43 | def jsmin(js): 44 | """ 45 | returns a minified version of the javascript string 46 | """ 47 | if not is_3: 48 | if cStringIO and not isinstance(js, unicode): 49 | # strings can use cStringIO for a 3x performance 50 | # improvement, but unicode (in python2) cannot 51 | klass = cStringIO.StringIO 52 | else: 53 | klass = StringIO.StringIO 54 | else: 55 | klass = io.StringIO 56 | ins = klass(js) 57 | outs = klass() 58 | JavascriptMinify(ins, outs).minify() 59 | return outs.getvalue() 60 | 61 | 62 | class JavascriptMinify(object): 63 | """ 64 | Minify an input stream of javascript, writing 65 | to an output stream 66 | """ 67 | 68 | def __init__(self, instream=None, outstream=None): 69 | self.ins = instream 70 | self.outs = outstream 71 | 72 | def minify(self, instream=None, outstream=None): 73 | if instream and outstream: 74 | self.ins, self.outs = instream, outstream 75 | 76 | self.is_return = False 77 | self.return_buf = '' 78 | 79 | def write(char): 80 | # all of this is to support literal regular expressions. 81 | # sigh 82 | if char in 'return': 83 | self.return_buf += char 84 | self.is_return = self.return_buf == 'return' 85 | self.outs.write(char) 86 | if self.is_return: 87 | self.return_buf = '' 88 | 89 | read = self.ins.read 90 | 91 | space_strings = "abcdefghijklmnopqrstuvwxyz"\ 92 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" 93 | starters, enders = '{[(+-', '}])+-"\'' 94 | newlinestart_strings = starters + space_strings 95 | newlineend_strings = enders + space_strings 96 | do_newline = False 97 | do_space = False 98 | escape_slash_count = 0 99 | doing_single_comment = False 100 | previous_before_comment = '' 101 | doing_multi_comment = False 102 | in_re = False 103 | in_quote = '' 104 | quote_buf = [] 105 | 106 | previous = read(1) 107 | if previous == '\\': 108 | escape_slash_count += 1 109 | next1 = read(1) 110 | if previous == '/': 111 | if next1 == '/': 112 | doing_single_comment = True 113 | elif next1 == '*': 114 | doing_multi_comment = True 115 | previous = next1 116 | next1 = read(1) 117 | else: 118 | in_re = True # literal regex at start of script 119 | write(previous) 120 | elif not previous: 121 | return 122 | elif previous >= '!': 123 | if previous in "'\"": 124 | in_quote = previous 125 | write(previous) 126 | previous_non_space = previous 127 | else: 128 | previous_non_space = ' ' 129 | if not next1: 130 | return 131 | 132 | while 1: 133 | next2 = read(1) 134 | if not next2: 135 | last = next1.strip() 136 | if not (doing_single_comment or doing_multi_comment)\ 137 | and last not in ('', '/'): 138 | if in_quote: 139 | write(''.join(quote_buf)) 140 | write(last) 141 | break 142 | if doing_multi_comment: 143 | if next1 == '*' and next2 == '/': 144 | doing_multi_comment = False 145 | if previous_before_comment and previous_before_comment in space_strings: 146 | do_space = True 147 | next2 = read(1) 148 | elif doing_single_comment: 149 | if next1 in '\r\n': 150 | doing_single_comment = False 151 | while next2 in '\r\n': 152 | next2 = read(1) 153 | if not next2: 154 | break 155 | if previous_before_comment in ')}]': 156 | do_newline = True 157 | elif previous_before_comment in space_strings: 158 | write('\n') 159 | elif in_quote: 160 | quote_buf.append(next1) 161 | 162 | if next1 == in_quote: 163 | numslashes = 0 164 | for c in reversed(quote_buf[:-1]): 165 | if c != '\\': 166 | break 167 | else: 168 | numslashes += 1 169 | if numslashes % 2 == 0: 170 | in_quote = '' 171 | write(''.join(quote_buf)) 172 | elif next1 in '\r\n': 173 | if previous_non_space in newlineend_strings \ 174 | or previous_non_space > '~': 175 | while 1: 176 | if next2 < '!': 177 | next2 = read(1) 178 | if not next2: 179 | break 180 | else: 181 | if next2 in newlinestart_strings \ 182 | or next2 > '~' or next2 == '/': 183 | do_newline = True 184 | break 185 | elif next1 < '!' and not in_re: 186 | if (previous_non_space in space_strings \ 187 | or previous_non_space > '~') \ 188 | and (next2 in space_strings or next2 > '~'): 189 | do_space = True 190 | elif previous_non_space in '-+' and next2 == previous_non_space: 191 | # protect against + ++ or - -- sequences 192 | do_space = True 193 | elif self.is_return and next2 == '/': 194 | # returning a regex... 195 | write(' ') 196 | elif next1 == '/': 197 | if do_space: 198 | write(' ') 199 | if in_re: 200 | if previous != '\\' or (not escape_slash_count % 2) or next2 in 'gimy': 201 | in_re = False 202 | write('/') 203 | elif next2 == '/': 204 | doing_single_comment = True 205 | previous_before_comment = previous_non_space 206 | elif next2 == '*': 207 | doing_multi_comment = True 208 | previous_before_comment = previous_non_space 209 | previous = next1 210 | next1 = next2 211 | next2 = read(1) 212 | else: 213 | in_re = previous_non_space in '(,=:[?!&|;' or self.is_return # literal regular expression 214 | write('/') 215 | else: 216 | if do_space: 217 | do_space = False 218 | write(' ') 219 | if do_newline: 220 | write('\n') 221 | do_newline = False 222 | 223 | write(next1) 224 | if not in_re and next1 in "'\"": 225 | in_quote = next1 226 | quote_buf = [] 227 | 228 | previous = next1 229 | next1 = next2 230 | 231 | if previous >= '!': 232 | previous_non_space = previous 233 | 234 | if previous == '\\': 235 | escape_slash_count += 1 236 | else: 237 | escape_slash_count = 0 238 | -------------------------------------------------------------------------------- /modules/jsmin/__main__.py: -------------------------------------------------------------------------------- 1 | import sys, os, glob 2 | from jsmin import JavascriptMinify 3 | 4 | for f in sys.argv[1:]: 5 | with open(f, 'r') as js: 6 | minifier = JavascriptMinify(js, sys.stdout) 7 | minifier.minify() 8 | sys.stdout.write('\n') 9 | 10 | 11 | -------------------------------------------------------------------------------- /modules/jsmin/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import jsmin 3 | import sys 4 | 5 | class JsTests(unittest.TestCase): 6 | def _minify(self, js): 7 | return jsmin.jsmin(js) 8 | 9 | def assertEqual(self, thing1, thing2): 10 | if thing1 != thing2: 11 | print(repr(thing1), repr(thing2)) 12 | raise AssertionError 13 | return True 14 | 15 | def assertMinified(self, js_input, expected): 16 | minified = jsmin.jsmin(js_input) 17 | assert minified == expected, "%r != %r" % (minified, expected) 18 | 19 | def testQuoted(self): 20 | js = r''' 21 | Object.extend(String, { 22 | interpret: function(value) { 23 | return value == null ? '' : String(value); 24 | }, 25 | specialChar: { 26 | '\b': '\\b', 27 | '\t': '\\t', 28 | '\n': '\\n', 29 | '\f': '\\f', 30 | '\r': '\\r', 31 | '\\': '\\\\' 32 | } 33 | }); 34 | 35 | ''' 36 | expected = r"""Object.extend(String,{interpret:function(value){return value==null?'':String(value);},specialChar:{'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','\\':'\\\\'}});""" 37 | self.assertMinified(js, expected) 38 | 39 | def testSingleComment(self): 40 | js = r'''// use native browser JS 1.6 implementation if available 41 | if (Object.isFunction(Array.prototype.forEach)) 42 | Array.prototype._each = Array.prototype.forEach; 43 | 44 | if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { 45 | 46 | // hey there 47 | function() {// testing comment 48 | foo; 49 | //something something 50 | 51 | location = 'http://foo.com;'; // goodbye 52 | } 53 | //bye 54 | ''' 55 | expected = r""" 56 | if(Object.isFunction(Array.prototype.forEach)) 57 | Array.prototype._each=Array.prototype.forEach;if(!Array.prototype.indexOf)Array.prototype.indexOf=function(item,i){ function(){ foo; location='http://foo.com;';}""" 58 | # print expected 59 | self.assertMinified(js, expected) 60 | 61 | def testEmpty(self): 62 | self.assertMinified('', '') 63 | self.assertMinified(' ', '') 64 | self.assertMinified('\n', '') 65 | self.assertMinified('\r\n', '') 66 | self.assertMinified('\t', '') 67 | 68 | 69 | def testMultiComment(self): 70 | js = r""" 71 | function foo() { 72 | print('hey'); 73 | } 74 | /* 75 | if(this.options.zindex) { 76 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 77 | this.element.style.zIndex = this.options.zindex; 78 | } 79 | */ 80 | another thing; 81 | """ 82 | expected = r"""function foo(){print('hey');} 83 | another thing;""" 84 | self.assertMinified(js, expected) 85 | 86 | def testLeadingComment(self): 87 | js = r"""/* here is a comment at the top 88 | 89 | it ends here */ 90 | function foo() { 91 | alert('crud'); 92 | } 93 | 94 | """ 95 | expected = r"""function foo(){alert('crud');}""" 96 | self.assertMinified(js, expected) 97 | 98 | def testBlockCommentStartingWithSlash(self): 99 | self.assertMinified('A; /*/ comment */ B', 'A;B') 100 | 101 | def testBlockCommentEndingWithSlash(self): 102 | self.assertMinified('A; /* comment /*/ B', 'A;B') 103 | 104 | def testLeadingBlockCommentStartingWithSlash(self): 105 | self.assertMinified('/*/ comment */ A', 'A') 106 | 107 | def testLeadingBlockCommentEndingWithSlash(self): 108 | self.assertMinified('/* comment /*/ A', 'A') 109 | 110 | def testEmptyBlockComment(self): 111 | self.assertMinified('/**/ A', 'A') 112 | 113 | def testBlockCommentMultipleOpen(self): 114 | self.assertMinified('/* A /* B */ C', 'C') 115 | 116 | def testJustAComment(self): 117 | self.assertMinified(' // a comment', '') 118 | 119 | def test_issue_10(self): 120 | js = ''' 121 | files = [{name: value.replace(/^.*\\\\/, '')}]; 122 | // comment 123 | A 124 | ''' 125 | expected = '''files=[{name:value.replace(/^.*\\\\/,'')}]; A''' 126 | self.assertMinified(js, expected) 127 | 128 | def testRe(self): 129 | js = r''' 130 | var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); 131 | return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); 132 | });''' 133 | expected = r"""var str=this.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"/g,'');return(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);});""" 134 | self.assertMinified(js, expected) 135 | 136 | def testIgnoreComment(self): 137 | js = r""" 138 | var options_for_droppable = { 139 | overlap: options.overlap, 140 | containment: options.containment, 141 | tree: options.tree, 142 | hoverclass: options.hoverclass, 143 | onHover: Sortable.onHover 144 | } 145 | 146 | var options_for_tree = { 147 | onHover: Sortable.onEmptyHover, 148 | overlap: options.overlap, 149 | containment: options.containment, 150 | hoverclass: options.hoverclass 151 | } 152 | 153 | // fix for gecko engine 154 | Element.cleanWhitespace(element); 155 | """ 156 | expected = r"""var options_for_droppable={overlap:options.overlap,containment:options.containment,tree:options.tree,hoverclass:options.hoverclass,onHover:Sortable.onHover} 157 | var options_for_tree={onHover:Sortable.onEmptyHover,overlap:options.overlap,containment:options.containment,hoverclass:options.hoverclass} 158 | Element.cleanWhitespace(element);""" 159 | self.assertMinified(js, expected) 160 | 161 | def testHairyRe(self): 162 | js = r""" 163 | inspect: function(useDoubleQuotes) { 164 | var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { 165 | var character = String.specialChar[match[0]]; 166 | return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); 167 | }); 168 | if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; 169 | return "'" + escapedString.replace(/'/g, '\\\'') + "'"; 170 | }, 171 | 172 | toJSON: function() { 173 | return this.inspect(true); 174 | }, 175 | 176 | unfilterJSON: function(filter) { 177 | return this.sub(filter || Prototype.JSONFilter, '#{1}'); 178 | }, 179 | """ 180 | expected = r"""inspect:function(useDoubleQuotes){var escapedString=this.gsub(/[\x00-\x1f\\]/,function(match){var character=String.specialChar[match[0]];return character?character:'\\u00'+match[0].charCodeAt().toPaddedString(2,16);});if(useDoubleQuotes)return'"'+escapedString.replace(/"/g,'\\"')+'"';return"'"+escapedString.replace(/'/g,'\\\'')+"'";},toJSON:function(){return this.inspect(true);},unfilterJSON:function(filter){return this.sub(filter||Prototype.JSONFilter,'#{1}');},""" 181 | self.assertMinified(js, expected) 182 | 183 | def testLiteralRe(self): 184 | js = r""" 185 | myString.replace(/\\/g, '/'); 186 | console.log("hi"); 187 | """ 188 | expected = r"""myString.replace(/\\/g,'/');console.log("hi");""" 189 | self.assertMinified(js, expected) 190 | 191 | js = r''' return /^data:image\//i.test(url) || 192 | /^(https?|ftp|file|about|chrome|resource):/.test(url); 193 | ''' 194 | expected = r'''return /^data:image\//i.test(url)||/^(https?|ftp|file|about|chrome|resource):/.test(url);''' 195 | self.assertMinified(js, expected) 196 | 197 | def testNoBracesWithComment(self): 198 | js = r""" 199 | onSuccess: function(transport) { 200 | var js = transport.responseText.strip(); 201 | if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check 202 | throw 'Server returned an invalid collection representation.'; 203 | this._collection = eval(js); 204 | this.checkForExternalText(); 205 | }.bind(this), 206 | onFailure: this.onFailure 207 | }); 208 | """ 209 | expected = r"""onSuccess:function(transport){var js=transport.responseText.strip();if(!/^\[.*\]$/.test(js)) 210 | throw'Server returned an invalid collection representation.';this._collection=eval(js);this.checkForExternalText();}.bind(this),onFailure:this.onFailure});""" 211 | self.assertMinified(js, expected) 212 | 213 | def testSpaceInRe(self): 214 | js = r""" 215 | num = num.replace(/ /g,''); 216 | """ 217 | self.assertMinified(js, "num=num.replace(/ /g,'');") 218 | 219 | def testEmptyString(self): 220 | js = r''' 221 | function foo('') { 222 | 223 | } 224 | ''' 225 | self.assertMinified(js, "function foo(''){}") 226 | 227 | def testDoubleSpace(self): 228 | js = r''' 229 | var foo = "hey"; 230 | ''' 231 | self.assertMinified(js, 'var foo="hey";') 232 | 233 | def testLeadingRegex(self): 234 | js = r'/[d]+/g ' 235 | self.assertMinified(js, js.strip()) 236 | 237 | def testLeadingString(self): 238 | js = r"'a string in the middle of nowhere'; // and a comment" 239 | self.assertMinified(js, "'a string in the middle of nowhere';") 240 | 241 | def testSingleCommentEnd(self): 242 | js = r'// a comment\n' 243 | self.assertMinified(js, '') 244 | 245 | def testInputStream(self): 246 | try: 247 | from StringIO import StringIO 248 | except ImportError: 249 | from io import StringIO 250 | 251 | ins = StringIO(r''' 252 | function foo('') { 253 | 254 | } 255 | ''') 256 | outs = StringIO() 257 | m = jsmin.JavascriptMinify() 258 | m.minify(ins, outs) 259 | output = outs.getvalue() 260 | assert output == "function foo(''){}" 261 | 262 | def testUnicode(self): 263 | instr = u'\u4000 //foo' 264 | expected = u'\u4000' 265 | output = jsmin.jsmin(instr) 266 | self.assertEqual(output, expected) 267 | 268 | def testCommentBeforeEOF(self): 269 | self.assertMinified("//test\r\n", "") 270 | 271 | def testCommentInObj(self): 272 | self.assertMinified("""{ 273 | a: 1,//comment 274 | }""", "{a:1,}") 275 | 276 | def testCommentInObj2(self): 277 | self.assertMinified("{a: 1//comment\r\n}", "{a:1\n}") 278 | 279 | def testImplicitSemicolon(self): 280 | # return \n 1 is equivalent with return; 1 281 | # so best make sure jsmin retains the newline 282 | self.assertMinified("return;//comment\r\na", "return;a") 283 | 284 | def testImplicitSemicolon2(self): 285 | self.assertMinified("return//comment...\r\na", "return\na") 286 | 287 | def testSingleComment2(self): 288 | self.assertMinified('x.replace(/\//, "_")// slash to underscore', 289 | 'x.replace(/\//,"_")') 290 | 291 | def testSlashesNearComments(self): 292 | original = ''' 293 | { a: n / 2, } 294 | // comment 295 | ''' 296 | expected = '''{a:n/2,}''' 297 | self.assertMinified(original, expected) 298 | 299 | def testReturn(self): 300 | original = ''' 301 | return foo;//comment 302 | return bar;''' 303 | expected = 'return foo; return bar;' 304 | self.assertMinified(original, expected) 305 | 306 | def test_space_plus(self): 307 | original = '"s" + ++e + "s"' 308 | expected = '"s"+ ++e+"s"' 309 | self.assertMinified(original, expected) 310 | 311 | def test_no_final_newline(self): 312 | original = '"s"' 313 | expected = '"s"' 314 | self.assertMinified(original, expected) 315 | 316 | def test_space_with_regex_repeats(self): 317 | original = '/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");' 318 | self.assertMinified(original, original) # there should be nothing jsmin can do here 319 | 320 | def test_space_with_regex_repeats_not_at_start(self): 321 | original = 'aaa;/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");' 322 | self.assertMinified(original, original) # there should be nothing jsmin can do here 323 | 324 | def test_space_in_regex(self): 325 | original = '/a (a)/.test("a")' 326 | self.assertMinified(original, original) 327 | 328 | def test_angular_1(self): 329 | original = '''var /** holds major version number for IE or NaN for real browsers */ 330 | msie, 331 | jqLite, // delay binding since jQuery could be loaded after us.''' 332 | minified = jsmin.jsmin(original) 333 | self.assertTrue('var msie' in minified) 334 | 335 | def test_angular_2(self): 336 | original = 'var/* comment */msie;' 337 | expected = 'var msie;' 338 | self.assertMinified(original, expected) 339 | 340 | def test_angular_3(self): 341 | original = 'var /* comment */msie;' 342 | expected = 'var msie;' 343 | self.assertMinified(original, expected) 344 | 345 | def test_angular_4(self): 346 | original = 'var /* comment */ msie;' 347 | expected = 'var msie;' 348 | self.assertMinified(original, expected) 349 | 350 | def test_angular_4(self): 351 | original = 'a/b' 352 | self.assertMinified(original, original) 353 | 354 | if __name__ == '__main__': 355 | unittest.main() 356 | --------------------------------------------------------------------------------