Skip to content

Instantly share code, notes, and snippets.

@cowboy
Created February 16, 2012 21:06
Show Gist options
  • Select an option

  • Save cowboy/1847848 to your computer and use it in GitHub Desktop.

Select an option

Save cowboy/1847848 to your computer and use it in GitHub Desktop.
Packify grunt task (in a gist just for now)
/*
* grunt
* https://github.com/cowboy/grunt
*
* Copyright (c) 2012 "Cowboy" Ben Alman
* Licensed under the MIT license.
* http://benalman.com/about/license/
*/
/*global config:true, task:true*/
config.init({
lint: {
all: ['grunt.js']
},
min: {
'dist/project.min.js': ['src/project.js']
},
packify: {
'dist/project.pck.js': ['dist/project.min.js']
},
jshint: {
options: {
curly: true,
eqeqeq: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
sub: true,
undef: true,
boss: true,
eqnull: true,
evil: true
},
globals: {
file: true,
log: true,
option: true
}
}
});
// Default task.
task.registerTask('default', 'lint min packify');
task.registerBasicTask('packify', 'Pack some JavaScript nice and small', function(data, name) {
var files = file.expand(data);
// Concat specified files.
var max = task.helper('concat', files);
// Packify source.
var packified = task.helper('packify', max);
// Write packified source.
file.write(name, packified);
// Fail task if errors were logged.
if (task.hadErrors()) { return false; }
// Otherwise, print a success message....
log.writeln('File "' + name + '" created.');
// ...and report some size information.
task.helper('packify_info', packified, max);
});
task.registerHelper('packify', function(src) {
// Single quotes need to be escaped, so use double-quotes in your input
// source whenever possible.
var script = src.replace(/'/g, "\\'");
// Replace any non-space whitespace with spaces (shouldn't be necessary).
script = script.replace(/\s+/g, ' ');
// Return number of chars saved by replacing `count` occurences of `string`.
function getSavings(string, count) {
return (string.length - 1) * (count - 1) - 2;
}
// Just trying to keep things DRY here... Let's match some patterns!
function getReMatch(pattern, text) {
var re = new RegExp(pattern.replace(/(\W)/g, '\\$1'), 'g');
return [text.match(re) || [], re];
}
var potentials = {};
var potentialsList = [];
var map = '';
var i, chunk, matches, savings, re, potential, char;
// Look for recurring patterns between 2 and 20 characters in length (could
// have been between 2 and len / 2, but that gets REALLY slow).
for (var chunkSize = 2, len = script.length; chunkSize <= 20; chunkSize++) {
// Start at the beginning of the input string, go to the end.
for (i = 0; i < len - chunkSize; i++) {
// Grab the "chunk" at the current position.
chunk = script.substr(i, chunkSize);
if (!potentials[chunk]) {
// Find the number of chunk matches in the input script.
matches = getReMatch(chunk, script)[0];
// If any matches, save this chunk as a potential pattern. By using an
// object instead of an array, we don't have to worry about uniquing
// the array as new potentials will just overwrite previous potentials.
if (getSavings(chunk, matches.length) >= 0) {
potentials[chunk] = matches.length;
}
}
}
}
// Since we'll need to sort the potentials, create an array from the object.
for (i in potentials) {
if (potentials.hasOwnProperty(i)) {
potentialsList.push({pattern: i, count: potentials[i]});
}
}
// Potentials get sorted first by byte savings, then by # of occurrences
// (favoring smaller count, longer patterns), then lexicographically.
function sortPotentials(a, b) {
return getSavings(b.pattern, b.count) - getSavings(a.pattern, a.count) ||
a.count - b.count ||
(a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0);
}
// Loop over all the potential patterns, unless we run out of replacement
// chars first. Dealing with 7-bit ASCII, valid replacement chars are 1-31
// & 127 (excluding ASCII 10 & 13).
for (var charCode = 0; potentialsList.length && charCode < 127; ) {
// Re-calculate match counts.
for (i = 0, len = potentialsList.length; i < len; i++) {
potential = potentialsList[i];
matches = getReMatch(potential.pattern, script)[0];
potential.count = matches.length;
}
// Sort the array of potentials such that replacements that will yield the
// highest byte savings come first.
potentialsList.sort(sortPotentials);
// Get the current best potential replacement.
potential = potentialsList.shift();
// Find all chunk matches in the input string.
chunk = potential.pattern;
matches = getReMatch( chunk, script );
re = matches[1];
matches = matches[0];
// Ensure that replacing this potential pattern still actually saves bytes.
savings = getSavings(chunk, matches.length);
if (savings >= 0) {
// Increment the current replacement character.
charCode = ++charCode === 10 ? 11 :
charCode === 13 ? 14 :
charCode === 32 ? 127 :
charCode;
// Get the replacement char.
char = String.fromCharCode(charCode);
if (option('debug')) {
log.debug(charCode, char, matches.length, chunk, savings);
}
// Replace the pattern with the replacement character.
script = script.replace(re, char);
// Add the char + pattern combo into the map of replacements.
map += char + chunk;
}
}
// For each group of 1 low ASCII char / 1+ regular ASCII chars combo in the
// map string, replace the low ASCII char in the script string with the
// remaining regular ASCII chars, then eval the script string. Using with in
// this manner ensures that the temporary _ var won't be leaked.
var result = "" +
"with({_:'" + script + "'})" +
"'" + map + "'.replace(/.([ -~]+)/g,function(x,y){" +
"_=_.replace(RegExp(x[0],'g'),y)" +
"})," +
"eval(_)";
// Return the result if successful, otherwise false on failure.
return eval(result.replace('eval(_)', '_')) === src || option('debug') ?
result : false;
});
// Output some size info about the packified source.
task.registerHelper('packify_info', function(packified, max) {
var color = packified.length < max.length ? 'green' : 'red';
log.writeln('Uncompressed size: ' + String(max.length)[color] + ' bytes.');
log.writeln('Packified size: ' + String(packified.length)[color] + ' bytes.');
});
@cowboy
Copy link
Copy Markdown
Author

cowboy commented Feb 16, 2012

Also, FWIW, I might put this in grunt. If I don't, I'll make it readily available to people. Either way, have fun!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment