├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── index.js ├── lib ├── get-background-image-declarations.js ├── get-meta-info-for-declaration.js ├── map-over-styles-and-transform-background-image-declarations.js ├── spriter-util.js ├── transform-file-with-sprite-sheet-data.js └── transform-map.js ├── package.json └── test ├── gulpfile.js ├── test-css ├── background-image.css ├── background.css ├── background.min.css ├── expected │ ├── background.min.css │ ├── external-image.css │ ├── keyframes.css │ ├── multiple-declarations-same-image.css │ ├── overall-include-explicit.css │ └── overall-include-implicit.css ├── external-image.css ├── keyframes.css ├── meta-include.css ├── minimal-for-bare-testing.css ├── multiple-backgrounds.css ├── multiple-declarations-same-image.css ├── non-existent-image.css └── overall.css ├── test-images ├── aenean-purple.png ├── dummy-blue.png ├── gif.gif ├── hello-orange.png ├── ipsum-cyan.png ├── jpg.jpg ├── lorem-green.png ├── massa-red.png ├── no-spritesheet.png ├── no-spritesheet.psd ├── transparent-hex.png └── vitae-yellow.png └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # Commenting this out is preferred by some people, see 3 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 4 | node_modules 5 | 6 | # Users Environment Variables 7 | .lock-wscript 8 | 9 | 10 | todo.md 11 | # Ignore the spritesheet that gets generated from running tests, etc 12 | spritesheet.png 13 | 14 | 15 | # Ignore .idea 16 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | 4 | todo.md 5 | spritesheet.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 0.11 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # v0.4.0 - 2017-2-6 3 | 4 | - Fix background images in `keyframes` at-rule 5 | - Thank you to [@dqmmpb](https://github.com/dqmmpb) for the [contribution](https://github.com/MadLittleMods/gulp-css-spriter/pull/7) 6 | 7 | 8 | # ... 9 | 10 | 11 | # v0.1.0 - 2015-4-29 12 | 13 | - First release 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/gulp-css-spriter.svg)](http://badge.fury.io/js/gulp-css-spriter) [![Build Status](https://travis-ci.org/MadLittleMods/gulp-css-spriter.svg?branch=master)](https://travis-ci.org/MadLittleMods/gulp-css-spriter) 2 | 3 | # gulp-css-spriter 4 | 5 | `gulp-css-spriter`, a [gulp](http://gulpjs.com/) plugin, looks through the CSS you pipe in and gathers all of the background images. It then creates a sprite sheet and updates the references in the CSS. 6 | 7 | You can easily exclude/include certain background image declarations using meta info in your styles([*see meta section below*](#meta-options)) and `includeMode` option([*see options section below*](#options)) depending on your use case. 8 | 9 | # Install 10 | 11 | ``` 12 | npm install gulp-css-spriter 13 | ``` 14 | 15 | # About 16 | 17 | `gulp-css-spriter` uses [spritesmith](https://www.npmjs.com/package/spritesmith) behind the scenes for creating the sprite sheet. 18 | 19 | # Usage 20 | 21 | ## Basic usage 22 | 23 | This is most likely the setup you will probably end up using. 24 | 25 | ```js 26 | var gulp = require('gulp'); 27 | var spriter = require('gulp-css-spriter'); 28 | 29 | gulp.task('css', function() { 30 | return gulp.src('./src/css/styles.css') 31 | .pipe(spriter({ 32 | // The path and file name of where we will save the sprite sheet 33 | 'spriteSheet': './dist/images/spritesheet.png', 34 | // Because we don't know where you will end up saving the CSS file at this point in the pipe, 35 | // we need a litle help identifying where it will be. 36 | 'pathToSpriteSheetFromCSS': '../images/spritesheet.png' 37 | })) 38 | .pipe(gulp.dest('./dist/css')); 39 | }); 40 | ``` 41 | 42 | ## Barebones usage 43 | 44 | The slimmest usage possible. 45 | 46 | ```js 47 | var gulp = require('gulp'); 48 | var spriter = require('gulp-css-spriter'); 49 | 50 | gulp.task('css', function() { 51 | return gulp.src('./styles.css') 52 | .pipe(spriter()) 53 | .pipe(gulp.dest('./')); 54 | }); 55 | ``` 56 | 57 | ## Minify CSS output usage 58 | 59 | If you want to use [@meta data](#meta-options) but are using a preprocessor such as Sass or Less, you will need to use a output style that doesn't strip comments. After piping the CSS through `gulp-css-spriter`, you can then run it through a CSS minifier(separate plugin), such as [`gulp-minify-css`](https://www.npmjs.com/package/gulp-minify-css). 60 | 61 | ```js 62 | var gulp = require('gulp'); 63 | var spriter = require('gulp-css-spriter'); 64 | var minifyCSS = require('gulp-minify-css'); // https://www.npmjs.com/package/gulp-minify-css 65 | 66 | gulp.task('css', function() { 67 | return gulp.src('./styles.css') 68 | .pipe(spriter()) 69 | .pipe(minifyCSS()) 70 | .pipe(gulp.dest('./')); 71 | }); 72 | ``` 73 | 74 | # Options 75 | 76 | - `options`: object - hash of options 77 | - `includeMode`: string - Determines whether meta data is necessary or not 78 | - Values: 'implicit', 'explicit' 79 | - Default: 'implicit' 80 | - For example, if `explicit`, you must have meta `include` as `true` in order for the image declarations to be included in the spritesheet: `/* @meta {"spritesheet": {"include": true}} */` 81 | - If left default at `implicit`, all images will be included in the spritesheet; except for image declarations with meta `include` as `false`: `/* @meta {"spritesheet": {"include": false}} */` 82 | - `spriteSheet`: string - The path and file name of where we will save the sprite sheet 83 | - Default: 'spritesheet.png' 84 | - `pathToSpriteSheetFromCSS`: string - Because we don't know where you will end up saving the CSS file at this point in the pipe, we need a litle help identifying where it will be. We will use this as the reference to the sprite sheet image in the CSS piped in. 85 | - Default: 'spritesheet.png' 86 | - `spriteSheetBuildCallback`: function - Same as the [spritesmith callback](https://www.npmjs.com/package/spritesmith#-spritesmith-params-callback-) 87 | - Default: null 88 | - Callback has a parameters as so: `function(err, result)` 89 | - `result.image`: Binary string representation of image 90 | - `result.coordinates`: Object mapping filename to {x, y, width, height} of image 91 | - `result.properties`: Object with metadata about spritesheet {width, height} 92 | - `silent`: bool - We ignore any images that are not found but are supposed to be sprited by default 93 | - Default: true 94 | - `shouldVerifyImagesExist`: bool - Check to make sure each image declared in the CSS exists before passing it to the spriter. Although silenced by default(`options.silent`), if an image is not found, an error is thrown. 95 | - Default: true 96 | - `spritesmithOptions`: object - Any option you pass in here, will be passed through to spritesmith. [See spritesmith options documenation](https://www.npmjs.com/package/spritesmith#-spritesmith-params-callback-) 97 | - Default: {} 98 | - `outputIndent`: bool - Used to format output CSS. You should be using a separate beautifier plugin. The reason the output code is reformatted is because it is easier to "parse->stringify" than "replace in place". 99 | - Default: '\t' 100 | 101 | 102 | # What we emit 103 | 104 | `gulp-css-spriter` emits the transformed CSS with updated image references to the sprite sheet as a normal Gulp [vinyl file](https://www.npmjs.com/package/vinyl). 105 | 106 | We also attach the binary sprite sheet image in `chunk.spriteSheet` in case you want to consume it later down the pipe. 107 | 108 | 109 | # Meta info 110 | 111 | `gulp-css-spriter` uses a JSON format to add info onto CSS declarations. 112 | 113 | The example below will exclude this declaration from the spritesheet. 114 | ```css 115 | /* @meta {"spritesheet": {"include": false}} */ 116 | background: url('../images/dummy-blue.png'); 117 | ``` 118 | 119 | Please note that if you are compiling from Sass/Less and are not getting correct results, to check the outputted CSS and make sure the comments are still in tact and on the line you expect. For Sass, use multiline `/* */` comment syntax and put them above declarations. This is because gulp-sass/node-sass/libsass removes single line comments and puts mult-line comments that are on the same line as a declaration, below the declaraton. 120 | 121 | The `@meta` comment data can be above or on the same line as the declaration for it to apply. 122 | ```css 123 | /* @meta {"spritesheet": {"include": false}} */ 124 | background: url('../images/dummy-blue.png'); /* @meta {"spritesheet": {"include": false}} */ 125 | ``` 126 | 127 | ## Meta options 128 | 129 | - `spritesheet`: object - hash of options that `gulp-css-spriter` will factor in 130 | - `include`: bool - determines whether or not the declaration should be included in the spritesheet. This can be left undefined if the `includeMode` is 'implicit' 131 | 132 | 133 | 134 | # What we emit 135 | 136 | `gulp-css-spriter` transforms your CSS image paths to the spritesheet appropriately then emits the CSS as a normal Gulp [vinyl file](https://www.npmjs.com/package/vinyl). 137 | 138 | - Gulp [vinyl file](https://www.npmjs.com/package/vinyl). We emit the CSS you passed in with transformed image paths 139 | 140 | ## Events 141 | 142 | ### `.on('log', function(message) { })` 143 | 144 | We emit log messages such as when a image defined in the CSS can't be found on disk. 145 | 146 | ### `.on('error', function(err) { })` 147 | 148 | A normal gulp error. There are a variety of errors. See source code for more details. 149 | 150 | 151 | 152 | # Testing 153 | 154 | We have a series of unit tests. We use [Mocha](http://mochajs.org/). 155 | 156 | Install Mocha globally: 157 | ``` 158 | npm install -g mocha 159 | ``` 160 | 161 | Run tests with: `mocha` or `npm test` 162 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // gulp-css-spriter: https://www.npmjs.com/package/gulp-css-spriter 2 | // Sprite Sheet Generation from CSS source files. 3 | // 4 | // By: Eric Eastwood: EricEastwood.com 5 | // 6 | // Meta info looks like: `/* @meta {"spritesheet": {"include": false}} */` 7 | 8 | var fs = require('fs-extra'); 9 | var path = require('path'); 10 | 11 | var Promise = require('bluebird'); 12 | var outputFile = Promise.promisify(fs.outputFile); 13 | var stat = Promise.promisify(fs.stat); 14 | 15 | var through = require('through2'); 16 | var extend = require('extend') 17 | var gutil = require('gulp-util'); 18 | 19 | var css = require('css'); 20 | var spritesmith = require('spritesmith'); 21 | var spritesmithBuild = Promise.promisify(spritesmith); 22 | 23 | 24 | var spriterUtil = require('./lib/spriter-util'); 25 | var getBackgroundImageDeclarations = require('./lib/get-background-image-declarations'); 26 | var transformFileWithSpriteSheetData = require('./lib/transform-file-with-sprite-sheet-data'); 27 | 28 | 29 | 30 | 31 | 32 | // consts 33 | const PLUGIN_NAME = 'gulp-css-spriter'; 34 | 35 | 36 | var spriter = function(options) { 37 | 38 | var defaults = { 39 | // ('implicit'|'explicit') 40 | 'includeMode': 'implicit', 41 | // The path and file name of where we will save the sprite sheet 42 | 'spriteSheet': 'spritesheet.png', 43 | // Because we don't know where you will end up saving the CSS file at this point in the pipe, 44 | // we need a litle help identifying where it will be. 45 | 'pathToSpriteSheetFromCSS': 'spritesheet.png', 46 | // Same as the spritesmith callback `function(err, result)` 47 | // result.image: Binary string representation of image 48 | // result.coordinates: Object mapping filename to {x, y, width, height} of image 49 | // result.properties: Object with metadata about spritesheet {width, height} 50 | 'spriteSheetBuildCallback': null, 51 | // If true, we ignore any images that are not found on disk 52 | // Note: this plugin will still emit an error if you do not verify that the images exist 53 | 'silent': true, 54 | // Check to make sure each image declared in the CSS exists before passing it to the spriter. 55 | // Although silenced by default(`options.silent`), if an image is not found, an error is thrown. 56 | 'shouldVerifyImagesExist': true, 57 | // Any option you pass in here, will be passed through to spritesmith 58 | // https://www.npmjs.com/package/spritesmith#-spritesmith-params-callback- 59 | 'spritesmithOptions': {}, 60 | // Used to format output CSS 61 | // You should be using a separate beautifier plugin 62 | 'outputIndent': '\t' 63 | }; 64 | 65 | var settings = extend({}, defaults, options); 66 | 67 | // Keep track of all the chunks that come in so that we can re-emit in the flush 68 | var chunkList = []; 69 | // We use an object for imageMap so we don't get any duplicates 70 | var imageMap = {}; 71 | // Check to make sure all of the images exist(`options.shouldVerifyImagesExist`) before trying to sprite them 72 | var imagePromiseArray = []; 73 | 74 | var stream = through.obj(function(chunk, enc, cb) { 75 | // http://nodejs.org/docs/latest/api/stream.html#stream_transform_transform_chunk_encoding_callback 76 | //console.log('transform'); 77 | 78 | // Each `chunk` is a vinyl file: https://www.npmjs.com/package/vinyl 79 | // chunk.cwd 80 | // chunk.base 81 | // chunk.path 82 | // chunk.contents 83 | 84 | 85 | if (chunk.isStream()) { 86 | self.emit('error', new gutil.PluginError(PLUGIN_NAME, 'Cannot operate on stream')); 87 | } 88 | else if (chunk.isBuffer()) { 89 | var contents = String(chunk.contents); 90 | 91 | var styles; 92 | try { 93 | styles = css.parse(contents, { 94 | 'silent': settings.silent, 95 | 'source': chunk.path 96 | }); 97 | } 98 | catch(err) { 99 | err.message = 'Something went wrong when parsing the CSS: ' + err.message; 100 | self.emit('log', err.message); 101 | 102 | // Emit an error if necessary 103 | if(!settings.silent) { 104 | self.emit('error', err); 105 | } 106 | } 107 | 108 | // Gather a list of all of the image declarations 109 | var chunkBackgroundImageDeclarations = getBackgroundImageDeclarations(styles, settings.includeMode); 110 | 111 | 112 | // Go through each declaration and gather the image paths 113 | // We find the new images that we found in this chunk verify they exist below 114 | // We use an object so we don't get any duplicates 115 | var newImagesfFromChunkMap = {}; 116 | var backgroundURLMatchAllRegex = new RegExp(spriterUtil.backgroundURLRegex.source, "gi"); 117 | chunkBackgroundImageDeclarations.forEach(function(declaration) { 118 | 119 | // Match each background image in the declaration (there could be multiple background images per value) 120 | spriterUtil.matchBackgroundImages(declaration.value, function(imagePath) { 121 | imagePath = path.join(path.dirname(chunk.path), imagePath); 122 | 123 | // If not already in the overall list of images collected 124 | // Add to the queue/list of images to be verified 125 | if(!imageMap[imagePath]) { 126 | newImagesfFromChunkMap[imagePath] = true; 127 | } 128 | 129 | // Add it to the main overall list to keep track 130 | imageMap[imagePath] = true; 131 | }); 132 | }); 133 | 134 | // Filter out any images that do not exist depending on `settings.shouldVerifyImagesExist` 135 | Object.keys(newImagesfFromChunkMap).forEach(function(imagePath) { 136 | var filePromise; 137 | if(settings.shouldVerifyImagesExist) { 138 | filePromise = stat(imagePath).then(function() { 139 | return { 140 | doesExist: true, 141 | path: imagePath 142 | }; 143 | }, function() { 144 | return { 145 | doesExist: false, 146 | path: imagePath 147 | }; 148 | }); 149 | } 150 | else { 151 | // If they don't want us to verify it exists, just pass it on with a undefined `doesExist` property 152 | filePromise = Promise.resolve({ 153 | doesExist: undefined, 154 | path: imagePath 155 | }); 156 | } 157 | 158 | imagePromiseArray.push(filePromise); 159 | }); 160 | 161 | 162 | // Keep track of each chunk and what declarations go with it 163 | // Because the positions/line numbers pertain to that chunk only 164 | chunkList.push(chunk); 165 | 166 | } 167 | 168 | 169 | // "call callback when the transform operation is complete." 170 | cb(); 171 | 172 | }, function(cb) { 173 | // http://nodejs.org/docs/latest/api/stream.html#stream_transform_flush_callback 174 | //console.log('flush'); 175 | var self = this; 176 | 177 | // Create an verified image list when all of the async checks have finished 178 | var imagesVerifiedPromise = Promise.settle(imagePromiseArray).then(function(results) { 179 | var imageList = []; 180 | Array.prototype.forEach.call(results, function(result) { 181 | imageInfo = result.value(); 182 | 183 | if(imageInfo.doesExist === true || imageInfo.doesExist === undefined) { 184 | imageList.push(imageInfo.path); 185 | } 186 | else { 187 | // Tell them that we could not find the image 188 | var logMessage = 'Image could not be found: ' + imageInfo.path; 189 | self.emit('log', logMessage); 190 | 191 | // Emit an error if necessary 192 | if(!settings.silent) { 193 | self.emit('error', new Error(logMessage)); 194 | } 195 | } 196 | }); 197 | 198 | return imageList; 199 | }); 200 | 201 | 202 | // Start spriting once we know the true list of images that exist 203 | imagesVerifiedPromise.then(function(imageList) { 204 | 205 | // Generate the spritesheet 206 | var spritesmithOptions = extend({}, settings.spritesmithOptions, { src: imageList }); 207 | 208 | var spriteSmithBuildPromise = spritesmithBuild(spritesmithOptions); 209 | 210 | spriteSmithBuildPromise.then(function(result) { 211 | 212 | var whenImageDealtWithPromise = new Promise(function(resolve, reject) { 213 | // Save out the spritesheet image 214 | if(settings.spriteSheet) { 215 | var spriteSheetSavedPromise = outputFile(settings.spriteSheet, result.image, 'binary').then(function() { 216 | 217 | //console.log("The file was saved!"); 218 | 219 | // Push all of the chunks back on the pipe 220 | chunkList.forEach(function(chunk) { 221 | 222 | var transformedChunk = chunk.clone(); 223 | 224 | try { 225 | transformedChunk = transformFileWithSpriteSheetData(transformedChunk, result.coordinates, settings.pathToSpriteSheetFromCSS, settings.includeMode, settings.silent, settings.outputIndent); 226 | } 227 | catch(err) { 228 | err.message = 'Something went wrong when transforming chunks: ' + err.message; 229 | self.emit('log', err.message); 230 | 231 | // Emit an error if necessary 232 | if(!settings.silent) { 233 | self.emit('error', err); 234 | } 235 | 236 | reject(err); 237 | } 238 | 239 | 240 | // Attach the spritesheet in case someone wants to use it down the pipe 241 | transformedChunk.spritesheet = result.image; 242 | 243 | // Push it back on the main pipe 244 | self.push(transformedChunk); 245 | }); 246 | 247 | 248 | }).catch(function(err) { 249 | settings.spriteSheetBuildCallback(err, null); 250 | reject(err); 251 | }); 252 | 253 | 254 | spriteSheetSavedPromise.then(function() { 255 | 256 | // Call a callback from the settings the user can hook onto 257 | if(settings.spriteSheetBuildCallback) { 258 | settings.spriteSheetBuildCallback(null, result); 259 | } 260 | 261 | resolve(); 262 | }); 263 | } 264 | else { 265 | resolve(); 266 | } 267 | }); 268 | 269 | whenImageDealtWithPromise.finally(function() { 270 | // "call callback when the flush operation is complete." 271 | cb(); 272 | }); 273 | 274 | 275 | }, function(err) { 276 | if(err) { 277 | err.message = 'Error creating sprite sheet image:\n' + err.message; 278 | self.emit('error', new gutil.PluginError(PLUGIN_NAME, err)); 279 | } 280 | }); 281 | 282 | 283 | }); 284 | 285 | 286 | 287 | 288 | 289 | }); 290 | 291 | // returning the file stream 292 | return stream; 293 | }; 294 | 295 | 296 | module.exports = spriter; -------------------------------------------------------------------------------- /lib/get-background-image-declarations.js: -------------------------------------------------------------------------------- 1 | 2 | var mapOverStylesAndTransformBackgroundImageDeclarations = require('./map-over-styles-and-transform-background-image-declarations'); 3 | 4 | 5 | // Pass in a styles object from `css.parse` 6 | // See main module for `includeMode` values 7 | function getBackgroundImageDeclarations(styles, includeMode) { 8 | includeMode = includeMode || 'implicit'; 9 | 10 | // First get all of the background image declarations 11 | var backgroundImageDeclarations = []; 12 | mapOverStylesAndTransformBackgroundImageDeclarations(styles, includeMode, function(declaration) { 13 | backgroundImageDeclarations.push(declaration); 14 | }); 15 | 16 | return backgroundImageDeclarations; 17 | } 18 | 19 | 20 | 21 | 22 | 23 | module.exports = getBackgroundImageDeclarations; -------------------------------------------------------------------------------- /lib/get-meta-info-for-declaration.js: -------------------------------------------------------------------------------- 1 | var extend = require('extend'); 2 | 3 | 4 | function getMetaInfoForDeclaration(declarations, declarationIndex) { 5 | var resultantMetaData = {}; 6 | 7 | if(declarationIndex > 0 && declarationIndex < declarations.length) { 8 | var mainDeclaration = declarations[declarationIndex]; 9 | if(mainDeclaration) { 10 | 11 | // Meta data can exist before or on the same line as the declaration. 12 | // Both Meta blocks are valid for the background property 13 | // ex. 14 | // /* @meta {"spritesheet": {"include": false}} */ 15 | // background: url('../images/aenean-purple.png'); /* @meta {"sprite": {"skip": true}} */ 16 | var beforeDeclaration = declarations[declarationIndex-1]; 17 | var afterDeclaration = declarations[declarationIndex+1]; 18 | 19 | 20 | if(beforeDeclaration) { 21 | // The before declaration should be valid no matter what (even if multiple lines above) 22 | // The parse function does all the nice checking for us 23 | extend(resultantMetaData, parseCommentDecarationForMeta(beforeDeclaration)); 24 | } 25 | 26 | if(afterDeclaration) { 27 | //console.log(mainDeclaration); 28 | //console.log(afterDeclaration); 29 | //console.log(afterDeclaration.position.start.line, mainDeclaration.position.start.line); 30 | // Make sure that the comment starts on the same line as the main declaration 31 | if((((afterDeclaration || {}).position || {}).start || {}).line === (((mainDeclaration || {}).position || {}).start || {}).line) { 32 | extend(resultantMetaData, parseCommentDecarationForMeta(afterDeclaration)); 33 | } 34 | } 35 | } 36 | } 37 | 38 | 39 | return resultantMetaData; 40 | } 41 | 42 | function parseCommentDecarationForMeta(declaration) { 43 | if(declaration.type === "comment") { 44 | //console.log(declaration); 45 | 46 | var metaMatches = declaration.comment.match(/@meta\s*({.*?}(?!}))/); 47 | 48 | if(metaMatches) { 49 | var parsedMeta = {}; 50 | try { 51 | parsedMeta = JSON.parse(metaMatches[1]); 52 | } 53 | catch(e) { 54 | //console.warn('Meta info was found but failed was not valid JSON'); 55 | } 56 | 57 | return parsedMeta; 58 | } 59 | } 60 | } 61 | 62 | 63 | 64 | module.exports = getMetaInfoForDeclaration; -------------------------------------------------------------------------------- /lib/map-over-styles-and-transform-background-image-declarations.js: -------------------------------------------------------------------------------- 1 | var extend = require('extend'); 2 | 3 | var spriterUtil = require('./spriter-util'); 4 | var getMetaInfoForDeclaration = require('./get-meta-info-for-declaration'); 5 | var transformMap = require('./transform-map'); 6 | 7 | 8 | 9 | function mapOverStylesAndTransformBackgroundImageDeclarations(styles, includeMode, cb) { 10 | // Map over all 11 | return mapOverStylesAndTransformAllBackgroundImageDeclarations(styles, function(declaration) { 12 | // Then filter down to only the proper ones (according to their meta data) 13 | if(shouldIncludeFactoringInMetaData(declaration.meta, includeMode)) { 14 | return cb.apply(null, arguments); 15 | } 16 | }); 17 | } 18 | 19 | // Boolean function to determine if the meta data permits using this declaration 20 | function shouldIncludeFactoringInMetaData(meta, includeMode) { 21 | var metaIncludeValue = (meta && meta.spritesheet && meta.spritesheet.include); 22 | var shouldIncludeBecauseImplicit = includeMode === 'implicit' && (metaIncludeValue === undefined || metaIncludeValue); 23 | var shouldIncludeBecauseExplicit = includeMode === 'explicit' && metaIncludeValue; 24 | var shouldInclude = shouldIncludeBecauseImplicit || shouldIncludeBecauseExplicit; 25 | 26 | // Only return declartions that shouldn't be skipped 27 | return shouldInclude; 28 | } 29 | 30 | 31 | 32 | // Pass in a styles object from `css.parse` 33 | // Loop over all of the styles and transform/modify the background image declarations 34 | // Returns a new styles object that has the transformed declarations 35 | function mapOverStylesAndTransformAllBackgroundImageDeclarations(styles, cb) { 36 | 37 | function transformDeclaration(declaration, declarationIndex, declarations) { 38 | // Clone the declartion to keep it immutable 39 | var transformedDeclaration = extend(true, {}, declaration); 40 | transformedDeclaration = attachInfoToDeclaration(declarations, declarationIndex); 41 | 42 | // background-image always has a url 43 | if(transformedDeclaration.property === 'background-image') { 44 | return cb(transformedDeclaration, declarationIndex, declarations); 45 | } 46 | // Background is a shorthand property so make sure `url()` is in there 47 | else if(transformedDeclaration.property === 'background') { 48 | var hasImageValue = spriterUtil.backgroundURLRegex.test(transformedDeclaration.value); 49 | 50 | if(hasImageValue) { 51 | return cb(transformedDeclaration, declarationIndex, declarations); 52 | } 53 | } 54 | 55 | // Wrap in an object so that the declaration doesn't get interpreted 56 | return { 57 | 'value': transformedDeclaration 58 | }; 59 | } 60 | 61 | // Clone the declartion to keep it immutable 62 | var transformedStyles = extend(true, {}, styles); 63 | 64 | // Go over each background `url()` declarations 65 | transformedStyles.stylesheet.rules.map(function(rule, ruleIndex) { 66 | if(rule.type === 'rule') { 67 | rule.declarations = transformMap(rule.declarations, transformDeclaration); 68 | } 69 | 70 | if(rule.type === 'keyframes') { 71 | // Get keyframe from keyframes 72 | rule.keyframes = transformMap(rule.keyframes, function(keyframe, keyframeIndex, keyframes) { 73 | // Get declarations from keyframe 74 | keyframe.declarations = transformMap(keyframe.declarations, transformDeclaration); 75 | }); 76 | } 77 | 78 | return rule; 79 | }); 80 | 81 | return transformedStyles; 82 | } 83 | 84 | // We do NOT directly modify the declaration in the rule 85 | // We pass the whole rule and current index so we can properly look at the metaData around each declaration 86 | // and add it to the declaration 87 | function attachInfoToDeclaration(declarations, declarationIndex) 88 | { 89 | if(declarations.length > declarationIndex) { 90 | // Clone the declartion to keep it immutable 91 | var declaration = extend(true, {}, declarations[declarationIndex]); 92 | 93 | var declarationMetaInfo = getMetaInfoForDeclaration(declarations, declarationIndex); 94 | 95 | // Add the meta into to the declaration 96 | declaration.meta = extend(true, {}, declaration.meta, declarationMetaInfo); 97 | 98 | return declaration; 99 | } 100 | 101 | return null; 102 | } 103 | 104 | 105 | 106 | module.exports = mapOverStylesAndTransformBackgroundImageDeclarations; 107 | -------------------------------------------------------------------------------- /lib/spriter-util.js: -------------------------------------------------------------------------------- 1 | 2 | var backgroundURLRegex = (/(.*?url\(["\']?)(.*?\.(?:png|jpg|gif))(["\']?\).*?;?)/i); 3 | 4 | 5 | function matchBackgroundImages(declarationValue, cb) { 6 | var backgroundURLMatchAllRegex = new RegExp(backgroundURLRegex.source, "gi"); 7 | 8 | return declarationValue.replace(backgroundURLMatchAllRegex, function(match, p1, p2, p3, offset, string) { 9 | var imagePath = p2; 10 | 11 | return p1 + cb(imagePath) + p3; 12 | }); 13 | } 14 | 15 | 16 | 17 | module.exports = { 18 | 'backgroundURLRegex': backgroundURLRegex, 19 | 'matchBackgroundImages':matchBackgroundImages 20 | }; 21 | -------------------------------------------------------------------------------- /lib/transform-file-with-sprite-sheet-data.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var extend = require('extend'); 3 | 4 | var css = require('css'); 5 | 6 | var spriterUtil = require('./spriter-util'); 7 | var mapOverStylesAndTransformBackgroundImageDeclarations = require('./map-over-styles-and-transform-background-image-declarations'); 8 | 9 | var backgroundURLMatchAllRegex = new RegExp(spriterUtil.backgroundURLRegex.source, "gi"); 10 | 11 | 12 | // Replace all the paths that need replacing 13 | function transformFileWithSpriteSheetData(vinylFile, coordinateMap, pathToSpriteSheetFromCSS, /*optional*/includeMode, /*optional*/isSilent, /*optional*/outputIndent) { 14 | includeMode = includeMode ? includeMode : 'implicit'; 15 | isSilent = (isSilent !== undefined) ? isSilent : false; 16 | outputIndent = outputIndent ? outputIndent : '\t'; 17 | 18 | // Clone the declartion to keep it immutable 19 | var resultantFile = vinylFile.clone(); 20 | 21 | if(resultantFile) { 22 | 23 | var styles = css.parse(String(resultantFile.contents), { 24 | 'silent': isSilent, 25 | 'source': vinylFile.path 26 | }); 27 | 28 | styles = mapOverStylesAndTransformBackgroundImageDeclarations(styles, includeMode, function(declaration) { 29 | 30 | var coordList = []; 31 | declaration.value = spriterUtil.matchBackgroundImages(declaration.value, function(imagePath) { 32 | 33 | var coords = coordinateMap[path.join(path.dirname(resultantFile.path), imagePath)]; 34 | //console.log('coords', coords); 35 | 36 | // Make sure there are coords for this image in the sprite sheet, otherwise we won't include it 37 | if(coords) { 38 | coordList.push("-" + coords.x + "px -" + coords.y + "px"); 39 | 40 | // If there are coords in the spritemap for this image, lets use the spritemap 41 | return pathToSpriteSheetFromCSS; 42 | } 43 | 44 | return imagePath; 45 | }); 46 | 47 | return { 48 | 'value': declaration, 49 | /* */ 50 | // Add the appropriate background position according to the spritemap 51 | 'insertElements': (function() { 52 | if(coordList.length > 0) { 53 | return { 54 | type: 'declaration', 55 | property: 'background-position', 56 | value: coordList.join(', ') 57 | }; 58 | } 59 | })() 60 | /* */ 61 | }; 62 | }); 63 | 64 | //console.log(styles.stylesheet.rules[0].declarations); 65 | 66 | // Put it back into string form 67 | var resultantContents = css.stringify(styles, { 68 | indent: outputIndent 69 | }); 70 | //console.log(resultantContents); 71 | resultantFile.contents = new Buffer(resultantContents); 72 | } 73 | 74 | return resultantFile; 75 | } 76 | 77 | module.exports = transformFileWithSpriteSheetData; -------------------------------------------------------------------------------- /lib/transform-map.js: -------------------------------------------------------------------------------- 1 | var extend = require('extend'); 2 | 3 | function transformMap(arr, cb) { 4 | var resultantArray = extend(true, [], arr); 5 | 6 | for(var i = 0; i < resultantArray.length; i++) { 7 | var el = resultantArray[i]; 8 | 9 | var result = cb(el, i, resultantArray); 10 | 11 | var defaults = { 12 | value: el, 13 | insertElements: [], 14 | appendElements: [] 15 | }; 16 | 17 | // You can pass in a bare value or as the `value` property of an object 18 | result = typeof result === 'object' ? result : { value: result }; 19 | // Massage the result into shape 20 | result = extend({}, defaults, result); 21 | 22 | 23 | // Transform the current value 24 | resultantArray[i] = result.value ? result.value : result; 25 | 26 | // Insert after the current element 27 | var insertElements = [].concat(result.insertElements); 28 | if(insertElements.length > 0) { 29 | Array.prototype.splice.apply(resultantArray, [i+1, 0].concat(insertElements)); 30 | } 31 | 32 | // Add the elements onto the end 33 | var appendElements = [].concat(result.appendElements); 34 | if(appendElements.length > 0) { 35 | resultantArray = resultantArray.concat(appendElements); 36 | } 37 | } 38 | 39 | return resultantArray; 40 | } 41 | 42 | 43 | module.exports = transformMap; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-css-spriter", 3 | "version": "0.4.0", 4 | "description": "Sprite Sheet Generation from CSS source files. The best and different approach to sprite sheets.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/MadLittleMods/gulp-css-spriter.git" 9 | }, 10 | "keywords": [ 11 | "css", 12 | "gulp", 13 | "gulpplugin", 14 | "gulpfriendly", 15 | "sprite", 16 | "spritesheet", 17 | "sass", 18 | "less" 19 | ], 20 | "author": "Eric Eastwood (http://ericeastwood.com/)", 21 | "license": "MIT", 22 | "bugs": "https://github.com/MadLittleMods/gulp-css-spriter/issues", 23 | "scripts": { 24 | "test": "mocha" 25 | }, 26 | "dependencies": { 27 | "bluebird": "^2.9.3", 28 | "css": "^2.1.0", 29 | "extend": "^2.0.0", 30 | "fs-extra": "^0.14.0", 31 | "gulp-util": "^3.0.1", 32 | "spritesmith": "^1.0.3", 33 | "through2": "^0.6.2" 34 | }, 35 | "devDependencies": { 36 | "chai": "^1.10.0", 37 | "chai-as-promised": "^4.1.1", 38 | "event-stream": "^3.2.2", 39 | "gulp": "^3.8.10", 40 | "mocha": "^2.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/gulpfile.js: -------------------------------------------------------------------------------- 1 | // Manual way to run/try the plugin 2 | 3 | // Include gulp 4 | var gulp = require('gulp'); 5 | var es = require('event-stream'); 6 | 7 | var spriter = require('../'); 8 | 9 | 10 | gulp.task('sprite', function() { 11 | 12 | // './test-css/minimal-for-bare-testing.css' 13 | return gulp.src('./test-css/overall.css') 14 | .pipe(spriter({ 15 | 'includeMode': 'implicit', 16 | 'spriteSheet': './dist/images/spritesheet.png', 17 | 'pathToSpriteSheetFromCSS': '../images/spritesheet.png' 18 | })) 19 | .pipe(es.wait(function(err, body) { 20 | console.log(arguments); 21 | })); 22 | }); 23 | 24 | 25 | // Default Task 26 | gulp.task('default', ['sprite']); -------------------------------------------------------------------------------- /test/test-css/background-image.css: -------------------------------------------------------------------------------- 1 | .vitae 2 | { 3 | width: 64px; 4 | height: 64px; 5 | 6 | background-image: url('../test-images/vitae-yellow.png'); 7 | } 8 | 9 | .qwerqwer 10 | { 11 | width: 200px; 12 | width: 400px; 13 | } -------------------------------------------------------------------------------- /test/test-css/background.css: -------------------------------------------------------------------------------- 1 | .aenean 2 | { 3 | width: 120px; 4 | height: 100px; 5 | background: url('../test-images/aenean-purple.png'); 6 | } 7 | 8 | .massa 9 | { 10 | width: 120px; 11 | height: 100px; 12 | background: #ffff00 url('../test-images/vitae-yellow.png'); 13 | } 14 | 15 | 16 | .loremdummy 17 | { 18 | overflow: hidden; 19 | width: 200px; 20 | width: 400px; 21 | 22 | background: #cccccc; 23 | } -------------------------------------------------------------------------------- /test/test-css/background.min.css: -------------------------------------------------------------------------------- 1 | .aenean{width:120px;height:100px;background:url(../test-images/aenean-purple.png)}.massa{width:120px;height:100px;background:url(../test-images/vitae-yellow.png) #ff0}.loremdummy{overflow:hidden;width:200px;width:400px;background:#ccc} -------------------------------------------------------------------------------- /test/test-css/expected/background.min.css: -------------------------------------------------------------------------------- 1 | .aenean { 2 | width: 120px; 3 | height: 100px; 4 | background: url(spritesheet.png); 5 | background-position: -0px -64px; 6 | } 7 | 8 | .massa { 9 | width: 120px; 10 | height: 100px; 11 | background: url(spritesheet.png) #ff0; 12 | background-position: -0px -0px; 13 | } 14 | 15 | .loremdummy { 16 | overflow: hidden; 17 | width: 200px; 18 | width: 400px; 19 | background: #ccc; 20 | } -------------------------------------------------------------------------------- /test/test-css/expected/external-image.css: -------------------------------------------------------------------------------- 1 | .external-image { 2 | width: 538px; 3 | height: 190px; 4 | background: url('https://www.google.com/images/srpr/logo11w.png'); 5 | } -------------------------------------------------------------------------------- /test/test-css/expected/keyframes.css: -------------------------------------------------------------------------------- 1 | .aenean { 2 | width: 120px; 3 | height: 100px; 4 | background: url('spritesheet.png'); 5 | background-position: -0px -144px; 6 | } 7 | 8 | .massa { 9 | width: 120px; 10 | height: 100px; 11 | background: #ffff00 url('spritesheet.png'); 12 | background-position: -0px -0px; 13 | } 14 | 15 | .loremdummy { 16 | overflow: hidden; 17 | width: 200px; 18 | width: 400px; 19 | background: #cccccc; 20 | } 21 | 22 | .animation { 23 | animation: keyframes 1s; 24 | } 25 | 26 | @keyframes keyframes { 27 | 0% { 28 | background: url('spritesheet.png'); 29 | background-position: -0px -244px; 30 | } 31 | 32 | 100% { 33 | background: url('spritesheet.png'); 34 | background-position: -0px -64px; 35 | } 36 | } -------------------------------------------------------------------------------- /test/test-css/expected/multiple-declarations-same-image.css: -------------------------------------------------------------------------------- 1 | .aenean { 2 | width: 120px; 3 | height: 100px; 4 | background: url('spritesheet.png'); 5 | background-position: -0px -0px; 6 | } 7 | 8 | .aenean2 { 9 | width: 120px; 10 | height: 100px; 11 | /* @meta {"spritesheet": {"include": false}} */ 12 | background: url('../test-images/aenean-purple.png'); 13 | } 14 | 15 | .aenean3 { 16 | width: 120px; 17 | height: 100px; 18 | background: url('spritesheet.png'); 19 | background-position: -0px -0px; 20 | } -------------------------------------------------------------------------------- /test/test-css/expected/overall-include-explicit.css: -------------------------------------------------------------------------------- 1 | .aenean { 2 | width: 120px; 3 | height: 100px; 4 | background: url('../test-images/aenean-purple.png'); 5 | } 6 | 7 | .aenean-double-quotes { 8 | width: 120px; 9 | height: 100px; 10 | background: url("../test-images/aenean-purple.png"); 11 | } 12 | 13 | .jpg { 14 | width: 100px; 15 | height: 160px; 16 | background: url('../test-images/jpg.jpg'); 17 | } 18 | 19 | .gif { 20 | width: 100px; 21 | height: 160px; 22 | background: url('../test-images/gif.gif'); 23 | } 24 | 25 | .massa { 26 | width: 120px; 27 | height: 100px; 28 | background: #ffff00 url('../test-images/massa-red.png'); 29 | } 30 | 31 | .not-going-to-be-found { 32 | width: 120px; 33 | height: 100px; 34 | background: #ffff00 url('../test-images/not-going-to-be-found.png'); 35 | } 36 | 37 | .loremdummy { 38 | overflow: hidden; 39 | width: 200px; 40 | width: 400px; 41 | background: #cccccc; 42 | } 43 | 44 | .vitae { 45 | width: 64px; 46 | height: 64px; 47 | background-image: url('../test-images/vitae-yellow.png'); 48 | } 49 | 50 | .qwerqwer { 51 | width: 200px; 52 | width: 400px; 53 | } 54 | 55 | .hello-with-hex { 56 | width: 300px; 57 | height: 80px; 58 | background: url('../test-images/transparent-hex.png'), url('../test-images/hello-orange.png'); 59 | } 60 | 61 | .meta-include-false { 62 | width: 150px; 63 | height: 100px; 64 | /* @meta {"spritesheet": {"include": false}} */ 65 | background: url('../test-images/no-spritesheet.png'); 66 | } 67 | 68 | .meta-include-true { 69 | width: 64px; 70 | height: 64px; 71 | /* @meta {"spritesheet": {"include": true}} */ 72 | background-image: url('spritesheet.png'); 73 | background-position: -0px -0px; 74 | } 75 | 76 | .external-image { 77 | width: 538px; 78 | height: 190px; 79 | background: url('https://www.google.com/images/srpr/logo11w.png'); 80 | } -------------------------------------------------------------------------------- /test/test-css/expected/overall-include-implicit.css: -------------------------------------------------------------------------------- 1 | .aenean { 2 | width: 120px; 3 | height: 100px; 4 | background: url('spritesheet.png'); 5 | background-position: -0px -308px; 6 | } 7 | 8 | .aenean-double-quotes { 9 | width: 120px; 10 | height: 100px; 11 | background: url("spritesheet.png"); 12 | background-position: -0px -308px; 13 | } 14 | 15 | .jpg { 16 | width: 100px; 17 | height: 160px; 18 | background: url('spritesheet.png'); 19 | background-position: -0px -408px; 20 | } 21 | 22 | .gif { 23 | width: 100px; 24 | height: 160px; 25 | background: url('spritesheet.png'); 26 | background-position: -0px -0px; 27 | } 28 | 29 | .massa { 30 | width: 120px; 31 | height: 100px; 32 | background: #ffff00 url('spritesheet.png'); 33 | background-position: -0px -100px; 34 | } 35 | 36 | .not-going-to-be-found { 37 | width: 120px; 38 | height: 100px; 39 | background: #ffff00 url('../test-images/not-going-to-be-found.png'); 40 | } 41 | 42 | .loremdummy { 43 | overflow: hidden; 44 | width: 200px; 45 | width: 400px; 46 | background: #cccccc; 47 | } 48 | 49 | .vitae { 50 | width: 64px; 51 | height: 64px; 52 | background-image: url('spritesheet.png'); 53 | background-position: -0px -164px; 54 | } 55 | 56 | .qwerqwer { 57 | width: 200px; 58 | width: 400px; 59 | } 60 | 61 | .hello-with-hex { 62 | width: 300px; 63 | height: 80px; 64 | background: url('spritesheet.png'), url('spritesheet.png'); 65 | background-position: -0px -50px, -0px -228px; 66 | } 67 | 68 | .meta-include-false { 69 | width: 150px; 70 | height: 100px; 71 | /* @meta {"spritesheet": {"include": false}} */ 72 | background: url('../test-images/no-spritesheet.png'); 73 | } 74 | 75 | .meta-include-true { 76 | width: 64px; 77 | height: 64px; 78 | /* @meta {"spritesheet": {"include": true}} */ 79 | background-image: url('spritesheet.png'); 80 | background-position: -0px -568px; 81 | } 82 | 83 | .external-image { 84 | width: 538px; 85 | height: 190px; 86 | background: url('https://www.google.com/images/srpr/logo11w.png'); 87 | } -------------------------------------------------------------------------------- /test/test-css/external-image.css: -------------------------------------------------------------------------------- 1 | .external-image 2 | { 3 | width: 538px; 4 | height: 190px; 5 | background: url('https://www.google.com/images/srpr/logo11w.png'); 6 | } -------------------------------------------------------------------------------- /test/test-css/keyframes.css: -------------------------------------------------------------------------------- 1 | .aenean 2 | { 3 | width: 120px; 4 | height: 100px; 5 | background: url('../test-images/aenean-purple.png'); 6 | } 7 | 8 | .massa 9 | { 10 | width: 120px; 11 | height: 100px; 12 | background: #ffff00 url('../test-images/vitae-yellow.png'); 13 | } 14 | 15 | 16 | .loremdummy 17 | { 18 | overflow: hidden; 19 | width: 200px; 20 | width: 400px; 21 | 22 | background: #cccccc; 23 | } 24 | 25 | .animation { 26 | animation: keyframes 1s; 27 | } 28 | 29 | @keyframes keyframes { 30 | 0% { 31 | background: url('../test-images/dummy-blue.png'); 32 | } 33 | 34 | 100% { 35 | background: url('../test-images/hello-orange.png'); 36 | } 37 | } -------------------------------------------------------------------------------- /test/test-css/meta-include.css: -------------------------------------------------------------------------------- 1 | .meta-include-false 2 | { 3 | width: 150px; 4 | height: 100px; 5 | /* @meta {"spritesheet": {"include": false}} */ 6 | background: url('../test-images/no-spritesheet.png'); 7 | } 8 | 9 | .meta-include-true 10 | { 11 | width: 64px; 12 | height: 64px; 13 | 14 | /* @meta {"spritesheet": {"include": true}} */ 15 | background-image: url('../test-images/vitae-yellow.png'); 16 | } -------------------------------------------------------------------------------- /test/test-css/minimal-for-bare-testing.css: -------------------------------------------------------------------------------- 1 | .massa 2 | { 3 | background: url('../test-images/vitae-yellow.png'); 4 | } -------------------------------------------------------------------------------- /test/test-css/multiple-backgrounds.css: -------------------------------------------------------------------------------- 1 | .hello-with-hex 2 | { 3 | width: 300px; 4 | height: 80px; 5 | 6 | background: url('../test-images/transparent-hex.png'), url('../test-images/hello-orange.png'); 7 | } 8 | 9 | .massa-with-hex 10 | { 11 | width: 64px; 12 | height: 64px; 13 | 14 | background-image: url('../test-images/transparent-hex.png'), url('../test-images/massa-red.png'); 15 | } -------------------------------------------------------------------------------- /test/test-css/multiple-declarations-same-image.css: -------------------------------------------------------------------------------- 1 | .aenean 2 | { 3 | width: 120px; 4 | height: 100px; 5 | background: url('../test-images/aenean-purple.png'); 6 | } 7 | 8 | .aenean2 9 | { 10 | width: 120px; 11 | height: 100px; 12 | /* @meta {"spritesheet": {"include": false}} */ 13 | background: url('../test-images/aenean-purple.png'); 14 | } 15 | 16 | .aenean3 17 | { 18 | width: 120px; 19 | height: 100px; 20 | background: url('../test-images/aenean-purple.png'); 21 | } -------------------------------------------------------------------------------- /test/test-css/non-existent-image.css: -------------------------------------------------------------------------------- 1 | .not-going-to-be-found 2 | { 3 | width: 120px; 4 | height: 100px; 5 | background: #ffff00 url('../test-images/not-going-to-be-found.png'); 6 | } -------------------------------------------------------------------------------- /test/test-css/overall.css: -------------------------------------------------------------------------------- 1 | .aenean 2 | { 3 | width: 120px; 4 | height: 100px; 5 | background: url('../test-images/aenean-purple.png'); 6 | } 7 | 8 | .aenean-double-quotes 9 | { 10 | width: 120px; 11 | height: 100px; 12 | background: url("../test-images/aenean-purple.png"); 13 | } 14 | 15 | .jpg 16 | { 17 | width: 100px; 18 | height: 160px; 19 | background: url('../test-images/jpg.jpg'); 20 | } 21 | 22 | .gif 23 | { 24 | width: 100px; 25 | height: 160px; 26 | background: url('../test-images/gif.gif'); 27 | } 28 | 29 | .massa 30 | { 31 | width: 120px; 32 | height: 100px; 33 | background: #ffff00 url('../test-images/massa-red.png'); 34 | } 35 | 36 | .not-going-to-be-found 37 | { 38 | width: 120px; 39 | height: 100px; 40 | background: #ffff00 url('../test-images/not-going-to-be-found.png'); 41 | } 42 | 43 | .loremdummy 44 | { 45 | overflow: hidden; 46 | width: 200px; 47 | width: 400px; 48 | 49 | background: #cccccc; 50 | } 51 | 52 | .vitae 53 | { 54 | width: 64px; 55 | height: 64px; 56 | 57 | background-image: url('../test-images/vitae-yellow.png'); 58 | } 59 | 60 | .qwerqwer 61 | { 62 | width: 200px; 63 | width: 400px; 64 | } 65 | 66 | .hello-with-hex 67 | { 68 | width: 300px; 69 | height: 80px; 70 | 71 | background: url('../test-images/transparent-hex.png'), url('../test-images/hello-orange.png'); 72 | } 73 | 74 | .meta-include-false 75 | { 76 | width: 150px; 77 | height: 100px; 78 | /* @meta {"spritesheet": {"include": false}} */ 79 | background: url('../test-images/no-spritesheet.png'); 80 | } 81 | 82 | .meta-include-true 83 | { 84 | width: 64px; 85 | height: 64px; 86 | 87 | /* @meta {"spritesheet": {"include": true}} */ 88 | background-image: url('../test-images/dummy-blue.png'); 89 | } 90 | 91 | .external-image 92 | { 93 | width: 538px; 94 | height: 190px; 95 | background: url('https://www.google.com/images/srpr/logo11w.png'); 96 | } -------------------------------------------------------------------------------- /test/test-images/aenean-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/aenean-purple.png -------------------------------------------------------------------------------- /test/test-images/dummy-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/dummy-blue.png -------------------------------------------------------------------------------- /test/test-images/gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/gif.gif -------------------------------------------------------------------------------- /test/test-images/hello-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/hello-orange.png -------------------------------------------------------------------------------- /test/test-images/ipsum-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/ipsum-cyan.png -------------------------------------------------------------------------------- /test/test-images/jpg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/jpg.jpg -------------------------------------------------------------------------------- /test/test-images/lorem-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/lorem-green.png -------------------------------------------------------------------------------- /test/test-images/massa-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/massa-red.png -------------------------------------------------------------------------------- /test/test-images/no-spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/no-spritesheet.png -------------------------------------------------------------------------------- /test/test-images/no-spritesheet.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/no-spritesheet.psd -------------------------------------------------------------------------------- /test/test-images/transparent-hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/transparent-hex.png -------------------------------------------------------------------------------- /test/test-images/vitae-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MadLittleMods/gulp-css-spriter/bff7f92c55337ca937cd8e8ea1e25321aa7842ae/test/test-images/vitae-yellow.png -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var Promise = require('bluebird'); 3 | 4 | var chai = require('chai'); 5 | var expect = require('chai').expect; 6 | var chaiAsPromised = require('chai-as-promised'); 7 | chai.use(chaiAsPromised); 8 | 9 | var extend = require('extend'); 10 | 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | var readFile = Promise.promisify(fs.readFile); 14 | 15 | var gutil = require('gulp-util'); 16 | 17 | var css = require('css'); 18 | 19 | 20 | // The main gulp plugin to test 21 | var spriter = require('../'); 22 | 23 | // Test out some individual components 24 | var getBackgroundImageDeclarations = require('../lib/get-background-image-declarations'); 25 | var transformMap = require('../lib/transform-map'); 26 | 27 | 28 | 29 | 30 | // We use 'algorithm': 'top-down' because it easier to have a consistent packing so the expected doesn't have to be updated 31 | describe('gulp-css-spriter', function() { 32 | it('should emit a buffer', function() { 33 | var spriterPromise = spriterTest({}).then(function(result) { 34 | return result.isBuffer(); 35 | }); 36 | 37 | return expect(spriterPromise).to.eventually.equal(true); 38 | }); 39 | 40 | it('should work with minified css', function() { 41 | return compareSpriterResultsToExpected('test/test-css/background.min.css', 'test/test-css/expected/background.min.css', { 42 | 'spritesmithOptions': { 43 | 'algorithm': 'top-down' 44 | } 45 | }); 46 | }); 47 | 48 | it('should not try to sprite external images', function() { 49 | return compareSpriterResultsToExpected('test/test-css/external-image.css', 'test/test-css/expected/external-image.css', { 50 | 'spritesmithOptions': { 51 | 'algorithm': 'top-down' 52 | } 53 | }); 54 | }); 55 | 56 | it('should sprite properly when the same image source is used in multiple declarations. And one of the declarations is excluded via meta data', function() { 57 | return compareSpriterResultsToExpected('test/test-css/multiple-declarations-same-image.css', 'test/test-css/expected/multiple-declarations-same-image.css', { 58 | 'spritesmithOptions': { 59 | 'algorithm': 'top-down' 60 | } 61 | }); 62 | }); 63 | 64 | 65 | // Declarations in keyframes will be sprited 66 | it('should work with css animation keyframes', function() { 67 | return compareSpriterResultsToExpected('test/test-css/keyframes.css', 'test/test-css/expected/keyframes.css', { 68 | 'spritesmithOptions': { 69 | 'algorithm': 'top-down' 70 | } 71 | }); 72 | }); 73 | 74 | // All declarations will be included except those with explcit `includeMode` false meta data 75 | it('should work in implicit mode `options.includeMode`', function() { 76 | return compareSpriterResultsToExpected('test/test-css/overall.css', 'test/test-css/expected/overall-include-implicit.css', { 77 | 'includeMode': 'implicit', 78 | 'spritesmithOptions': { 79 | 'algorithm': 'top-down' 80 | } 81 | }); 82 | }); 83 | 84 | // Only declarations with explicit `includeMode` true meta data, will be sprited 85 | it('should work in explicit mode `options.includeMode`', function() { 86 | return compareSpriterResultsToExpected('test/test-css/overall.css', 'test/test-css/expected/overall-include-explicit.css', { 87 | 'includeMode': 'explicit', 88 | 'spritesmithOptions': { 89 | 'algorithm': 'top-down' 90 | } 91 | }); 92 | }); 93 | 94 | it('should throw error with non-existent file when `options.silent` is false', function() { 95 | var spriterPromise = spriterTest({ 96 | 'silent': false 97 | }, 'test/test-css/non-existent-image.css'); 98 | 99 | return expect(spriterPromise).to.eventually.be.rejected; 100 | }); 101 | 102 | 103 | it('should verify images `options.shouldVerifyImagesExist`', function() { 104 | 105 | // This should throw 106 | var spriterPromiseNoCheck = spriterTest({ 107 | 'shouldVerifyImagesExist': false 108 | }, 'test/test-css/non-existent-image.css'); 109 | 110 | // This should pass because we verify first 111 | var spriterPromiseCheck = spriterTest({ 112 | 'shouldVerifyImagesExist': true 113 | }, 'test/test-css/non-existent-image.css'); 114 | 115 | return Promise.all([ 116 | expect(spriterPromiseNoCheck).to.eventually.be.rejected, 117 | expect(spriterPromiseCheck).to.eventually.be.fulfilled 118 | ]); 119 | }); 120 | 121 | it('should call `includeMode.spriteSheetBuildCallback` when done', function() { 122 | return spriteSheetBuildCallbackResultTest({}).then(function(result) { 123 | return Promise.all([ 124 | expect(result).to.have.property('image'), 125 | expect(result).to.have.property('coordinates'), 126 | expect(result).to.have.property('properties') 127 | ]); 128 | }); 129 | }); 130 | 131 | it('should pass options through to spritesmith using `options.spritesmithOptions`', function() { 132 | // We make sure the spritesmith options were passed by using opposite-style stacking algorithms 133 | // and then comparing the width/height of both 134 | var testDifferentStackingPromise = Promise.all([ 135 | buildCallbackWithAlgorithmPromise('top-down'), 136 | buildCallbackWithAlgorithmPromise('left-right') 137 | ]); 138 | 139 | return testDifferentStackingPromise.then(function(res) { 140 | var verticalStackingData = res[0]; 141 | var horizontalStackingData = res[1]; 142 | 143 | // Make sure the two proportions are different 144 | return Promise.all([ 145 | expect(verticalStackingData.properties.height).to.be.above(horizontalStackingData.properties.height), 146 | expect(horizontalStackingData.properties.width).to.be.above(verticalStackingData.properties.width) 147 | ]); 148 | 149 | }); 150 | 151 | function buildCallbackWithAlgorithmPromise(algorithm) { 152 | var extraSpriterOps = { 153 | spritesmithOptions: { 154 | algorithm: algorithm 155 | } 156 | }; 157 | 158 | return spriteSheetBuildCallbackResultTest(extraSpriterOps); 159 | } 160 | 161 | }); 162 | 163 | 164 | // Get a promise that resolves with the transformed file/chunks 165 | function spriterTest(spriterOptions, filePath) { 166 | spriterOptions = spriterOptions || {}; 167 | filePath = filePath || 'test/test-css/overall.css'; 168 | 169 | var whenSpriterDonePromise = new Promise(function(resolve, reject) { 170 | 171 | readFile(filePath).then(function(contents) { 172 | contents = String(contents); 173 | 174 | // create the fake file 175 | var fakeFile = new gutil.File({ 176 | base: process.cwd(), 177 | cwd: process.cwd(), 178 | path: path.join(process.cwd(), filePath), 179 | contents: new Buffer(contents) 180 | }); 181 | 182 | // Create a spriter plugin stream 183 | var mySpriter = spriter(spriterOptions); 184 | 185 | // wait for the file to come back out 186 | mySpriter.on('data', function(file) { 187 | resolve(file); 188 | }); 189 | 190 | mySpriter.on('error', function(err) { 191 | reject(err); 192 | }); 193 | 194 | mySpriter.on('end', function() { 195 | resolve(); 196 | }); 197 | 198 | // write the fake file to it 199 | mySpriter.write(fakeFile); 200 | mySpriter.end(); 201 | 202 | }, function(err) { 203 | reject(err); 204 | }); 205 | }); 206 | 207 | return whenSpriterDonePromise; 208 | } 209 | 210 | // Get a promise representing the result of `options.spriteSheetBuildCallback` 211 | function spriteSheetBuildCallbackResultTest(opts, filePath) { 212 | return new Promise(function(resolve, reject) { 213 | var spriterOpts = extend({}, { 214 | spriteSheetBuildCallback: function(err, result) { 215 | if(err) { 216 | reject(err); 217 | } 218 | else { 219 | resolve(result); 220 | } 221 | } 222 | }, opts); 223 | 224 | spriterTest(spriterOpts, filePath).then(function(file) { 225 | // nothing 226 | }, function(err) { 227 | reject(err); 228 | }); 229 | }); 230 | } 231 | 232 | 233 | function compareSpriterResultsToExpected(actualPath, expectedPath, options) { 234 | options = options || {}; 235 | 236 | var spriterPromise = spriterTest(options, actualPath).then(function(result) { 237 | console.log(String(result.contents)); 238 | return String(result.contents); 239 | }); 240 | 241 | return readFile(expectedPath).then(function(expectedResult) { 242 | return expect(spriterPromise).to.eventually.equal(String(expectedResult)); 243 | }); 244 | } 245 | 246 | 247 | }); 248 | 249 | 250 | 251 | 252 | describe('lib/getBackgroundImageDeclarations(...)', function() { 253 | 254 | it('should work with single background declarations', function() { 255 | return testGetBackgroundImageDeclarationsFromFile('test/test-css/background.css', 2); 256 | }); 257 | 258 | it('should work with single background-image declarations', function() { 259 | return testGetBackgroundImageDeclarationsFromFile('test/test-css/background-image.css', 1); 260 | }); 261 | 262 | it('should work with mulitple images defined in background(-image) declarations', function() { 263 | return testGetBackgroundImageDeclarationsFromFile('test/test-css/multiple-backgrounds.css', 2); 264 | }); 265 | 266 | it('should factor in the `include` meta data', function() { 267 | return testGetBackgroundImageDeclarationsFromFile('test/test-css/meta-include.css', 1); 268 | }); 269 | 270 | it('should work with minified css', function() { 271 | return testGetBackgroundImageDeclarationsFromFile('test/test-css/background.min.css', 2); 272 | }); 273 | 274 | it('should work with single background declarations in keyframes', function() { 275 | return testGetBackgroundImageDeclarationsFromFile('test/test-css/keyframes.css', 4); 276 | }); 277 | 278 | function testGetBackgroundImageDeclarationsFromFile(filePath, numExpectedDeclarations) { 279 | return readFile(filePath).then(function(contents) { 280 | contents = String(contents); 281 | 282 | var styles = css.parse(contents, { 283 | 'silent': false 284 | }); 285 | var imageDeclarations = getBackgroundImageDeclarations(styles); 286 | 287 | return expect((imageDeclarations || []).length).to.equal(numExpectedDeclarations); 288 | }); 289 | } 290 | }); 291 | 292 | 293 | 294 | 295 | describe('lib/transformMap(...)', function() { 296 | var testArray; 297 | 298 | beforeEach(function() { 299 | testArray = [1, 2, 3]; 300 | }); 301 | 302 | it('should transform value with bare value', function() { 303 | var result = transformMap(testArray, function(el) { 304 | if(el == 2) { 305 | return 2.1; 306 | } 307 | }); 308 | expect(result).to.deep.equal([1, 2.1, 3]); 309 | }); 310 | 311 | it('should transform value with value property', function() { 312 | var result = transformMap(testArray, function(el) { 313 | if(el == 2) { 314 | return { 315 | value: 2.1 316 | }; 317 | } 318 | }); 319 | expect(result).to.deep.equal([1, 2.1, 3]); 320 | }); 321 | 322 | it('should insert with bare value', function() { 323 | var result = transformMap(testArray, function(el) { 324 | if(el == 2) { 325 | return { 326 | insertElements: 2.5 327 | }; 328 | } 329 | }); 330 | expect(result).to.deep.equal([1, 2, 2.5, 3]); 331 | }); 332 | 333 | it('should insert with array', function() { 334 | var result = transformMap(testArray, function(el) { 335 | if(el == 2) { 336 | return { 337 | insertElements: [2.5, 2.8] 338 | }; 339 | } 340 | }); 341 | expect(result).to.deep.equal([1, 2, 2.5, 2.8, 3]); 342 | }); 343 | 344 | it('should append bare value', function() { 345 | var result = transformMap(testArray, function(el) { 346 | if(el == 2) { 347 | return { 348 | appendElements: 4 349 | }; 350 | } 351 | }); 352 | expect(result).to.deep.equal([1, 2, 3, 4]); 353 | }); 354 | 355 | it('should append with array', function() { 356 | var result = transformMap(testArray, function(el) { 357 | if(el == 2) { 358 | return { 359 | appendElements: [4, 5] 360 | }; 361 | } 362 | }); 363 | expect(result).to.deep.equal([1, 2, 3, 4, 5]); 364 | }); 365 | 366 | it('should iterate over the "extra" added elements', function() { 367 | var iterateResult = []; 368 | var result = transformMap(testArray, function(el) { 369 | iterateResult.push(el); 370 | 371 | if(el == 2) { 372 | return { 373 | insertElements: [2.5, 2.8], 374 | appendElements: [4, 5] 375 | }; 376 | } 377 | }); 378 | 379 | expect(iterateResult).to.deep.equal([1, 2, 2.5, 2.8, 3, 4, 5]); 380 | }); 381 | }); 382 | --------------------------------------------------------------------------------