grunt-responsive-images-extender
Version:
Extend HTML image tags with srcset and sizes attributes to leverage native responsive images.
206 lines (169 loc) • 6.32 kB
JavaScript
/*
* grunt-responsive-images-extender
* https://github.com/smaxtastic/grunt-responsive-images-extender
*
* Copyright (c) 2014 Stephan Max
* Licensed under the MIT license.
*
* Extend HTML image tags with srcset and sizes attributes to leverage native responsive images.
*
* @author Stephan Max (http://stephanmax.is)
* @version 2.0.0
*/
module.exports = function(grunt) {
'use strict';
var fs = require('fs');
var path = require('path');
var cheerio = require('cheerio');
var sizeOf = require('image-size');
var DEFAULT_OPTIONS = {
separator: '-',
baseDir: '',
ignore: [],
srcsetAttributeName: 'srcset'
};
grunt.registerMultiTask('responsive_images_extender', 'Extend HTML image tags with srcset and sizes attributes to leverage native responsive images.', function() {
var numOfFiles = this.files.length;
var options = this.options(DEFAULT_OPTIONS);
var imgCount = 0;
var parseAndExtendImg = function(filepath) {
var content = grunt.file.read(filepath);
var $ = cheerio.load(content, {decodeEntities: false});
var imgElems = $('img:not(' + options.ignore.join(', ') + ')');
imgElems.each(function() {
var normalizeImagePath = function(src) {
var pathPrefix;
if (path.isAbsolute(src)) {
pathPrefix = options.baseDir;
}
else {
pathPrefix = path.dirname(filepath);
}
return path.parse(path.join(pathPrefix, src));
};
var findMatchingImages = function(path) {
var files = fs.readdirSync(path.dir);
var imageMatch = new RegExp(path.name + '(' + options.separator + '[^' + options.separator + ']*)?' + path.ext + '$');
return files.filter(function(filename) {
return imageMatch.test(filename);
});
};
var buildSrcMap = function(imageNames) {
var srcMap = {};
imageNames.forEach(function(imageName) {
srcMap[imageName] = sizeOf(path.join(imagePath.dir, imageName)).width;
});
return srcMap;
};
var buildSrcset = function(srcMap, width) {
var srcset = [];
var candidate;
for (var img in srcMap) {
candidate = path.posix.join(path.dirname(imgSrc), img);
if (width !== undefined) {
candidate += ' ' + Math.round(srcMap[img] / width * 100) / 100 + 'x';
}
else {
candidate += ' ' + srcMap[img] + 'w';
}
srcset.push(candidate);
}
if (options.srcsetAttributeName !== DEFAULT_OPTIONS.srcsetAttributeName) {
imgElem.attr(DEFAULT_OPTIONS.srcsetAttributeName, null);
}
return srcset.join(', ');
};
var buildSizes = function(sizeList) {
var sizes = [];
sizeList.forEach(function(s) {
var actualSize = srcMap[imagePath.name + imagePath.ext] + 'px';
var cond = s.cond.replace('%size%', actualSize);
var size = s.size.replace('%size%', actualSize);
sizes.push(
cond === 'default' ? size : '(' + cond + ') ' + size
);
});
return sizes.join(', ');
};
var setSrcAttribute = function() {
switch (options.srcAttribute) {
case 'none':
imgElem.attr('src', null);
break;
case 'smallest':
var smallestImage = Object.keys(srcMap).map(function(k) {
return [k, srcMap[k]];
}).reduce(function(a, b) {
return b[1] < a[1] ? b : a;
});
imgElem.attr('src', path.posix.join(path.dirname(imgSrc), smallestImage[0]));
break;
default:
}
};
var imgElem = $(this);
var imgWidth = imgElem.attr('width');
var imgSrc = imgElem.attr('src');
var useSizes = 'sizes' in options;
var isResponsive = imgWidth === undefined;
var hasSrcset = imgElem.attr(options.srcsetAttributeName) !== undefined;
var hasSizes = imgElem.attr('sizes') !== undefined;
var imagePath;
var imageMatches;
var srcMap;
if (hasSrcset && (!isResponsive || (isResponsive && hasSizes) || !useSizes)) {
return;
}
imagePath = normalizeImagePath(imgSrc);
imageMatches = findMatchingImages(imagePath);
switch (imageMatches.length) {
case 0:
grunt.verbose.error('Found no file for ' + imgSrc.cyan);
return;
case 1:
grunt.verbose.error('Found only one file for ' + imgSrc.cyan);
return;
default:
grunt.verbose.ok('Found ' + imageMatches.length.cyan + ' files for ' + imgSrc.cyan + ': ' + imageMatches);
}
srcMap = buildSrcMap(imageMatches);
if (!isResponsive && imgWidth > 0) {
imgElem.attr(options.srcsetAttributeName, buildSrcset(srcMap, imgWidth));
setSrcAttribute();
}
else {
if (!hasSrcset) {
imgElem.attr(options.srcsetAttributeName, buildSrcset(srcMap));
setSrcAttribute();
}
if (!hasSizes && useSizes) {
options.sizes.some(function (s) {
if (imgElem.is(s.selector)) {
imgElem.attr('sizes', buildSizes(s.sizeList));
setSrcAttribute();
return true;
}
});
}
}
});
return {content: $.html(), count: imgElems.length};
};
this.files.forEach(function(file) {
var contents = file.src.filter(function(filepath) {
if (!grunt.file.exists(filepath)) {
grunt.log.warn('Source file "' + filepath + '" not found.');
return false;
} else {
return true;
}
}).map(function(filepath) {
var result = parseAndExtendImg(filepath);
imgCount += result.count;
return result.content;
}).join('\n');
grunt.file.write(file.dest, contents);
});
grunt.log.ok('Processed ' + imgCount.toString().cyan + ' <img> ' + grunt.util.pluralize(imgCount, 'tag/tags'));
});
};