├── .gitignore ├── README ├── demo ├── css │ ├── stylesheet1.css │ └── stylesheet2.css ├── js │ └── test.js ├── single-file │ └── view-with-inline-styles.html └── views │ ├── view1.html │ └── view2.html ├── munch ├── muncher ├── __init__.py ├── config.py ├── muncher.py ├── sizetracker.py ├── util.py └── varfactory.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.opt.html 3 | *.opt.js 4 | *.opt.css 5 | demo/css_opt 6 | demo/js_opt 7 | demo/views_opt 8 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | -------------- 2 | ABOUT 3 | -------------- 4 | 5 | HTML Muncher is a Python utility that rewrites CSS, HTML, and JavaScript files in order to save precious bytes and obfuscate your code 6 | 7 | if your stylesheet starts out looking like this: 8 | 9 | .file2 #special { 10 | font-size: 1.5em; 11 | color: #F737FF; 12 | } 13 | 14 | .file2 #special2 { 15 | letter-spacing: 0; 16 | } 17 | 18 | .box { 19 | border: 2px solid #aaa; 20 | -webkit-border-radius: 5px; 21 | background: #eee; 22 | padding: 5px; 23 | } 24 | 25 | it will be rewritten as 26 | 27 | .a #a { 28 | font-size: 1.5em; 29 | color: #F737FF; 30 | } 31 | 32 | .a #b { 33 | letter-spacing: 0; 34 | } 35 | 36 | .b { 37 | border: 2px solid #aaa; 38 | -webkit-border-radius: 5px; 39 | background: #eee; 40 | padding: 5px; 41 | } 42 | 43 | 44 | -------------- 45 | INSTALLATION 46 | -------------- 47 | 48 | easy_install http://htmlmuncher.com/htmlmuncher.egg 49 | 50 | OR: 51 | 52 | download the source from http://github.com/ccampbell/html-muncher 53 | cd html-muncher 54 | python setup.py install 55 | 56 | 57 | -------------- 58 | USAGE 59 | -------------- 60 | http://htmlmuncher.com/#usage 61 | 62 | OR: 63 | 64 | munch --help 65 | 66 | 67 | -------------- 68 | EXAMPLES 69 | -------------- 70 | 71 | to update a bunch of stylesheets and views: 72 | munch --css demo/css --html demo/views 73 | 74 | to update a single file with inline styles/javascript: 75 | munch --html demo/single-file/view-with-inline-styles.html 76 | 77 | you can also select specific files: 78 | munch --css file1.css,file2.css --html view1.html,view2.html 79 | 80 | or you can mix and match files and directories 81 | munch --css /my/css/directory,global.css --html /view/directory1,/view/directory2,/view/directory3,template.html 82 | -------------------------------------------------------------------------------- /demo/css/stylesheet1.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: helvetica; 3 | } 4 | 5 | h1 { 6 | text-align: center; 7 | font-size: 2em; 8 | } 9 | 10 | .red { 11 | color: red; 12 | } 13 | 14 | .blue { 15 | color: blue; 16 | } 17 | 18 | .green { 19 | color: green; 20 | } 21 | 22 | .box.purple { 23 | color: purple; 24 | } 25 | 26 | .underline { 27 | text-decoration: underline; 28 | } 29 | 30 | #special { 31 | color: orange; 32 | font-style: italic; 33 | } 34 | 35 | .italic { 36 | font-style: italic; 37 | } 38 | 39 | #special2 { 40 | letter-spacing: .5em; 41 | color: grey; 42 | } 43 | 44 | #new_id, #special, #special2 { 45 | font-size: 1em; 46 | } -------------------------------------------------------------------------------- /demo/css/stylesheet2.css: -------------------------------------------------------------------------------- 1 | .file2 #special { 2 | font-size: 1.5em; 3 | color: #F737FF; 4 | } 5 | 6 | .file2 #special2 { 7 | letter-spacing: 0; 8 | } 9 | 10 | .box { 11 | border: 2px solid #aaa; 12 | -webkit-border-radius: 5px; 13 | background: #eee; 14 | padding: 5px; 15 | } -------------------------------------------------------------------------------- /demo/js/test.js: -------------------------------------------------------------------------------- 1 | $ = { 2 | qs: function(query) { 3 | return document.querySelector(query); 4 | } 5 | }; 6 | 7 | window.onload = function() 8 | { 9 | $.qs("#special").innerHTML = "new text for this paragraph"; 10 | document.getElementById("special").innerHTML = "change it again"; 11 | var italic = document.getElementsByClassName('italic'); 12 | 13 | // mootools 14 | var test = $('test'); 15 | if (test.hasClass("dont_know")) { 16 | test.removeClass("dont_know"); 17 | test.addClass('now_i_know'); 18 | } 19 | 20 | var class_thing = $('class_thing'); 21 | class_thing.addClass(test, "whatever"); 22 | class_thing.removeClass(test, "whatever"); 23 | $.qs(".dont_know", class_thing).value; 24 | var cool = $.qs("#one_id.class_thing", test); 25 | var another_weird_thing = $.qs(".class1.class2 #another_id"); 26 | $.qs(".selector1 > .selector2 .selector3"); 27 | var test = document.querySelector(".selector1"); 28 | } 29 | -------------------------------------------------------------------------------- /demo/single-file/view-with-inline-styles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | view with inline styles 4 | 77 | 78 | 79 |

test page with inline styles

80 | 81 |
82 | 89 | 90 |
91 |
92 |

this is a green box

93 |
94 | 95 |
96 |

this is a blue box

97 |
98 | 99 |

this is paragraph

100 |
101 |
102 | 103 | 111 | 115 | 116 | -------------------------------------------------------------------------------- /demo/views/view1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test page 1 changed 4 | 5 | 6 | 7 |

this is the first test page

8 |

Assertively leverage existing scalable growth strategies with revolutionary value. Distinctively recaptiualize top-line models rather than leveraged e-commerce. Quickly engineer orthogonal e-markets via holistic human capital.

9 |

this is a test with two classes

10 |

Appropriately incubate collaborative imperatives after team building networks. Uniquely enhance sticky e-services for value-added solutions. Seamlessly parallel task value-added quality vectors before state of the art methods of empowerment.

11 |

Interactively strategize plug-and-play platforms whereas efficient infrastructures. Synergistically negotiate user-centric metrics rather than worldwide quality vectors. Holisticly deliver bleeding-edge leadership vis-a-vis world-class architectures.

12 |

Professionally iterate multifunctional systems before optimal materials. Efficiently reintermediate wireless total linkage with distributed portals. Globally exploit resource-leveling human capital without economically sound infomediaries.

13 |

Intrinsicly streamline user-centric users before visionary scenarios. Dynamically repurpose seamless channels via multifunctional process improvements. Professionally synthesize exceptional infrastructures without premier vortals.

14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/views/view2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test page 2 4 | 5 | 6 | 7 | 8 |

this is the second test page

9 |

Assertively leverage existing scalable growth strategies with revolutionary value. Distinctively recaptiualize top-line models rather than leveraged e-commerce. Quickly engineer orthogonal e-markets via holistic human capital.

10 |

Appropriately incubate collaborative imperatives after team building networks. Uniquely enhance sticky e-services for value-added solutions. Seamlessly parallel task value-added quality vectors before state of the art methods of empowerment.

11 |

Professionally iterate multifunctional systems before optimal materials. Efficiently reintermediate wireless total linkage with distributed portals. Globally exploit resource-leveling human capital without economically sound infomediaries.

12 |

Intrinsicly streamline user-centric users before visionary scenarios. Dynamically repurpose seamless channels via multifunctional process improvements. Professionally synthesize exceptional infrastructures without premier vortals.

13 | 14 | 15 | -------------------------------------------------------------------------------- /munch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | from muncher.config import Config 19 | from muncher.muncher import Muncher 20 | 21 | config = Config() 22 | config.processArgs() 23 | muncher = Muncher(config) 24 | muncher.run() 25 | -------------------------------------------------------------------------------- /muncher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/html-muncher/86c425b0a3db2409f3de11ed282da78aeb94ec6c/muncher/__init__.py -------------------------------------------------------------------------------- /muncher/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2011 Craig Campbell 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys, getopt 17 | from muncher import Muncher 18 | 19 | class Config(object): 20 | """configuration object for handling all config options for html-muncher""" 21 | def __init__(self): 22 | """config object constructor 23 | 24 | Returns: 25 | void 26 | 27 | """ 28 | self.css = [] 29 | self.views = [] 30 | self.js = [] 31 | self.ignore = [] 32 | self.class_selectors = ["getElementsByClassName", "hasClass", "addClass", "removeClass"] 33 | self.id_selectors = ["getElementById"] 34 | self.custom_selectors = ["document.querySelector"] 35 | self.framework = None 36 | self.view_extension = "html" 37 | self.js_manifest = None 38 | self.show_savings = False 39 | self.compress_html = False 40 | self.rewrite_constants = False 41 | self.verbose = False 42 | 43 | def getArgCount(self): 44 | """gets the count of how many arguments are present 45 | 46 | Returns: 47 | int 48 | 49 | """ 50 | return len(sys.argv) 51 | 52 | def setIgnore(self, value): 53 | """sets what classes and ids we should ignore and not shorten 54 | 55 | Arguments: 56 | value -- comma separated list of classes or ids 57 | 58 | Returns: 59 | void 60 | 61 | """ 62 | for name in value.split(","): 63 | self.ignore.append(name) 64 | 65 | def setCustomSelectors(self, value): 66 | for value in value.split(","): 67 | self.custom_selectors.append(value.lstrip(".")) 68 | 69 | def addClassSelectors(self, value): 70 | for value in value.split(","): 71 | self.class_selectors.append(value) 72 | 73 | def addIdSelectors(self, value): 74 | for value in value.split(","): 75 | self.id_selectors.append(value) 76 | 77 | def setCssFiles(self, value): 78 | for value in value.split(","): 79 | self.css.append(value.rstrip("/")) 80 | 81 | def setViewFiles(self, value): 82 | for value in value.split(","): 83 | self.views.append(value.rstrip("/")) 84 | 85 | def setJsFiles(self, value): 86 | for value in value.split(","): 87 | self.js.append(value.rstrip("/")) 88 | 89 | def setFramework(self, name): 90 | self.framework = name.lower() 91 | if self.framework == "jquery": 92 | self.custom_selectors.append("$") 93 | self.custom_selectors.append("jQuery") 94 | elif self.framework == "mootools": 95 | self.id_selectors.append("$") 96 | self.custom_selectors.append("getElement") 97 | 98 | def processArgs(self): 99 | """processes arguments passed in via command line and sets config settings accordingly 100 | 101 | Returns: 102 | void 103 | 104 | """ 105 | try: 106 | opts, args = getopt.getopt(sys.argv[1:], "", ["css=", "views=", "html=", "js=", "help", "view-ext=", "ignore=", "framework=", "selectors=", "class-selectors=", "id-selectors=", "compress-html", "show-savings", "verbose", "js-manifest=", "rewrite-constants"]) 107 | except: 108 | Muncher.showUsage() 109 | 110 | views_set = False 111 | 112 | for key, value in opts: 113 | if key == "--help": 114 | Muncher.showUsage() 115 | elif key == "--css": 116 | self.setCssFiles(value) 117 | elif key == "--views" or key == "--html": 118 | views_set = True 119 | self.setViewFiles(value) 120 | elif key == "--js": 121 | self.setJsFiles(value) 122 | elif key == "--ignore": 123 | self.setIgnore(value) 124 | elif key == "--view-ext": 125 | self.view_extension = value 126 | elif key == "--framework": 127 | self.setFramework(value) 128 | elif key == "--selectors": 129 | self.setCustomSelectors(value) 130 | elif key == "--class-selectors": 131 | self.addClassSelectors(value) 132 | elif key == "--id-selectors": 133 | self.addIdSelectors(value) 134 | elif key == "--compress-html": 135 | self.compress_html = True 136 | elif key == "--show-savings": 137 | self.show_savings = True 138 | elif key == "--verbose": 139 | self.verbose = True 140 | elif key == "--js-manifest": 141 | self.js_manifest = value 142 | elif key == "--rewrite-constants": 143 | self.rewrite_constants = True 144 | 145 | # you have to at least have a view 146 | if views_set is False: 147 | Muncher.showUsage() 148 | -------------------------------------------------------------------------------- /muncher/muncher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2011 Craig Campbell 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys, re, glob, os 17 | from operator import itemgetter 18 | from util import Util 19 | from varfactory import VarFactory 20 | from sizetracker import SizeTracker 21 | 22 | class Muncher(object): 23 | def __init__(self, config): 24 | """constructor 25 | 26 | Returns: 27 | void 28 | 29 | """ 30 | self.id_counter = {} 31 | self.class_counter = {} 32 | self.id_map = {} 33 | self.class_map = {} 34 | self.config = config 35 | 36 | @staticmethod 37 | def showUsage(): 38 | """shows usage information for this script""" 39 | print "\n---------------------------------" 40 | print " html-muncher" 41 | print "---------------------------------" 42 | 43 | print "\n" + '\033[91m' + "USAGE:" + '\033[0m' 44 | print "munch --css file1.css,/path/to/css1,file2.css,file3.css --html /path/to/views1,file1.html,/path/to/views2/,file3.html --js main.js,/path/to/js" 45 | print "\n" + '\033[91m' + "REQUIRED ARGUMENTS:" + '\033[0m' 46 | print "--html {path/to/views} html files to rewrite (comma separated list of directories and files)" 47 | print "\n" + '\033[91m' + "OPTIONAL ARGUMENTS:" + '\033[0m' 48 | print "--css {path/to/css} css files to rewrite (comma separated list of directories and files)" 49 | print "" 50 | print "--js {path/to/js} js files to rewrite (comma separated list of directories and files)" 51 | print "" 52 | print "--view-ext {extension} sets the extension to look for in the view directory (defaults to html)" 53 | print "" 54 | print "--ignore {classes,ids} comma separated list of classes or ids to ignore when rewriting css (ie .sick_class,#sweet_id)" 55 | print "" 56 | print "--compress-html strips new line characters to compress html files specified with --html" 57 | print " be careful when using this becuase it has not been thoroughly tested" 58 | print "" 59 | print "--framework name of js framework to use for selectors (currently only jquery or mootools)" 60 | print "" 61 | print "--selectors comma separated custom selectors using css selectors" 62 | print " for example if you have $.qs(\"#test .div\") this param would be qs" 63 | print "" 64 | print "--id-selectors comma separated id selectors with strings" 65 | print " for example if you are using .addId(\"test\") this param would be addId" 66 | print "" 67 | print "--class-selectors comma separated class selectors with strings" 68 | print " for example if you have selectClass(\"my_class\") this param would be selectClass" 69 | print "" 70 | print "--js-manifest path to a js file containing class name/id constants" 71 | print "" 72 | print "--rewrite-constants when using a manifest file this will take any constants with values as strings" 73 | print " and rewrite the values to be numbers" 74 | print "" 75 | print "--show-savings will output how many bytes were saved by munching" 76 | print "" 77 | print "--verbose output more information while the script runs" 78 | print "" 79 | print "--help shows this menu\n" 80 | sys.exit(2) 81 | 82 | def run(self): 83 | """runs the optimizer and does all the magic 84 | 85 | Returns: 86 | void 87 | 88 | """ 89 | self.output("searching for classes and ids...", False) 90 | 91 | if self.config.js_manifest is not None: 92 | self.outputJsWarnings() 93 | 94 | self.processCss() 95 | self.processViews() 96 | 97 | if self.config.js_manifest is None: 98 | self.processJs() 99 | else: 100 | self.processJsManifest() 101 | 102 | self.output("mapping classes and ids to new names...", False) 103 | # maps all classes and ids found to shorter names 104 | self.processMaps() 105 | 106 | # optimize everything 107 | self.output("munching css files...", False) 108 | self.optimizeFiles(self.config.css, self.optimizeCss) 109 | 110 | self.output("munching html files...", False) 111 | self.optimizeFiles(self.config.views, self.optimizeHtml, self.config.view_extension, self.config.compress_html) 112 | 113 | self.output("munching js files...", False) 114 | 115 | if self.config.js_manifest is None: 116 | self.optimizeFiles(self.config.js, self.optimizeJavascript) 117 | else: 118 | self.optimizeJsManifest() 119 | 120 | self.output("done", False) 121 | 122 | if self.config.show_savings: 123 | self.output(SizeTracker.savings(), False) 124 | 125 | def outputJsWarnings(self): 126 | pass 127 | 128 | def output(self, text, verbose_only = True): 129 | """outputs text during the script run 130 | 131 | Arguments: 132 | text -- string of text to output 133 | verbose_only -- should we only show this in verbose mode? 134 | 135 | Returns: 136 | void 137 | 138 | """ 139 | if verbose_only and not self.config.verbose: 140 | return 141 | 142 | print text 143 | 144 | def processCssDirectory(self, file): 145 | """processes a directory of css files 146 | 147 | Arguments: 148 | file -- path to directory 149 | 150 | Returns: 151 | void 152 | 153 | """ 154 | if ".svn" in file: 155 | return 156 | 157 | for dir_file in Util.getFilesFromDir(file): 158 | if Util.isDir(dir_file): 159 | self.processCssDirectory(dir_file) 160 | continue 161 | 162 | self.processCssFile(dir_file) 163 | 164 | def processCss(self): 165 | """gets all css files from config and processes them to see what to replace 166 | 167 | Returns: 168 | void 169 | 170 | """ 171 | files = self.config.css 172 | for file in files: 173 | if not Util.isDir(file): 174 | self.processCssFile(file) 175 | continue 176 | self.processCssDirectory(file) 177 | 178 | def processViewDirectory(self, file): 179 | """processes a directory of view files 180 | 181 | Arguments: 182 | file -- path to directory 183 | 184 | Returns: 185 | void 186 | 187 | """ 188 | if ".svn" in file: 189 | return 190 | 191 | for dir_file in Util.getFilesFromDir(file): 192 | if Util.isDir(dir_file): 193 | self.processViewDirectory(dir_file) 194 | continue 195 | 196 | self.processView(dir_file) 197 | 198 | def processViews(self): 199 | """processes all view files 200 | 201 | Returns: 202 | void 203 | 204 | """ 205 | files = self.config.views 206 | for file in files: 207 | if not Util.isDir(file): 208 | self.processView(file) 209 | continue 210 | self.processViewDirectory(file) 211 | 212 | def processJsDirectory(self, file): 213 | """processes a directory of js files 214 | 215 | Arguments: 216 | file -- path to directory 217 | 218 | Returns: 219 | void 220 | 221 | """ 222 | if ".svn" in file: 223 | return 224 | 225 | for dir_file in Util.getFilesFromDir(file): 226 | if Util.isDir(dir_file): 227 | self.processJsDirectory(dir_file) 228 | continue 229 | self.processJsFile(dir_file) 230 | 231 | def processJs(self): 232 | """gets all js files from config and processes them to see what to replace 233 | 234 | Returns: 235 | void 236 | 237 | """ 238 | files = self.config.js 239 | for file in files: 240 | if not Util.isDir(file): 241 | self.processJsFile(file) 242 | continue 243 | self.processJsDirectory(file) 244 | 245 | def processView(self, file): 246 | """processes a single view file 247 | 248 | Arguments: 249 | file -- path to directory 250 | 251 | """ 252 | self.processCssFile(file, True) 253 | self.processJsFile(file, True) 254 | 255 | def processCssFile(self, path, inline = False): 256 | """processes a single css file to find all classes and ids to replace 257 | 258 | Arguments: 259 | path -- path to css file to process 260 | 261 | Returns: 262 | void 263 | 264 | """ 265 | contents = Util.fileGetContents(path) 266 | if inline is True: 267 | blocks = self.getCssBlocks(contents) 268 | contents = "" 269 | for block in blocks: 270 | contents = contents + block 271 | 272 | ids_found = re.findall(r'((?)', '', content, re.MULTILINE) 642 | return content 643 | 644 | def optimizeCss(self, path): 645 | """replaces classes and ids with new values in a css file 646 | 647 | Arguments: 648 | path -- string path to css file to optimize 649 | 650 | Returns: 651 | string 652 | 653 | """ 654 | css = Util.fileGetContents(path) 655 | return self.replaceCss(css) 656 | 657 | def optimizeHtml(self, path): 658 | """replaces classes and ids with new values in an html file 659 | 660 | Uses: 661 | Muncher.replaceHtml 662 | 663 | Arguments: 664 | path -- string path to file to optimize 665 | 666 | Returns: 667 | string 668 | 669 | """ 670 | html = Util.fileGetContents(path) 671 | html = self.replaceHtml(html) 672 | html = self.optimizeCssBlocks(html) 673 | html = self.optimizeJavascriptBlocks(html) 674 | 675 | return html 676 | 677 | def replaceHtml(self, html): 678 | """replaces classes and ids with new values in an html file 679 | 680 | Arguments: 681 | html -- contents to replace 682 | 683 | Returns: 684 | string 685 | 686 | """ 687 | html = self.replaceHtmlIds(html) 688 | html = self.replaceHtmlClasses(html) 689 | return html 690 | 691 | def replaceHtmlIds(self, html): 692 | """replaces any instances of ids in html markup 693 | 694 | Arguments: 695 | html -- contents of file to replaces ids in 696 | 697 | Returns: 698 | string 699 | 700 | """ 701 | for key, value in self.id_map.items(): 702 | key = key[1:] 703 | value = value[1:] 704 | html = html.replace("id=\"" + key + "\"", "id=\"" + value + "\"") 705 | 706 | return html 707 | 708 | def replaceClassBlock(self, class_block, key, value): 709 | """replaces a class string with the new class name 710 | 711 | Arguments: 712 | class_block -- string from what would be found within class="{class_block}" 713 | key -- current class 714 | value -- new class 715 | 716 | Returns: 717 | string 718 | 719 | """ 720 | key_length = len(key) 721 | classes = class_block.split(" ") 722 | i = 0 723 | for class_name in classes: 724 | if class_name == key: 725 | classes[i] = value 726 | 727 | # allows support for things like a.class_name as one of the js selectors 728 | elif key[0] in (".", "#") and class_name[-key_length:] == key: 729 | classes[i] = class_name.replace(key, value) 730 | i = i + 1 731 | 732 | return " ".join(classes) 733 | 734 | def replaceHtmlClasses(self, html): 735 | """replaces any instances of classes in html markup 736 | 737 | Arguments: 738 | html -- contents of file to replace classes in 739 | 740 | Returns: 741 | string 742 | 743 | """ 744 | for key, value in self.class_map.items(): 745 | key = key[1:] 746 | value = value[1:] 747 | class_blocks = re.findall(r'class\=((\'|\")(.*?)(\'|\"))', html) 748 | for class_block in class_blocks: 749 | new_block = self.replaceClassBlock(class_block[2], key, value) 750 | html = html.replace("class=" + class_block[0], "class=" + class_block[1] + new_block + class_block[3]) 751 | 752 | return html 753 | 754 | def optimizeCssBlocks(self, html): 755 | """rewrites css blocks that are part of an html file 756 | 757 | Arguments: 758 | html -- contents of file we are replacing 759 | 760 | Returns: 761 | string 762 | 763 | """ 764 | result_css = "" 765 | matches = self.getCssBlocks(html) 766 | for match in matches: 767 | match = self.replaceCss(match) 768 | result_css = result_css + match 769 | 770 | if len(matches): 771 | return html.replace(matches[0], result_css) 772 | 773 | return html 774 | 775 | @staticmethod 776 | def getCssBlocks(html): 777 | """searches a file and returns all css blocks 778 | 779 | Arguments: 780 | html -- contents of file we are replacing 781 | 782 | Returns: 783 | list 784 | 785 | """ 786 | return re.compile(r'\(.*)\<\/style\>', re.DOTALL).findall(html) 787 | 788 | def replaceCss(self, css): 789 | """single call to handle replacing ids and classes 790 | 791 | Arguments: 792 | css -- contents of file to replace 793 | 794 | Returns: 795 | string 796 | 797 | """ 798 | css = self.replaceCssFromDictionary(self.class_map, css) 799 | css = self.replaceCssFromDictionary(self.id_map, css) 800 | return css 801 | 802 | def replaceCssFromDictionary(self, dictionary, css): 803 | """replaces any instances of classes and ids based on a dictionary 804 | 805 | Arguments: 806 | dictionary -- map of classes or ids to replace 807 | css -- contents of css to replace 808 | 809 | Returns: 810 | string 811 | 812 | """ 813 | # this really should be done better 814 | for key, value in dictionary.items(): 815 | css = css.replace(key + "{", value + "{") 816 | css = css.replace(key + " {", value + " {") 817 | css = css.replace(key + "#", value + "#") 818 | css = css.replace(key + " #", value + " #") 819 | css = css.replace(key + ".", value + ".") 820 | css = css.replace(key + " .", value + " .") 821 | css = css.replace(key + ",", value + ",") 822 | css = css.replace(key + " ", value + " ") 823 | css = css.replace(key + ":", value + ":") 824 | # if key == ".svg": 825 | # print "replacing " + key + " with " + value 826 | 827 | return css 828 | 829 | def optimizeJavascriptBlocks(self, html): 830 | """rewrites javascript blocks that are part of an html file 831 | 832 | Arguments: 833 | html -- contents of file we are replacing 834 | 835 | Returns: 836 | string 837 | 838 | """ 839 | matches = self.getJsBlocks(html) 840 | 841 | for match in matches: 842 | new_js = match 843 | if self.config.compress_html: 844 | matches = re.findall(r'((:?)\/\/.*?\n|\/\*.*?\*\/)', new_js, re.DOTALL) 845 | for single_match in matches: 846 | if single_match[1] == ':': 847 | continue 848 | new_js = new_js.replace(single_match[0], ''); 849 | new_js = self.replaceJavascript(new_js) 850 | html = html.replace(match, new_js) 851 | 852 | return html 853 | 854 | @staticmethod 855 | def getJsBlocks(html): 856 | """searches a file and returns all javascript blocks: 857 | 858 | Arguments: 859 | html -- contents of file we are replacing 860 | 861 | Returns: 862 | list 863 | 864 | """ 865 | return re.compile(r'\(.*?)\<\/script\>', re.DOTALL).findall(html) 866 | 867 | def optimizeJavascript(self, path): 868 | """optimizes javascript for a specific file 869 | 870 | Arguments: 871 | path -- path to js file on disk that we are optimizing 872 | 873 | Returns: 874 | string -- contents to replace file with 875 | 876 | """ 877 | js = Util.fileGetContents(path) 878 | return self.replaceJavascript(js) 879 | 880 | def replaceJavascript(self, js): 881 | """single call to handle replacing ids and classes 882 | 883 | Arguments: 884 | js -- contents of file to replace 885 | 886 | Returns: 887 | string 888 | 889 | """ 890 | js = self.replaceJsFromDictionary(self.id_map, js) 891 | js = self.replaceJsFromDictionary(self.class_map, js) 892 | return js 893 | 894 | @staticmethod 895 | def getJsSelectors(js, config): 896 | """finds all js selectors within a js block 897 | 898 | Arguments: 899 | js -- contents of js file to search 900 | 901 | Returns: 902 | list 903 | 904 | """ 905 | valid_selectors = "|".join(config.custom_selectors) + "|" + "|".join(config.id_selectors) + "|" + "|".join(config.class_selectors) 906 | valid_selectors = valid_selectors.replace('$', '\$') 907 | return re.findall(r'(' + valid_selectors + ')(\(([^<>]*?)\))', js, re.DOTALL) 908 | 909 | def replaceJsFromDictionary(self, dictionary, js): 910 | """replaces any instances of classes and ids based on a dictionary 911 | 912 | Arguments: 913 | dictionary -- map of classes or ids to replace 914 | js -- contents of javascript to replace 915 | 916 | Returns: 917 | string 918 | 919 | """ 920 | for key, value in dictionary.items(): 921 | blocks = self.getJsSelectors(js, self.config) 922 | for block in blocks: 923 | if key[0] == "#" and block[0] in self.config.class_selectors: 924 | continue 925 | 926 | if key[0] == "." and block[0] in self.config.id_selectors: 927 | continue 928 | 929 | old_selector = block[0] + block[1] 930 | 931 | # custom selectors 932 | if block[0] in self.config.custom_selectors: 933 | new_selector = old_selector.replace(key + ".", value + ".") 934 | new_selector = new_selector.replace(key + " ", value + " ") 935 | new_selector = new_selector.replace(key + "\"", value + "\"") 936 | new_selector = new_selector.replace(key + "\'", value + "\'") 937 | else: 938 | new_selector = old_selector.replace("'" + key[1:] + "'", "'" + value[1:] + "'") 939 | new_selector = new_selector.replace("\"" + key[1:] + "\"", "\"" + value[1:] + "\"") 940 | 941 | js = js.replace(old_selector, new_selector) 942 | 943 | return js 944 | -------------------------------------------------------------------------------- /muncher/sizetracker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2011 Craig Campbell 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys, os, gzip 17 | from util import Util 18 | 19 | class SizeTracker(object): 20 | original_size = 0 21 | original_size_gzip = 0 22 | new_size = 0 23 | new_size_gzip = 0 24 | 25 | @staticmethod 26 | def addSize(path, new = False): 27 | 28 | # gzip the file to get that size 29 | gzip_path = path + '.gz' 30 | f_in = open(path, 'rb') 31 | f_out = gzip.open(gzip_path, 'wb') 32 | f_out.writelines(f_in) 33 | f_out.close() 34 | f_in.close() 35 | 36 | size = os.path.getsize(path) 37 | gzip_size = os.path.getsize(gzip_path) 38 | 39 | if new is False: 40 | SizeTracker.original_size += size 41 | SizeTracker.original_size_gzip += gzip_size 42 | else: 43 | SizeTracker.new_size += size 44 | SizeTracker.new_size_gzip += gzip_size 45 | 46 | Util.unlink(gzip_path) 47 | 48 | @staticmethod 49 | def trackFile(path, new_path): 50 | SizeTracker.addSize(path) 51 | SizeTracker.addSize(new_path, True) 52 | 53 | @staticmethod 54 | def getSize(bytes): 55 | if bytes < 1024: 56 | return str(bytes) + " bytes" 57 | 58 | kb = float(bytes) / 1024 59 | kb = round(kb, 2) 60 | return str(kb) + " KB" 61 | 62 | @staticmethod 63 | def savings(): 64 | percent = 100 - (float(SizeTracker.new_size) / float(SizeTracker.original_size)) * 100 65 | gzip_percent = 100 - (float(SizeTracker.new_size_gzip) / float(SizeTracker.original_size_gzip)) * 100 66 | 67 | string = "\noriginal size: " + SizeTracker.getSize(SizeTracker.original_size) + " (" + SizeTracker.getSize(SizeTracker.original_size_gzip) + " gzipped)" 68 | string += "\nmunched size: " + SizeTracker.getSize(SizeTracker.new_size) + " (" + SizeTracker.getSize(SizeTracker.new_size_gzip) + " gzipped)" 69 | string += "\n saved " + str(round(percent, 2)) + "% off the original size (" + str(round(gzip_percent, 2)) + "% off the gzipped size)\n" 70 | return string 71 | -------------------------------------------------------------------------------- /muncher/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2011 Craig Campbell 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os, shutil, glob 17 | 18 | class Util: 19 | """collection of various utility functions""" 20 | @staticmethod 21 | def fileExists(path): 22 | """determines if a file exists 23 | 24 | Arguments: 25 | path -- path to file on disk 26 | 27 | Returns: 28 | bool 29 | 30 | """ 31 | return os.path.isfile(path) 32 | 33 | @staticmethod 34 | def isDir(path): 35 | """determines if a path is a directory 36 | 37 | Arguments: 38 | path -- path on disk 39 | 40 | Returns: 41 | bool 42 | 43 | """ 44 | return os.path.isdir(path) 45 | 46 | @staticmethod 47 | def getFilesFromDir(path, extension = ""): 48 | path = path + "/*" 49 | 50 | if not extension == "": 51 | path = path + "." + extension.lstrip(".") 52 | 53 | return glob.glob(path) 54 | 55 | @staticmethod 56 | def dump(obj): 57 | """displays an object as a string for debugging 58 | 59 | Arguments: 60 | obj -- object 61 | 62 | Returns: 63 | string 64 | 65 | """ 66 | for attr in dir(obj): 67 | print "obj.%s = %s" % (attr, getattr(obj, attr)) 68 | 69 | @staticmethod 70 | def getExtension(path): 71 | """gets the extension from a file 72 | 73 | Arguments: 74 | path -- string of the file name 75 | 76 | Returns: 77 | string 78 | 79 | """ 80 | return path.split(".").pop() 81 | 82 | @staticmethod 83 | def prependExtension(ext, path): 84 | current_ext = Util.getExtension(path) 85 | return path.replace("." + current_ext, "." + ext + "." + current_ext) 86 | 87 | @staticmethod 88 | def getBasePath(path): 89 | """gets the base directory one level up from the current path 90 | 91 | Arguments: 92 | path -- path to file or directory 93 | 94 | Returns: 95 | string 96 | 97 | """ 98 | bits = path.split("/") 99 | last_bit = bits.pop() 100 | return "/".join(bits) 101 | # return "/".join(bits).rstrip(last_bit) 102 | 103 | @staticmethod 104 | def getFileName(path): 105 | return path.replace(Util.getBasePath(path), "").lstrip("/") 106 | 107 | @staticmethod 108 | def unlink(path): 109 | """deletes a file on disk 110 | 111 | Arguments: 112 | path -- path to file on disk 113 | 114 | Returns: 115 | void 116 | 117 | """ 118 | if Util.fileExists(path): 119 | os.unlink(path) 120 | 121 | @staticmethod 122 | def unlinkDir(path): 123 | """removes an entire directory on disk 124 | 125 | Arguments: 126 | path -- path to directory to remove 127 | 128 | Returns: 129 | void 130 | 131 | """ 132 | try: 133 | shutil.rmtree(path) 134 | except: 135 | pass 136 | 137 | @staticmethod 138 | def fileGetContents(path): 139 | """gets the contents of a file 140 | 141 | Arguments: 142 | path -- path to file on disk 143 | 144 | Returns: 145 | string 146 | 147 | """ 148 | if not Util.fileExists(path): 149 | print "file does not exist at path " + path 150 | print "skipping" 151 | file = open(path, "r") 152 | contents = file.read() 153 | file.close() 154 | return contents 155 | 156 | @staticmethod 157 | def filePutContents(path, contents): 158 | """puts contents into a file 159 | 160 | Arguments: 161 | path -- path to file to write to 162 | contents -- contents to put into file 163 | 164 | Returns: 165 | void 166 | 167 | """ 168 | file = open(path, "w") 169 | file.write(contents) 170 | file.close() 171 | 172 | @staticmethod 173 | def keyInTupleList(key, tuple_list): 174 | """checks a list of tuples for the given key""" 175 | for tuple in tuple_list: 176 | if tuple[0] == key: 177 | return True 178 | return False 179 | -------------------------------------------------------------------------------- /muncher/varfactory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2011 Craig Campbell 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import math 17 | 18 | class VarFactory: 19 | """class to keep multiple counters and turn numeric counters into alphabetical ones""" 20 | types = {} 21 | letters = map(chr, range(97, 123)) 22 | 23 | @staticmethod 24 | def getNext(type): 25 | """gets the next letter name based on counter name 26 | 27 | Arguments: 28 | type -- name of counter we want the next value for 29 | 30 | Returns: 31 | string 32 | 33 | """ 34 | i = VarFactory.getVersion(type) 35 | return VarFactory.getSmallName(i) 36 | 37 | @staticmethod 38 | def getVersion(type): 39 | """gets the next number in the counter for this type 40 | 41 | Arguments: 42 | type -- name of counter we are incrementing 43 | 44 | Resturns: 45 | int 46 | 47 | """ 48 | if not type in VarFactory.types: 49 | VarFactory.types[type] = 0 50 | return 0 51 | 52 | VarFactory.types[type] += 1 53 | 54 | return VarFactory.types[type] 55 | 56 | @staticmethod 57 | def getSmallName(index): 58 | """gets a letter index based on the numeric index 59 | 60 | Arguments: 61 | index -- the number you are looking for 62 | 63 | Returns: 64 | string 65 | 66 | """ 67 | # total number of combinations for this index size 68 | combinations = 0 69 | letters = 0 70 | while (combinations + (((letters - 1) * 26) - 1) < index): 71 | letters += 1 72 | combinations = int(math.pow(len(VarFactory.letters), letters)) 73 | 74 | if (index > 701): 75 | raise Exception("until my math skillz get better we can only support 702 possibilities!") 76 | 77 | a = int(index) + 1 78 | 79 | if a < 27: 80 | return chr(a + 96) 81 | 82 | b = 0 83 | while a > 26: 84 | b += 1 85 | a = a - 26 86 | 87 | b = chr(b + 96) 88 | a = chr(a + 96) 89 | return b + a 90 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup(name='htmlmuncher', 6 | version='1.0', 7 | description='Utility that rewrites CSS, HTML, and JavaScript files in order to save bytes and obfuscate your code.', 8 | author='Craig Campbell', 9 | author_email='iamcraigcampbell@gmail.com', 10 | url='http://htmlmuncher.com', 11 | packages=['muncher'], 12 | scripts=['munch'] 13 | ) 14 | --------------------------------------------------------------------------------