[
  {
    "path": ".gitignore",
    "content": "*.pyc\n*.opt.html\n*.opt.js\n*.opt.css\ndemo/css_opt\ndemo/js_opt\ndemo/views_opt\n"
  },
  {
    "path": "README",
    "content": "--------------\n  ABOUT\n--------------\n\nHTML Muncher is a Python utility that rewrites CSS, HTML, and JavaScript files in order to save precious bytes and obfuscate your code\n\nif your stylesheet starts out looking like this:\n\n.file2 #special {\n    font-size: 1.5em;\n    color: #F737FF;\n}\n\n.file2 #special2 {\n    letter-spacing: 0;\n}\n\n.box {\n    border: 2px solid #aaa;\n    -webkit-border-radius: 5px;\n    background: #eee;\n    padding: 5px;\n}\n\nit will be rewritten as\n\n.a #a {\n    font-size: 1.5em;\n    color: #F737FF;\n}\n\n.a #b {\n    letter-spacing: 0;\n}\n\n.b {\n    border: 2px solid #aaa;\n    -webkit-border-radius: 5px;\n    background: #eee;\n    padding: 5px;\n}\n\n\n--------------\n INSTALLATION\n--------------\n\neasy_install http://htmlmuncher.com/htmlmuncher.egg\n\nOR:\n\ndownload the source from http://github.com/ccampbell/html-muncher\ncd html-muncher\npython setup.py install\n\n\n--------------\n USAGE\n--------------\nhttp://htmlmuncher.com/#usage\n\nOR:\n\nmunch --help\n\n\n--------------\n EXAMPLES\n--------------\n\nto update a bunch of stylesheets and views:\nmunch --css demo/css --html demo/views\n\nto update a single file with inline styles/javascript:\nmunch --html demo/single-file/view-with-inline-styles.html\n\nyou can also select specific files:\nmunch --css file1.css,file2.css --html view1.html,view2.html\n\nor you can mix and match files and directories\nmunch --css /my/css/directory,global.css --html /view/directory1,/view/directory2,/view/directory3,template.html\n"
  },
  {
    "path": "demo/css/stylesheet1.css",
    "content": "body {\n    font-family: helvetica;\n}\n\nh1 {\n    text-align: center;\n    font-size: 2em;\n}\n\n.red {\n    color: red;\n}\n\n.blue {\n    color: blue;\n}\n\n.green {\n    color: green;\n}\n\n.box.purple {\n    color: purple;\n}\n\n.underline {\n    text-decoration: underline;\n}\n\n#special {\n    color: orange;\n    font-style: italic;\n}\n\n.italic {\n    font-style: italic;\n}\n\n#special2 {\n    letter-spacing: .5em;\n    color: grey;\n}\n\n#new_id, #special, #special2 {\n    font-size: 1em;\n}"
  },
  {
    "path": "demo/css/stylesheet2.css",
    "content": ".file2 #special {\n    font-size: 1.5em;\n    color: #F737FF;\n}\n\n.file2 #special2 {\n    letter-spacing: 0;\n}\n\n.box {\n    border: 2px solid #aaa;\n    -webkit-border-radius: 5px;\n    background: #eee;\n    padding: 5px;\n}"
  },
  {
    "path": "demo/js/test.js",
    "content": "$ = {\n    qs: function(query) {\n        return document.querySelector(query);\n    }\n};\n\nwindow.onload = function()\n{\n    $.qs(\"#special\").innerHTML = \"new text for this paragraph\";\n    document.getElementById(\"special\").innerHTML = \"change it again\";\n    var italic = document.getElementsByClassName('italic');\n\n    // mootools\n    var test = $('test');\n    if (test.hasClass(\"dont_know\")) {\n        test.removeClass(\"dont_know\");\n        test.addClass('now_i_know');\n    }\n\n    var class_thing = $('class_thing');\n    class_thing.addClass(test, \"whatever\");\n    class_thing.removeClass(test, \"whatever\");\n    $.qs(\".dont_know\", class_thing).value;\n    var cool = $.qs(\"#one_id.class_thing\", test);\n    var another_weird_thing = $.qs(\".class1.class2 #another_id\");\n    $.qs(\".selector1 > .selector2 .selector3\");\n    var test = document.querySelector(\".selector1\");\n}\n"
  },
  {
    "path": "demo/single-file/view-with-inline-styles.html",
    "content": "<html>\n<head>\n    <title>view with inline styles</title>\n    <style type=\"text/css\">\n        .content {\n            padding: 25px 0px 50px 0px;\n            overflow: hidden;\n        }\n\n        a {\n            color: #FF5F1E;\n        }\n\n        .content, #footer {\n            width: 800px;\n        }\n\n        ul.menu {\n            width: 200px;\n            float: left;\n        }\n\n        #main {\n            width: 500px;\n            float: left;\n        }\n\n        ul {\n            padding: 0px;\n            margin: 0px;\n            list-style: none;\n        }\n\n        ul li {\n            margin-top: 20px;\n        }\n\n        ul li.first_child {\n            margin-top: 0px;\n        }\n\n        .box {\n            width: 500px;\n            border: 1px solid #aaa;\n            padding: 5px;\n            -moz-border-radius: 5px;\n            -webkit-border-radius: 5px;\n            margin-bottom: 15px;\n            background: #fff url(\"../test.jpg\");\n        }\n\n        .green {\n            background: #ACFFB9;\n        }\n\n        .blue {\n            background: #C5D7FF;\n        }\n\n        #footer ul {\n            text-align: center;\n        }\n\n        #footer ul li {\n            display: inline;\n            margin-left: 20px;\n        }\n\n        #footer ul li.first_child {\n            margin-left: 0px;\n        }\n\n        p.special span {\n            font-weight: bold;\n        }\n    </style>\n</head>\n<body>\n    <h1>test page with inline styles</h1>\n\n    <div class=\"content\">\n        <ul class=\"menu\">\n            <li class=\"first_child\"><a href=\"#\">link 1</a></li>\n            <li class=\"unused_class\"><a href=\"#\">link 2</a></li>\n            <li><a href=\"#\">link 3</a></li>\n            <li><a href=\"#\">link 4</a></li>\n            <li><a href=\"#\">link 5</a></li>\n        </ul>\n\n        <div id=\"main\">\n            <div class=\"box green\">\n                <p>this is a green box</p>\n            </div>\n\n            <div class=\"box blue\">\n                <p>this is a blue box</p>\n            </div>\n\n            <p class=\"special\">this is <span>paragraph</span></p>\n        </div>\n    </div>\n\n    <div id=\"footer\">\n        <ul>\n            <li class=\"first_child\"><a href=\"#\">footer link 1</a></li>\n            <li><a href=\"#\">footer link 2</a></li>\n            <li><a href=\"#\">footer link 3</a></li>\n            <li><a href=\"#\">footer link 4</a></li>\n        </ul>\n    </div>\n    <script type=\"text/javascript\">\n        $('.special').hide();\n        $(\"#main .box\").hide();\n    </script>\n</body>\n</html>"
  },
  {
    "path": "demo/views/view1.html",
    "content": "<html>\n<head>\n    <title>test page 1 changed</title>\n    <link href=\"../css/stylesheet1.css\" rel=\"stylesheet\" type=\"text/css\" />\n</head>\n<body>\n    <h1>this is the first test page</h1>\n    <p class=\"red\">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.</p>\n    <p class=\"box purple\">this is a test with two classes</p>\n    <p class=\"blue\">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.</p>\n    <p class=\"green underline\">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.</p>\n    <p id=\"special\">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.</p>\n    <p id=\"special2\" class=\"italic\">Intrinsicly streamline user-centric users before visionary scenarios. Dynamically repurpose seamless channels via multifunctional process improvements. Professionally synthesize exceptional infrastructures without premier vortals.</p>\n    <script type=\"text/javascript\" src=\"../js/test.js\"></script>\n</body>\n</html>"
  },
  {
    "path": "demo/views/view2.html",
    "content": "<html>\n<head>\n    <title>test page 2</title>\n    <link href=\"../css/stylesheet1.css\" rel=\"stylesheet\" type=\"text/css\" />\n    <link href=\"../css/stylesheet2.css\" rel=\"stylesheet\" type=\"text/css\" />\n</head>\n<body class=\"file2\">\n    <h1>this is the second test page</h1>\n    <p class=\"red\">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.</p>\n    <p class=\"box blue\">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.</p>\n    <p id=\"special\">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.</p>\n    <p id=\"special2\" class=\"italic\">Intrinsicly streamline user-centric users before visionary scenarios. Dynamically repurpose seamless channels via multifunctional process improvements. Professionally synthesize exceptional infrastructures without premier vortals.</p>\n</body>\n</html>\n"
  },
  {
    "path": "munch",
    "content": "#!/usr/bin/env python\n\n# Copyright 2011 Craig Campbell\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport sys\nfrom muncher.config import Config\nfrom muncher.muncher import Muncher\n\nconfig = Config()\nconfig.processArgs()\nmuncher = Muncher(config)\nmuncher.run()\n"
  },
  {
    "path": "muncher/__init__.py",
    "content": ""
  },
  {
    "path": "muncher/config.py",
    "content": "#!/usr/bin/env python\n# Copyright 2011 Craig Campbell\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport sys, getopt\nfrom muncher import Muncher\n\nclass Config(object):\n    \"\"\"configuration object for handling all config options for html-muncher\"\"\"\n    def __init__(self):\n        \"\"\"config object constructor\n\n        Returns:\n        void\n\n        \"\"\"\n        self.css = []\n        self.views = []\n        self.js = []\n        self.ignore = []\n        self.class_selectors = [\"getElementsByClassName\", \"hasClass\", \"addClass\", \"removeClass\"]\n        self.id_selectors = [\"getElementById\"]\n        self.custom_selectors = [\"document.querySelector\"]\n        self.framework = None\n        self.view_extension = \"html\"\n        self.js_manifest = None\n        self.show_savings = False\n        self.compress_html = False\n        self.rewrite_constants = False\n        self.verbose = False\n\n    def getArgCount(self):\n        \"\"\"gets the count of how many arguments are present\n\n        Returns:\n        int\n\n        \"\"\"\n        return len(sys.argv)\n\n    def setIgnore(self, value):\n        \"\"\"sets what classes and ids we should ignore and not shorten\n\n        Arguments:\n        value -- comma separated list of classes or ids\n\n        Returns:\n        void\n\n        \"\"\"\n        for name in value.split(\",\"):\n            self.ignore.append(name)\n\n    def setCustomSelectors(self, value):\n        for value in value.split(\",\"):\n            self.custom_selectors.append(value.lstrip(\".\"))\n\n    def addClassSelectors(self, value):\n        for value in value.split(\",\"):\n            self.class_selectors.append(value)\n\n    def addIdSelectors(self, value):\n        for value in value.split(\",\"):\n            self.id_selectors.append(value)\n\n    def setCssFiles(self, value):\n        for value in value.split(\",\"):\n            self.css.append(value.rstrip(\"/\"))\n\n    def setViewFiles(self, value):\n        for value in value.split(\",\"):\n            self.views.append(value.rstrip(\"/\"))\n\n    def setJsFiles(self, value):\n        for value in value.split(\",\"):\n            self.js.append(value.rstrip(\"/\"))\n\n    def setFramework(self, name):\n        self.framework = name.lower()\n        if self.framework == \"jquery\":\n            self.custom_selectors.append(\"$\")\n            self.custom_selectors.append(\"jQuery\")\n        elif self.framework == \"mootools\":\n            self.id_selectors.append(\"$\")\n            self.custom_selectors.append(\"getElement\")\n\n    def processArgs(self):\n        \"\"\"processes arguments passed in via command line and sets config settings accordingly\n\n        Returns:\n        void\n\n        \"\"\"\n        try:\n            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\"])\n        except:\n            Muncher.showUsage()\n\n        views_set = False\n\n        for key, value in opts:\n            if key == \"--help\":\n                Muncher.showUsage()\n            elif key == \"--css\":\n                self.setCssFiles(value)\n            elif key == \"--views\" or key == \"--html\":\n                views_set = True\n                self.setViewFiles(value)\n            elif key == \"--js\":\n                self.setJsFiles(value)\n            elif key == \"--ignore\":\n                self.setIgnore(value)\n            elif key == \"--view-ext\":\n                self.view_extension = value\n            elif key == \"--framework\":\n                self.setFramework(value)\n            elif key == \"--selectors\":\n                self.setCustomSelectors(value)\n            elif key == \"--class-selectors\":\n                self.addClassSelectors(value)\n            elif key == \"--id-selectors\":\n                self.addIdSelectors(value)\n            elif key == \"--compress-html\":\n                self.compress_html = True\n            elif key == \"--show-savings\":\n                self.show_savings = True\n            elif key == \"--verbose\":\n                self.verbose = True\n            elif key == \"--js-manifest\":\n                self.js_manifest = value\n            elif key == \"--rewrite-constants\":\n                self.rewrite_constants = True\n\n        # you have to at least have a view\n        if views_set is False:\n            Muncher.showUsage()\n"
  },
  {
    "path": "muncher/muncher.py",
    "content": "#!/usr/bin/env python\n# Copyright 2011 Craig Campbell\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport sys, re, glob, os\nfrom operator import itemgetter\nfrom util import Util\nfrom varfactory import VarFactory\nfrom sizetracker import SizeTracker\n\nclass Muncher(object):\n    def __init__(self, config):\n        \"\"\"constructor\n\n        Returns:\n        void\n\n        \"\"\"\n        self.id_counter = {}\n        self.class_counter = {}\n        self.id_map = {}\n        self.class_map = {}\n        self.config = config\n\n    @staticmethod\n    def showUsage():\n        \"\"\"shows usage information for this script\"\"\"\n        print \"\\n---------------------------------\"\n        print \" html-muncher\"\n        print \"---------------------------------\"\n\n        print \"\\n\" + '\\033[91m' + \"USAGE:\" + '\\033[0m'\n        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\"\n        print \"\\n\" + '\\033[91m' + \"REQUIRED ARGUMENTS:\" + '\\033[0m'\n        print \"--html {path/to/views}       html files to rewrite (comma separated list of directories and files)\"\n        print \"\\n\" + '\\033[91m' + \"OPTIONAL ARGUMENTS:\" + '\\033[0m'\n        print \"--css {path/to/css}          css files to rewrite (comma separated list of directories and files)\"\n        print \"\"\n        print \"--js {path/to/js}            js files to rewrite (comma separated list of directories and files)\"\n        print \"\"\n        print \"--view-ext {extension}       sets the extension to look for in the view directory (defaults to html)\"\n        print \"\"\n        print \"--ignore {classes,ids}       comma separated list of classes or ids to ignore when rewriting css (ie .sick_class,#sweet_id)\"\n        print \"\"\n        print \"--compress-html              strips new line characters to compress html files specified with --html\"\n        print \"                             be careful when using this becuase it has not been thoroughly tested\"\n        print \"\"\n        print \"--framework                  name of js framework to use for selectors (currently only jquery or mootools)\"\n        print \"\"\n        print \"--selectors                  comma separated custom selectors using css selectors\"\n        print \"                             for example if you have $.qs(\\\"#test .div\\\") this param would be qs\"\n        print \"\"\n        print \"--id-selectors               comma separated id selectors with strings\"\n        print \"                             for example if you are using .addId(\\\"test\\\") this param would be addId\"\n        print \"\"\n        print \"--class-selectors            comma separated class selectors with strings\"\n        print \"                             for example if you have selectClass(\\\"my_class\\\") this param would be selectClass\"\n        print \"\"\n        print \"--js-manifest                path to a js file containing class name/id constants\"\n        print \"\"\n        print \"--rewrite-constants          when using a manifest file this will take any constants with values as strings\"\n        print \"                             and rewrite the values to be numbers\"\n        print \"\"\n        print \"--show-savings               will output how many bytes were saved by munching\"\n        print \"\"\n        print \"--verbose                    output more information while the script runs\"\n        print \"\"\n        print \"--help                       shows this menu\\n\"\n        sys.exit(2)\n\n    def run(self):\n        \"\"\"runs the optimizer and does all the magic\n\n        Returns:\n        void\n\n        \"\"\"\n        self.output(\"searching for classes and ids...\", False)\n\n        if self.config.js_manifest is not None:\n            self.outputJsWarnings()\n\n        self.processCss()\n        self.processViews()\n\n        if self.config.js_manifest is None:\n            self.processJs()\n        else:\n            self.processJsManifest()\n\n        self.output(\"mapping classes and ids to new names...\", False)\n        # maps all classes and ids found to shorter names\n        self.processMaps()\n\n        # optimize everything\n        self.output(\"munching css files...\", False)\n        self.optimizeFiles(self.config.css, self.optimizeCss)\n\n        self.output(\"munching html files...\", False)\n        self.optimizeFiles(self.config.views, self.optimizeHtml, self.config.view_extension, self.config.compress_html)\n\n        self.output(\"munching js files...\", False)\n\n        if self.config.js_manifest is None:\n            self.optimizeFiles(self.config.js, self.optimizeJavascript)\n        else:\n            self.optimizeJsManifest()\n\n        self.output(\"done\", False)\n\n        if self.config.show_savings:\n            self.output(SizeTracker.savings(), False)\n\n    def outputJsWarnings(self):\n        pass\n\n    def output(self, text, verbose_only = True):\n        \"\"\"outputs text during the script run\n\n        Arguments:\n        text -- string of text to output\n        verbose_only -- should we only show this in verbose mode?\n\n        Returns:\n        void\n\n        \"\"\"\n        if verbose_only and not self.config.verbose:\n            return\n\n        print text\n\n    def processCssDirectory(self, file):\n        \"\"\"processes a directory of css files\n\n        Arguments:\n        file -- path to directory\n\n        Returns:\n        void\n\n        \"\"\"\n        if \".svn\" in file:\n            return\n\n        for dir_file in Util.getFilesFromDir(file):\n            if Util.isDir(dir_file):\n                self.processCssDirectory(dir_file)\n                continue\n\n            self.processCssFile(dir_file)\n\n    def processCss(self):\n        \"\"\"gets all css files from config and processes them to see what to replace\n\n        Returns:\n        void\n\n        \"\"\"\n        files = self.config.css\n        for file in files:\n            if not Util.isDir(file):\n                self.processCssFile(file)\n                continue\n            self.processCssDirectory(file)\n\n    def processViewDirectory(self, file):\n        \"\"\"processes a directory of view files\n\n        Arguments:\n        file -- path to directory\n\n        Returns:\n        void\n\n        \"\"\"\n        if \".svn\" in file:\n            return\n\n        for dir_file in Util.getFilesFromDir(file):\n            if Util.isDir(dir_file):\n                self.processViewDirectory(dir_file)\n                continue\n\n            self.processView(dir_file)\n\n    def processViews(self):\n        \"\"\"processes all view files\n\n        Returns:\n        void\n\n        \"\"\"\n        files = self.config.views\n        for file in files:\n            if not Util.isDir(file):\n                self.processView(file)\n                continue\n            self.processViewDirectory(file)\n\n    def processJsDirectory(self, file):\n        \"\"\"processes a directory of js files\n\n        Arguments:\n        file -- path to directory\n\n        Returns:\n        void\n\n        \"\"\"\n        if \".svn\" in file:\n            return\n\n        for dir_file in Util.getFilesFromDir(file):\n            if Util.isDir(dir_file):\n                self.processJsDirectory(dir_file)\n                continue\n            self.processJsFile(dir_file)\n\n    def processJs(self):\n        \"\"\"gets all js files from config and processes them to see what to replace\n\n        Returns:\n        void\n\n        \"\"\"\n        files = self.config.js\n        for file in files:\n            if not Util.isDir(file):\n                self.processJsFile(file)\n                continue\n            self.processJsDirectory(file)\n\n    def processView(self, file):\n        \"\"\"processes a single view file\n\n        Arguments:\n        file -- path to directory\n\n        \"\"\"\n        self.processCssFile(file, True)\n        self.processJsFile(file, True)\n\n    def processCssFile(self, path, inline = False):\n        \"\"\"processes a single css file to find all classes and ids to replace\n\n        Arguments:\n        path -- path to css file to process\n\n        Returns:\n        void\n\n        \"\"\"\n        contents = Util.fileGetContents(path)\n        if inline is True:\n            blocks = self.getCssBlocks(contents)\n            contents = \"\"\n            for block in blocks:\n                contents = contents + block\n\n        ids_found = re.findall(r'((?<!\\:\\s)(?<!\\:)#\\w+)(\\.|\\{|,|\\s|#)', contents, re.DOTALL)\n        classes_found = re.findall(r'(?!\\.[0-9])\\.\\w+', contents)\n        self.addIds(ids_found)\n        self.addClasses(classes_found)\n\n    def processJsFile(self, path, inline = False):\n        \"\"\"processes a single js file to find all classes and ids to replace\n\n        Arguments:\n        path -- path to css file to process\n\n        Returns:\n        void\n\n        \"\"\"\n        contents = Util.fileGetContents(path)\n        if inline is True:\n            blocks = self.getJsBlocks(contents)\n            contents = \"\"\n            for block in blocks:\n                contents = contents + block\n\n        selectors = self.getJsSelectors(contents, self.config)\n        for selector in selectors:\n            if selector[0] in self.config.id_selectors:\n                if ',' in selector[2]:\n                    id_to_add = re.search(r'(\\'|\\\")(.*?)(\\'|\\\")', selector[2])\n                    if id_to_add is None:\n                        continue\n\n                    if not id_to_add.group(2):\n                        continue\n\n                    self.addId(\"#\" + id_to_add.group(2))\n\n                # if this is something like document.getElementById(variable) don't add it\n                if not '\\'' in selector[2] and not '\"' in selector[2]:\n                    continue\n\n                self.addId(\"#\" + selector[2].strip(\"\\\"\").strip(\"'\"))\n                continue\n\n            if selector[0] in self.config.class_selectors:\n                class_to_add = re.search(r'(\\'|\\\")(.*?)(\\'|\\\")', selector[2])\n                if class_to_add is None:\n                    continue\n\n                if not class_to_add.group(2):\n                    continue\n\n                self.addClass(\".\" + class_to_add.group(2))\n                continue\n\n            if selector[0] in self.config.custom_selectors:\n                matches = re.findall(r'((#|\\.)[a-zA-Z0-9_]*)', selector[2])\n                for match in matches:\n                    if match[1] == \"#\":\n                        self.addId(match[0])\n                        continue\n\n                    self.addClass(match[0])\n\n    def processJsManifest(self):\n        contents = Util.fileGetContents(self.config.js_manifest)\n        ids = re.findall(r'\\s+?(var\\s)?\\${1}([A-Z0-9_]+)\\s?=\\s?[\\'|\\\"](.*?)[\\'|\\\"][,|;]', contents)\n        classes = re.findall(r'\\s+?(var\\s)?\\${2}([A-Z0-9_]+)\\s?=\\s?[\\'|\\\"](.*?)[\\'|\\\"][,|;]', contents)\n\n        self.manifest_ids = {}\n        self.manifest_classes = {}\n\n        for id in ids:\n            self.addId(\"#\" + id[2])\n            self.manifest_ids[id[1]] = id[2]\n\n        for manifest_class in classes:\n            self.addClass(\".\" + manifest_class[2])\n            self.manifest_classes[manifest_class[1]] = manifest_class[2]\n\n    def optimizeJsManifest(self):\n        contents = Util.fileGetContents(self.config.js_manifest)\n\n        for key, value in self.manifest_ids.items():\n            if \"#\" + value in self.id_map:\n                contents = re.sub(r'((?<!\\$)\\${1}[A-Z0-9_]+\\s?=\\s?[\\'|\\\"])(' + value + ')([\\'|\\\"][,|;])', r'\\1' + self.id_map[\"#\" + value].replace(\"#\", \"\") + r'\\3', contents)\n\n        for key, value in self.manifest_classes.items():\n            if \".\" + value in self.class_map:\n                contents = re.sub(r'(\\${2}[A-Z0-9_]+\\s?=\\s?[\\'|\\\"])(' + value + ')([\\'|\\\"][,|;])', r'\\1' + self.class_map[\".\" + value].replace(\".\", \"\") + r'\\3', contents)\n\n        if self.config.rewrite_constants:\n            constants = re.findall(r'(\\s+?(var\\s)?([A-Z0-9_]+)\\s?=\\s?[\\'|\\\"](.*?)[\\'|\\\"][,|;])', contents)\n            new_constants = {}\n            i = 0\n            for constant in constants:\n                # underscore variables are ignored\n                if constant[2][0] == \"_\":\n                    continue\n\n                i += 1\n                new_constant = re.sub(r'=(.*)([,|;])','= ' + str(i) + r'\\2', constant[0])\n                contents = contents.replace(constant[0], new_constant)\n\n        new_manifest = Util.prependExtension(\"opt\", self.config.js_manifest)\n        Util.filePutContents(new_manifest, contents)\n\n        if self.config.show_savings:\n            SizeTracker.trackFile(self.config.js_manifest, new_manifest)\n\n    def processMaps(self):\n        \"\"\"loops through classes and ids to process to determine shorter names to use for them\n        and creates a dictionary with these mappings\n\n        Returns:\n        void\n\n        \"\"\"\n        # reverse sort so we can figure out the biggest savings\n        classes = self.class_counter.items()\n        classes.sort(key = itemgetter(1), reverse=True)\n\n        for class_name, savings in classes:\n            small_class = \".\" + VarFactory.getNext(\"class\")\n\n            # adblock extensions may block class \"ad\" so we should never generate it\n            # also if the generated class already exists as a class to be processed\n            # we can't use it or bad things will happen\n            while small_class == \".ad\" or Util.keyInTupleList(small_class, classes):\n                small_class = \".\" + VarFactory.getNext(\"class\")\n\n            self.class_map[class_name] = small_class\n\n        ids = self.id_counter.items()\n        ids.sort(key = itemgetter(1), reverse=True)\n\n        for id, savings in ids:\n            small_id = \"#\" + VarFactory.getNext(\"id\")\n\n            # same holds true for ids as classes\n            while small_id == \"#ad\" or Util.keyInTupleList(small_id, ids):\n                small_id = \"#\" + VarFactory.getNext(\"id\")\n\n            self.id_map[id] = small_id\n\n    def incrementIdCounter(self, name):\n        \"\"\"called for every time an id is added to increment the bytes we will save\n\n        Arguments:\n        name -- string of id\n\n        Returns:\n        void\n\n        \"\"\"\n        length = len(name)\n\n        if not name in self.id_counter:\n            self.id_counter[name] = length\n            return\n\n        self.id_counter[name] += length\n\n    def incrementClassCounter(self, name):\n        \"\"\"called for every time a class is added to increment the bytes we will save\n\n        Arguments:\n        name -- string of class\n\n        Returns:\n        void\n\n        \"\"\"\n        length = len(name)\n\n        if not name in self.class_counter:\n            self.class_counter[name] = length\n            return\n\n        self.class_counter[name] += length\n\n    def incrementCounter(self, name):\n        \"\"\"called everytime a class or id is added\n\n        Arguments:\n        name -- string of class or id name\n\n        Returns:\n        void\n\n        \"\"\"\n        if name[0] == \"#\":\n            return self.incrementIdCounter(name)\n\n        return self.incrementClassCounter(name)\n\n    def addId(self, id):\n        \"\"\"adds a single id to the master list of ids\n\n        Arguments:\n        id -- single id to add\n\n        Returns:\n        void\n\n        \"\"\"\n        if id in self.config.ignore or id is '#':\n            return\n\n        # skip $ ids from manifest\n        if self.config.js_manifest is not None and id[1] == '$':\n            return\n\n        self.incrementCounter(id)\n\n    def addIds(self, ids):\n        \"\"\"adds a list of ids to the master id list to replace\n\n        Arguments:\n        ids -- list of ids to add\n\n        Returns:\n        void\n\n        \"\"\"\n        for id in ids:\n            self.addId(id[0])\n\n    def addClass(self, class_name):\n        \"\"\"adds a single class to the master list of classes\n\n        Arguments:\n        class_name -- single class to add\n\n        Returns:\n        void\n\n        \"\"\"\n        if class_name in self.config.ignore or class_name is '.':\n            return\n\n        # skip $$ class names from manifest\n        if self.config.js_manifest is not None and class_name[1:2] == '$$':\n            return\n\n        self.incrementCounter(class_name)\n\n    def addClasses(self, classes):\n        \"\"\"adds a list of classes to the master class list to replace\n\n        Arguments:\n        classes -- list of classes to add\n\n        Returns:\n        void\n\n        \"\"\"\n        for class_name in classes:\n            self.addClass(class_name)\n\n    def optimizeFiles(self, paths, callback, extension = \"\", minimize = False):\n        \"\"\"loops through a bunch of files and directories, runs them through a callback, then saves them to disk\n\n        Arguments:\n        paths -- array of files and directories\n        callback -- function to process each file with\n\n        Returns:\n        void\n\n        \"\"\"\n        for file in paths:\n            if not Util.isDir(file):\n                self.optimizeFile(file, callback, minimize)\n                continue\n\n            self.optimizeDirectory(file, callback, extension, minimize)\n\n    def optimizeFile(self, file, callback, minimize = False, new_path = None, prepend = \"opt\"):\n        \"\"\"optimizes a single file\n\n        Arguments:\n        file -- path to file\n        callback -- function to run the file through\n        minimize -- whether or not we should minimize the file contents (html)\n        prepend -- what extension to prepend\n\n        Returns:\n        void\n\n        \"\"\"\n        content = callback(file)\n        if new_path is None:\n            new_path = Util.prependExtension(prepend, file)\n        if minimize is True:\n            self.output(\"minimizing \" + file)\n            content = self.minimize(content)\n        self.output(\"optimizing \" + file + \" to \" + new_path)\n        Util.filePutContents(new_path, content)\n\n        if self.config.show_savings:\n            SizeTracker.trackFile(file, new_path)\n\n    def prepareDirectory(self, path):\n        if \".svn\" in path:\n            return True\n\n        if Util.isDir(path):\n            return False\n\n        Util.unlinkDir(path)\n        self.output(\"creating directory \" + path)\n        os.mkdir(path)\n        return False\n\n    def optimizeDirectory(self, path, callback, extension = \"\", minimize = False):\n        \"\"\"optimizes a directory\n\n        Arguments:\n        path -- path to directory\n        callback -- function to run the file through\n        extension -- extension to search for in the directory\n        minimize -- whether or not we should minimize the file contents (html)\n\n        Returns:\n        void\n\n        \"\"\"\n        directory = path + \"_opt\"\n        skip = self.prepareDirectory(directory)\n        if skip is True:\n            return\n\n        for dir_file in Util.getFilesFromDir(path, extension):\n            if Util.isDir(dir_file):\n                self.optimizeSubdirectory(dir_file, callback, directory, extension, minimize)\n                continue\n\n            new_path = directory + \"/\" + Util.getFileName(dir_file)\n            self.optimizeFile(dir_file, callback, minimize, new_path)\n\n    def optimizeSubdirectory(self, path, callback, new_path, extension = \"\", minimize = False):\n        \"\"\"optimizes a subdirectory within a directory being optimized\n\n        Arguments:\n        path -- path to directory\n        callback -- function to run the file through\n        new_path -- path to optimized parent directory\n        extension -- extension to search for in the directory\n        minimize -- whether or not we should minimize the file contents (html)\n\n        Returns:\n        void\n\n        \"\"\"\n        subdir_path = new_path + \"/\" + path.split(\"/\").pop()\n        skip = self.prepareDirectory(subdir_path)\n        if skip is True:\n            return\n\n        for dir_file in Util.getFilesFromDir(path, extension):\n            if Util.isDir(dir_file):\n                self.optimizeSubdirectory(dir_file, callback, subdir_path, extension, minimize)\n                continue\n\n            new_file_path = subdir_path + \"/\" + Util.getFileName(dir_file)\n            self.optimizeFile(dir_file, callback, minimize, new_file_path)\n\n    def minimize(self, content):\n        content = re.sub(r'\\n', '', content)\n        content = re.sub(r'\\s\\s+', '', content)\n        content = re.sub(r'(<!--(?!\\[if)(.*?)-->)', '', content, re.MULTILINE)\n        return content\n\n    def optimizeCss(self, path):\n        \"\"\"replaces classes and ids with new values in a css file\n\n        Arguments:\n        path -- string path to css file to optimize\n\n        Returns:\n        string\n\n        \"\"\"\n        css = Util.fileGetContents(path)\n        return self.replaceCss(css)\n\n    def optimizeHtml(self, path):\n        \"\"\"replaces classes and ids with new values in an html file\n\n        Uses:\n        Muncher.replaceHtml\n\n        Arguments:\n        path -- string path to file to optimize\n\n        Returns:\n        string\n\n        \"\"\"\n        html = Util.fileGetContents(path)\n        html = self.replaceHtml(html)\n        html = self.optimizeCssBlocks(html)\n        html = self.optimizeJavascriptBlocks(html)\n\n        return html\n\n    def replaceHtml(self, html):\n        \"\"\"replaces classes and ids with new values in an html file\n\n        Arguments:\n        html -- contents to replace\n\n        Returns:\n        string\n\n        \"\"\"\n        html = self.replaceHtmlIds(html)\n        html = self.replaceHtmlClasses(html)\n        return html\n\n    def replaceHtmlIds(self, html):\n        \"\"\"replaces any instances of ids in html markup\n\n        Arguments:\n        html -- contents of file to replaces ids in\n\n        Returns:\n        string\n\n        \"\"\"\n        for key, value in self.id_map.items():\n            key = key[1:]\n            value = value[1:]\n            html = html.replace(\"id=\\\"\" + key + \"\\\"\", \"id=\\\"\" + value + \"\\\"\")\n\n        return html\n\n    def replaceClassBlock(self, class_block, key, value):\n        \"\"\"replaces a class string with the new class name\n\n        Arguments:\n        class_block -- string from what would be found within class=\"{class_block}\"\n        key -- current class\n        value -- new class\n\n        Returns:\n        string\n\n        \"\"\"\n        key_length = len(key)\n        classes = class_block.split(\" \")\n        i = 0\n        for class_name in classes:\n            if class_name == key:\n                classes[i] = value\n\n            # allows support for things like a.class_name as one of the js selectors\n            elif key[0] in (\".\", \"#\") and class_name[-key_length:] == key:\n                classes[i] = class_name.replace(key, value)\n            i = i + 1\n\n        return \" \".join(classes)\n\n    def replaceHtmlClasses(self, html):\n        \"\"\"replaces any instances of classes in html markup\n\n        Arguments:\n        html -- contents of file to replace classes in\n\n        Returns:\n        string\n\n        \"\"\"\n        for key, value in self.class_map.items():\n            key = key[1:]\n            value = value[1:]\n            class_blocks = re.findall(r'class\\=((\\'|\\\")(.*?)(\\'|\\\"))', html)\n            for class_block in class_blocks:\n                new_block = self.replaceClassBlock(class_block[2], key, value)\n                html = html.replace(\"class=\" + class_block[0], \"class=\" + class_block[1] + new_block + class_block[3])\n\n        return html\n\n    def optimizeCssBlocks(self, html):\n        \"\"\"rewrites css blocks that are part of an html file\n\n        Arguments:\n        html -- contents of file we are replacing\n\n        Returns:\n        string\n\n        \"\"\"\n        result_css = \"\"\n        matches = self.getCssBlocks(html)\n        for match in matches:\n            match = self.replaceCss(match)\n            result_css = result_css + match\n\n        if len(matches):\n            return html.replace(matches[0], result_css)\n\n        return html\n\n    @staticmethod\n    def getCssBlocks(html):\n        \"\"\"searches a file and returns all css blocks <style type=\"text/css\"></style>\n\n        Arguments:\n        html -- contents of file we are replacing\n\n        Returns:\n        list\n\n        \"\"\"\n        return re.compile(r'\\<style.*?\\>(.*)\\<\\/style\\>', re.DOTALL).findall(html)\n\n    def replaceCss(self, css):\n        \"\"\"single call to handle replacing ids and classes\n\n        Arguments:\n        css -- contents of file to replace\n\n        Returns:\n        string\n\n        \"\"\"\n        css = self.replaceCssFromDictionary(self.class_map, css)\n        css = self.replaceCssFromDictionary(self.id_map, css)\n        return css\n\n    def replaceCssFromDictionary(self, dictionary, css):\n        \"\"\"replaces any instances of classes and ids based on a dictionary\n\n        Arguments:\n        dictionary -- map of classes or ids to replace\n        css -- contents of css to replace\n\n        Returns:\n        string\n\n        \"\"\"\n        # this really should be done better\n        for key, value in dictionary.items():\n            css = css.replace(key + \"{\", value + \"{\")\n            css = css.replace(key + \" {\", value + \" {\")\n            css = css.replace(key + \"#\", value + \"#\")\n            css = css.replace(key + \" #\", value + \" #\")\n            css = css.replace(key + \".\", value + \".\")\n            css = css.replace(key + \" .\", value + \" .\")\n            css = css.replace(key + \",\", value + \",\")\n            css = css.replace(key + \" \", value + \" \")\n            css = css.replace(key + \":\", value + \":\")\n            # if key == \".svg\":\n                # print \"replacing \" + key + \" with \" + value\n\n        return css\n\n    def optimizeJavascriptBlocks(self, html):\n        \"\"\"rewrites javascript blocks that are part of an html file\n\n        Arguments:\n        html -- contents of file we are replacing\n\n        Returns:\n        string\n\n        \"\"\"\n        matches = self.getJsBlocks(html)\n\n        for match in matches:\n            new_js = match\n            if self.config.compress_html:\n                matches = re.findall(r'((:?)\\/\\/.*?\\n|\\/\\*.*?\\*\\/)', new_js, re.DOTALL)\n                for single_match in matches:\n                    if single_match[1] == ':':\n                        continue\n                    new_js = new_js.replace(single_match[0], '');\n            new_js = self.replaceJavascript(new_js)\n            html = html.replace(match, new_js)\n\n        return html\n\n    @staticmethod\n    def getJsBlocks(html):\n        \"\"\"searches a file and returns all javascript blocks: <script type=\"text/javascript\"></script>\n\n        Arguments:\n        html -- contents of file we are replacing\n\n        Returns:\n        list\n\n        \"\"\"\n        return re.compile(r'\\<script(?! src).*?\\>(.*?)\\<\\/script\\>', re.DOTALL).findall(html)\n\n    def optimizeJavascript(self, path):\n        \"\"\"optimizes javascript for a specific file\n\n        Arguments:\n        path -- path to js file on disk that we are optimizing\n\n        Returns:\n        string -- contents to replace file with\n\n        \"\"\"\n        js = Util.fileGetContents(path)\n        return self.replaceJavascript(js)\n\n    def replaceJavascript(self, js):\n        \"\"\"single call to handle replacing ids and classes\n\n        Arguments:\n        js -- contents of file to replace\n\n        Returns:\n        string\n\n        \"\"\"\n        js = self.replaceJsFromDictionary(self.id_map, js)\n        js = self.replaceJsFromDictionary(self.class_map, js)\n        return js\n\n    @staticmethod\n    def getJsSelectors(js, config):\n        \"\"\"finds all js selectors within a js block\n\n        Arguments:\n        js -- contents of js file to search\n\n        Returns:\n        list\n\n        \"\"\"\n        valid_selectors = \"|\".join(config.custom_selectors) + \"|\" + \"|\".join(config.id_selectors) + \"|\" + \"|\".join(config.class_selectors)\n        valid_selectors = valid_selectors.replace('$', '\\$')\n        return re.findall(r'(' + valid_selectors + ')(\\(([^<>]*?)\\))', js, re.DOTALL)\n\n    def replaceJsFromDictionary(self, dictionary, js):\n        \"\"\"replaces any instances of classes and ids based on a dictionary\n\n        Arguments:\n        dictionary -- map of classes or ids to replace\n        js -- contents of javascript to replace\n\n        Returns:\n        string\n\n        \"\"\"\n        for key, value in dictionary.items():\n            blocks = self.getJsSelectors(js, self.config)\n            for block in blocks:\n                if key[0] == \"#\" and block[0] in self.config.class_selectors:\n                    continue\n\n                if key[0] == \".\" and block[0] in self.config.id_selectors:\n                    continue\n\n                old_selector = block[0] + block[1]\n\n                # custom selectors\n                if block[0] in self.config.custom_selectors:\n                    new_selector = old_selector.replace(key + \".\", value + \".\")\n                    new_selector = new_selector.replace(key + \" \", value + \" \")\n                    new_selector = new_selector.replace(key + \"\\\"\", value + \"\\\"\")\n                    new_selector = new_selector.replace(key + \"\\'\", value + \"\\'\")\n                else:\n                    new_selector = old_selector.replace(\"'\" + key[1:] + \"'\", \"'\" + value[1:] + \"'\")\n                    new_selector = new_selector.replace(\"\\\"\" + key[1:] + \"\\\"\", \"\\\"\" + value[1:] + \"\\\"\")\n\n                js = js.replace(old_selector, new_selector)\n\n        return js\n"
  },
  {
    "path": "muncher/sizetracker.py",
    "content": "#!/usr/bin/env python\n# Copyright 2011 Craig Campbell\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport sys, os, gzip\nfrom util import Util\n\nclass SizeTracker(object):\n    original_size = 0\n    original_size_gzip = 0\n    new_size = 0\n    new_size_gzip = 0\n\n    @staticmethod\n    def addSize(path, new = False):\n\n        # gzip the file to get that size\n        gzip_path = path + '.gz'\n        f_in = open(path, 'rb')\n        f_out = gzip.open(gzip_path, 'wb')\n        f_out.writelines(f_in)\n        f_out.close()\n        f_in.close()\n\n        size = os.path.getsize(path)\n        gzip_size = os.path.getsize(gzip_path)\n\n        if new is False:\n            SizeTracker.original_size += size\n            SizeTracker.original_size_gzip += gzip_size\n        else:\n            SizeTracker.new_size += size\n            SizeTracker.new_size_gzip += gzip_size\n\n        Util.unlink(gzip_path)\n\n    @staticmethod\n    def trackFile(path, new_path):\n        SizeTracker.addSize(path)\n        SizeTracker.addSize(new_path, True)\n\n    @staticmethod\n    def getSize(bytes):\n        if bytes < 1024:\n            return str(bytes) + \" bytes\"\n\n        kb = float(bytes) / 1024\n        kb = round(kb, 2)\n        return str(kb) + \" KB\"\n\n    @staticmethod\n    def savings():\n        percent = 100 - (float(SizeTracker.new_size) / float(SizeTracker.original_size)) * 100\n        gzip_percent = 100 - (float(SizeTracker.new_size_gzip) / float(SizeTracker.original_size_gzip)) * 100\n\n        string = \"\\noriginal size:   \" + SizeTracker.getSize(SizeTracker.original_size) + \" (\" + SizeTracker.getSize(SizeTracker.original_size_gzip) + \" gzipped)\"\n        string += \"\\nmunched size:    \" + SizeTracker.getSize(SizeTracker.new_size) + \" (\" + SizeTracker.getSize(SizeTracker.new_size_gzip) + \" gzipped)\"\n        string += \"\\n                 saved \" + str(round(percent, 2)) + \"% off the original size (\" + str(round(gzip_percent, 2)) + \"% off the gzipped size)\\n\"\n        return string\n"
  },
  {
    "path": "muncher/util.py",
    "content": "#!/usr/bin/env python\n# Copyright 2011 Craig Campbell\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport os, shutil, glob\n\nclass Util:\n    \"\"\"collection of various utility functions\"\"\"\n    @staticmethod\n    def fileExists(path):\n        \"\"\"determines if a file exists\n\n        Arguments:\n        path -- path to file on disk\n\n        Returns:\n        bool\n\n        \"\"\"\n        return os.path.isfile(path)\n\n    @staticmethod\n    def isDir(path):\n        \"\"\"determines if a path is a directory\n\n        Arguments:\n        path -- path on disk\n\n        Returns:\n        bool\n\n        \"\"\"\n        return os.path.isdir(path)\n\n    @staticmethod\n    def getFilesFromDir(path, extension = \"\"):\n        path = path + \"/*\"\n\n        if not extension == \"\":\n            path = path + \".\" + extension.lstrip(\".\")\n\n        return glob.glob(path)\n\n    @staticmethod\n    def dump(obj):\n        \"\"\"displays an object as a string for debugging\n\n        Arguments:\n        obj -- object\n\n        Returns:\n        string\n\n        \"\"\"\n        for attr in dir(obj):\n            print \"obj.%s = %s\" % (attr, getattr(obj, attr))\n\n    @staticmethod\n    def getExtension(path):\n        \"\"\"gets the extension from a file\n\n        Arguments:\n        path -- string of the file name\n\n        Returns:\n        string\n\n        \"\"\"\n        return path.split(\".\").pop()\n\n    @staticmethod\n    def prependExtension(ext, path):\n        current_ext = Util.getExtension(path)\n        return path.replace(\".\" + current_ext, \".\" + ext + \".\" + current_ext)\n\n    @staticmethod\n    def getBasePath(path):\n        \"\"\"gets the base directory one level up from the current path\n\n        Arguments:\n        path -- path to file or directory\n\n        Returns:\n        string\n\n        \"\"\"\n        bits = path.split(\"/\")\n        last_bit = bits.pop()\n        return \"/\".join(bits)\n        # return \"/\".join(bits).rstrip(last_bit)\n\n    @staticmethod\n    def getFileName(path):\n        return path.replace(Util.getBasePath(path), \"\").lstrip(\"/\")\n\n    @staticmethod\n    def unlink(path):\n        \"\"\"deletes a file on disk\n\n        Arguments:\n        path -- path to file on disk\n\n        Returns:\n        void\n\n        \"\"\"\n        if Util.fileExists(path):\n            os.unlink(path)\n\n    @staticmethod\n    def unlinkDir(path):\n        \"\"\"removes an entire directory on disk\n\n        Arguments:\n        path -- path to directory to remove\n\n        Returns:\n        void\n\n        \"\"\"\n        try:\n            shutil.rmtree(path)\n        except:\n            pass\n\n    @staticmethod\n    def fileGetContents(path):\n        \"\"\"gets the contents of a file\n\n        Arguments:\n        path -- path to file on disk\n\n        Returns:\n        string\n\n        \"\"\"\n        if not Util.fileExists(path):\n            print \"file does not exist at path \" + path\n            print \"skipping\"\n        file = open(path, \"r\")\n        contents = file.read()\n        file.close()\n        return contents\n\n    @staticmethod\n    def filePutContents(path, contents):\n        \"\"\"puts contents into a file\n\n        Arguments:\n        path -- path to file to write to\n        contents -- contents to put into file\n\n        Returns:\n        void\n\n        \"\"\"\n        file = open(path, \"w\")\n        file.write(contents)\n        file.close()\n\n    @staticmethod\n    def keyInTupleList(key, tuple_list):\n        \"\"\"checks a list of tuples for the given key\"\"\"\n        for tuple in tuple_list:\n            if tuple[0] == key:\n                return True\n        return False\n"
  },
  {
    "path": "muncher/varfactory.py",
    "content": "#!/usr/bin/env python\n# Copyright 2011 Craig Campbell\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport math\n\nclass VarFactory:\n    \"\"\"class to keep multiple counters and turn numeric counters into alphabetical ones\"\"\"\n    types = {}\n    letters = map(chr, range(97, 123))\n\n    @staticmethod\n    def getNext(type):\n        \"\"\"gets the next letter name based on counter name\n\n        Arguments:\n        type -- name of counter we want the next value for\n\n        Returns:\n        string\n\n        \"\"\"\n        i = VarFactory.getVersion(type)\n        return VarFactory.getSmallName(i)\n\n    @staticmethod\n    def getVersion(type):\n        \"\"\"gets the next number in the counter for this type\n\n        Arguments:\n        type -- name of counter we are incrementing\n\n        Resturns:\n        int\n\n        \"\"\"\n        if not type in VarFactory.types:\n            VarFactory.types[type] = 0\n            return 0\n\n        VarFactory.types[type] += 1\n\n        return VarFactory.types[type]\n\n    @staticmethod\n    def getSmallName(index):\n        \"\"\"gets a letter index based on the numeric index\n\n        Arguments:\n        index -- the number you are looking for\n\n        Returns:\n        string\n\n        \"\"\"\n        # total number of combinations for this index size\n        combinations = 0\n        letters = 0\n        while (combinations + (((letters - 1) * 26) - 1) < index):\n            letters += 1\n            combinations = int(math.pow(len(VarFactory.letters), letters))\n\n        if (index > 701):\n            raise Exception(\"until my math skillz get better we can only support 702 possibilities!\")\n\n        a = int(index) + 1\n\n        if a < 27:\n            return chr(a + 96)\n\n        b = 0\n        while a > 26:\n            b += 1\n            a = a - 26\n\n        b = chr(b + 96)\n        a = chr(a + 96)\n        return b + a\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n\nfrom setuptools import setup\n\nsetup(name='htmlmuncher',\n    version='1.0',\n    description='Utility that rewrites CSS, HTML, and JavaScript files in order to save bytes and obfuscate your code.',\n    author='Craig Campbell',\n    author_email='iamcraigcampbell@gmail.com',\n    url='http://htmlmuncher.com',\n    packages=['muncher'],\n    scripts=['munch']\n)\n"
  }
]