diff --git a/bin/cli.js b/bin/cli.js index e640d79..24c2509 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -21,7 +21,7 @@ var opts = require('nomnom') .option('base64', { abbr: 'b', flag: true, - help: 'instead of creating a sprite, write base64 encoded images to css (css file will be written to )' + help: 'create css with base64 encoded sprite (css file will be written to )' }) .option('cssPath', { abbr: 'c', @@ -32,7 +32,7 @@ var opts = require('nomnom') .option('name', { abbr: 'n', default: 'sprite.png', - help: 'name of the sprite file' + help: 'name of sprite file' }) .option('processor', { abbr: 'p', @@ -40,6 +40,11 @@ var opts = require('nomnom') default: 'css', help: 'output format of the css. one of css, less, sass, scss or stylus' }) + .option('retina', { + abbr: 'r', + flag: true, + help: 'generate both retina and standard sprites. src images have to be in retina resolution' + }) .option('style', { abbr: 's', help: 'file to write css to, if ommited no css is written' @@ -58,6 +63,9 @@ var opts = require('nomnom') default: 'vertical', help: 'orientation of the sprite image' }) + .option('prefix', { + help: 'prefix for the class name used in css (without .)' + }) .script('css-sprite') .parse(); diff --git a/index.js b/index.js index 65250d8..baf91c5 100644 --- a/index.js +++ b/index.js @@ -1,17 +1,22 @@ 'use strict'; var sprite = require('./lib/css-sprite'); -var es = require('event-stream'); +var through2 = require('through2'); var vfs = require('vinyl-fs'); -var gfs = require('graceful-fs'); +var fs = require('graceful-fs'); var mkdirp = require('mkdirp'); var path = require('path'); var replaceExtension = require('./lib/replace-extension'); var _ = require('lodash'); +var noop = function () {}; -var writeFile = function (file, cb) { +var writeFile = function (file, enc, cb) { + var stream = this; mkdirp(file.base, function () { - gfs.writeFile(file.path, file.contents, cb); + fs.writeFile(file.path, file.contents, function () { + stream.push(file); + cb(); + }); }); }; @@ -23,6 +28,7 @@ var defaults = { cssPath: '../images', processor: 'css', orientation: 'vertical', + retina: false, margin: 5 }; @@ -44,7 +50,8 @@ module.exports = { } vfs.src(opts.src) .pipe(sprite(opts)) - .pipe(es.map(writeFile)) + .pipe(through2.obj(writeFile)) + .on('data', noop) .on('end', function () { if (_.isFunction(cb)) { cb(); diff --git a/lib/css-sprite.js b/lib/css-sprite.js index 77f81ab..74662a6 100644 --- a/lib/css-sprite.js +++ b/lib/css-sprite.js @@ -1,6 +1,6 @@ 'use strict'; -var es = require('event-stream'); +var through2 = require('through2'); var Canvas = require('canvas'); var lodash = require('lodash'); var path = require('path'); @@ -10,93 +10,160 @@ var imageinfo = require('imageinfo'); var replaceExtension = require('./replace-extension'); var Image = Canvas.Image; +// json2css template +json2css.addTemplate('sprite', require(path.join(__dirname, 'templates/sprite.js'))); + module.exports = function (opt) { opt = lodash.extend({}, {name: 'sprite.png', margin: 5, processor: 'css', cssPath: '../images', orientation: 'vertical'}, opt); + opt.styleExtension = (opt.processor === 'stylus') ? 'styl' : opt.processor; var sprites = []; var ctxHeight = 0; var ctxWidth = 0; - function queueImages (file) { + function queue (file, img, cb) { + sprites.push({ + 'img': img, + 'name': replaceExtension(file.relative, '').replace(/\/|\\|\ /g, '-'), + 'x': opt.orientation === 'vertical' ? opt.margin : ctxWidth + opt.margin, + 'y': opt.orientation === 'vertical' ? ctxHeight + opt.margin: opt.margin, + 'width': img.width, + 'height': img.height, + 'image': path.join(opt.cssPath, opt.name) + }); + + if (opt.orientation === 'vertical') { + ctxHeight = ctxHeight + img.height + 2 * opt.margin; + if (img.width + 2 * opt.margin > ctxWidth) { + ctxWidth = img.width + 2 * opt.margin; + } + } + else { + ctxWidth = ctxWidth + img.width + 2 * opt.margin; + if (img.height + 2 * opt.margin > ctxHeight) { + ctxHeight = img.height + 2 * opt.margin; + } + } + + cb(); + } + + function queueImages (file, enc, cb) { if (file.isNull()) { + cb(); return; // ignore } if (file.isStream()) { - return this.emit('error', new Error('Streaming not supported')); + cb(new Error('Streaming not supported')); + return; // ignore } - var queue = function (img) { - sprites.push({ - 'img': (!opt.base64) ? img : null, - 'name': replaceExtension(file.relative, '').replace(/\/|\\/g, '-'), - 'x': (!opt.base64) ? (opt.orientation === 'vertical' ? opt.margin : ctxWidth + opt.margin) : 0, - 'y': (!opt.base64) ? (opt.orientation === 'vertical' ? ctxHeight + opt.margin: opt.margin) : 0, - 'width': img.width, - 'height': img.height, - 'total_width': img.width, - 'total_height': img.height, - 'image': (!opt.base64) ? path.join(opt.cssPath, opt.name) : 'data:' + imageinfo(file.contents).mimeType + ';base64,' + file.contents.toString('base64') - }); - - if (!opt.base64) { - if (opt.orientation === 'vertical') { - ctxHeight = ctxHeight + img.height + 2 * opt.margin; - if (img.width + 2 * opt.margin > ctxWidth) { - ctxWidth = img.width + 2 * opt.margin; - } - } - else { - ctxWidth = ctxWidth + img.width + 2 * opt.margin; - if (img.height + 2 * opt.margin > ctxHeight) { - ctxHeight = img.height + 2 * opt.margin; - } - } - } - }; - var img = new Image(); img.src = file.contents; if (img.complete) { - queue(img); + queue(file, img, cb); } else { img.onload = function () { - queue(img); + queue(file, img, cb); }; } - } - function endStream () { - if (sprites.length === 0) { - return this.emit('end'); - } + function createCanvas () { var canvas = new Canvas(ctxWidth, ctxHeight); - if (!opt.base64) { - var ctx = canvas.getContext('2d'); - lodash.each(sprites, function (sprite) { - sprite.total_width = ctxWidth; - sprite.total_height = ctxHeight; - ctx.drawImage(sprite.img, sprite.x, sprite.y, sprite.width, sprite.height); + var ctx = canvas.getContext('2d'); + lodash.each(sprites, function (sprite) { + sprite.total_width = ctxWidth; + sprite.total_height = ctxHeight; + ctx.drawImage(sprite.img, sprite.x, sprite.y, sprite.width, sprite.height); + }); + return canvas; + } + + function createNonRetinaCanvas (retinaCanvas) { + var canvas = new Canvas(retinaCanvas.width / 2, retinaCanvas.height / 2); + var ctx = canvas.getContext('2d'); + ctx.drawImage(retinaCanvas, 0, 0, retinaCanvas.width / 2, retinaCanvas.height / 2); + return canvas; + } + + function createStyle (sprite, retinaSprite) { + if (retinaSprite) { + sprites.unshift({ + name: retinaSprite.relative, + type: 'retina', + image: (!opt.base64) ? path.join(opt.cssPath, retinaSprite.relative) : 'data:' + imageinfo(retinaSprite.canvas.toBuffer()).mimeType + ';base64,' + retinaSprite.canvas.toBuffer().toString('base64'), + total_width: sprite.canvas.width, + total_height: sprite.canvas.height + }); + lodash(sprites).each(function (sprite, i) { + sprites[i].x = sprite.x / 2; + sprites[i].y = sprite.y / 2; + sprites[i].width = sprite.width / 2; + sprites[i].height = sprite.height / 2; }); } - if (opt.style || opt.base64) { - var css = json2css(sprites, {'format': opt.processor}); - this.emit('data', new File({ - base: (!opt.base64) ? path.dirname(opt.style) : opt.out, - path: (!opt.base64 || (opt.base64 && opt.style)) ? opt.style : path.join(opt.out, replaceExtension(opt.name, '.' + opt.processor)), - contents: new Buffer(css) - })); + + sprites.unshift({ + name: sprite.relative, + type: 'sprite', + image: (!opt.base64) ? path.join(opt.cssPath, sprite.relative) : 'data:' + imageinfo(sprite.canvas.toBuffer()).mimeType + ';base64,' + sprite.canvas.toBuffer().toString('base64'), + total_width: sprite.canvas.width, + total_height: sprite.canvas.height + }); + return json2css(sprites, {'format': 'sprite', formatOpts: {'cssClass': opt.prefix, 'processor': opt.processor}}); + } + + function createSprite (cb) { + var sprite, nonRetinaSprite, style; + if (sprites.length === 0) { + cb(); + return; // ignore } - if (!opt.base64) { - this.emit('data', new File({ + sprite = { + base: opt.out, + relative: opt.name, + path: path.join(opt.out, opt.name), + canvas: createCanvas() + }; + + if (opt.retina) { + sprite.path = replaceExtension(sprite.path, '') + '-x2.png'; + sprite.relative = replaceExtension(sprite.relative, '') + '-x2.png'; + nonRetinaSprite = { base: opt.out, + relative: opt.name, path: path.join(opt.out, opt.name), - contents: new Buffer(canvas.toBuffer()) + canvas: createNonRetinaCanvas(sprite.canvas) + }; + } + + if (!opt.base64) { + if (opt.retina) { + this.push(new File({ + base: nonRetinaSprite.base, + path: nonRetinaSprite.path, + contents: new Buffer(nonRetinaSprite.canvas.toBuffer()) + })); + } + this.push(new File({ + base: sprite.base, + path: sprite.path, + contents: new Buffer(sprite.canvas.toBuffer()) + })); + } + + if (opt.style || opt.base64) { + style = opt.retina ? createStyle(nonRetinaSprite, sprite) : createStyle(sprite); + this.push(new File({ + base: !opt.base64 ? path.dirname(opt.style) : opt.out, + path: opt.style ? opt.style : path.join(opt.out, replaceExtension(opt.name, '.' + opt.styleExtension)), + contents: new Buffer(style) })); } - this.emit('end'); + cb(); } - return es.through(queueImages, endStream); + return through2.obj(queueImages, createSprite); }; diff --git a/lib/templates/css.mustache b/lib/templates/css.mustache new file mode 100644 index 0000000..9144ef5 --- /dev/null +++ b/lib/templates/css.mustache @@ -0,0 +1,23 @@ +{{#sprite}} +{{class}} { + background-image: url('{{{escaped_image}}}'); +} + +{{/sprite}} +{{#retina}} +@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) { + {{class}} { + background-image: url('{{{escaped_image}}}'); + background-size: {{px.total_width}} {{px.total_height}}; + } +} + +{{/retina}} +{{#items}} +{{class}} { + background-position: {{px.offset_x}} {{px.offset_y}}; + width: {{px.width}}; + height: {{px.height}}; +} + +{{/items}} diff --git a/lib/templates/less.mustache b/lib/templates/less.mustache new file mode 100644 index 0000000..2af0a5b --- /dev/null +++ b/lib/templates/less.mustache @@ -0,0 +1,41 @@ +{{#items}} +@{{name}}: {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}}; +{{/items}} + +.sprite-width(@sprite) { + width: extract(@sprite, 3); +} + +.sprite-height(@sprite) { + height: extract(@sprite, 4); +} + +.sprite-position(@sprite) { + @sprite-offset-x: extract(@sprite, 1); + @sprite-offset-y: extract(@sprite, 2); + background-position: @sprite-offset-x @sprite-offset-y; +} + +.sprite(@sprite) { + .sprite-position(@sprite); + background-repeat: no-repeat; + overflow: hidden; + display: block; + .sprite-width(@sprite); + .sprite-height(@sprite); +} + +{{#sprite}} +{{class}} { + background-image: url('{{{escaped_image}}}'); +} + +{{/sprite}} +{{#retina}} +@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) { + {{class}} { + background-image: url('{{{escaped_image}}}'); + background-size: {{px.total_width}} {{px.total_height}}; + } +} +{{/retina}} diff --git a/lib/templates/sass.mustache b/lib/templates/sass.mustache new file mode 100644 index 0000000..8467027 --- /dev/null +++ b/lib/templates/sass.mustache @@ -0,0 +1,35 @@ +{{#items}} +${{name}}: {{px.offset_x}} {{px.offset_y}} {{px.width}} {{px.height}} +{{/items}} + +=sprite-width($sprite) + width: nth($sprite, 3) + +=sprite-height($sprite) + height: nth($sprite, 4) + +=sprite-position($sprite) + $sprite-offset-x: nth($sprite, 1) + $sprite-offset-y: nth($sprite, 2) + background-position: $sprite-offset-x $sprite-offset-y + +=sprite($sprite) + +sprite-position($sprite) + background-repeat: no-repeat + overflow: hidden + display: block + +sprite-width($sprite) + +sprite-height($sprite) + +{{#sprite}} +{{class}} + background-image: url('{{{escaped_image}}}') + +{{/sprite}} +{{#retina}} +@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) + {{class}} + background-image: url('{{{escaped_image}}}') + background-size: {{px.total_width}} {{px.total_height}} + +{{/retina}} diff --git a/lib/templates/scss.mustache b/lib/templates/scss.mustache new file mode 100644 index 0000000..5f062a5 --- /dev/null +++ b/lib/templates/scss.mustache @@ -0,0 +1,41 @@ +{{#items}} +${{name}}: {{px.offset_x}} {{px.offset_y}} {{px.width}} {{px.height}}; +{{/items}} + +@mixin sprite-width($sprite) { + width: nth($sprite, 3); +} + +@mixin sprite-height($sprite) { + height: nth($sprite, 4); +} + +@mixin sprite-position($sprite) { + $sprite-offset-x: nth($sprite, 1); + $sprite-offset-y: nth($sprite, 2); + background-position: $sprite-offset-x $sprite-offset-y; +} + +@mixin sprite($sprite) { + @include sprite-position($sprite); + background-repeat: no-repeat; + overflow: hidden; + display: block; + @include sprite-width($sprite); + @include sprite-height($sprite); +} + +{{#sprite}} +{{class}} { + background-image: url('{{{escaped_image}}}'); +} + +{{/sprite}} +{{#retina}} +@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) { + {{class}} { + background-image: url('{{{escaped_image}}}'); + background-size: {{px.total_width}} {{px.total_height}}; + } +} +{{/retina}} diff --git a/lib/templates/sprite.js b/lib/templates/sprite.js new file mode 100644 index 0000000..a8044a1 --- /dev/null +++ b/lib/templates/sprite.js @@ -0,0 +1,56 @@ +'use strict'; + +// Load in local modules +var fs = require('graceful-fs'); +var mustache = require('mustache'); +var tmpl = { + 'css': fs.readFileSync(__dirname + '/css.mustache', 'utf8'), + 'scss': fs.readFileSync(__dirname + '/scss.mustache', 'utf8'), + 'sass': fs.readFileSync(__dirname + '/sass.mustache', 'utf8'), + 'less': fs.readFileSync(__dirname + '/less.mustache', 'utf8'), + 'stylus': fs.readFileSync(__dirname + '/stylus.mustache', 'utf8') +}; + +// Define our css template fn ({items, options}) -> css +function cssTemplate (params) { + // Localize parameters + var items = params.items; + var options = params.options; + var tmplParams = { + sprite: null, + retina: null, + items: [], + options: options + }; + + var classFn = function (name, sep) { + if (options.cssClass) { + return '.' + options.cssClass + sep + name; + } + else { + return '.icon' + sep + name; + } + }; + + // Add class to each of the options + items.forEach(function saveClass (item) { + if (item.type === 'sprite') { + item['class'] = classFn('', ''); + tmplParams.sprite = item; + } + else if (item.type === 'retina') { + item['class'] = classFn('', ''); + tmplParams.retina = item; + } + else { + item['class'] = classFn(item.name, '-'); + tmplParams.items.push(item); + } + }); + // Render and return CSS + var css = mustache.render(tmpl[options.processor], tmplParams); + return css; +} + +// Export our CSS template +module.exports = cssTemplate; diff --git a/lib/templates/stylus.mustache b/lib/templates/stylus.mustache new file mode 100644 index 0000000..c6acee5 --- /dev/null +++ b/lib/templates/stylus.mustache @@ -0,0 +1,33 @@ +{{#items}} +${{name}} = {{px.offset_x}} {{px.offset_y}} {{px.width}} {{px.height}} +{{/items}} + +sprite-width($sprite) + width $sprite[2] + +sprite-height($sprite) + height $sprite[3] + +sprite-position($sprite) + background-position $sprite[0] $sprite[1] + +sprite($sprite) + sprite-position($sprite) + background-repeat no-repeat + overflow hidden + display block + sprite-width($sprite) + sprite-height($sprite) + +{{#sprite}} +{{class}} + background-image url('{{{escaped_image}}}') + +{{/sprite}} +{{#retina}} +@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) + {{class}} + background-image url('{{{escaped_image}}}') + background-size {{px.total_width}} {{px.total_height}} + +{{/retina}} diff --git a/package.json b/package.json index 91d5d04..873a12a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-sprite", - "version": "0.6.3", + "version": "0.7.0-beta1", "description": "css sprite generator", "license": "MIT", "repository": { @@ -26,8 +26,8 @@ "test": "mocha --reporter spec", "coveralls": "istanbul cover _mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", "coverage": "istanbul cover _mocha --report html -- -R spec", - "hint": "jshint lib/*.js index.js", - "style": "jscs test/*.js lib/*.js index.js" + "hint": "jshint lib/**/*.js index.js", + "style": "jscs test/*.js lib/**/*.js index.js" }, "main": "./index.js", "keywords": [ @@ -49,17 +49,24 @@ "lodash": "^2.4.1", "nomnom": "^1.6.2", "vinyl": "^0.2.3", - "event-stream": "^3.1.0", "graceful-fs": "^2.0.1", "mkdirp": "^0.3.5", "gaze": "^0.5.0", - "imageinfo": "^1.0.4" + "imageinfo": "^1.0.4", + "through2": "^0.4.1", + "mustache": "^0.8.1" }, "devDependencies": { "mocha": "^1.17.1", "mocha-lcov-reporter": "^0.0.1", "coveralls": "^2.8.0", "istanbul": "^0.2.6", - "should": "^3.1.3" + "should": "^3.1.3", + "async": "^0.2.10", + "stylus": "^0.42.3", + "node-sass": "^0.8.1", + "less": "^1.7.0", + "clean-css": "^2.1.5", + "grunt": "^0.4.3" } } diff --git a/readme.md b/readme.md index 69e8bf0..b960454 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,11 @@ > A css sprite generator. -> Generates a sprite file and the propper css file out of a directory with images. It can also generate style files with base64 encoded images. +> Generates sprites and propper css files out of a directory of images. + +> Supports retina sprites. + +> Can inline base64 encoded sprites. ## Requirements @@ -35,14 +39,16 @@ out path of directory to write sprite file to src glob strings to find source images to put into the sprite Options: - -b, --base64 instead of creating a sprite, write base64 encoded images to css (css file will be written to ) + -b, --base64 create css with base64 encoded sprite (css file will be written to ) -c, --css-image-path http path to images on the web server (relative to css path or absolute path) [../images] - -n, --name name of the sprite file [sprite.png] + -n, --name name of sprite file [sprite.png] -p, --processor output format of the css. one of css, less, sass, scss or stylus [css] - -s, --style file to write css to, if omitted no css is written + -r, --retina generate both retina and standard sprites. src images have to be in retina resolution + -s, --style file to write css to, if ommited no css is written -w, --watch continuously create sprite --margin margin in px between tiles [5] --orientation orientation of the sprite image [vertical] + --prefix prefix for the class name used in css (without .) [icon] ``` ## Programatic usage @@ -54,13 +60,16 @@ sprite.create(options, cb); ### Options * **src:** Array or string of globs to find source images to put into the sprite. [required] * **out:** path of directory to write sprite file to [process.cwd()] -* **name:** name of the sprite file [sprite.png] -* **style:** file to write css to, if omitted no css is written +* **base64:** when true instead of creating a sprite writes base64 encoded images to css (css file will be written to ``) * **cssPath:** http path to images on the web server (relative to css path or absolute) [../images] +* **name:** name of the sprite file [sprite.png] * **processor:** output format of the css. one of css, less, sass, scss or stylus [css] -* **orientation:** orientation of the sprite image [vertical] +* **retina:** generate both retina and standard sprites. src images have to be in retina resolution +* **style:** file to write css to, if omitted no css is written * **margin:** margin in px between tiles [5] -* **base64:** when true instead of creating a sprite writes base64 encoded images to css (css file will be written to ``````) +* **orientation:** orientation of the sprite image [vertical] +* **prefix:** prefix for the class name used in css (without .) [icon] + ### Example ``` @@ -151,3 +160,66 @@ module.exports = function(grunt) { ``` Options to use `css-sprite` with [Grunt](http://gruntjs.com) are the same as for the `sprite.create` function with the exception of `src` and `out`. + + +## Usage with [sass](http://sass-lang.com/) / [less](http://lesscss.org/) / [stylus](http://learnboost.github.io/stylus/) + +#### [scss](http://sass-lang.com/) example + +``` +@import 'sprite'; // the generated style file (sprite.scss) + +// camera icon (camera.png in src directory) +.icon-camera { + @include sprite($camera); +} + +// cart icon (cart.png in src directory) +.icon-cart { + @include sprite($cart); +} +``` + +#### [sass](http://sass-lang.com/) example + +``` +@import 'sprite' // the generated style file (sprite.sass) + +// camera icon (camera.png in src directory) +.icon-camera + +sprite($camera) + +// cart icon (cart.png in src directory) +.icon-cart + +sprite($cart) +``` + +#### [less](http://lesscss.org/) example + +``` +@import 'sprite'; // the generated style file (sprite.less) + +// camera icon (camera.png in src directory) +.icon-camera { + .sprite(@camera); +} + +// cart icon (cart.png in src directory) +.icon-cart { + .sprite(@cart); +} +``` + +#### [stylus](http://learnboost.github.io/stylus/) example + +``` +@import 'sprite' // the generated style file (sprite.styl) + +// camera icon (camera.png in src directory) +.icon-camera + sprite($camera) + +// cart icon (cart.png in src directory) +.icon-cart + sprite($cart) +``` \ No newline at end of file diff --git a/tasks/css_sprite.js b/tasks/css_sprite.js index 753cd60..fa7bc8b 100644 --- a/tasks/css_sprite.js +++ b/tasks/css_sprite.js @@ -22,6 +22,7 @@ module.exports = function(grunt) { cssPath: '../images', processor: 'css', orientation: 'vertical', + retina: false, margin: 5 }); diff --git a/test/commandline.js b/test/commandline.js index 320b8e5..efe38e6 100644 --- a/test/commandline.js +++ b/test/commandline.js @@ -26,8 +26,8 @@ describe('css-sprite cli (bin/cli.js)', function () { fs.readFile('./test/dist/sprite.png', function (err, png) { var img = new Image(); img.src = png; - img.width.should.equal(56); - img.height.should.equal(125); + img.width.should.equal(138); + img.height.should.equal(552); fs.unlinkSync('./test/dist/sprite.png'); fs.rmdirSync('./test/dist'); done(); diff --git a/test/css-sprite.js b/test/css-sprite.js index 2d2074f..e116bed 100644 --- a/test/css-sprite.js +++ b/test/css-sprite.js @@ -4,9 +4,10 @@ var should = require('should'); var sprite = require('../lib/css-sprite'); var path = require('path'); var vfs = require('vinyl-fs'); -var es = require('event-stream'); +var through2 = require('through2'); var Canvas = require('canvas'); var Image = Canvas.Image; +var noop = function () {}; require('mocha'); @@ -17,15 +18,16 @@ describe('css-sprite (lib/css-sprite.js)', function () { out: './dist/img', name: 'sprites.png' })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { var img = new Image(); img.src = file.contents; file.path.should.equal('dist/img/sprites.png'); file.relative.should.equal('sprites.png'); - img.width.should.equal(56); - img.height.should.equal(125); + img.width.should.equal(138); + img.height.should.equal(552); cb(); })) + .on('data', noop) .on('end', done); }); it('should return a object stream with a bigger sprite', function (done) { @@ -35,13 +37,14 @@ describe('css-sprite (lib/css-sprite.js)', function () { name: 'sprites.png', margin: 20 })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { var img = new Image(); img.src = file.contents; - img.width.should.equal(86); - img.height.should.equal(185); + img.width.should.equal(168); + img.height.should.equal(672); cb(); })) + .on('data', noop) .on('end', done); }); it('should return a object stream with a horizontal sprite', function (done) { @@ -51,15 +54,16 @@ describe('css-sprite (lib/css-sprite.js)', function () { name: 'sprites.png', orientation: 'horizontal' })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { var img = new Image(); img.src = file.contents; file.path.should.equal('dist/img/sprites.png'); file.relative.should.equal('sprites.png'); - img.width.should.equal(110); - img.height.should.equal(69); + img.width.should.equal(552); + img.height.should.equal(138); cb(); })) + .on('data', noop) .on('end', done); }); it('should return a object stream with a sprite and a css file', function (done) { @@ -70,7 +74,7 @@ describe('css-sprite (lib/css-sprite.js)', function () { name: 'sprites.png', style: './dist/css/sprites.css' })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { if (file.relative.indexOf('png') > -1) { png = file; } @@ -79,6 +83,7 @@ describe('css-sprite (lib/css-sprite.js)', function () { } cb(); })) + .on('data', noop) .on('end', function () { png.should.be.ok; png.path.should.equal('dist/img/sprites.png'); @@ -86,7 +91,67 @@ describe('css-sprite (lib/css-sprite.js)', function () { css.should.be.ok; css.path.should.equal('./dist/css/sprites.css'); css.relative.should.equal('sprites.css'); - css.contents.toString('utf-8').should.containEql('.icon-floppy-disk'); + css.contents.toString('utf-8').should.containEql('.icon-camera'); + css.contents.toString('utf-8').should.containEql('.icon-cart'); + css.contents.toString('utf-8').should.containEql('.icon-command'); + css.contents.toString('utf-8').should.containEql('.icon-font'); + done(); + }); + }); + it('should return a object stream with retina sprite, normal sprite and css with media query', function (done) { + var png = [], css; + vfs.src('./test/fixtures/**') + .pipe(sprite({ + out: './dist/img', + name: 'sprites.png', + style: './dist/css/sprites.css', + retina: true + })) + .pipe(through2.obj(function (file, enc, cb) { + if (file.relative.indexOf('png') > -1) { + png.push(file) + } + else { + css = file; + } + cb(); + })) + .on('data', noop) + .on('end', function () { + var normal = new Image(); + var retina = new Image(); + normal.src = png[0].contents; + retina.src = png[1].contents; + png.length.should.equal(2); + png[0].relative.should.equal('sprites.png'); + png[1].relative.should.equal('sprites-x2.png'); + retina.width.should.equal(normal.width * 2); + retina.height.should.equal(normal.height * 2); + css.contents.toString('utf-8').should.containEql('@media'); + done(); + }); + }); + it('should return a object stream with a css file with custom class names', function (done) { + var css; + vfs.src('./test/fixtures/**') + .pipe(sprite({ + out: './dist/img', + name: 'sprites.png', + style: './dist/css/sprites.css', + prefix: 'test-selector' + })) + .pipe(through2.obj(function (file, enc, cb) { + if (file.relative.indexOf('css') > -1) { + css = file; + } + cb(); + })) + .on('data', noop) + .on('end', function () { + css.should.be.ok; + css.path.should.equal('./dist/css/sprites.css'); + css.relative.should.equal('sprites.css'); + css.contents.toString('utf-8').should.containEql('.test-selector'); done(); }); }); @@ -99,7 +164,7 @@ describe('css-sprite (lib/css-sprite.js)', function () { processor: 'scss', style: './dist/css/sprites.scss' })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { if (file.relative.indexOf('png') > -1) { png = file; } @@ -108,6 +173,7 @@ describe('css-sprite (lib/css-sprite.js)', function () { } cb(); })) + .on('data', noop) .on('end', function () { png.should.be.ok; png.path.should.equal('dist/img/sprites.png'); @@ -115,21 +181,41 @@ describe('css-sprite (lib/css-sprite.js)', function () { css.should.be.ok; css.path.should.equal('./dist/css/sprites.scss'); css.relative.should.equal('sprites.scss'); - css.contents.toString('utf-8').should.containEql('$floppy-disk'); + css.contents.toString('utf-8').should.containEql('$camera'); + css.contents.toString('utf-8').should.containEql('$cart'); + css.contents.toString('utf-8').should.containEql('$command'); + css.contents.toString('utf-8').should.containEql('$font'); done(); }); }); - it('should return a object stream with a css file with base64 encode images in it', function (done) { + it('should return a object stream with a css file with base64 encoded sprite', function (done) { vfs.src('./test/fixtures/**') .pipe(sprite({ base64: true, out: './dist/css' })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { + file.relative.should.equal('sprite.css'); + file.contents.toString('utf-8').should.containEql('data:image/png;base64'); + cb(); + })) + .on('data', noop) + .on('end', done); + }); + it('should return a object stream with a css with media query and base64 encoded sprite', function (done) { + vfs.src('./test/fixtures/**') + .pipe(sprite({ + out: './dist/img', + base64: true, + retina: true + })) + .pipe(through2.obj(function (file, enc, cb) { file.relative.should.equal('sprite.css'); + file.contents.toString('utf-8').should.containEql('@media'); file.contents.toString('utf-8').should.containEql('data:image/png;base64'); cb(); })) + .on('data', noop) .on('end', done); }); it('should do nothing when no files match', function (done) { @@ -139,10 +225,11 @@ describe('css-sprite (lib/css-sprite.js)', function () { out: './dist/img', name: 'sprites.png' })) - .pipe(es.map(function (f, cb) { + .pipe(through2.obj(function (f, cb) { file = file; cb(); })) + .on('data', noop) .on('end', function () { file.should.not.ok; done(); diff --git a/test/fixtures/camera.png b/test/fixtures/camera.png new file mode 100644 index 0000000..dcb90c8 Binary files /dev/null and b/test/fixtures/camera.png differ diff --git a/test/fixtures/cart.png b/test/fixtures/cart.png new file mode 100644 index 0000000..91ac4e4 Binary files /dev/null and b/test/fixtures/cart.png differ diff --git a/test/fixtures/command.png b/test/fixtures/command.png new file mode 100644 index 0000000..f022f07 Binary files /dev/null and b/test/fixtures/command.png differ diff --git a/test/fixtures/floppy-disk.png b/test/fixtures/floppy-disk.png deleted file mode 100755 index b170c57..0000000 Binary files a/test/fixtures/floppy-disk.png and /dev/null differ diff --git a/test/fixtures/font.png b/test/fixtures/font.png new file mode 100644 index 0000000..aa2b42f Binary files /dev/null and b/test/fixtures/font.png differ diff --git a/test/fixtures/fruit-computer.png b/test/fixtures/fruit-computer.png deleted file mode 100755 index 58a77a5..0000000 Binary files a/test/fixtures/fruit-computer.png and /dev/null differ diff --git a/test/styles.js b/test/styles.js new file mode 100644 index 0000000..3e54b9c --- /dev/null +++ b/test/styles.js @@ -0,0 +1,63 @@ +'use strict'; + +var should = require('should'); +var sprite = require('../index'); +var async = require('async'); +var fs = require('graceful-fs'); +var path = require('path'); +var sass = require('node-sass'); +var less = require('less'); +var stylus = require('stylus'); +var CleanCSS = require('clean-css'); + +describe('styles (lib/templates)', function () { + it('should create identical styles', function (done) { + async.eachSeries(['scss', 'less', 'stylus'], + function (proc, cb) { + sprite.create({ + src: ['./test/fixtures/*.png'], + out: './test/dist', + processor: proc, + base64: true + }, cb); + }, + function () { + async.series([ + function (cb) { + sass.render({ + file: path.join(__dirname, 'styles/style.scss'), + outputStyle: 'compressed', + success: function (css) { + cb(null, css); + }, + error: function (err) { + cb(err, null); + } + }); + }, + function (cb) { + var parser = new(less.Parser)({ + paths: [path.join(__dirname, 'dist/')] + }); + parser.parse(fs.readFileSync(path.join(__dirname, 'styles/style.less')).toString(), function (e, tree) { + cb(null, tree.toCSS({compress: true})); + }); + }, + function (cb) { + stylus(fs.readFileSync(path.join(__dirname, 'styles/style.styl')).toString()) + .set('paths', [path.join(__dirname, 'dist/')]) + .render(cb); + } + ], function (err, results) { + new CleanCSS().minify(results[0]).should.equal(new CleanCSS().minify(results[1])); + new CleanCSS().minify(results[1]).should.equal(new CleanCSS().minify(results[2])); + new CleanCSS().minify(results[2]).should.equal(new CleanCSS().minify(results[0])); + fs.unlinkSync('./test/dist/sprite.less'); + fs.unlinkSync('./test/dist/sprite.scss'); + fs.unlinkSync('./test/dist/sprite.styl'); + fs.rmdirSync('./test/dist'); + done(); + }); + }); + }); +}); diff --git a/test/styles/style.less b/test/styles/style.less new file mode 100644 index 0000000..efbceb0 --- /dev/null +++ b/test/styles/style.less @@ -0,0 +1,5 @@ +@import "../dist/sprite.less"; + +.icon-camera { + .sprite(@camera); +} diff --git a/test/styles/style.scss b/test/styles/style.scss new file mode 100644 index 0000000..8152913 --- /dev/null +++ b/test/styles/style.scss @@ -0,0 +1,5 @@ +@import "../dist/sprite.scss"; + +.icon-camera { + @include sprite($camera); +} diff --git a/test/styles/style.styl b/test/styles/style.styl new file mode 100644 index 0000000..27d64f0 --- /dev/null +++ b/test/styles/style.styl @@ -0,0 +1,4 @@ +@import 'sprite' + +.icon-camera + sprite($camera) diff --git a/test/wrapper.js b/test/wrapper.js index 337690b..5bf7034 100644 --- a/test/wrapper.js +++ b/test/wrapper.js @@ -4,9 +4,10 @@ var should = require('should'); var sprite = require('../index'); var fs = require('fs'); var vfs = require('vinyl-fs'); -var es = require('event-stream'); +var through2 = require('through2'); var Canvas = require('canvas'); var Image = Canvas.Image; +var noop = function () {}; require('mocha'); @@ -21,8 +22,8 @@ describe('css-sprite wrapper (index.js)', function () { fs.readFile('./test/dist/sprite.png', function (err, png) { var img = new Image(); img.src = png; - img.width.should.equal(56); - img.height.should.equal(125); + img.width.should.equal(138); + img.height.should.equal(552); fs.unlinkSync('./test/dist/sprite.png'); fs.rmdirSync('./test/dist'); done(); @@ -39,7 +40,10 @@ describe('css-sprite wrapper (index.js)', function () { fs.existsSync('./test/dist/sprite.png').should.be.true; fs.existsSync('./test/dist/sprite.css').should.be.true; fs.readFile('./test/dist/sprite.css', {encoding: 'utf-8'}, function (err, css) { - css.should.containEql('.icon-floppy-disk'); + css.should.containEql('.icon-camera'); + css.should.containEql('.icon-cart'); + css.should.containEql('.icon-command'); + css.should.containEql('.icon-font'); fs.unlinkSync('./test/dist/sprite.png'); fs.unlinkSync('./test/dist/sprite.css'); fs.rmdirSync('./test/dist'); @@ -57,7 +61,10 @@ describe('css-sprite wrapper (index.js)', function () { fs.existsSync('./test/dist/sprite.png').should.be.true; fs.existsSync('./test/dist/sprite.css').should.be.true; fs.readFile('./test/dist/sprite.css', {encoding: 'utf-8'}, function (err, css) { - css.should.containEql('.icon-floppy-disk'); + css.should.containEql('.icon-camera'); + css.should.containEql('.icon-cart'); + css.should.containEql('.icon-command'); + css.should.containEql('.icon-font'); fs.unlinkSync('./test/dist/sprite.png'); fs.unlinkSync('./test/dist/sprite.css'); fs.rmdirSync('./test/dist'); @@ -70,14 +77,15 @@ describe('css-sprite wrapper (index.js)', function () { .pipe(sprite.stream({ name: 'sprite.png' })) - .pipe(es.map(function (file, cb) { + .pipe(through2.obj(function (file, enc, cb) { var img = new Image(); img.src = file.contents; file.relative.should.equal('sprite.png'); - img.width.should.equal(56); - img.height.should.equal(125); + img.width.should.equal(138); + img.height.should.equal(552); cb(); })) + .on('data', noop) .on('end', done); }); it('should throw error when missing out dir', function () {