Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added alt-function to support RetinaJS libraries. #21

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"boss": true,
"eqnull": true,
"node": true,
"es5": true
"es5": true,
"predef": [
"RegExp"
]
}
23 changes: 22 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = function(grunt) {
test: {
flatten: true,
expand: true,
src: ['test/fixtures/*.txt'],
src: ['test/fixtures/*.{txt,png}'],
dest: 'tmp/',
},
},
Expand All @@ -56,6 +56,27 @@ module.exports = function(grunt) {
},
src: ['tmp/international.txt']
},
alt_options: {
options: {
alternatesPattern: /[@_]\dx/
},
src: ['tmp/Open*.png']
},
alt_options_default: {
src: ['tmp/Default*.png']
},
alt_options_off: {
options: {
alternatesPattern: null
},
src: ['tmp/Off*.png']
},
alt_options_array: {
options: {
alternatesPattern: [ /-(hover|active)/, /[@_]\dx/ ]
},
src: ['tmp/Array*.png']
}
},

// Unit tests.
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
# grunt-rev [![Build Status](https://travis-ci.org/cbas/grunt-rev.png)](https://travis-ci.org/cbas/grunt-rev)
# grunt-rev [![Build Status](https://travis-ci.org/TinyCarrier/grunt-rev.png)](https://travis-ci.org/cbas/grunt-rev)

> Static file asset revisioning through content hashing

## About this fork

Added functionality to use the same hash for alternate versions of an image, so that you will be able to use
frameworks like RetinaJS to support hi-res images for retina displays.

We considered forking RetinaJS instead, however we didn't find a good solution for handling images included in CSS.

Here is an example usage for the alt-feature:

...
rev: {
options: {
alternatesPattern: /[@_]\dx/
},
src: ['tmp/*.png']
},
...

Any image files identified by the regexp `[@_]\dx` include the file without this particular pattern, will get the same hash prefix. For example:

Open-Source-Logo.png <--- the 'original' image
Open-Source-Logo@2x.png <--- the alternate version

... becomes:

3d0793d5.Open-Source-Logo.png
3d0793d5.Open-Source-Logo@2x.png

... despite they are different images with different hashes. The hash is calculated from the content of all images.

You can specify `alternatesPattern` as a `RegExp` object or an array of `RegExp`.

* `/[@_]\dx/` is the default regular expression for `alternatesPattern`
* To turn off `alternatesPattern` set it to `null`

## Getting Started
_If you haven't used [grunt][] before, be sure to check out the [Getting Started][] guide._

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "grunt-rev",
"description": "Static file asset revisioning through content hashing",
"version": "0.1.0",
"version": "0.2.0",
"homepage": "https://github.com/cbas/grunt-rev",
"author": {
"name": "Sebastiaan Deckers",
Expand Down Expand Up @@ -38,4 +38,4 @@
"keywords": [
"gruntplugin"
]
}
}
130 changes: 120 additions & 10 deletions tasks/rev.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,145 @@ var fs = require('fs'),
module.exports = function(grunt) {

function md5(filepath, algorithm, encoding, fileEncoding) {
var hash = crypto.createHash(algorithm);
grunt.log.verbose.write('Hashing ' + filepath + '...');
hash.update(grunt.file.read(filepath), fileEncoding);
var hash = crypto.createHash(algorithm), i;
if (grunt.util.kindOf(filepath) === 'array') {
grunt.log.verbose.write('Hashing group "' + filepath.join('", "') + '" ...');
for (i = 0; i < filepath.length; i += 1) {
hash.update(grunt.file.read(filepath[i]), fileEncoding);
}
} else {
grunt.log.verbose.write('Hashing ' + filepath + '...');
hash.update(grunt.file.read(filepath), fileEncoding);
}
return hash.digest(encoding);
}

function findMatchingFiles(matchData, matchDataAll) {
var baseFilename = matchData[1] + matchData[3],
machingFiles = [baseFilename], i;
for (i = 0; i < matchDataAll.length; i += 1) {
if (matchData[1] === matchDataAll[i][1] && matchData[3] === matchDataAll[i][3]) {
machingFiles.push(matchDataAll[i][0]);
}
}

return machingFiles;
}

function combineGroups(pattern, matchData) {
var groups = [],
iFiles, filesLength = matchData.matching.length,
matchingFiles,
matchedNames = [];

for (iFiles = 0; iFiles < filesLength; iFiles += 1) {
if (matchedNames.indexOf(matchData.matching[iFiles][0]) >= 0) {
continue;
}
matchingFiles = findMatchingFiles(matchData.matching[iFiles], matchData.matching);
matchedNames = matchedNames.concat(matchingFiles);
groups.push(matchingFiles);
}

return groups;
}

grunt.registerMultiTask('rev', 'Prefix static asset file names with a content hash', function() {

var options = this.options({
alternatesPattern: /[@_]\dx/,
encoding: 'utf8',
algorithm: 'md5',
length: 8
});

var alternatesPatterns = [];
if (options.alternatesPattern === null) {
alternatesPatterns = [];
} else if (grunt.util.kindOf(options.alternatesPattern) === 'array') {
alternatesPatterns = options.alternatesPattern;
} else {
alternatesPatterns = [options.alternatesPattern];
}

var i, match, regexp, tempRegexp, tempRegexpFunction, numRegexpGroups,
regexps = [],
matchingFiles = {},
patternsLength = alternatesPatterns.length;

tempRegexpFunction = function($0, $1) {
return $1 ? $0 : '###';
};

// Prepare regexps
for (i = 0; i < patternsLength; i += 1) {
if (grunt.util.kindOf(alternatesPatterns[i]) !== 'regexp') {
grunt.fatal('alternates pattern must be a regular expression');
}
tempRegexp = alternatesPatterns[i].source.replace(/(\\)?\(/g, tempRegexpFunction);
numRegexpGroups = tempRegexp.split('###').length - 1;
regexp = new RegExp('^(.*)(' + alternatesPatterns[i].source + ')(\\.(?:jpg|png|gif|webp))$');
regexps.push(regexp);
matchingFiles[alternatesPatterns[i].source] = { regexp: regexp, numRegexpGroups: numRegexpGroups, matching: [] };
}

// Find matching files
var matchData;
this.files.forEach(function(filePair) {
filePair.src.forEach(function(f) {
for (i = 0; i < patternsLength; i += 1) {
match = regexps[i].exec(f);
matchData = matchingFiles[alternatesPatterns[i].source];
if (match !== null) {
matchData.matching.push([match[0], match[1], match[2], match[3 + matchData.numRegexpGroups]]);
// Limitation: a file can only match one pattern
break;
}
}
});
});

var hash = md5(f, options.algorithm, 'hex', options.encoding),
prefix = hash.slice(0, options.length),
renamed = [prefix, path.basename(f)].join('.'),
outPath = path.resolve(path.dirname(f), renamed);
// Combine into groups
var combinedGroups, pattern, groups = [];
for (pattern in matchingFiles) {
if (matchingFiles.hasOwnProperty(pattern)) {
combinedGroups = combineGroups(pattern, matchingFiles[pattern]);
groups = groups.concat(combinedGroups);
}
}

// Handle groups
var group, j, k, hash, prefix, renamed, outPath, f, handledFiles = [];
for (k = 0; k < groups.length; k += 1) {
group = groups[k];
hash = md5(group, options.algorithm, 'hex', options.encoding);
prefix = hash.slice(0, options.length);
grunt.verbose.ok(hash);
for (j = 0; j < group.length; j += 1) {
f = group[j];
renamed = [prefix, path.basename(f)].join('.');
outPath = path.resolve(path.dirname(f), renamed);

grunt.verbose.ok().ok(hash);
fs.renameSync(f, outPath);
grunt.log.write(f + ' ').ok(renamed);
handledFiles.push(f);
}
}

// Handle the rest of the files
this.files.forEach(function(filePair) {
filePair.src.forEach(function(f) {
if (handledFiles.indexOf(f) < 0) {
hash = md5(f, options.algorithm, 'hex', options.encoding);
prefix = hash.slice(0, options.length);
renamed = [prefix, path.basename(f)].join('.');
outPath = path.resolve(path.dirname(f), renamed);

grunt.verbose.ok().ok(hash);
fs.renameSync(f, outPath);
grunt.log.write(f + ' ').ok(renamed);
}
});
});

});

};
Binary file added test/fixtures/Array-Source-Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Array-Source-Logo_2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Array-Source-Logo_4x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Array-Source-button-active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Array-Source-button-hover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Array-Source-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Default-Source-Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Default-Source-Logo_2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Off-Source-Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Off-Source-Logo@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Open-Source-Divider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Open-Source-Divider@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Open-Source-Logo-No-Alt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Open-Source-Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Open-Source-Logo@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/Open-Source-Logo@4x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions test/rev_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,65 @@ exports.rev = {
var exists = grunt.file.exists('tmp/faa07745.international.txt');
test.ok(exists, '8 character MD5 hash prefix for international content');

test.done();
},
alt_options: function(test) {
test.expect(6);

var existsOriginal = grunt.file.exists('tmp/ecea3614.Open-Source-Logo.png');
test.ok(existsOriginal, '8 character MD5 hash prefix for original image 1');
var existsAlt = grunt.file.exists('tmp/ecea3614.Open-Source-Logo@2x.png');
test.ok(existsAlt, '8 character MD5 hash prefix for alt image 1 @ 2x');
var existsAlt2 = grunt.file.exists('tmp/ecea3614.Open-Source-Logo@2x.png');
test.ok(existsAlt2, '8 character MD5 hash prefix for alt image 1 @ 4x');

var existsOriginalV2 = grunt.file.exists('tmp/de0ee580.Open-Source-Divider.png');
test.ok(existsOriginalV2, '8 character MD5 hash prefix for original image 2');
var existsAltV2 = grunt.file.exists('tmp/de0ee580.Open-Source-Divider@2x.png');
test.ok(existsAlt, '8 character MD5 hash prefix for alt image 2 @ 2x');

var existsAltNo = grunt.file.exists('tmp/9aa33131.Open-Source-Logo-No-Alt.png');
test.ok(existsAltNo, '8 character MD5 hash prefix for not alt image');

test.done();
},
alt_options_default: function(test) {
test.expect(2);

var existsOriginal = grunt.file.exists('tmp/45c45315.Default-Source-Logo.png');
test.ok(existsOriginal, '8 character MD5 hash prefix for original image');
var existsAlt = grunt.file.exists('tmp/45c45315.Default-Source-Logo_2x.png');
test.ok(existsAlt, '8 character MD5 hash prefix for alt image');

test.done();
},
alt_options_off: function(test) {
test.expect(2);

var existsOriginal = grunt.file.exists('tmp/89e088f6.Off-Source-Logo.png');
test.ok(existsOriginal, '8 character MD5 hash prefix for original image');
var existsAlt = grunt.file.exists('tmp/66ff15cd.Off-Source-Logo@2x.png');
test.ok(existsAlt, '8 character MD5 hash prefix for alt image');

test.done();
},
alt_options_array: function(test) {
test.expect(6);

var exists, expectedFiles = [
'tmp/c6aa5421.Array-Source-button.png',
'tmp/c6aa5421.Array-Source-button-hover.png',
'tmp/c6aa5421.Array-Source-button-active.png',
'tmp/a09661e5.Array-Source-Logo.png',
'tmp/a09661e5.Array-Source-Logo_2x.png',
'tmp/a09661e5.Array-Source-Logo_4x.png'
];

for (var i = 0; i < expectedFiles.length; i++) {
exists = grunt.file.exists(expectedFiles[i]);
test.ok(exists, 'File expected: ' + expectedFiles[i]);
}

test.done();
}
};