├── .gitignore ├── LICENSE ├── README.md ├── bin └── munch ├── munch.js ├── package.json └── usage /.gitignore: -------------------------------------------------------------------------------- 1 | .muncher 2 | node_modules 3 | test-assets 4 | npm-debug.log 5 | map.json 6 | parsers -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 John Rocela 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Munch.js 2 | 3 | Munch.js is a utility that rewrites classes and ids in CSS, HTML, and JavaScript files in order to save precious bytes and obfuscate your code. based on [Craig Campbell](http://www.craigiam.com/)'s [HTML-Muncher](http://htmlmuncher.com). 4 | 5 | Install by using `npm install -g munch` 6 | 7 | This module uses [Hashids](https://npmjs.org/package/hashids) to generate the class and ID names. Just to look more googly. 8 | 9 | **Please take note that I am not confident that this library is perfect for every kind of project out there. please use sparingly.** 10 | 11 | **This library was made solely for the deployment process, and is done at the first steps before minifying the assets** 12 | 13 | ## USAGE 14 | This is a command line utility so it is preferred that you install this globally using the `-g` flag. 15 | 16 | You can run `munch` using the format below 17 | ``` 18 | munch --css file1.css,/path/to/css1,file2.css,file3.css --view /path/to/views1,file1.html,/path/to/views2/,file3.html --js main.js,/path/to/js 19 | ``` 20 | 21 | ### REQUIRED ARGUMENTS: 22 | 23 | #### --view {path/to/views} 24 | html files to rewrite (comma separated list of directories and files) 25 | 26 | ### OPTIONAL ARGUMENTS: 27 | 28 | #### --css {path/to/css} 29 | css files to rewrite (comma separated list of directories and files) 30 | 31 | #### --js {path/to/js} 32 | js files to rewrite (comma separated list of directories and files) 33 | 34 | #### --manifest 35 | a file where all options available for muncher are stored. this can be any json files or a .muncher file by default. useful for deployment purposes. 36 | 37 | #### --view-ext {extension} 38 | sets the extension to look for in the view directory (defaults to html) 39 | 40 | #### --css-ext 41 | sets the extension to look for in the css directory (defaults to css) 42 | 43 | #### --js-ext 44 | sets the extension to look for in the js directory (defaults to js) 45 | 46 | #### --ignore {classes,ids} 47 | comma separated list of classes or ids to ignore when rewriting css (ie .sick_class, #sweet_id) 48 | 49 | #### --compress-view 50 | strips new line characters to compress html files specified with --html 51 | 52 | #### --compress-css 53 | strips new line characters to compress css files specified with --css 54 | 55 | #### --map 56 | writes all mapped ids and classes into a file specified 57 | 58 | #### --read 59 | uses all the mapped ids and classes in a specified file to be used as a dictionary for the rewrite process 60 | 61 | #### --parsers 62 | modules for parsing and writing (comma separated list of module names) check out https://github.com/jmrocela/munch-jquery for an example. 63 | 64 | #### --show-savings 65 | will output how many bytes were saved by munching 66 | 67 | #### --silent 68 | do not output any information about the process (not recommended) -------------------------------------------------------------------------------- /bin/munch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var cwd = path.join(path.dirname(fs.realpathSync(__filename)), '../'); 6 | 7 | require(cwd + 'munch.js').run(); 8 | -------------------------------------------------------------------------------- /munch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MUNCH.js 3 | * http://jmrocela.github.com/munchjs 4 | * 5 | * munch.js is a utility that rewrites classes and ids in CSS, HTML, and JavaScript files 6 | * in order to save precious bytes and obfuscate your code. 7 | * 8 | * Copyright (c) 2013 John Rocela 9 | * Licensed under the MIT license. 10 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 11 | */ 12 | 13 | 'use strict'; 14 | 15 | var path = require('path'), 16 | glob = require('glob'), 17 | fs = require('fs'), 18 | jsdom = require('jsdom').jsdom, 19 | $ = require('jquery'), 20 | clc = require('cli-color'), 21 | parse = require('css-parse'), 22 | Hashids = require('hashids'), 23 | hashids = new Hashids("use the force harry"); 24 | 25 | /** 26 | * MUNCHER!!!! 27 | * 28 | * @param args Object an optimist.args object. 29 | */ 30 | var Muncher = function(args) { 31 | // tokens from files within views, css and js together 32 | this.map = { 33 | "id": {}, 34 | "class": {} 35 | }; 36 | 37 | // files, keep track of sizes 38 | this.files = { } 39 | 40 | // token counter 41 | this.mapCounter = 0; 42 | 43 | // ignore classes 44 | this.ignoreClasses = [ ]; 45 | this.ignoreIds = [ ]; 46 | 47 | // custom parser collection 48 | this.parsers = { 49 | "js": [] 50 | } 51 | 52 | this.writers = { 53 | "js": [] 54 | } 55 | 56 | // pass to the init function 57 | if (args) { 58 | this.init(args); 59 | } 60 | } 61 | 62 | Muncher.prototype.init = function(args) { 63 | 64 | // a reference to `this` 65 | var that = this; 66 | 67 | // set the ignore maps for Ids and Classes 68 | this.ignore = args['ignore'] || ''; 69 | this.ignore.split(',').forEach(function(ign) { 70 | ign = ign.replace(/\s/,''); 71 | if (ign.indexOf('.') === 0) that.ignoreClasses.push(ign.replace('.', '')); 72 | if (ign.indexOf('#') === 0) that.ignoreIds.push(ign.replace('#', '')); 73 | }); 74 | 75 | // set the default view extension 76 | this.extensions = { 77 | "view": args['view-ext'] || '.html', 78 | "css": args['css-ext'] || '.css', 79 | "js": args['js-ext'] || '.js' 80 | } 81 | 82 | // you may want to use another way for compressing CSS and JS 83 | this.compress = { 84 | "view": (args['compress-view'] === true), 85 | "css": (args['compress-css'] === true), 86 | "js": (args['compress-js'] === true) 87 | } 88 | 89 | // if we want to nag the CLI 90 | this.silent = (args['silent'] === true); 91 | this.showSavings = (args['show-savings'] === true); 92 | 93 | // flag if we only want to map the Ids and Classes for later use 94 | this.mapFile = args['map']; 95 | 96 | this.readFile = args['read']; 97 | 98 | // paths given from the configuration 99 | this.paths = { 100 | "view": args['view'], 101 | "css": args['css'], 102 | "js": args['js'] 103 | } 104 | 105 | // chainable 106 | return this; 107 | } 108 | 109 | /** 110 | * run 111 | * 112 | * sets various options to run the Muncher 113 | */ 114 | Muncher.prototype.run = function() { 115 | 116 | if (!this.readFile) { 117 | // run through the HTML files 118 | if (this.paths["view"]) this.parseDir(this.paths["view"], "view"); 119 | if (this.paths['css']) this.parseDir(this.paths["css"], "css"); 120 | if (this.paths['js']) this.parseDir(this.paths["js"], "js"); 121 | } else { 122 | this.read(this.readFile); 123 | } 124 | 125 | // map feature 126 | if (typeof this.mapFile === 'string' && !this.readFile) { 127 | 128 | if (this.mapFile) { 129 | var map = { id: [], class: [] }; 130 | 131 | for (var key in this.map["id"]) { 132 | map["id"].push(key); 133 | } 134 | 135 | for (var key in this.map["class"]) { 136 | map["class"].push(key); 137 | } 138 | 139 | fs.writeFileSync(this.mapFile, JSON.stringify(map, null, '\t')); 140 | } 141 | 142 | this.echo('-------------------------------'); 143 | this.echo(clc.bold('Wrote ' + this.mapCounter + ' ids and classes in ' + this.mapFile)); 144 | this.echo('-------------------------------\n'); 145 | return; 146 | } else { 147 | this.echo('-------------------------------'); 148 | this.echo(clc.bold('Mapped ' + this.mapCounter + ' IDs and Classes')); 149 | this.echo('-------------------------------\n'); 150 | } 151 | 152 | // we do it again so this we are sure we have everything we need 153 | if (this.paths["view"]) this.buildDir(this.paths["view"], "view"); 154 | if (this.paths['css']) this.buildDir(this.paths["css"], "css"); 155 | if (this.paths['js']) this.buildDir(this.paths["js"], "js"); 156 | 157 | } 158 | 159 | /** 160 | * read 161 | * 162 | * use a map file to serve as the dictionary 163 | * 164 | * @param read String the path to the mapfile 165 | */ 166 | Muncher.prototype.read = function(read) { 167 | var manifest = JSON.parse(fs.readFileSync(read, 'utf-8').toString()), 168 | that = this; 169 | 170 | $.each(manifest["id"], function(i, id) { 171 | that.addId(id); 172 | }); 173 | 174 | $.each(manifest["class"], function(i, cls) { 175 | that.addClass(cls); 176 | }); 177 | 178 | } 179 | 180 | /** 181 | * parseDir 182 | * 183 | * parse directories according to context 184 | * 185 | * @param path String the path of the directory 186 | * @param context String either view, js or css 187 | */ 188 | Muncher.prototype.parseDir = function(path, context) { 189 | var that = this; 190 | 191 | that.echo(clc.bold('Processing ' + context)); 192 | 193 | that.paths[context].split(',').forEach(function(path) { 194 | if (fs.statSync(path).isDirectory()) { 195 | var files = glob.sync(path.replace(/\/$/, '') + '/**/*' + that.extensions[context]); 196 | 197 | files.forEach(function(file) { 198 | that.parse(file, context); 199 | }); 200 | 201 | } else if (fs.statSync(path).isFile()) { 202 | that.parse(path, context); 203 | } 204 | }); 205 | 206 | that.echo(clc.green.bold('Finished!\n')); 207 | 208 | } 209 | 210 | /** 211 | * buildDir 212 | * 213 | * build directories according to context 214 | * 215 | * @param path String the path of the directory 216 | * @param context String either view, js or css 217 | */ 218 | Muncher.prototype.buildDir = function(path, context) { 219 | var that = this; 220 | 221 | that.echo(clc.bold('Rewriting ' + context)); 222 | 223 | that.paths[context].split(',').forEach(function(path) { 224 | if (fs.lstatSync(path).isDirectory()) { 225 | var files = glob.sync(path.replace(/\/$/, '') + '/**/*' + that.extensions[context]); 226 | 227 | files.forEach(function(file) { 228 | that.build(file, context); 229 | }); 230 | 231 | } else if (fs.lstatSync(path).isFile()) { 232 | that.build(path, context); 233 | } 234 | }); 235 | 236 | that.echo(clc.green.bold('Finished!\n')); 237 | } 238 | 239 | /** 240 | * echo 241 | * 242 | * wrap the console.log method 243 | * 244 | * @param message String 245 | */ 246 | Muncher.prototype.echo = function(message) { 247 | if (!this.silent) console.log(message); 248 | } 249 | 250 | /** 251 | * parse 252 | * 253 | * parse files according to context 254 | * 255 | * @param file String the file name of the document 256 | * @param context String either view, js or css 257 | */ 258 | Muncher.prototype.parse = function(file, context) { 259 | 260 | if (fs.existsSync(file)) { 261 | this.echo(file); 262 | 263 | var content = fs.readFileSync(file, 'utf8').toString(); 264 | 265 | switch (context) { 266 | case "view": 267 | this.parseHtml(content); 268 | break; 269 | case "css": 270 | this.parseCss(content); 271 | break; 272 | case "js": 273 | this.parseJs(content); 274 | break; 275 | } 276 | 277 | } else { 278 | this.echo(clc.red(file + ' doesn\'t exist')); 279 | } 280 | 281 | } 282 | 283 | /** 284 | * build 285 | * 286 | * build files according to context 287 | * 288 | * @param file String the file name of the document 289 | * @param context String either view, js or css 290 | */ 291 | Muncher.prototype.build = function(file, context) { 292 | 293 | var content = fs.readFileSync(file, 'utf8').toString(); 294 | 295 | switch (context) { 296 | case "view": 297 | this.rewriteHtml(content, file); 298 | break; 299 | case "css": 300 | this.rewriteCss(content, file); 301 | break; 302 | case "js": 303 | this.rewriteJs(content, file); 304 | break; 305 | } 306 | 307 | } 308 | 309 | /** 310 | * addCss 311 | * 312 | * adds Classes to the CLASS map 313 | * 314 | * @param cl String 315 | */ 316 | Muncher.prototype.addClass = function(cl) { 317 | var that = this; 318 | 319 | var addClass = function(cls) { 320 | if (that.ignoreClasses.indexOf(cls) > -1) return true; // shoul be a list of no-nos 321 | if (!that.map["class"][cls]) { 322 | that.map["class"][cls] = hashids.encrypt(that.mapCounter); 323 | that.mapCounter++; 324 | } 325 | } 326 | 327 | if (typeof cl == 'object'){ 328 | if (cl) { 329 | cl.forEach(function(pass) { 330 | addClass(pass); 331 | }); 332 | } 333 | } else { 334 | addClass(cl); 335 | } 336 | } 337 | 338 | /** 339 | * addId 340 | * 341 | * adds Ids to the ID map 342 | * 343 | * @param id String 344 | */ 345 | Muncher.prototype.addId = function(id) { 346 | if (!this.map["id"][id]) { 347 | if (!this.ignoreIds.indexOf(id)) return true; // shoul be a list of no-nos 348 | this.map["id"][id] = hashids.encrypt(this.mapCounter); 349 | this.mapCounter++; 350 | } 351 | } 352 | 353 | /** 354 | * parseCssSelector 355 | * 356 | * parse CSS strings to get their classes and ids 357 | * 358 | * @param css String the css string 359 | */ 360 | Muncher.prototype.parseCssSelector = function(selector) { 361 | var that = this, 362 | match = null, 363 | tid = selector.match(/#[\w\-]+/gi), 364 | tcl = selector.match(/\.[\w\-]+/gi); 365 | 366 | if (tid) { 367 | tid.forEach(function(match) { 368 | var id = match.replace('#', ''); 369 | that.addId(id); 370 | }); 371 | } 372 | if (tcl) { 373 | tcl.forEach(function(match) { 374 | var cl = match.replace('.', ''); 375 | that.addClass(cl); 376 | }); 377 | } 378 | } 379 | 380 | /** 381 | * parseHtml 382 | * 383 | * parse HTML documents to get their classes and ids 384 | * 385 | * @param html String the html document 386 | */ 387 | Muncher.prototype.parseHtml = function(html) { 388 | var that = this, 389 | html = $(html); 390 | 391 | html.find('*').each(function(i, elem) { 392 | var target = html.find(elem), 393 | id = target.attr('id'), 394 | classes = target.attr('class'); 395 | 396 | if (id) { 397 | that.addId(id); 398 | } 399 | 400 | if (classes) { 401 | var newClass = []; 402 | 403 | classes.split(' ').forEach(function(cl) { 404 | that.addClass(cl); 405 | }); 406 | 407 | } 408 | 409 | if (target.is('style')) { 410 | var style = target.text(); 411 | that.parseCss(style); 412 | } 413 | 414 | }); 415 | 416 | // parse JS 417 | var script = ''; 418 | html.filter('script').each(function(i, tag) { 419 | script += this.text || this.textContent || this.innerHTML || ''; 420 | }); 421 | if (script != '') { 422 | that.parseJs(script); 423 | } 424 | 425 | } 426 | 427 | /** 428 | * parseCss 429 | * 430 | * parse CSS documents to get their classes and ids 431 | * 432 | * @param css String the css document 433 | */ 434 | Muncher.prototype.parseCss = function(css) { 435 | var that = this, 436 | css = parse(css), 437 | styles = []; 438 | 439 | $.each(css.stylesheet.rules, function(i, style) { 440 | if (style.media) { 441 | styles = styles.concat(style.rules); 442 | } 443 | 444 | if (!style.selectors) return true; 445 | 446 | styles.push(css.stylesheet.rules[i]); 447 | }); 448 | 449 | $.each(styles, function(o, style) { 450 | style.selectors.forEach(function(selector) { 451 | that.parseCssSelector(selector); 452 | }); 453 | }); 454 | } 455 | 456 | /** 457 | * parseJs 458 | * 459 | * parse JS documents to get their classes and ids 460 | * 461 | * @param js String the js document 462 | */ 463 | Muncher.prototype.parseJs = function(js) { 464 | var that = this, 465 | match; 466 | 467 | // custom parsers 468 | if (this.parsers.js.length > 0) { 469 | this.parsers.js.forEach(function(cb) { 470 | cb.call(that, js); 471 | }); 472 | } 473 | 474 | // id and class 475 | var pass4 = /getElementsByClassName\([\'"](.*?)[\'"]/gi; 476 | while ((match = pass4.exec(js)) !== null) { 477 | this.addClass(match[1].split(' ')); 478 | } 479 | 480 | var pass5 = /getElementById\([\'"](.*?)[\'"]/gi; 481 | while ((match = pass5.exec(js)) !== null) { 482 | this.addId(match[1]); 483 | } 484 | 485 | // attr 486 | var pass7 = /setAttribute\([\'"](id|class)[\'"],\s[\'"](.+?)[\'"]/gi; 487 | while ((match = pass7.exec(js)) !== null) { 488 | if (match[1] == 'class') this.addClass(match[2].split(' ')); 489 | if (match[1] == 'id') this.addId(match[2]); 490 | } 491 | 492 | } 493 | 494 | /** 495 | * rewriteHtml 496 | * 497 | * replaces the ids and classes in the files specified 498 | * 499 | * @param html String the html document 500 | * @param to String the file name 501 | */ 502 | Muncher.prototype.rewriteHtml = function(html, to) { 503 | var that = this, 504 | document = jsdom(html), 505 | html = $(document); 506 | 507 | that.files[to] = fs.statSync(to).size; 508 | 509 | html.find('*').each(function(i, elem) { 510 | var target = html.find(elem), 511 | id = target.attr('id'), 512 | classes = target.attr('class'); 513 | 514 | if (id) { 515 | if (!that.ignoreIds.indexOf(id)) return true; 516 | target.attr('id', that.map["id"][id]); 517 | } 518 | 519 | if (classes) { 520 | var newClass = []; 521 | classes.split(' ').forEach(function(cl) { 522 | if (!that.ignoreClasses.indexOf(cl)) return true; 523 | if (that.map["class"][cl]) { 524 | target.removeClass(cl).addClass(that.map["class"][cl]); 525 | } 526 | }); 527 | } 528 | 529 | }); 530 | 531 | // write 532 | html = document.innerHTML; 533 | html = this.rewriteJsBlock(html); 534 | html = this.rewriteCssBlock(html, this.compress['view']); 535 | 536 | fs.writeFileSync(to + '.munched', (this.compress['view']) ? this.compressHtml(html): html); 537 | 538 | var percent = 100 - ((fs.statSync(to + '.munched').size / this.files[to]) * 100); 539 | var savings = (that.showSavings) ? clc.blue.bold(percent.toFixed(2) + '%') + ' Saved for ': ''; 540 | that.echo(savings + to + '.munched'); 541 | } 542 | 543 | /** 544 | * rewriteCssString 545 | * 546 | * rewrite a CSS String 547 | * 548 | * @param css String the css document 549 | */ 550 | Muncher.prototype.rewriteCssString = function(css) { 551 | var that = this, 552 | text = css, 553 | styles = [], 554 | css = parse(text); 555 | 556 | $.each(css.stylesheet.rules, function(i, style) { 557 | if (style.media) { 558 | styles = styles.concat(style.rules); 559 | } 560 | 561 | if (!style.selectors) return true; 562 | 563 | styles.push(css.stylesheet.rules[i]); 564 | }); 565 | 566 | $.each(styles, function(u, style) { 567 | style.selectors.forEach(function(selector) { 568 | var original = selector, 569 | tid = selector.match(/#[\w\-]+/gi), 570 | tcl = selector.match(/\.[\w\-]+/gi); 571 | 572 | if (tid) { 573 | $.each(tid, function(i, match) { 574 | match = match.replace('#', ''); 575 | if (that.ignoreIds.indexOf(match) > -1) return true; 576 | selector = selector.replace(new RegExp("#" + match.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), "gi"), '#' + that.map["id"][match]); 577 | }); 578 | } 579 | if (tcl) { 580 | $.each(tcl, function(o, match) { 581 | match = match.replace('.', ''); 582 | if (that.ignoreClasses.indexOf(match) > -1) return true; 583 | selector = selector.replace(new RegExp("\\." + match.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), "gi"), '.' + that.map["class"][match]); 584 | }); 585 | } 586 | 587 | text = text.replace(original, selector); 588 | }); 589 | }); 590 | 591 | return text; 592 | } 593 | 594 | /** 595 | * rewriteCssBlock 596 | * 597 | * rewrite a CSS block in an html document 598 | * 599 | * @param html String the html document 600 | * @param compress Boolean flag whether to compress the CSS block 601 | */ 602 | Muncher.prototype.rewriteCssBlock = function(html, compress) { 603 | var that = this, 604 | document = jsdom(html), 605 | html = $(document); 606 | 607 | html.find('*').each(function(i, elem) { 608 | var target = html.find(elem); 609 | 610 | if (target.is('style')) { 611 | var text = that.rewriteCssString(target.text()); 612 | target.text(compress ? that.compressCss(text): text); 613 | } 614 | 615 | }); 616 | 617 | return document.innerHTML; 618 | } 619 | 620 | /** 621 | * rewriteCss 622 | * 623 | * rewrite a CSS file 624 | * 625 | * @param css String the css document 626 | * @param to String the file name 627 | */ 628 | Muncher.prototype.rewriteCss = function(css, to) { 629 | var that = this, 630 | text = that.rewriteCssString(css); 631 | 632 | that.files[to] = fs.statSync(to).size; 633 | 634 | fs.writeFileSync(to + '.munched', (this.compress['css']) ? this.compressCss(text): text); 635 | 636 | var percent = 100 - ((fs.statSync(to + '.munched').size / this.files[to]) * 100); 637 | var savings = (that.showSavings) ? clc.blue.bold(percent.toFixed(2) + '%') + ' Saved for ': ''; 638 | that.echo(savings + to + '.munched'); 639 | } 640 | 641 | /** 642 | * rewriteJsString 643 | * 644 | * rewrite a JS String 645 | * 646 | * @param js String the js document 647 | */ 648 | Muncher.prototype.rewriteJsString = function(js) { 649 | var that = this, 650 | match = null; 651 | 652 | // id and class 653 | var pass4 = /getElementsByClassName\([\'"](.*?)[\'"]/gi; 654 | while ((match = pass4.exec(js)) !== null) { 655 | if (that.ignoreClasses.indexOf(match[1]) > -1) continue; 656 | var passed = match[0].replace(new RegExp(match[1], "gi"), that.map["class"][match[1]]); 657 | js = js.replace(match[0], passed); 658 | } 659 | 660 | var pass5 = /getElementById\([\'"](.*?)[\'"]/gi; 661 | while ((match = pass5.exec(js)) !== null) { 662 | if (that.ignoreIds.indexOf(match[1]) > -1) continue; 663 | var passed = match[0].replace(new RegExp(match[1], "gi"), that.map["id"][match[1]]); 664 | js = js.replace(match[0], passed); 665 | } 666 | 667 | // attr 668 | var pass7 = /setAttribute\([\'"](id|class)[\'"],\s[\'"](.+?)[\'"]/gi; 669 | while ((match = pass7.exec(js)) !== null) { 670 | var key = (match[1] == 'id') ? 'id': 'class'; 671 | if (key == 'class') { 672 | var passed = match[0], 673 | splitd = match[2].split(' '); 674 | $.each(splitd, function(i, cls) { 675 | if (that.ignoreClasses.indexOf(cls) > -1) return true; 676 | passed = passed.replace(new RegExp(cls, "gi"), that.map[key][cls]); 677 | }); 678 | } else { 679 | if (that.ignoreIds.indexOf(match[2]) > -1) continue; 680 | var passed = match[0].replace(new RegExp(match[2], "gi"), that.map[key][match[2]]); 681 | } 682 | js = js.replace(match[0], passed); 683 | } 684 | 685 | // custom parsers 686 | if (this.writers.js.length > 0) { 687 | this.writers.js.forEach(function(cb) { 688 | js = cb.call(that, js); 689 | }); 690 | } 691 | 692 | return js; 693 | } 694 | 695 | /** 696 | * rewriteJsBlock 697 | * 698 | * rewrite a JS block in an html document 699 | * 700 | * @param html String the html document 701 | * @param compress Boolean flag whether to compress the JS block 702 | */ 703 | Muncher.prototype.rewriteJsBlock = function(html, compress) { 704 | var that = this, 705 | document = jsdom(html), 706 | html = $(document); 707 | 708 | var match; 709 | 710 | html.find('script').each(function(i, elem) { 711 | var target = html.find(elem), 712 | js = that.rewriteJsString(target.text()); 713 | 714 | target.text(js); 715 | }); 716 | 717 | var block = (compress) ? this.compressJs(document.innerHTML): document.innerHTML; 718 | 719 | return block; 720 | } 721 | 722 | /** 723 | * rewriteJs 724 | * 725 | * rewrite a JS file 726 | * 727 | * @param js String the js document 728 | * @param to String the file name 729 | */ 730 | Muncher.prototype.rewriteJs = function(js, to) { 731 | var that = this; 732 | 733 | that.files[to] = fs.statSync(to).size; 734 | 735 | js = that.rewriteJsString(js); 736 | 737 | fs.writeFileSync(to + '.munched', (this.compress['js']) ? this.compressJs(js): js); 738 | 739 | var percent = 100 - ((fs.statSync(to + '.munched').size / this.files[to]) * 100); 740 | var savings = (that.showSavings) ? clc.blue.bold(percent.toFixed(2) + '%') + ' Saved for ': ''; 741 | that.echo(savings + to + '.munched'); 742 | } 743 | 744 | /** 745 | * compressHtml 746 | * 747 | * Compress HTML Files to save a couple of bytes. Someone tell me where I got this. I need to 748 | * credit them. I forgot :( 749 | * 750 | * @param html String The HTML string to be minified 751 | * @param compressHead Boolean Option whether the
tag should be compressed as well 752 | */ 753 | Muncher.prototype.compressHtml = function(html, compressHead){ 754 | var allHTML = html, 755 | headHTML = '', 756 | removeThis = '', 757 | headstatus = compressHead || true; 758 | 759 | if (headstatus != true) { 760 | //Compress all the things! 761 | allHTML = allHTML.replace(/(\r\n|\n|\r|\t)/gm, ''); 762 | allHTML = allHTML.replace(/\s+/g, ' '); 763 | } else { 764 | //Don't compress the head 765 | allHTML = allHTML.replace(new RegExp(''; 769 | var i = allHTML.indexOf(bodySplit) != -1; 770 | 771 | if (i == true) { 772 | var bodySplit = ''; 773 | var tempo = allHTML.split(new RegExp(bodySplit, 'i')); 774 | headHTML = tempo[0]; 775 | allHTML = tempo[1]; 776 | } else { 777 | bodySplit = ''; 778 | } 779 | 780 | allHTML = allHTML.replace(/(\r\n|\n|\r|\t)/gm, ''); 781 | allHTML = allHTML.replace(/\s+/g, ' '); 782 | allHTML = headHTML + bodySplit + '\n' + allHTML.replace(//gm, ''); 783 | } 784 | 785 | return allHTML; 786 | } 787 | 788 | /** 789 | * compressCss 790 | * 791 | * A simple CSS minifier. removes all newlines, tabs and spaces. also strips out comments. 792 | * 793 | * @param css String The CSS string to be minified 794 | */ 795 | Muncher.prototype.compressCss = function(css) { 796 | css = css.replace(/(\r\n|\n|\r|\t)/gm, ""); 797 | css = css.replace(/\s+/g, " "); 798 | return css.replace(/\/\*(.*?)\*\//gm, ""); 799 | } 800 | 801 | /** 802 | * compressJs 803 | * 804 | * A placeholder for future use perhaps? 805 | * 806 | * @param js String The JS string to be minified 807 | */ 808 | Muncher.prototype.compressJs = function(js) { 809 | return js; 810 | } 811 | 812 | /** 813 | * addJsParser 814 | * 815 | * plug different JS parsers here. Parsers are loaded dynamically from the `parsers` folder 816 | * 817 | * @param cb Function the callback for parsing JS Strings 818 | */ 819 | Muncher.prototype.addJsParser = function(cb) { 820 | if (typeof cb == 'function') { 821 | this.parsers.js.push(cb); 822 | } 823 | } 824 | 825 | /** 826 | * addJsWriter 827 | * 828 | * plug different JS writers here. Writers are loaded dynamically from the `parsers` folder 829 | * 830 | * @param cb Function the callback for writing JS Strings 831 | */ 832 | Muncher.prototype.addJsWriter = function(cb) { 833 | if (typeof cb == 'function') { 834 | this.writers.js.push(cb); 835 | } 836 | } 837 | 838 | /** 839 | * module_exists 840 | * 841 | * Check if a module exists 842 | * 843 | * @param module_exists String the module name 844 | */ 845 | function module_exists(name) { 846 | try { 847 | return require.resolve(name); 848 | } catch(e) { 849 | return false 850 | } 851 | } 852 | 853 | /** 854 | * let's not forget to expose this 855 | */ 856 | exports.run = function() { 857 | // fetch the script options from CLI 858 | var args = require('optimist') 859 | .usage(fs.readFileSync('./usage').toString()) 860 | .demand(['view']) 861 | .argv; 862 | 863 | // we have a settings file specifically specified or args is empty 864 | if (args['manifest']) { 865 | args['manifest'] = (typeof args['manifest'] == 'string') ? args['manifest']: '.muncher'; 866 | 867 | // see if the file exists and get it 868 | if (fs.existsSync(args['manifest'])) { 869 | args = JSON.parse(fs.readFileSync(args['manifest'])); 870 | 871 | // normalize everything 872 | if (typeof args.view == 'object') args.view = args.view.join(','); 873 | if (typeof args.css == 'object') args.css = args.css.join(','); 874 | if (typeof args.js == 'object') args.js = args.js.join(','); 875 | if (typeof args.ignore == 'object') args.ignore = args.ignore.join(','); 876 | if (typeof args.parsers == 'object') args.parsers = args.parsers.join(','); 877 | } 878 | } 879 | 880 | // check if args is usable. 881 | if (!args) { 882 | console.log('There are no options specified. Aborting'); 883 | return; 884 | } else if (!args['silent']) { 885 | // echo a pretty name if we are allowed to 886 | console.log(clc.red('\n __ ___ __ _ ')); 887 | console.log(clc.red(' / |/ /_ _____ ____/ / (_)__')); 888 | console.log(clc.red(' / /|_/ / // / _ \\/ __/ _ \\ / (_-<')); 889 | console.log(clc.red('/_/ /_/\\_,_/_//_/\\__/_//_/_/ /___/')); 890 | console.log(clc.red(' |___/ \n')); 891 | console.log(clc.white('Copyright (c) 2013 John Rocela