grunt-responsive-images-extender
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -26,3 +26,3 @@ /* | ||
clean: { | ||
tests: ['tmp'] | ||
tests: ['test/tmp'] | ||
}, | ||
@@ -32,5 +32,8 @@ | ||
responsive_images_extender: { | ||
options: { | ||
baseDir: 'test' | ||
}, | ||
default_options: { | ||
files: { | ||
'tmp/default_options': 'test/fixtures/testing' | ||
'test/tmp/default_options.html': 'test/fixtures/testing.html' | ||
} | ||
@@ -64,17 +67,12 @@ }, | ||
files: { | ||
'tmp/use_sizes': 'test/fixtures/testing' | ||
'test/tmp/use_sizes.html': 'test/fixtures/testing.html' | ||
} | ||
}, | ||
retina: { | ||
polyfill_lazyloading: { | ||
options: { | ||
srcsetRetina: [{ | ||
suffix: '_x1.5', | ||
value: '1.5x' | ||
},{ | ||
suffix: '_x2', | ||
value: '2x' | ||
}] | ||
srcsetAttributeName: 'data-srcset', | ||
srcAttribute: 'none' | ||
}, | ||
files: { | ||
'tmp/retina': 'test/fixtures/testing' | ||
'test/tmp/polyfill_lazyloading.html': 'test/fixtures/testing.html' | ||
} | ||
@@ -85,19 +83,3 @@ }, | ||
ignore: ['.ignore-me'], | ||
srcset: [{ | ||
suffix: '-200', | ||
value: '200w' | ||
},{ | ||
suffix: '-400', | ||
value: '400w' | ||
},{ | ||
suffix: '-800', | ||
value: '800w' | ||
}], | ||
srcsetRetina: [{ | ||
suffix: '_x1.5', | ||
value: '1.5x' | ||
},{ | ||
suffix: '_x2', | ||
value: '2x' | ||
}], | ||
srcAttribute: 'smallest', | ||
sizes: [{ | ||
@@ -118,3 +100,3 @@ selector: '.fig-hero img', | ||
files: { | ||
'tmp/all': 'test/fixtures/testing' | ||
'test/tmp/all.html': 'test/fixtures/testing.html' | ||
} | ||
@@ -139,3 +121,2 @@ } | ||
grunt.registerTask('default', ['jshint', 'test']); | ||
}; |
{ | ||
"name": "grunt-responsive-images-extender", | ||
"description": "Extend HTML image tags with srcset and sizes attributes to leverage native responsive images.", | ||
"version": "1.0.0", | ||
"version": "2.0.0", | ||
"homepage": "https://github.com/smaxtastic/grunt-responsive-images-extender", | ||
@@ -18,10 +18,5 @@ "author": { | ||
}, | ||
"licenses": [ | ||
{ | ||
"type": "MIT", | ||
"url": "https://github.com/smaxtastic/grunt-responsive-images-extender/blob/master/LICENSE-MIT" | ||
} | ||
], | ||
"license": "MIT", | ||
"engines": { | ||
"node": ">= 0.8.0" | ||
"node": ">= 0.12.0" | ||
}, | ||
@@ -32,6 +27,6 @@ "scripts": { | ||
"devDependencies": { | ||
"grunt": "~0.4.5", | ||
"grunt-contrib-clean": "^0.5.0", | ||
"grunt-contrib-jshint": "^0.9.2", | ||
"grunt-contrib-clean": "^0.5.0", | ||
"grunt-contrib-nodeunit": "^0.3.3", | ||
"grunt": "~0.4.5" | ||
"grunt-contrib-nodeunit": "^0.3.3" | ||
}, | ||
@@ -45,2 +40,4 @@ "peerDependencies": { | ||
"responsive", | ||
"responsivedesign", | ||
"rwd", | ||
"img", | ||
@@ -53,8 +50,8 @@ "image", | ||
"srcset", | ||
"sizes", | ||
"responsivedesign" | ||
"sizes" | ||
], | ||
"dependencies": { | ||
"cheerio": "^0.17.0" | ||
"cheerio": "^0.19.0", | ||
"image-size": "^0.3.5" | ||
} | ||
} |
101
README.md
@@ -28,3 +28,3 @@ # grunt-responsive-images-extender | ||
This plugin uses [Cheerio](https://github.com/cheeriojs/cheerio) to traverse and modify the DOM. | ||
This plugin uses [Cheerio](https://github.com/cheeriojs/cheerio) to traverse/modify the DOM and [image-size](https://github.com/netroy/image-size) to read the image sizes straight from the image files. You **don't have to configure** the `srcset` or `srcsetRetina` option anymore, since they get built automatically based on the present image files. | ||
@@ -48,8 +48,14 @@ In your project's Gruntfile, add a section named `responsive_images_extender` to the data object passed into `grunt.initConfig()`. | ||
* **options.srcset**<br> | ||
*Type:* `Array`<br> | ||
*Default:* `[{suffix: '-small', value: '320w'}, {suffix: '-medium', value: '640w'}, {suffix: '-large', value: '1024w'}]`<br> | ||
* **options.separator**<br> | ||
*Type:* `String`<br> | ||
*Default:* `'-'`<br> | ||
An array of objects containing the suffixes and sizes of our source set. The default values match those of the [responsive_images](https://github.com/andismith/grunt-responsive-images/) task for smooth collaboration. | ||
The separator used for naming your resized images. | ||
* **options.baseDir**<br> | ||
*Type:* `String`<br> | ||
*Default:* `''`<br> | ||
The base directory of the site you are serving. This enables Grunt to access your image files when you use absolute paths to reference them in your HTML code. Ignore this option if you are using relative paths only. | ||
* **options.sizes**<br> | ||
@@ -60,3 +66,3 @@ *Type:* `Array`<br> | ||
An array of objects containing the selectors (standard CSS selectors, like `.some-class`, `#an-id` or `img[src^="http://"]`) and their respective size tableau. An example could look like this: | ||
```js | ||
@@ -91,8 +97,4 @@ sizes: [{ | ||
* **options.srcsetRetina**<br> | ||
*Type:* `Array`<br> | ||
*Default:* none<br> | ||
You can use the placeholder `%size%` which gets replaced by the actual width of the current image (this can come in handy to specify a maximum width with a breakpoint, e.g. `(min-width: 400px) 400px`). | ||
An array of objects containing the suffixes and sizes of our source set for non-responsive images (that is, images with an explicitly set `width` attribute in pixels). Use this array if you want to provide the browser images in different resolutions for use on high-DPR or retina devices. | ||
* **options.ignore**<br> | ||
@@ -102,8 +104,23 @@ *Type:* `Array`<br> | ||
An array of selectors you want to ignore. | ||
An array of standard CSS selectors of image tags you want to ignore. | ||
* **options.srcsetAttributeName**<br> | ||
*Type:* `String`<br> | ||
*Default:* `'srcset'`<br> | ||
Overwrite the name of the `srcset` attribute with something else, for example some lazy loaders require `data-srcset`. | ||
* **options.srcAttribute**<br> | ||
*Type:* `String`<br> | ||
*Default:* none<br> | ||
Set the `src` attribute to: | ||
- `'none'`: Delete the `src` attribute which is necessary to avoid duplicate downloads when you are using a polyfill. *Please note that this is not valid HTML, though.* | ||
- `'smallest'`: Set the `src` fallback to the smallest image. | ||
- Do not use this option to leave `src` untouched. | ||
### Usage Examples | ||
#### Default Options | ||
Using the default options will make the task search for HTML `<img>` tags that have no `width` or `srcset` attribute already. | ||
@@ -132,3 +149,3 @@ ```js | ||
into this: | ||
into this (the image sizes are arbitrarily chosen and read directly from the files): | ||
@@ -139,3 +156,4 @@ ```html | ||
simple-medium.jpg 640w, | ||
simple-large.jpg 1024w" | ||
simple-large.jpg 1024w, | ||
simple.jpg 2000w" | ||
title="A simple image"> | ||
@@ -145,3 +163,3 @@ ``` | ||
#### Custom Options | ||
Use the options to refine your tasks, e.g. to add a `sizes` attribute or a set of sources for retina-ready fixed-width images. | ||
Use the options to refine your tasks, e.g. to add a `sizes` attribute, a different separator, or a different `src` value. `<img>` tags with a `width` attribute automatically trigger the use of `x` descriptors. | ||
@@ -153,19 +171,5 @@ ```js | ||
options: { | ||
srcset: [{ | ||
suffix: '-200', | ||
value: '200w' | ||
},{ | ||
suffix: '-400', | ||
value: '400w' | ||
},{ | ||
suffix: '-800', | ||
value: '800w' | ||
}], | ||
srcsetRetina: [{ | ||
suffix: '_x1.5', | ||
value: '1.5x' | ||
},{ | ||
suffix: '_x2', | ||
value: '2x' | ||
}], | ||
separator: '@', | ||
baseDir: 'build', | ||
srcAttribute: 'smallest', | ||
sizes: [{ | ||
@@ -201,3 +205,3 @@ selector: '.article-img', | ||
<img src="non_responsive.png" width="150"> | ||
<img src="simple.jpg" width="200"> | ||
``` | ||
@@ -208,6 +212,7 @@ | ||
```html | ||
<img alt="A simple image" src="simple.jpg" class=".article-img" | ||
srcset="simple-200.jpg 200w, | ||
simple-400.jpg 400w, | ||
simple-800.jpg 800w" | ||
<img alt="A simple image" src="simple@200.jpg" class=".article-img" | ||
srcset="simple@200.jpg 200w, | ||
simple@400.jpg 400w, | ||
simple@800.jpg 800w, | ||
simple.jpg 1600w" | ||
sizes="(max-width: 30em) 100vw, | ||
@@ -217,5 +222,7 @@ (max-width: 50em) 50vw, | ||
<img src="non_responsive.png" width="150" | ||
srcset="non_responsive_x1.5.png 1.5x, | ||
non_responsive_x2.png 2x"> | ||
<img src="simple@200.jpg" width="200" | ||
srcset="simple@200.jpg 1x, | ||
simple@400.jpg 2x, | ||
simple@800.jpg 4x, | ||
simple.jpg 8x"> | ||
``` | ||
@@ -249,3 +256,3 @@ | ||
* **grunt-responsive-images** | ||
Use this [task](https://github.com/andismith/grunt-responsive-images/) to generate images with different sizes. | ||
@@ -262,2 +269,10 @@ | ||
*2.0.0* | ||
* `srcset` is built automatically based on the image sizes read directly from the files. `x` descriptors are triggered for images with `width` attribute. | ||
* Removed `srcset` and `srcsetRetina` option. | ||
* Added the `srcAttribute` option to delete `src` for polyfills or set the smallest image as a fallback. | ||
* Added the `srcsetAttributeName` option to use for example `data-srcset` for lazy loaders. | ||
* Added the `%size%` placeholder to use the image size inside the `sizes` rules | ||
*1.0.0* | ||
@@ -270,2 +285,2 @@ | ||
* Initial commit and first version | ||
* Initial commit and first version |
@@ -7,101 +7,170 @@ /* | ||
* Licensed under the MIT license. | ||
* | ||
* | ||
* Extend HTML image tags with srcset and sizes attributes to leverage native responsive images. | ||
* | ||
* @author Stephan Max (http://twitter.com/smaxtastic) | ||
* @version 0.1.0 | ||
* | ||
* @author Stephan Max (http://stephanmax.is) | ||
* @version 2.0.0 | ||
*/ | ||
'use strict'; | ||
module.exports = function(grunt) { | ||
'use strict'; | ||
module.exports = function(grunt) { | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var cheerio = require('cheerio'); | ||
var sizeOf = require('image-size'); | ||
var DEFAULT_OPTIONS = { | ||
separator: '-', | ||
baseDir: '', | ||
ignore: [], | ||
srcset: [{ | ||
suffix: '-small', | ||
value: '320w' | ||
},{ | ||
suffix: '-medium', | ||
value: '640w' | ||
},{ | ||
suffix: '-large', | ||
value: '1024w' | ||
}] | ||
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, | ||
options = this.options(DEFAULT_OPTIONS), | ||
processedImages = 0; | ||
function buildAttributeList(optionList, buildAttribute) { | ||
var attributeList = []; | ||
optionList.forEach(function(o) { | ||
attributeList.push(buildAttribute(o)); | ||
}); | ||
return attributeList.join(', '); | ||
} | ||
function parseAndExtendImg(content, options) { | ||
var $ = cheerio.load(content), | ||
images = $('img:not(' + options.ignore.join(', ') + ')'); | ||
images.each(function() { | ||
var separatorPos, filePath, fileExt, | ||
image = $(this); | ||
function buildSrc(option) { | ||
return filePath + option.suffix + fileExt + ' ' + option.value; | ||
} | ||
function buildSize(option) { | ||
if (option.cond === 'default') { | ||
return option.size; | ||
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 { | ||
return '(' + option.cond + ') ' + option.size; | ||
pathPrefix = path.dirname(filepath); | ||
} | ||
} | ||
var retinaReady = 'srcsetRetina' in options, | ||
useSizes = 'sizes' in options, | ||
isNonResponsive = image.attr('width') !== undefined, | ||
hasSrcset = image.attr('srcset') !== undefined, | ||
hasSizes = image.attr('sizes') !== undefined; | ||
// Don't process <img> tags unnecessarily | ||
if ((isNonResponsive && hasSrcset) || | ||
(isNonResponsive && !hasSrcset && !retinaReady) || | ||
(!isNonResponsive && hasSrcset && hasSizes) || | ||
(hasSrcset && !useSizes)) { | ||
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.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.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; | ||
} | ||
filePath = image.attr('src'); | ||
if (filePath === undefined) { | ||
grunt.log.verbose.error('Found an image without a source: ' + $.html(image)); | ||
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); | ||
} | ||
separatorPos = filePath.lastIndexOf('.'); | ||
fileExt = filePath.slice(separatorPos); | ||
filePath = filePath.slice(0, separatorPos); | ||
if (isNonResponsive) { | ||
image.attr('srcset', buildAttributeList(options.srcsetRetina, buildSrc)); | ||
grunt.log.verbose.ok('Detected width attribute for ' + filePath + fileExt + ' (not responsive, but retina-ready)'); | ||
srcMap = buildSrcMap(imageMatches); | ||
if (!isResponsive && imgWidth > 0) { | ||
imgElem.attr(options.srcsetAttributeName, buildSrcset(srcMap, imgWidth)); | ||
setSrcAttribute(); | ||
} | ||
else { | ||
if (!hasSrcset) { | ||
image.attr('srcset', buildAttributeList(options.srcset, buildSrc)); | ||
grunt.log.verbose.ok('Extend ' + filePath + fileExt + ' with srcset attribute'); | ||
imgElem.attr(options.srcsetAttributeName, buildSrcset(srcMap)); | ||
setSrcAttribute(); | ||
} | ||
if (!hasSizes && useSizes) { | ||
options.sizes.some(function (s) { | ||
if (image.is(s.selector)) { | ||
image.attr('sizes', buildAttributeList(s.sizeList, buildSize)); | ||
grunt.log.verbose.ok('Extend ' + filePath + fileExt + ' with sizes attribute (selector: ' + s.selector + ')'); | ||
if (imgElem.is(s.selector)) { | ||
imgElem.attr('sizes', buildSizes(s.sizeList)); | ||
setSrcAttribute(); | ||
return true; | ||
@@ -113,19 +182,23 @@ } | ||
}); | ||
return {content: $.html(), count: images.length}; | ||
} | ||
grunt.log.writeln('Found ' + numOfFiles.toString().cyan + ' ' + grunt.util.pluralize(numOfFiles, 'file/files')); | ||
return {content: $.html(), count: imgElems.length}; | ||
}; | ||
this.files.forEach(function(f) { | ||
var content = grunt.file.read(f.src); | ||
var result = parseAndExtendImg(content, options); | ||
var result; | ||
if (f.src.length === 0) { | ||
grunt.log.error('No files to process!'); | ||
return; | ||
} | ||
result = parseAndExtendImg(f.src); | ||
grunt.file.write(f.dest, result.content); | ||
processedImages += result.count; | ||
imgCount += result.count; | ||
}); | ||
grunt.log.writeln('Processed ' + processedImages.toString().cyan + ' <img> ' + grunt.util.pluralize(processedImages, 'tag/tags')); | ||
grunt.log.ok('Processed ' + imgCount.toString().cyan + ' <img> ' + grunt.util.pluralize(imgCount, 'tag/tags')); | ||
}); | ||
}; |
@@ -12,4 +12,4 @@ 'use strict'; | ||
var actual = grunt.file.read('tmp/default_options'); | ||
var expected = grunt.file.read('test/expected/default_options'); | ||
var actual = grunt.file.read('test/tmp/default_options.html'); | ||
var expected = grunt.file.read('test/expected/default_options.html'); | ||
test.equal(actual, expected, 'Should describe what the default behavior is.'); | ||
@@ -22,5 +22,5 @@ | ||
var actual = grunt.file.read('tmp/retina'); | ||
var expected = grunt.file.read('test/expected/retina'); | ||
test.equal(actual, expected, 'Should describe what the retina behavior is.'); | ||
var actual = grunt.file.read('test/tmp/polyfill_lazyloading.html'); | ||
var expected = grunt.file.read('test/expected/polyfill_lazyloading.html'); | ||
test.equal(actual, expected, 'Should describe what the polyfill and lazyloading behavior is.'); | ||
@@ -32,4 +32,4 @@ test.done(); | ||
var actual = grunt.file.read('tmp/use_sizes'); | ||
var expected = grunt.file.read('test/expected/use_sizes'); | ||
var actual = grunt.file.read('test/tmp/use_sizes.html'); | ||
var expected = grunt.file.read('test/expected/use_sizes.html'); | ||
test.equal(actual, expected, 'Should describe what the sizes attribute behavior is.'); | ||
@@ -42,4 +42,4 @@ | ||
var actual = grunt.file.read('tmp/all'); | ||
var expected = grunt.file.read('test/expected/all'); | ||
var actual = grunt.file.read('test/tmp/all.html'); | ||
var expected = grunt.file.read('test/expected/all.html'); | ||
test.equal(actual, expected, 'Should describe what the complete behavior is.'); | ||
@@ -46,0 +46,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
143472
18
304
272
3
1
+ Addedimage-size@^0.3.5
+ Addedboolbase@1.0.0(transitive)
+ Addedcheerio@0.19.0(transitive)
+ Addedcss-select@1.0.0(transitive)
+ Addedcss-what@1.0.0(transitive)
+ Addeddom-serializer@0.1.1(transitive)
+ Addeddomelementtype@1.3.1(transitive)
+ Addeddomhandler@2.3.0(transitive)
+ Addedhtmlparser2@3.8.3(transitive)
+ Addedimage-size@0.3.5(transitive)
+ Addedlodash@3.10.1(transitive)
+ Addednth-check@1.0.2(transitive)
- RemovedCSSselect@0.4.1(transitive)
- RemovedCSSwhat@0.4.7(transitive)
- Removedcheerio@0.17.0(transitive)
- Removeddom-serializer@0.0.1(transitive)
- Removeddomelementtype@1.1.3(transitive)
- Removeddomhandler@2.2.1(transitive)
- Removedhtmlparser2@3.7.3(transitive)
Updatedcheerio@^0.19.0