├── .gitignore ├── test ├── fixtures │ ├── B.css │ ├── a.png │ ├── b.png │ ├── a@2x.png │ ├── b@2x.png │ ├── A.css │ ├── sprite.png │ ├── stylesheet.css │ ├── stylesheet.scss │ └── stylesheet.retina.css ├── expectations │ ├── sprite.png │ ├── B.css │ ├── A.css │ ├── stylesheet.filter.css │ ├── stylesheet.retina.css │ ├── stylesheet.css │ ├── stylesheet.groupby.css │ └── stylesheet.scss └── test.js ├── package.json ├── .jshintrc ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /test/fixtures/B.css: -------------------------------------------------------------------------------- 1 | .b { 2 | background-image: url(/b.png); 3 | } -------------------------------------------------------------------------------- /test/fixtures/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/gulp-lazysprite/master/test/fixtures/a.png -------------------------------------------------------------------------------- /test/fixtures/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/gulp-lazysprite/master/test/fixtures/b.png -------------------------------------------------------------------------------- /test/fixtures/a@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/gulp-lazysprite/master/test/fixtures/a@2x.png -------------------------------------------------------------------------------- /test/fixtures/b@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/gulp-lazysprite/master/test/fixtures/b@2x.png -------------------------------------------------------------------------------- /test/fixtures/A.css: -------------------------------------------------------------------------------- 1 | /* test */ 2 | .a { 3 | font-weight: normal; 4 | background-image: url(/a.png); 5 | } -------------------------------------------------------------------------------- /test/fixtures/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/gulp-lazysprite/master/test/fixtures/sprite.png -------------------------------------------------------------------------------- /test/expectations/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/gulp-lazysprite/master/test/expectations/sprite.png -------------------------------------------------------------------------------- /test/fixtures/stylesheet.css: -------------------------------------------------------------------------------- 1 | .a { 2 | background-image: url(/a.png); 3 | } 4 | .b { 5 | background-image: url("/b.png"); 6 | } 7 | .c { 8 | background-image: url("/a.png"); 9 | } -------------------------------------------------------------------------------- /test/fixtures/stylesheet.scss: -------------------------------------------------------------------------------- 1 | .a { 2 | background: url(/a.png) no-repeat; 3 | } 4 | .b { 5 | background-image: image-url("b.png"); 6 | } 7 | .c { 8 | background-image: url("/a.png"); 9 | } -------------------------------------------------------------------------------- /test/expectations/B.css: -------------------------------------------------------------------------------- 1 | .b { 2 | width:25px; 3 | height:25px; 4 | background-image: url("sprite.png?v=8dcb75c4"); 5 | background-position: -0px -25px; 6 | background-size: 25px 50px!important; 7 | } -------------------------------------------------------------------------------- /test/expectations/A.css: -------------------------------------------------------------------------------- 1 | .a { 2 | font-weight: normal; 3 | width:25px; 4 | height:25px; 5 | background-image: url("sprite.png?v=8dcb75c4"); 6 | background-position: -0px -0px; 7 | background-size: 25px 50px!important; 8 | } -------------------------------------------------------------------------------- /test/fixtures/stylesheet.retina.css: -------------------------------------------------------------------------------- 1 | @media (min-resolution: 2dppx) { 2 | .a { 3 | background-image: url("/a@2x.png"); 4 | background-size: 25px 25px; 5 | } 6 | 7 | .b { 8 | background-image: url("/b@2x.png"); 9 | background-size: 25px 25px; 10 | } 11 | } -------------------------------------------------------------------------------- /test/expectations/stylesheet.filter.css: -------------------------------------------------------------------------------- 1 | .a { 2 | background-image: url(/a.png); 3 | } 4 | .b { 5 | width:25px; 6 | height:25px; 7 | background-image: url("sprite.png?v=b98fec40"); 8 | background-position: -0px -0px; 9 | background-size: 25px 25px!important; 10 | } 11 | .c { 12 | background-image: url("/a.png"); 13 | } -------------------------------------------------------------------------------- /test/expectations/stylesheet.retina.css: -------------------------------------------------------------------------------- 1 | @media (min-resolution: 2dppx) { 2 | .a { 3 | width:25px; 4 | height:25px; 5 | background-image: url("sprite.@2x.png?v=2d055014"); 6 | background-position: -0px -0px; 7 | background-size: 25px 50px!important; 8 | background-size: 25px 25px; 9 | } 10 | 11 | .b { 12 | width:25px; 13 | height:25px; 14 | background-image: url("sprite.@2x.png?v=2d055014"); 15 | background-position: -0px -25px; 16 | background-size: 25px 50px!important; 17 | background-size: 25px 25px; 18 | } 19 | } -------------------------------------------------------------------------------- /test/expectations/stylesheet.css: -------------------------------------------------------------------------------- 1 | .a { 2 | width:25px; 3 | height:25px; 4 | background-image: url("sprite.png?v=8dcb75c4"); 5 | background-position: -0px -0px; 6 | background-size: 25px 50px!important; 7 | } 8 | .b { 9 | width:25px; 10 | height:25px; 11 | background-image: url("sprite.png?v=8dcb75c4"); 12 | background-position: -0px -25px; 13 | background-size: 25px 50px!important; 14 | } 15 | .c { 16 | width:25px; 17 | height:25px; 18 | background-image: url("sprite.png?v=8dcb75c4"); 19 | background-position: -0px -0px; 20 | background-size: 25px 50px!important; 21 | } -------------------------------------------------------------------------------- /test/expectations/stylesheet.groupby.css: -------------------------------------------------------------------------------- 1 | .a { 2 | width:25px; 3 | height:25px; 4 | background-image: url("sprite.my.png?v=8dcb75c4"); 5 | background-position: -0px -0px; 6 | background-size: 25px 50px!important; 7 | } 8 | .b { 9 | width:25px; 10 | height:25px; 11 | background-image: url("sprite.my.png?v=8dcb75c4"); 12 | background-position: -0px -25px; 13 | background-size: 25px 50px!important; 14 | } 15 | .c { 16 | width:25px; 17 | height:25px; 18 | background-image: url("sprite.my.png?v=8dcb75c4"); 19 | background-position: -0px -0px; 20 | background-size: 25px 50px!important; 21 | } -------------------------------------------------------------------------------- /test/expectations/stylesheet.scss: -------------------------------------------------------------------------------- 1 | .a { 2 | width: 25px; 3 | height: 25px; 4 | background-image: url("sprite.png?v=8dcb75c4"); 5 | background-position: -0px-0px; 6 | background-size: 25px 50px!important; 7 | } 8 | 9 | .b { 10 | width: 25px; 11 | height: 25px; 12 | background-image: image-url("sprite.png?v=8dcb75c4"); 13 | background-position: -0px-25px; 14 | background-size: 25px 50px!important; 15 | } 16 | 17 | .c { 18 | width: 25px; 19 | height: 25px; 20 | background-image: url("sprite.png?v=8dcb75c4"); 21 | background-position: -0px-0px; 22 | background-size: 25px 50px!important; 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-lazysprite", 3 | "version": "0.6.4", 4 | "description": "Extended gulp-sprite-generator, dependencies up to date. plugin that generate sprites from your stylesheets", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://git@github.com/icai/gulp-lazysprite.git" 12 | }, 13 | "dependencies": { 14 | "colors": "^1.1.2", 15 | "gulp-util": "^3.0.7", 16 | "lodash": "^4.15.0", 17 | "q": "^1.4.1", 18 | "spritesmith": "^3.1.0", 19 | "through2": "^2.0.1" 20 | }, 21 | "devDependencies": { 22 | "chai": "*", 23 | "gulp": "*", 24 | "merge-stream": "*", 25 | "mocha": "*" 26 | }, 27 | "files": [ 28 | "index.js" 29 | ], 30 | "keywords": [ 31 | "css", 32 | "scss", 33 | "less", 34 | "sprite", 35 | "auto", 36 | "generate", 37 | "generator" 38 | ], 39 | "author": "icai", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/icai/gulp-lazysprite/issues" 43 | }, 44 | "homepage": "https://github.com/icai/gulp-lazysprite#readme" 45 | } 46 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | 2 | /**! 3 | * See http://www.jshint.com/docs/options/ for all available options. 4 | */ 5 | { 6 | /** 7 | * Enforcing options 8 | * See http://www.jshint.com/docs/options/#enforcing-options 9 | */ 10 | "bitwise": true, 11 | "camelcase": true, 12 | "expr": true, 13 | "curly": true, 14 | "eqeqeq": false, 15 | "immed": true, 16 | "latedef": true, 17 | "newcap": false, 18 | "noarg": true, 19 | "strict": false, 20 | "trailing": true, 21 | "undef": true, 22 | "unused": true, 23 | 24 | /** 25 | * Relaxing options 26 | * See http://www.jshint.com/docs/options/#relaxing-options 27 | */ 28 | "eqnull": true, 29 | "esnext": true, 30 | "smarttabs": true, 31 | 32 | /** 33 | * Environments 34 | * See http://www.jshint.com/docs/options/#environments 35 | */ 36 | "browser": true, 37 | "jquery": true, 38 | "node": true, 39 | 40 | /** 41 | * Globals 42 | * See http://www.jshint.com/docs/ 43 | */ 44 | "globals": { 45 | "ga": true, 46 | 47 | // RequireJS 48 | "define": true, 49 | "module": true, 50 | "require": true, 51 | "requirejs": true, 52 | "root": true, 53 | 54 | "before": true, 55 | 56 | 57 | // Jasmin 58 | "jasmine": true, 59 | "afterEach": true, 60 | "beforeEach": true, 61 | "describe": true, 62 | "expect": true, 63 | "it": true, 64 | "sinon": true 65 | } 66 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-lazysprite 2 | 3 | [![NPM](https://nodei.co/npm/gulp-lazysprite.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/gulp-lazysprite/) 4 | 5 | Better suit for `gulp-sass` and `node-sass-asset-functions`. 6 | 7 | 8 | > Generate sprites from stylesheets. 9 | 10 | Plugin that generate sprites from your stylesheets (using [spritesmith](https://github.com/Ensighten/spritesmith)) and then updates image references. 11 | 12 | ## Getting started 13 | 14 | If you haven't used [gulp](http://gulpjs.com) before, be sure to check out the [Getting Started](https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md) guide. 15 | 16 | Install with [npm](https://npmjs.org/package/gulp-lazysprite) 17 | 18 | ``` 19 | npm install gulp-lazysprite --save-dev 20 | ``` 21 | 22 | ## Overview 23 | 24 | Sprite generator is a gulp task, which accepts options object and returns two streams for style and image piping. 25 | 26 | Here quick example of simple way usage: 27 | 28 | ```javascript 29 | var gulp = require('gulp'); 30 | var sprite = require('gulp-lazysprite'); 31 | 32 | gulp.task('sprites', function() { 33 | var spriteOutput; 34 | 35 | spriteOutput = gulp.src("./src/css/*.css") 36 | .pipe(sprite({ 37 | baseUrl: "./src/image", 38 | spriteSheetName: "sprite.png", 39 | spriteSheetPath: "/dist/image" 40 | })); 41 | 42 | spriteOutput.css.pipe(gulp.dest("./dist/css")); 43 | spriteOutput.img.pipe(gulp.dest("./dist/image")); 44 | }); 45 | ``` 46 | 47 | Sass or Less: 48 | 49 | ```javascript 50 | gulp.task('style',function(){ 51 | var spriteOutput; 52 | spriteOutput = gulp.src(srcPath+'/css/**/*.@(scss|css)') 53 | .pipe(sass()) // .pipe(less()) 54 | .pipe(spriter({ 55 | baseUrl: "./", 56 | spriteSheetName:"[name].sprite.png",// repalce `[name]` to filename 57 | spriteSheetPath: "../images/sprite", 58 | filter: [ 59 | function(image) { 60 | return !(image.url.indexOf("?__sprite") === -1); 61 | } 62 | ] 63 | verbose:true 64 | })) 65 | 66 | spriteOutput.css.pipe(gulp.dest(distPath+'/css')); 67 | spriteOutput.img.pipe(gulp.dest(distPath+'/images/sprite')); 68 | }); 69 | 70 | ``` 71 | 72 | 73 | or handle sprites first. 74 | 75 | ```javascript 76 | 77 | var merge = require('merge-stream'); 78 | 79 | gulp.task('csssprite', ['copyscss'], function() { 80 | let spriteOutput; 81 | spriteOutput = gulp.src(config.scss + '/*.@(scss|css)') 82 | .pipe(plumber()) 83 | .pipe(spriter({ 84 | baseUrl: "./", 85 | spriteSheetName:"[name].sprite.png",// repalce `[name]` to filename 86 | spriteSheetPath: "../images/sprite", 87 | filter: [ 88 | function(image) { 89 | return !(image.url.indexOf("?__sprite") === -1); 90 | } 91 | ] 92 | verbose:true 93 | })); // css sprite gen 94 | spriteOutput.css.pipe(gulp.dest(config.destScss)); 95 | spriteOutput.img.pipe(gulp.dest(config.destImages)); 96 | return merge(spriteOutput.css, spriteOutput.img); 97 | }) 98 | 99 | // see the parameter `options.imageUrl` 100 | 101 | // after that handle sass/less 102 | 103 | ``` 104 | 105 | 106 | Of course you may need to have more flexible configuration for spriting. And this plugin can give you something more! 107 | 108 | ## Options 109 | 110 | Sprite generator options is an object, that mix [spritesmith](https://github.com/Ensighten/spritesmith) 111 | options and plugin specific options. 112 | 113 | **Spritesmith parameters** *(all is optional)*: 114 | 115 | Property | Necessary | Type | Plugin default value 116 | -------------|-----------|----------|--------------------- 117 | [engine] | no | `String` | `"pixelsmith"` 118 | [algorithm] | no | `String` | `"top-down"` 119 | [padding] | no | `Number` | `0` 120 | [engineOpts] | no | `Object` | `{}` 121 | [exportOpts] | no | `Object` | `{}` 122 | 123 | More detailed explanation you can find on the [official page of spritesmith](https://github.com/Ensighten/spritesmith). 124 | 125 | **Plugin options** are: 126 | 127 | Property | Necessary | Type | Plugin default value 128 | ------------------|-----------|--------------|----------- 129 | spriteSheetName | **yes** | `String` | `null` 130 | [spriteSheetPath] | no | `String` | `null` 131 | [styleSheetName] | np | `String` | `null` 132 | [baseUrl] | no | `String` | `"./"` 133 | [imageUrl] | no | `Object` | `{imagesPath: './images'}` 134 | [retina] | no | `Boolean` | `true` 135 | [filter] | no | `Function[]` | `[]` 136 | [groupBy] | no | `Function[]` | `[]` 137 | [accumulate] | no | `Boolean` | `false` 138 | [verbose] | no | `Boolean` | `false` 139 | 140 | More detailed explanation is below. 141 | 142 | #### options.spriteSheetName 143 | Type: `String` 144 | Default value: `null` 145 | 146 | The one and last necessary parameter. Defines which *base* will have the name of the output sprite. Base means that if you will 147 | group your sprites by some criteria, name will change. 148 | 149 | #### options.spriteSheetPath 150 | Type: `String` 151 | Default value: `null` 152 | 153 | Can define relative path of references in the output stylesheet. 154 | 155 | #### options.styleSheetName 156 | Type: `String` 157 | Default value: `null` 158 | 159 | Defines the name of the output stylesheet. 160 | 161 | #### options.baseUrl 162 | Type: `String` 163 | Default value: `./` 164 | 165 | Defines where to find relatively defined image references in the input stylesheet. 166 | 167 | #### options.imageUrl 168 | Type: `Object` 169 | Default value: `{imagesPath: './images'}` 170 | 171 | Defines imagesPath for sass (`image-url`) where to find relatively defined image references in the input stylesheet. 172 | 173 | #### options.retina 174 | Type: `Boolean` 175 | Default value: `true` 176 | 177 | Defines whether or not to search for retina mark in the filename. If `true` then it will look for `@{number}x` syntax. 178 | For example: `image@2x.png`. 179 | 180 | #### options.filter 181 | Type: `Function[]`, `Function` 182 | Default value: `[]` 183 | 184 | Defines which filters apply to images found in the input stylesheet. Each filer called with image object, explained below. Each filter must return `Boolean` or 185 | [thenable `Promise`](https://github.com/promises-aplus/promises-spec), that will be resolved with `Boolean`. Each filter 186 | applies in series. 187 | 188 | #### options.groupBy 189 | Type: `Function[]`, `Function` 190 | Default value: `[]` 191 | 192 | Defines logic of how to group images found in the input stylesheet. Each grouper called with image object, explained below. Each filter must return `String|Null` or 193 | [thenable `Promise`](https://github.com/promises-aplus/promises-spec), that will be resolved with `String|Null`. Each grouper 194 | applies in series. 195 | 196 | #### options.accumulate 197 | Type: `Boolean` 198 | Default value: `false` 199 | 200 | Tells sprite-generator to accumulate images from multiple stylesheets. This mean, that images, found in stylesheet `A.css` and `B.css` will be accumulated and grouped in common sprite. 201 | > Note, that if `options.accumulate == true` then `options.styleSheetName` will not be used. 202 | 203 | #### options.verbose 204 | Type: `Boolean` 205 | Default value: `false` 206 | 207 | ### Filtering and grouping 208 | 209 | Sprite generator can filter and group images from the input stylesheet. 210 | 211 | Built in filters: 212 | - based on meta `skip` boolean flag; 213 | - based on `fs.exists` method to check, whether file exists or not. 214 | 215 | Built in groupers: 216 | - based on @2x image naming syntax, will produce `sprite.@{number}x.png` naming. (`@{number}x` image group). 217 | 218 | You can of course define your own filters or groupers. It will all based on main argument - the image object. 219 | 220 | ### The Image object 221 | 222 | Every filter or grouper is called with `image` object, that have these properties: 223 | 224 | Property | Type | Explanation 225 | ------------|------------|--------------------- 226 | replacement | `String` | String, found by pattern in the input stylesheet 227 | url | `String` | Url for image fount in the input stylesheet 228 | path | `String` | Resolved path for the image 229 | group | `String[]` | List of string, representing groups of image 230 | isImageUrl | `Boolean` | Boolean flag of `image-url` 231 | isRetina | `Boolean` | Boolean flag of retina image (@2x syntax) 232 | retinaRatio | `Number` | Ratio of retina image (@2x, @3x => 2, 3) 233 | meta | `Object` | Object of meta properties, defined in doc block (will explain below). 234 | 235 | 236 | ### Doc block meta properties 237 | 238 | You can also define some properties for the filters and groupers in doc block via this syntax: 239 | 240 | `{css definition} /* @meta {valid json} */` 241 | 242 | Example: 243 | 244 | ```css 245 | 246 | .my_class { 247 | background-image: url("/images/my.png"); /* @meta {"sprite": {"skip": true}} */ 248 | } 249 | 250 | ``` 251 | 252 | ***Important!*** Only object in `sprite` property of meta will be available in image object for filters and groupers. 253 | 254 | 255 | ### Flexible example 256 | 257 | ```javascript 258 | 259 | var gulp = require('gulp'), 260 | sprite = require('gulp-lazysprite'), 261 | Q = require('q'), 262 | sizeOf = require('image-size'); 263 | 264 | gulp.task('sprites', function() { 265 | var spriteOutput; 266 | 267 | spriteOutput = gulp.src("./src/css/*.css") 268 | .pipe(sprite({ 269 | baseUrl: "./", 270 | spriteSheetName: "sprite.png", 271 | spriteSheetPath: "/dist/image", 272 | styleSheetName: "stylesheet.css", 273 | 274 | filter: [ 275 | // this is a copy of built in filter of meta skip 276 | // do not forget to set it up in your stylesheets using doc block /* */ 277 | function(image) { 278 | return !image.meta.skip; 279 | } 280 | ], 281 | 282 | groupBy: [ 283 | // group images by width 284 | // useful when building background repeatable sprites 285 | function(image) { 286 | var deferred = Q.defer(); 287 | 288 | sizeOf(image.path, function(err, size) { 289 | deferred.resolve(size.width.toString()); 290 | }); 291 | 292 | return deferred.promise; 293 | } 294 | ] 295 | }); 296 | 297 | spriteOutput.css.pipe(gulp.dest("./dist/css")); 298 | spriteOutput.img.pipe(gulp.dest("./dist/image")); 299 | }); 300 | 301 | ``` 302 | 303 | ## License 304 | 305 | Licensed under the MIT license. 306 | 307 | 308 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | assert = require('chai').assert, 3 | File = require('gulp-util').File, 4 | path = require('path'), 5 | through = require('through2'), 6 | gulp = require('gulp'), 7 | mergestream = require('merge-stream'), 8 | // gulpif = require("gulp-if"), 9 | // rev = require("gulp-rev"), 10 | sprite = require('./../index'); 11 | 12 | function removecomment(str){ 13 | return str.replace(/\/\*[^\*]+\*\//g,"").replace(/\/\/[^\n]*/g,""); 14 | } 15 | 16 | function clearStr(str) { 17 | return removecomment(str.replace(/[\s,\r,\n,\t]/gi, "")); 18 | } 19 | 20 | 21 | function merge(stream, stream2){ 22 | return mergestream(stream, stream2); 23 | } 24 | 25 | describe('gulp-lazysprite', function(){ 26 | 27 | var test, fixtures, expectations; 28 | 29 | before(function() { 30 | test = path.resolve(__dirname, '.'); 31 | fixtures = path.resolve(test, 'fixtures'); 32 | expectations = path.resolve(test, 'expectations'); 33 | }); 34 | 35 | 36 | it("should accumulate images and create common sprite from multiple stylesheets", function(done) { 37 | var config, stream, errors, stylesheet, index; 38 | 39 | index = ['A.css', 'B.css']; 40 | 41 | stylesheet = { 42 | fixtures: [path.resolve(fixtures, 'A.css'), path.resolve(fixtures, 'B.css')], 43 | expectations: [path.resolve(expectations, 'A.css'), path.resolve(expectations, 'B.css')], 44 | }; 45 | 46 | errors = []; 47 | 48 | config = { 49 | src: [], 50 | // engine: "auto", 51 | algorithm: "top-down", 52 | padding: 0, 53 | engineOpts: {}, 54 | exportOpts: {}, 55 | 56 | baseUrl: fixtures, 57 | spriteSheetName: "sprite.png", 58 | spriteSheetPath: null, 59 | filter: [], 60 | groupBy: [], 61 | accumulate: true 62 | }; 63 | 64 | stream = sprite(config); 65 | 66 | stream.img.on('data', function (file) { 67 | try { 68 | assert.equal(file.path, config.spriteSheetName); 69 | } catch (err) { 70 | errors.push(err); 71 | } 72 | }); 73 | 74 | stream.css.on('data', function (file) { 75 | var id; 76 | 77 | id = index.indexOf(file.path); 78 | 79 | 80 | try { 81 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectations[id]).toString())); 82 | } catch (err) { 83 | errors.push(err); 84 | } 85 | }); 86 | 87 | stream.write(new File({ 88 | base: test, 89 | path: stylesheet.fixtures[0], 90 | contents: new Buffer(fs.readFileSync(stylesheet.fixtures[0])) 91 | })); 92 | 93 | stream.write(new File({ 94 | base: test, 95 | path: stylesheet.fixtures[1], 96 | contents: new Buffer(fs.readFileSync(stylesheet.fixtures[1])) 97 | })); 98 | 99 | merge(stream.img, stream.css).on('finish', function() { 100 | done(errors[0]); 101 | }); 102 | 103 | stream.end(); 104 | }); 105 | 106 | it("Should create sprite and change refs in stylesheet", function(done) { 107 | var config, stream, errors, stylesheet; 108 | 109 | stylesheet = { 110 | fixture: path.resolve(fixtures, 'stylesheet.css'), 111 | expectation: path.resolve(expectations, 'stylesheet.css') 112 | }; 113 | 114 | errors = []; 115 | 116 | config = { 117 | src: [], 118 | // engine: "auto", 119 | algorithm: "top-down", 120 | padding: 0, 121 | engineOpts: {}, 122 | exportOpts: {}, 123 | 124 | baseUrl: fixtures, 125 | spriteSheetName: "sprite.png", 126 | styleSheetName: "stylesheet.sprite.css", 127 | spriteSheetPath: null, 128 | filter: [], 129 | groupBy: [] 130 | }; 131 | 132 | stream = sprite(config); 133 | 134 | stream.img.on('data', function (file) { 135 | try { 136 | assert.equal(file.path, config.spriteSheetName); 137 | } catch (err) { 138 | errors.push(err); 139 | } 140 | }); 141 | 142 | stream.css.on('data', function (file) { 143 | try { 144 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectation).toString())); 145 | assert.equal(file.path, config.styleSheetName); 146 | } catch (err) { 147 | errors.push(err); 148 | } 149 | }); 150 | 151 | stream.write(new File({ 152 | base: test, 153 | path: stylesheet.fixture, 154 | contents: new Buffer(fs.readFileSync(stylesheet.fixture)) 155 | })); 156 | 157 | merge(stream.img, stream.css).on('finish', function() { 158 | done(errors[0]); 159 | }); 160 | 161 | stream.end(); 162 | }); 163 | 164 | it("Should create sprite for retina and change refs in stylesheet", function(done) { 165 | var config, stream, errors, 166 | stylesheet; 167 | 168 | stylesheet = { 169 | fixture: path.resolve(fixtures, 'stylesheet.retina.css'), 170 | expectation: path.resolve(expectations, 'stylesheet.retina.css') 171 | }; 172 | 173 | errors = []; 174 | 175 | config = { 176 | src: [], 177 | // engine: "auto", 178 | algorithm: "top-down", 179 | padding: 0, 180 | engineOpts: {}, 181 | exportOpts: {}, 182 | 183 | baseUrl: fixtures, 184 | spriteSheetName: "sprite.png", 185 | styleSheetName: "stylesheet.sprite.css", 186 | spriteSheetPath: null, 187 | filter: [], 188 | groupBy: [] 189 | }; 190 | 191 | stream = sprite(config); 192 | 193 | stream.img.on('data', function (file) { 194 | try { 195 | assert.equal(file.path, 'sprite.@2x.png'); 196 | } catch (err) { 197 | errors.push(err); 198 | } 199 | }); 200 | 201 | stream.css.on('data', function (file) { 202 | try { 203 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectation).toString())); 204 | assert.equal(file.path, config.styleSheetName); 205 | } catch (err) { 206 | errors.push(err); 207 | } 208 | }); 209 | 210 | stream.write(new File({ 211 | base: test, 212 | path: stylesheet.fixture, 213 | contents: new Buffer(fs.readFileSync(stylesheet.fixture)) 214 | })); 215 | 216 | merge(stream.img, stream.css).on('finish', function() { 217 | done(errors[0]); 218 | }); 219 | 220 | stream.end(); 221 | }); 222 | 223 | it("Should create sprite using groupBy and change refs in stylesheet", function(done) { 224 | var config, stream, errors, 225 | stylesheet; 226 | 227 | stylesheet = { 228 | fixture: path.resolve(fixtures, 'stylesheet.css'), 229 | expectation: path.resolve(expectations, 'stylesheet.groupby.css') 230 | }; 231 | 232 | errors = []; 233 | 234 | config = { 235 | src: [], 236 | // engine: "auto", 237 | algorithm: "top-down", 238 | padding: 0, 239 | engineOpts: {}, 240 | exportOpts: {}, 241 | 242 | baseUrl: fixtures, 243 | spriteSheetName: "sprite.png", 244 | styleSheetName: "stylesheet.sprite.css", 245 | spriteSheetPath: null, 246 | filter: [], 247 | groupBy: [] 248 | }; 249 | 250 | config.groupBy.push(function(image) { 251 | return "my"; 252 | }); 253 | 254 | stream = sprite(config); 255 | 256 | stream.img.on('data', function (file) { 257 | try { 258 | assert.equal(file.path, 'sprite.my.png'); 259 | } catch (err) { 260 | errors.push(err); 261 | } 262 | }); 263 | 264 | stream.css.on('data', function (file) { 265 | try { 266 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectation).toString())); 267 | assert.equal(file.path, config.styleSheetName); 268 | } catch (err) { 269 | errors.push(err); 270 | } 271 | }); 272 | 273 | stream.write(new File({ 274 | base: test, 275 | path: stylesheet.fixture, 276 | contents: new Buffer(fs.readFileSync(stylesheet.fixture)) 277 | })); 278 | 279 | merge(stream.img, stream.css).on('finish', function() { 280 | done(errors[0]); 281 | }); 282 | 283 | stream.end(); 284 | }); 285 | 286 | it("Should create sprite using filter and change refs in stylesheet", function(done) { 287 | var config, stream, errors, 288 | stylesheet; 289 | 290 | stylesheet = { 291 | fixture: path.resolve(fixtures, 'stylesheet.css'), 292 | expectation: path.resolve(expectations, 'stylesheet.filter.css') 293 | }; 294 | 295 | errors = []; 296 | 297 | config = { 298 | src: [], 299 | // engine: "auto", 300 | algorithm: "top-down", 301 | padding: 0, 302 | engineOpts: {}, 303 | exportOpts: {}, 304 | 305 | baseUrl: fixtures, 306 | spriteSheetName: "sprite.png", 307 | styleSheetName: "stylesheet.sprite.css", 308 | spriteSheetPath: null, 309 | filter: [], 310 | groupBy: [] 311 | }; 312 | 313 | config.filter.push(function(image) { 314 | return image.url != "/a.png"; 315 | }); 316 | 317 | stream = sprite(config); 318 | 319 | stream.img.on('data', function (file) { 320 | try { 321 | assert.equal(file.path, 'sprite.png'); 322 | } catch (err) { 323 | errors.push(err); 324 | } 325 | }); 326 | 327 | stream.css.on('data', function (file) { 328 | try { 329 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectation).toString())); 330 | assert.equal(file.path, config.styleSheetName); 331 | } catch (err) { 332 | errors.push(err); 333 | } 334 | }); 335 | 336 | stream.write(new File({ 337 | base: test, 338 | path: stylesheet.fixture, 339 | contents: new Buffer(fs.readFileSync(stylesheet.fixture)) 340 | })); 341 | 342 | merge(stream.img, stream.css).on('finish', function() { 343 | done(errors[0]); 344 | }); 345 | 346 | stream.end(); 347 | }); 348 | 349 | it("Should create sprite reading meta in doc block and change refs in stylesheet", function(done) { 350 | var config, stream, errors, 351 | meta; 352 | 353 | errors = []; 354 | 355 | config = { 356 | baseUrl: fixtures, 357 | spriteSheetName: "sprite.png", 358 | filter: [] 359 | }; 360 | 361 | meta = { 362 | sprite: { 363 | some: true, 364 | prop: 1, 365 | yes: "no" 366 | } 367 | }; 368 | 369 | config.filter.push(function(image) { 370 | try { 371 | assert.deepEqual(image.meta, meta.sprite); 372 | } catch (err) { 373 | errors.push(err); 374 | } 375 | }); 376 | 377 | stream = sprite(config); 378 | 379 | stream.write(new File({ 380 | base: test, 381 | path: path.resolve(fixtures, 'stylesheetdddd.css'), 382 | contents: new Buffer('.a { background-image: url("sprite.retina-2x.png"); /* @meta ' + JSON.stringify(meta) + ' */ }') 383 | })); 384 | 385 | merge(stream.img, stream.css).on('finish', function() { 386 | done(errors[0]); 387 | }); 388 | 389 | stream.end(); 390 | }); 391 | 392 | it("Should pipe properly", function(done) { 393 | var config, stream, errors, stylesheet, 394 | piped; 395 | 396 | piped = { 397 | img: 0, 398 | css: 0, 399 | main: 0 400 | }; 401 | 402 | stylesheet = { 403 | fixture: path.resolve(fixtures, 'stylesheet.css'), 404 | expectation: path.resolve(expectations, 'stylesheet.css') 405 | }; 406 | 407 | errors = []; 408 | 409 | config = { 410 | baseUrl: fixtures, 411 | spriteSheetName: "sprite.png", 412 | styleSheetName: "stylesheet.sprite.css", 413 | spriteSheetPath: null, 414 | filter: [], 415 | groupBy: [] 416 | }; 417 | 418 | stream = sprite(config); 419 | 420 | stream.img.on('data', function (file) { 421 | try { 422 | assert.equal(file.path, config.spriteSheetName); 423 | } catch (err) { 424 | errors.push(err); 425 | } 426 | }); 427 | 428 | stream.css.on('data', function (file) { 429 | try { 430 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectation).toString())); 431 | assert.equal(file.path, config.styleSheetName); 432 | } catch (err) { 433 | errors.push(err); 434 | } 435 | }); 436 | 437 | stream.write(new File({ 438 | base: test, 439 | path: stylesheet.fixture, 440 | contents: new Buffer(fs.readFileSync(stylesheet.fixture)) 441 | })); 442 | 443 | 444 | // stream.pipe(through.obj(function(file, enc, done) { 445 | // piped.main++; 446 | // try { 447 | // assert.instanceOf(file, File, "Piped in a main stream obj is not a File"); 448 | // } catch (err) { 449 | // errors.push(err); 450 | // } 451 | // })); 452 | 453 | stream.css.pipe(through.obj(function(file, enc, done) { 454 | piped.css++; 455 | try { 456 | assert.instanceOf(file, File, "Piped in a css stream obj is not a File"); 457 | } catch (err) { 458 | errors.push(err); 459 | } 460 | })); 461 | 462 | stream.img.pipe(through.obj(function(file, enc, done) { 463 | piped.img++; 464 | try { 465 | assert.instanceOf(file, File, "Piped in a img stream obj is not a File"); 466 | } catch (err) { 467 | errors.push(err); 468 | } 469 | })); 470 | 471 | // stream.img 472 | // .pipe(rev()) 473 | // .pipe(through.obj(function(file, enc, done) { 474 | // console.log('revision', file.path); 475 | // this.push(file); 476 | // done(); 477 | // })) 478 | // .pipe(rev.manifest()) 479 | // .pipe(through.obj(function(file, enc, done) { 480 | // console.log('manifest', file); 481 | // this.push(file); 482 | // done(); 483 | // })); 484 | 485 | merge(stream.img, stream.css).on('finish', function() { 486 | try { 487 | assert.equal(1, piped.img, "No piped data in img stream"); 488 | assert.equal(1, piped.css, "No piped data in css stream"); 489 | // assert.equal(1, piped.main, "No piped data in main stream"); 490 | } catch (err) { 491 | errors.push(err); 492 | } 493 | 494 | done(errors[0]); 495 | }); 496 | 497 | stream.end(); 498 | }); 499 | 500 | 501 | it("Should create scss", function(done){ 502 | var config, stream, errors, stylesheet; 503 | 504 | stylesheet = { 505 | fixture: path.resolve(fixtures, 'stylesheet.scss'), 506 | expectation: path.resolve(expectations, 'stylesheet.scss') 507 | }; 508 | 509 | errors = []; 510 | 511 | config = { 512 | src: [], 513 | // engine: "auto", 514 | algorithm: "top-down", 515 | padding: 0, 516 | engineOpts: {}, 517 | exportOpts: {}, 518 | 519 | baseUrl: fixtures, 520 | imageUrl: { imagesPath: fixtures }, 521 | spriteSheetName: "sprite.png", 522 | // styleSheetName: "stylesheet.sprite.css", 523 | spriteSheetPath: null, 524 | filter: [], 525 | groupBy: [], 526 | verbose: true 527 | }; 528 | 529 | stream = sprite(config); 530 | 531 | stream.img.on('data', function (file) { 532 | try { 533 | assert.equal(file.path, config.spriteSheetName); 534 | } catch (err) { 535 | errors.push(err); 536 | } 537 | }); 538 | 539 | stream.css.on('data', function (file) { 540 | try { 541 | assert.equal(clearStr(file.contents.toString()), clearStr(fs.readFileSync(stylesheet.expectation).toString())); 542 | assert.equal(file.path, "stylesheet.scss"); 543 | } catch (err) { 544 | errors.push(err); 545 | } 546 | }); 547 | stream.img.pipe(gulp.dest('./test/fixtures')); 548 | 549 | merge(stream.img, stream.css).on('finish', function(){ 550 | setTimeout(function(){ 551 | done(errors[0]); 552 | }, 100); 553 | }); 554 | 555 | stream.write(new File({ 556 | base: test, 557 | path: stylesheet.fixture, 558 | contents: new Buffer(fs.readFileSync(stylesheet.fixture)) 559 | })); 560 | 561 | stream.end(); 562 | }); 563 | 564 | }); 565 | 566 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var 3 | path = require('path'), 4 | spritesmith = require('spritesmith'), 5 | _ = require('lodash'), 6 | colors = require('colors'), 7 | fs = require('fs'), 8 | gutil = require('gulp-util'), 9 | util = require("util"), 10 | Q = require('q'), 11 | through = require('through2'), 12 | crypto = require('crypto'), 13 | File = gutil.File, 14 | PLUGIN_NAME = "gulp-lazysprite", 15 | debug; 16 | var log = function() { 17 | var args, sig; 18 | args = Array.prototype.slice.call(arguments); 19 | sig = '[' + colors.green(PLUGIN_NAME) + ']'; 20 | args.unshift(sig); 21 | gutil.log.apply(gutil, args); 22 | }; 23 | 24 | var md5 = function(text,len){ 25 | var hash = crypto.createHash('md5').update(text).digest('hex'); 26 | if(len > 0){ 27 | hash = hash.substr(0,len); 28 | } 29 | return hash; 30 | }; 31 | 32 | var removecomment = function(str){ 33 | return str.replace(/\/\*[^\*]+\*\//g,"").replace(/\/\/[^\n]*/g,""); 34 | }; 35 | 36 | var async = (function(){ 37 | // pick from // "async": "^0.2.10" 38 | var _each = _.forEach, 39 | _map = _.map; 40 | var _filter = function (eachfn, arr, iterator, callback) { 41 | var results = []; 42 | arr = _map(arr, function (x, i) { 43 | return {index: i, value: x}; 44 | }); 45 | eachfn(arr, function (x, callback) { 46 | iterator(x.value, function (v) { 47 | if (v) { 48 | results.push(x); 49 | } 50 | callback(); 51 | }); 52 | }, function (err) { 53 | callback(_map(results.sort(function (a, b) { 54 | return a.index - b.index; 55 | }), function (x) { 56 | return x.value; 57 | })); 58 | }); 59 | }; 60 | var _asyncMap = function (eachfn, arr, iterator, callback) { 61 | arr = _map(arr, function (x, i) { 62 | return {index: i, value: x}; 63 | }); 64 | if (!callback) { 65 | eachfn(arr, function (x, callback) { 66 | iterator(x.value, function (err) { 67 | callback(err); 68 | }); 69 | }); 70 | } else { 71 | var results = []; 72 | eachfn(arr, function (x, callback) { 73 | iterator(x.value, function (err, v) { 74 | results[x.index] = v; 75 | callback(err); 76 | }); 77 | }, function (err) { 78 | callback(err, results); 79 | }); 80 | } 81 | }; 82 | var only_once = function(fn) { 83 | var called = false; 84 | return function() { 85 | if (called) { 86 | throw new Error("Callback was already called."); 87 | } 88 | called = true; 89 | fn.apply(global || root, arguments); 90 | } 91 | }; 92 | var each = function (arr, iterator, callback) { 93 | callback = callback || function () {}; 94 | if (!arr.length) { 95 | return callback(); 96 | } 97 | var completed = 0; 98 | _each(arr, function (x) { 99 | iterator(x, only_once(done) ); 100 | }); 101 | function done(err) { 102 | if (err) { 103 | callback(err); 104 | callback = function () {}; 105 | } 106 | else { 107 | completed += 1; 108 | if (completed >= arr.length) { 109 | callback(); 110 | } 111 | } 112 | } 113 | }; 114 | 115 | var doParallel = function (fn) { 116 | return function () { 117 | var args = Array.prototype.slice.call(arguments); 118 | return fn.apply(null, [each].concat(args)); 119 | }; 120 | }; 121 | 122 | var eachSeries = function (arr, iterator, callback) { 123 | callback = callback || function () {}; 124 | if (!arr.length) { 125 | return callback(); 126 | } 127 | var completed = 0; 128 | var iterate = function () { 129 | iterator(arr[completed], function (err) { 130 | if (err) { 131 | callback(err); 132 | callback = function () {}; 133 | } 134 | else { 135 | completed += 1; 136 | if (completed >= arr.length) { 137 | callback(); 138 | } 139 | else { 140 | iterate(); 141 | } 142 | } 143 | }); 144 | }; 145 | iterate(); 146 | }; 147 | 148 | var reduce = function (arr, memo, iterator, callback) { 149 | eachSeries(arr, function (x, callback) { 150 | iterator(memo, x, function (err, v) { 151 | memo = v; 152 | callback(err); 153 | }); 154 | }, function (err) { 155 | callback(err, memo); 156 | }); 157 | }; 158 | return { 159 | reduce: reduce, 160 | filter: doParallel(_filter), 161 | map: doParallel(_asyncMap), 162 | each: each, 163 | eachSeries: eachSeries 164 | }; 165 | })(); 166 | 167 | var getImages = (function() { 168 | var httpRegex, imageRegex, filePathRegex, pngRegex, retinaRegex, autoSizeRegex; 169 | imageRegex = new RegExp('[^{}]*{[^{]*?background(?:-image)?:\\s*((?:image-)?url\\((["\']?)([\\w\\d\\s!:./\\-\\_@]*\\.[\\w?#]+)\\2\\))[^;]*\\;(?:\\s*\\/\\*\\s*@meta\\s*(\\{.*\\})\\s*\\*\\/)?[^}]*?}', 'ig'); 170 | // imageRegex = new RegExp('background(?:-image)?:[\\s]?(?:image-)?url\\(["\']?([\\w\\d\\s!:./\\-\\_@]*\\.[\\w?#]+)["\']?\\)[^;]*\\;(?:\\s*\\/\\*\\s*@meta\\s*(\\{.*\\})\\s*\\*\\/)?', 'ig'); 171 | retinaRegex = new RegExp('@(\\d)x\\.[a-z]{3,4}$', 'ig'); 172 | httpRegex = new RegExp('http[s]?', 'ig'); 173 | pngRegex = new RegExp('\\.png(?:\\?\\w*)?$', 'i'); 174 | filePathRegex = new RegExp('["\']?([\\w\\d\\s!:./\\-\\_@]*\\.[\\w?#]+)["\']?', 'ig'); 175 | autoSizeRegex = new RegExp('{[^{]*?(?=(?:(?!width:)[\\s\\S])*width:\\s*([^;]+);)(?:(?!height:)[\\s\\S])*height:\\s*([^;]+);[^}]*?}','ig'); 176 | 177 | return function(file, options) { 178 | var reference, images, block, 179 | retina, filePath, 180 | url, image, meta, basename, 181 | makeRegexp, content; 182 | 183 | content = file.contents.toString(); 184 | 185 | images = []; 186 | 187 | basename = path.basename(file.path); 188 | 189 | 190 | makeRegexp = (function() { 191 | var matchOperatorsRe = /[|\\/{}()[\]^$+*?.]/g; 192 | 193 | return function(str) { 194 | return str.replace(matchOperatorsRe, '\\$&'); 195 | }; 196 | })(); 197 | 198 | while ((reference = imageRegex.exec(content)) != null) { 199 | 200 | block = reference[0]; 201 | url = reference[3]; 202 | meta = reference[4]; 203 | 204 | 205 | var isImageUrl = /background(?:-image)?:[\s]?image-url/.exec(block) != null; 206 | 207 | 208 | image = { 209 | replacement: new RegExp('background(?:-image)?:\\s*(?:image-)?url\\(\\s?(["\']?)\\s?'+ makeRegexp(url) + '\\s?\\1\\s?\\)[^;]*\\;', 'gi'), 210 | url: url, 211 | group: [], 212 | isRetina: false, 213 | retinaRatio: 1, 214 | autoSize: false, 215 | meta: {} 216 | }; 217 | 218 | if (httpRegex.test(url)) { 219 | options.verbose && log(colors.cyan(basename) + ' > ' + url + ' has been skipped as it\'s an external resource!'); 220 | continue; 221 | } 222 | 223 | if (!pngRegex.test(url)) { 224 | options.verbose && log(colors.cyan(basename) + ' > ' + url + ' has been skipped as it\'s not a PNG!'); 225 | continue; 226 | } 227 | 228 | if (meta) { 229 | try { 230 | meta = JSON.parse(meta); 231 | meta.sprite && (image.meta = meta.sprite); 232 | } catch (err) { 233 | log(colors.cyan(basename) + ' > ' + colors.white('Can not parse meta json for ' + url) + ': "' + colors.red(err) + '"'); 234 | } 235 | } 236 | 237 | if (options.retina && (retina = retinaRegex.exec(url))) { 238 | image.isRetina = true; 239 | image.retinaRatio = retina[1]; 240 | } 241 | 242 | filePath = filePathRegex.exec(url)[0].replace(/['"]/g, ''); 243 | filePath = filePath.replace(/\?\w*$/,''); 244 | if(isImageUrl){ 245 | filePath = path.resolve(options.imageUrl.imagesPath, filePath); 246 | } else{ 247 | // if url to image is relative 248 | if(filePath.charAt(0) === "/") { 249 | filePath = path.resolve(options.baseUrl + filePath); 250 | } else { 251 | filePath = path.resolve(file.path.substring(0, file.path.lastIndexOf(path.sep)), filePath); 252 | } 253 | } 254 | var nocommentblock = removecomment(block); 255 | if(!autoSizeRegex.test(nocommentblock)){ 256 | // \/\*[^\*]+\*\/ 257 | // \/\/[^\n]+ 258 | image.autoSize = true; 259 | } 260 | 261 | image.path = filePath; 262 | image.isImageUrl = isImageUrl; 263 | 264 | // reset lastIndex 265 | [httpRegex, pngRegex, retinaRegex, filePathRegex, autoSizeRegex].forEach(function(regex) { 266 | regex.lastIndex = 0; 267 | }); 268 | 269 | images.push(image); 270 | } 271 | 272 | // reset lastIndex 273 | imageRegex.lastIndex = 0; 274 | // remove nulls and duplicates 275 | images = _.chain(images) 276 | .filter() 277 | .uniqBy(function(image) { 278 | return image.path; 279 | }) 280 | .value(); 281 | return Q(images) 282 | // apply user filters 283 | .then(function(images) { 284 | return Q.Promise(function(resolve, reject) { 285 | async.reduce( 286 | options.filter, 287 | images, 288 | function(images, filter, next) { 289 | async.filter( 290 | images, 291 | function(image, ok) { 292 | Q(filter(image)).then(ok); 293 | }, 294 | function(images) { 295 | next(null, images); 296 | } 297 | ); 298 | }, 299 | function(err, images) { 300 | if (err) { 301 | return reject(err); 302 | } 303 | resolve(images); 304 | } 305 | ); 306 | }); 307 | }) 308 | // apply user group processors 309 | .then(function(images) { 310 | return Q.Promise(function(resolve, reject) { 311 | async.reduce( 312 | options.groupBy, 313 | images, 314 | function(images, groupBy, next) { 315 | async.map(images, function(image, done) { 316 | Q(groupBy(image)) 317 | .then(function(group) { 318 | if (group) { 319 | image.group.push(group); 320 | } 321 | 322 | done(null, image); 323 | }) 324 | .catch(done); 325 | }, next); 326 | }, 327 | function(err, images) { 328 | if (err) { 329 | return reject(err); 330 | } 331 | 332 | resolve(images); 333 | } 334 | ); 335 | }); 336 | }); 337 | }; 338 | })(); 339 | 340 | var callSpriteSmithWith = (function() { 341 | var GROUP_DELIMITER = ".", 342 | GROUP_MASK = "*"; 343 | 344 | // helper function to minimize user group names symbols collisions 345 | function mask(toggle) { 346 | var from, to; 347 | 348 | from = new RegExp("[" + (toggle ? GROUP_DELIMITER : GROUP_MASK) + "]", "gi"); 349 | to = toggle ? GROUP_MASK : GROUP_DELIMITER; 350 | 351 | return function(value) { 352 | return value.replace(from, to); 353 | }; 354 | } 355 | 356 | return function(images, options) { 357 | var all; 358 | all = _.chain(images) 359 | .groupBy(function(image) { 360 | var tmp; 361 | 362 | tmp = image.group.map(mask(true)); 363 | tmp.unshift('_'); 364 | 365 | return tmp.join(GROUP_DELIMITER); 366 | }) 367 | .map(function(images, tmp) { 368 | var config, ratio; 369 | config = _.merge({}, options, { 370 | src: _.map(images, 'path') 371 | }); 372 | 373 | // enlarge padding, if its retina 374 | if (_.every(images, function(image) {return image.isRetina;})) { 375 | ratio = _.chain(images).map('retinaRatio').uniq().value(); 376 | if (ratio.length == 1) { 377 | config.padding = config.padding * ratio[0]; 378 | } 379 | } 380 | return Q.nfcall(spritesmith.run, config).then(function(result) { 381 | tmp = tmp.split(GROUP_DELIMITER); 382 | tmp.shift(); 383 | // append info about sprite group 384 | result.group = tmp.map(mask(false)); 385 | 386 | return result; 387 | }); 388 | }) 389 | .value(); 390 | 391 | 392 | return Q.all(all).then(function(results) { 393 | debug.images+= images.length; 394 | debug.sprites+= results.length; 395 | return results; 396 | }); 397 | }; 398 | })(); 399 | 400 | var updateReferencesIn = (function() { 401 | var template; 402 | 403 | template = _.template( 404 | '<% if(autoSize){%>width: <%= isRetina ? (coordinates.width / retinaRatio) : coordinates.width %>px;\n '+ 405 | 'height: <%= isRetina ? (coordinates.height / retinaRatio) : coordinates.height %>px;\n <%}%>'+ 406 | 'background-image: <%= isImageUrl ? "image-": ""%>url("<%= spriteSheetPath %>?v=<%= fileHash %>");\n ' + 407 | 'background-position: -<%= isRetina ? (coordinates.x / retinaRatio) : coordinates.x %>px -<%= isRetina ? (coordinates.y / retinaRatio) : coordinates.y %>px;\n ' + 408 | 'background-size: <%= isRetina ? (properties.width / retinaRatio) : properties.width %>px <%= isRetina ? (properties.height / retinaRatio) : properties.height %>px!important;' 409 | ); 410 | 411 | return function(file) { 412 | var content = file.contents.toString(); 413 | 414 | return function(results) { 415 | results.forEach(function(images) { 416 | images.forEach(function(image) { 417 | image.fileHash = image.spriteHash; 418 | content = content.replace(image.replacement, template(image)); 419 | }); 420 | }); 421 | 422 | return Q(content); 423 | }; 424 | }; 425 | })(); 426 | 427 | var exportSprites = (function() { 428 | function makeSpriteSheetPath(spriteSheetName, group) { 429 | var path; 430 | 431 | group || (group = []); 432 | 433 | if (group.length === 0) { 434 | return spriteSheetName; 435 | } 436 | 437 | path = spriteSheetName.split('.'); 438 | Array.prototype.splice.apply(path, [path.length - 1, 0].concat(group)); 439 | 440 | return path.join('.'); 441 | } 442 | 443 | return function(stream, options, filename) { 444 | return function(results) { 445 | results = results.map(function(result) { 446 | var sprite; 447 | 448 | result.path = makeSpriteSheetPath(options.spriteSheetName, result.group); 449 | result.path = result.path.replace('[name]',filename); 450 | 451 | sprite = new File({ 452 | path: result.path, 453 | contents: new Buffer(result.image, 'binary') 454 | }); 455 | result.spriteHash = md5(result.image,8); 456 | stream.push(sprite); 457 | 458 | options.verbose && log('Spritesheet', result.path, 'has been created'); 459 | 460 | return result; 461 | }); 462 | 463 | return results; 464 | }; 465 | }; 466 | })(); 467 | 468 | var exportStylesheet = function(stream, options) { 469 | return function(content) { 470 | var stylesheet; 471 | 472 | stylesheet = new File({ 473 | path: options.styleSheetName, 474 | contents: new Buffer(content) 475 | }); 476 | 477 | stream.push(stylesheet); 478 | 479 | options.verbose && log('Stylesheet', options.styleSheetName, 'has been created'); 480 | }; 481 | }; 482 | 483 | var mapSpritesProperties = function(images, options) { 484 | return function(results) { 485 | return results.map(function(result) { 486 | return _.map(result.coordinates, function(coordinates, path) { 487 | return _.merge(_.find(images, {path: path}), { 488 | spriteHash : result.spriteHash, 489 | coordinates: coordinates, 490 | spriteSheetPath: options.spriteSheetPath ? options.spriteSheetPath + "/" + result.path : result.path, 491 | properties: result.properties 492 | }); 493 | }); 494 | }); 495 | }; 496 | }; 497 | 498 | module.exports = function(options) { 499 | 'use strict'; 500 | var stream, styleSheetStream, spriteSheetStream; 501 | 502 | debug = { 503 | sprites: 0, 504 | images: 0 505 | }; 506 | 507 | options = _.merge({ 508 | src: [], 509 | engine: "pixelsmith", //"pngsmith", //auto 510 | algorithm: "top-down", 511 | padding: 0, 512 | engineOpts: {}, 513 | exportOpts: { 514 | 515 | }, 516 | imgOpts: { 517 | timeout: 30000 518 | }, 519 | 520 | baseUrl: './', 521 | imageUrl: { 522 | imagesPath: './images' 523 | }, 524 | retina: true, 525 | styleSheetName: null, 526 | spriteSheetName: null, 527 | spriteSheetPath: null, 528 | filter: [], 529 | groupBy: [], 530 | accumulate: false, 531 | verbose: false 532 | }, options || {}); 533 | 534 | // check necessary properties 535 | ['spriteSheetName'].forEach(function(property) { 536 | if (!options[property]) { 537 | throw new gutil.PluginError(PLUGIN_NAME, '`' + property + '` is required'); 538 | } 539 | }); 540 | 541 | // prepare filters 542 | if (_.isFunction(options.filter)) { 543 | options.filter = [options.filter]; 544 | } 545 | 546 | // prepare groupers 547 | if (_.isFunction(options.groupBy)) { 548 | options.groupBy = [options.groupBy]; 549 | } 550 | 551 | // add meta skip filter 552 | options.filter.unshift(function(image) { 553 | image.meta.skip && options.verbose && log(image.path + ' has been skipped as it meta declares to skip'); 554 | return !image.meta.skip; 555 | }); 556 | 557 | // add not existing filter 558 | options.filter.push(function(image) { 559 | var deferred = Q.defer(); 560 | fs.exists(image.path, function(exists) { 561 | !exists && options.verbose && log(image.path + ' has been skipped as it does not exist!'); 562 | deferred.resolve(exists); 563 | }); 564 | 565 | return deferred.promise; 566 | }); 567 | 568 | // add retina grouper if needed 569 | if (options.retina) { 570 | options.groupBy.unshift(function(image) { 571 | if (image.isRetina) { 572 | return "@" + image.retinaRatio + "x"; 573 | } 574 | 575 | return null; 576 | }); 577 | } 578 | 579 | // create output streams 580 | 581 | styleSheetStream = through.obj(); 582 | spriteSheetStream = through.obj(); 583 | 584 | var accumulatedFiles = []; 585 | 586 | stream = through.obj( 587 | function(file, enc, done) { 588 | if (file.isNull()) { 589 | this.push(file); // Do nothing if no contents 590 | return done(); 591 | } 592 | 593 | if (file.isStream()) { 594 | this.emit('error', new gutil.PluginError(PLUGIN_NAME, 'Streams is not supported!')); 595 | return done(); 596 | } 597 | 598 | if (file.isBuffer()) { 599 | // postpone evaluation, if we accumulating 600 | if (options.accumulate) { 601 | accumulatedFiles.push(file); 602 | // stream.push(file); 603 | done(); 604 | return; 605 | } 606 | 607 | var filename = file.path.split(path.sep).pop().split('.')[0]; 608 | 609 | getImages(file, options) 610 | .then(function(images) { 611 | callSpriteSmithWith(images, options) 612 | .then(exportSprites(spriteSheetStream, options, filename)) 613 | .then(mapSpritesProperties(images, options)) 614 | .then(updateReferencesIn(file)) 615 | .then(exportStylesheet(styleSheetStream, _.extend({}, options, { styleSheetName: options.styleSheetName || path.basename(file.path) }))) 616 | .then(function() { 617 | // pipe source file 618 | // stream.push(file); 619 | done(); 620 | }) 621 | .catch(function(err) { 622 | stream.emit('error', new gutil.PluginError(PLUGIN_NAME, err)); 623 | done(); 624 | }); 625 | }); 626 | 627 | 628 | return null; 629 | } else { 630 | this.emit('error', new gutil.PluginError(PLUGIN_NAME, 'Something went wrong!')); 631 | return done(); 632 | } 633 | }, 634 | // flush 635 | function(done) { 636 | var pending; 637 | 638 | if (options.accumulate) { 639 | pending = Q 640 | .all(accumulatedFiles.map(function(file) { 641 | return getImages(file, options); 642 | })) 643 | .then(function(list) { 644 | var images; 645 | 646 | return _.chain(list) 647 | .reduce(function(images, portion) { 648 | return images.concat(portion); 649 | }, []) 650 | .uniqBy(function(image) { 651 | return image.path; 652 | }) 653 | .value(); 654 | }) 655 | .then(function(images) { 656 | return callSpriteSmithWith(images, options) 657 | .then(exportSprites(spriteSheetStream, options)) 658 | .then(mapSpritesProperties(images, options)) 659 | .then(function(results) { 660 | return Q.all(accumulatedFiles.map(function(file) { 661 | return updateReferencesIn(file)(results) 662 | .then(exportStylesheet(styleSheetStream, _.extend({}, options, { styleSheetName: path.basename(file.path) }))); 663 | })); 664 | }); 665 | }) 666 | .catch(function(err) { 667 | stream.emit('error', new gutil.PluginError(PLUGIN_NAME, err)); 668 | done(); 669 | }); 670 | } else { 671 | pending = Q(); 672 | } 673 | 674 | pending.then(function() { 675 | 676 | // end streams 677 | styleSheetStream.push(null); 678 | spriteSheetStream.push(null); 679 | 680 | log(util.format("Created %d sprite(s) from %d images, saved %s% requests", debug.sprites, debug.images, debug.images > 0 ? ((debug.sprites / debug.images) * 100).toFixed(1) : 0)); 681 | 682 | done(); 683 | }); 684 | } 685 | ); 686 | 687 | stream.css = styleSheetStream; 688 | stream.img = spriteSheetStream; 689 | 690 | return stream; 691 | }; 692 | --------------------------------------------------------------------------------