Permalink
Browse files

initial import

  • Loading branch information...
0 parents commit 73f663b3e0a9948d233381b4edc53e59e3b8190f @mikesmullin committed Dec 2, 2012
2 .gitignore
@@ -0,0 +1,2 @@
+node_modules
+npm-debug.log
21 LICENSE
@@ -0,0 +1,21 @@
+Copyright 2012 Smullin Design and other contributors
+http://smullindesign.com/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
95 README.md
@@ -0,0 +1,95 @@
+Stylus-Lemonade
+===============
+
+**Stylus-Lemonade** is a plugin for Node.js [Stylus](https://github.com/LearnBoost/stylus)
+which allows you to use functions like `sprite-position()`, `sprite-height()`, `image-width()`, `sprite-map()`, etc.
+within your `*.styl` markup to automatically fetch images and generate css sprites at render time.
+
+If you come from the Ruby on Rails community, you will immediately recognize conventions from Spriting
+with Compass/SASS, originally Lemonade.
+
+Install
+-------
+
+```bash
+sudo apt-get install libgd2-xpm-dev # on ubuntu; a libgd dependency
+npm install stylus-lemonade
+```
+
+Use in Javascript
+-----------------
+
+For the very latest and most comprehensive example, see [test/integration/server.js](https://github.com/mikesmullin/stylus-lemonade/blob/master/test/integration/server.js#L9).
+
+```javascript
+var stylus = require('stylus');
+stylus(markup_input)
+ .plugin('stylus-lemonade')
+ .render(function(err, css_output){
+ console.log(css_output);
+ });
+```
+
+Use in Stylus
+-------------
+
+For the very latest and most comprehensive examples, see [test/fixtures/private/stylesheets/application.styl](https://github.com/mikesmullin/stylus-lemonade/blob/master/test/fixtures/private/stylesheets/application.styl#L16).
+
+```sass
+$animated_flame = sprite-map('flame')
+#flame
+ background: url(sprite-url($animated_flame)) no-repeat
+ height: sprite-height($animated_flame, 'flame_a_0001')
+ width: sprite-width($animated_flame, 'flame_a_0001')
+.flame-frame-1
+ background-position: sprite-position($animated_flame, 'flame_a_0001') !important
+.flame-frame-2
+ background-position: sprite-position($animated_flame, 'flame_a_0002') !important
+.flame-frame-3
+ background-position: sprite-position($animated_flame, 'flame_a_0003') !important
+.flame-frame-4
+ background-position: sprite-position($animated_flame, 'flame_a_0004') !important
+.flame-frame-5
+ background-position: sprite-position($animated_flame, 'flame_a_0005') !important
+.flame-frame-6
+ background-position: sprite-position($animated_flame, 'flame_a_0006') !important
+```
+
+Will output CSS like this:
+
+```css
+#flame {
+ background: url(../images/flame-4e9c94d3fa.png) no-repeat;
+ height: 512px;
+ width: 512px;
+}
+.flame-frame-1 {
+ background-position: 0 0 !important;
+}
+.flame-frame-2 {
+ background-position: 0 -512px !important;
+}
+.flame-frame-3 {
+ background-position: 0 -1024px !important;
+}
+.flame-frame-4 {
+ background-position: 0 -1536px !important;
+}
+.flame-frame-5 {
+ background-position: 0 -2048px !important;
+}
+.flame-frame-6 {
+ background-position: 0 -2560px !important;
+}
+```
+
+And the image will turn out like this:
+
+ * [test/fixtures/public/images/flame-4e9c94d3fa.png](https://github.com/mikesmullin/stylus-lemonade/blob/master/test/fixtures/public/images/flame-4e9c94d3fa.png)
+
+Test
+----
+
+```bash
+npm test # build coffee, run mocha unit test, run chrome browser integration test
+```
436 lib/stylus-lemonade.js
@@ -0,0 +1,436 @@
+// Generated by CoffeeScript 1.4.0
+
+/**
+ * Lemonade: Automatically Generate CSS Sprites from Images with Stylus
+ * a Node.js + Stylus implementation
+ *
+ * Copyright 2012 Smullin Design and other contributors
+ * http://smullindesign.com/
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+
+(function() {
+ var Image, Lemonade, Sprite, async, fs, gd, instance,
+ __hasProp = {}.hasOwnProperty;
+
+ gd = require('node-gd');
+
+ async = require('async');
+
+ fs = require('fs');
+
+ instance = void 0;
+
+ Lemonade = (function() {
+
+ function Lemonade(options) {
+ this.options = options;
+ }
+
+ Lemonade.prototype.reset = function() {
+ var _base, _base1, _base2, _ref, _ref1, _ref2, _ref3;
+ if ((_ref = this.options) == null) {
+ this.options = {};
+ }
+ if ((_ref1 = (_base = this.options).image_path) == null) {
+ _base.image_path = './';
+ }
+ if ((_ref2 = (_base1 = this.options).sprite_path) == null) {
+ _base1.sprite_path = './';
+ }
+ if ((_ref3 = (_base2 = this.options).sprite_url) == null) {
+ _base2.sprite_url = './';
+ }
+ this.sprites = {};
+ return this.series = [];
+ };
+
+ Lemonade.prototype.infect = function(stylus_instance) {
+ var _this = this;
+ this.reset();
+ /**
+ * sprite-map(sprite, options)
+ * @param {String} sprite_filename
+ * name of sprite file to generate without .png extension
+ * default is 'sprite'
+ * @param {String} options
+ * (optional) css-like key: value; string of options for sprite engine
+ * @return {String}
+ */
+
+ stylus_instance.define('sprite-map', function(sprite, options) {
+ if (sprite == null) {
+ sprite = {
+ string: 'sprite'
+ };
+ }
+ if (options == null) {
+ options = {
+ string: ''
+ };
+ }
+ return "sprite:" + sprite.string + ";" + options.string;
+ });
+ /**
+ * sprite-url(map)
+ * @param {String} map
+ * string returned by sprite-map()
+ * @return {String}
+ * url to the sprite map's corresponding sprite file; for browsers
+ */
+
+ stylus_instance.define('sprite-url', function(map) {
+ return _this._generate_placeholder('URL', map.string);
+ });
+ /**
+ * sprite-position(map, png)
+ * the only function that actually triggers sprite generation
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {String}
+ * x, y coordinates of original image within compiled sprite
+ */
+
+ stylus_instance.define('sprite-position', function(map, png) {
+ return _this._generate_placeholder('POSITION', map.string, png.string);
+ });
+ /**
+ * sprite(map, png)
+ * the only function that actually triggers sprite generation
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {String}
+ * sprite image url() and x, y coordinates of original image
+ */
+
+ stylus_instance.define('sprite', function(map, png) {
+ return _this._generate_placeholder('URL_AND_IMAGE_POSITION', map.string, png.string);
+ });
+ /**
+ * sprite-width(map)
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {Integer}
+ * width in pixels
+ */
+
+ stylus_instance.define('sprite-width', function(map, png) {
+ return _this._generate_placeholder('WIDTH', map.string, png.string);
+ });
+ /**
+ * sprite-height(map)
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {Integer}
+ * height in pixels
+ */
+
+ stylus_instance.define('sprite-height', function(map, png) {
+ return _this._generate_placeholder('HEIGHT', map.string, png.string);
+ });
+ stylus_instance.on('end', function(css, callback) {
+ async.series(_this.series, function(err) {
+ var series, sprite, sprite_key, _fn, _ref;
+ if (err) {
+ return callback(err, css);
+ }
+ css = css.replace(/["']SPRITE_(.+?)_PLACEHOLDER\((.+?), (.*?)\)["']/g, function(match, key, sprite_key, png) {
+ var image, sprite;
+ sprite = _this.sprites[sprite_key];
+ image = sprite.images[png];
+ switch (key) {
+ case 'POSITION':
+ return image.coords();
+ case 'URL':
+ return sprite.digest_url();
+ case 'URL_AND_IMAGE_POSITION':
+ return "url(" + (sprite.digest_url()) + ") " + (image.coords());
+ case 'WIDTH':
+ return image.px(image.width);
+ case 'HEIGHT':
+ return image.px(image.height);
+ }
+ });
+ series = [];
+ _ref = _this.sprites;
+ _fn = function(sprite) {
+ return series.push(function(next) {
+ return sprite.render(next);
+ });
+ };
+ for (sprite_key in _ref) {
+ if (!__hasProp.call(_ref, sprite_key)) continue;
+ sprite = _ref[sprite_key];
+ _fn(sprite);
+ }
+ async.series(series, function(err) {
+ callback(null, css);
+ if (typeof _this.options.done === 'function') {
+ _this.options.done();
+ }
+ });
+ });
+ });
+ };
+
+ Lemonade.prototype._sprite_key_from_map = function(map) {
+ var matches;
+ if ((matches = map.match(/sprite:(.+?);/)) !== null) {
+ return matches[1];
+ } else {
+ return void 0;
+ }
+ };
+
+ Lemonade.prototype._generate_placeholder = function(key, map, png) {
+ var sprite, sprite_key, _base, _ref,
+ _this = this;
+ sprite_key = this._sprite_key_from_map(map);
+ sprite = (_ref = (_base = this.sprites)[sprite_key]) != null ? _ref : _base[sprite_key] = new Sprite(map);
+ if (png != null) {
+ this.series.push(function(callback) {
+ return sprite.add(png, callback);
+ });
+ }
+ return "SPRITE_" + key + "_PLACEHOLDER(" + sprite_key + ", " + (png != null ? png : png = '') + ")";
+ };
+
+ Lemonade.prototype.image_path = function(png) {
+ return this.options.image_path + png + '.png';
+ };
+
+ Lemonade.prototype.sprite_url = function(png) {
+ return this.options.sprite_url + png + '.png';
+ };
+
+ Lemonade.prototype.sprite_path = function(png) {
+ return this.options.sprite_path + png + '.png';
+ };
+
+ Lemonade.prototype.relative_path = function(file) {
+ return file.replace(process.cwd() + '/', '');
+ };
+
+ return Lemonade;
+
+ })();
+
+ Sprite = (function() {
+
+ function Sprite(map) {
+ var token, _i, _len, _ref;
+ this.options = {
+ repeat: 'no-repeat'
+ };
+ _ref = map.split(';').slice(0, -1);
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ token = _ref[_i];
+ token = token.split(':');
+ this.options[token[0].trim()] = token[1].trim().toLowerCase();
+ }
+ this.images = {};
+ this.x = 0;
+ this.y = 0;
+ this.width = 0;
+ this.height = 0;
+ this.png = void 0;
+ this._digest = void 0;
+ return;
+ }
+
+ Sprite.prototype.digest = function() {
+ var blob, image, key, _ref;
+ if (typeof this._digest !== 'undefined') {
+ return this._digest;
+ }
+ blob = '';
+ _ref = this.images;
+ for (key in _ref) {
+ if (!__hasProp.call(_ref, key)) continue;
+ image = _ref[key];
+ blob += image.toString() + '|';
+ }
+ return this._digest = require('crypto').createHash('md5').update(blob).digest('hex').substr(-10);
+ };
+
+ Sprite.prototype.digest_file = function() {
+ if (!this.digest()) {
+ void 0;
+ }
+ return instance.sprite_path(this.options.sprite + '-' + this.digest());
+ };
+
+ Sprite.prototype.digest_url = function() {
+ if (!this.digest()) {
+ void 0;
+ }
+ return instance.sprite_url(this.options.sprite + '-' + this.digest());
+ };
+
+ Sprite.prototype.add = function(file, callback) {
+ var image,
+ _this = this;
+ this._digest = void 0;
+ if (typeof this.images[file] !== 'undefined') {
+ this.images[file];
+ callback(null);
+ } else {
+ image = this.images[file] = new Image(file, this.x, this.y, function(err) {
+ if (err) {
+ return callback(err);
+ }
+ _this.width = Math.max(_this.width, image.width);
+ _this.y = _this.height += image.height;
+ return callback(null);
+ });
+ }
+ };
+
+ Sprite.prototype.render = function(callback) {
+ var key, series, sprite, transparency, _fn, _ref;
+ sprite = this;
+ sprite.png = gd.createTrueColor(sprite.width, sprite.height);
+ transparency = sprite.png.colorAllocateAlpha(0, 0, 0, 127);
+ sprite.png.fill(0, 0, transparency);
+ sprite.png.colorTransparent(transparency);
+ sprite.png.alphaBlending(0);
+ sprite.png.saveAlpha(1);
+ series = [];
+ _ref = sprite.images;
+ _fn = function(image) {
+ return series.push(function(next) {
+ image.open(function() {
+ var x, y, _i, _j, _ref1, _ref2, _ref3, _ref4;
+ switch (sprite.options.repeat) {
+ case 'no-repeat':
+ image.png.copy(sprite.png, image.x, image.y, 0, 0, image.width, image.height);
+ break;
+ case 'repeat-x':
+ for (x = _i = 0, _ref1 = sprite.width, _ref2 = image.width; 0 <= _ref1 ? _i <= _ref1 : _i >= _ref1; x = _i += _ref2) {
+ image.png.copy(sprite.png, x, image.y, 0, 0, image.width, image.height);
+ }
+ break;
+ case 'repeat-y':
+ for (y = _j = 0, _ref3 = sprite.height, _ref4 = image.height; 0 <= _ref3 ? _j <= _ref3 : _j >= _ref3; y = _j += _ref4) {
+ image.png.copy(sprite.png, image.x, y, 0, 0, image.width, image.height);
+ }
+ }
+ next();
+ });
+ });
+ };
+ for (key in _ref) {
+ if (!__hasProp.call(_ref, key)) continue;
+ _fn(sprite.images[key]);
+ }
+ async.series(series, function(err) {
+ var file, files, pattern, _i, _len;
+ if (err) {
+ callback(err);
+ }
+ pattern = sprite.digest_file().replace(/-[\w\d+]+\.png$/, '-*.png');
+ files = require('glob').sync(pattern);
+ for (_i = 0, _len = files.length; _i < _len; _i++) {
+ file = files[_i];
+ fs.unlinkSync(file);
+ }
+ sprite.png.savePng(sprite.digest_file(), 0, function() {
+ console.log("Wrote " + (instance.relative_path(sprite.digest_file())) + ".");
+ callback(null, sprite.digest_file());
+ });
+ });
+ };
+
+ return Sprite;
+
+ })();
+
+ Image = (function() {
+
+ function Image(file, x, y, callback) {
+ var _this = this;
+ this.file = file;
+ this.x = x;
+ this.y = y;
+ this.png = void 0;
+ this.height = void 0;
+ this.width = void 0;
+ this.absfile = instance.image_path(this.file);
+ this.open(function(err) {
+ if (err) {
+ return callback(err);
+ }
+ _this.height = _this.png.height;
+ _this.width = _this.png.width;
+ return callback(null);
+ });
+ return;
+ }
+
+ Image.prototype.toString = function() {
+ return "Image#file=" + this.file + ",x=" + this.x + ",y=" + this.y + ",width=" + this.width + ",height=" + this.height;
+ };
+
+ Image.prototype.open = function(callback) {
+ var _this = this;
+ return gd.openPng(this.absfile, function(err, png) {
+ if (err) {
+ return callback(err);
+ }
+ _this.png = png;
+ return callback(null);
+ });
+ };
+
+ Image.prototype.px = function(i) {
+ if (i === 0) {
+ return 0;
+ } else {
+ return i + 'px';
+ }
+ };
+
+ Image.prototype.coords = function() {
+ return this.px(this.x * -1) + ' ' + this.px(this.y * -1);
+ };
+
+ return Image;
+
+ })();
+
+ module.exports = function(stylus_instance, options) {
+ instance = new Lemonade(options);
+ if (stylus_instance != null) {
+ instance.infect(stylus_instance);
+ }
+ return instance;
+ };
+
+}).call(this);
49 package.json
@@ -0,0 +1,49 @@
+{
+ "name": "stylus-lemonade",
+ "description": "Automatically Generate CSS Sprites from Images with Stylus",
+ "version": "0.1.8",
+ "author": "Mike Smullin",
+ "maintainers": [
+ {
+ "name": "Mike Smullin",
+ "email": "mike@smullindesign.com"
+ }
+ ],
+ "homepage": "http://github.com/mikesmullin/stylus-lemonade",
+ "repository": {
+ "type": "git",
+ "url": "http://github.com/mikesmullin/stylus-lemonade.git"
+ },
+ "main": "./lib/stylus-lemonade",
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "http://github.com/mikesmullin/stylus-lemonade/blob/master/LICENSE"
+ }
+ ],
+ "dependencies": {
+ "node-gd": ">= 0.1.8",
+ "async": "~0.1.22",
+ "crypto": "0.0.3",
+ "node-glob": "git://github.com/isaacs/node-glob.git"
+ },
+ "devDependencies": {
+ "stylus": "https://github.com/mikesmullin/stylus/tarball/master",
+ "mocha": "~1.7.3"
+ },
+ "engine": [
+ "node >=0.3.0"
+ ],
+ "keywords": [
+ "stylus",
+ "sprites",
+ "css",
+ "sprite",
+ "images"
+ ],
+ "scripts": {
+ "pretest": "coffee -o lib src/stylus-lemonade.coffee",
+ "test": "cd test/unit && ./test",
+ "posttest": "cd test/integration && ./start"
+ }
+}
312 src/stylus-lemonade.coffee
@@ -0,0 +1,312 @@
+###*
+ * Lemonade: Automatically Generate CSS Sprites from Images with Stylus
+ * a Node.js + Stylus implementation
+ *
+ * Copyright 2012 Smullin Design and other contributors
+ * http://smullindesign.com/
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+###
+
+gd = require 'node-gd'
+async = require 'async'
+fs = require 'fs'
+#pathlib = require 'path'
+#exec = require('child_process').exec
+instance = undefined
+
+class Lemonade
+ constructor: (@options) ->
+
+ reset: ->
+ @options ?= {}
+ @options.image_path ?= './'
+ @options.sprite_path ?= './'
+ @options.sprite_url ?= './'
+ @sprites = {}
+ @series = []
+
+ infect: (stylus_instance) ->
+ # garbage collection
+ @reset()
+
+ ###*
+ * sprite-map(sprite, options)
+ * @param {String} sprite_filename
+ * name of sprite file to generate without .png extension
+ * default is 'sprite'
+ * @param {String} options
+ * (optional) css-like key: value; string of options for sprite engine
+ * @return {String}
+ ###
+ stylus_instance.define 'sprite-map', (sprite, options) ->
+ sprite ?= { string: 'sprite' }
+ options ?= { string: '' }
+ "sprite:#{sprite.string};#{options.string}"
+
+ ###*
+ * sprite-url(map)
+ * @param {String} map
+ * string returned by sprite-map()
+ * @return {String}
+ * url to the sprite map's corresponding sprite file; for browsers
+ ###
+ stylus_instance.define 'sprite-url', (map) =>
+ @_generate_placeholder 'URL', map.string
+
+ ###*
+ * sprite-position(map, png)
+ * the only function that actually triggers sprite generation
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {String}
+ * x, y coordinates of original image within compiled sprite
+ ###
+ stylus_instance.define 'sprite-position', (map, png) =>
+ @_generate_placeholder 'POSITION', map.string, png.string
+
+ ###*
+ * sprite(map, png)
+ * the only function that actually triggers sprite generation
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {String}
+ * sprite image url() and x, y coordinates of original image
+ ###
+ stylus_instance.define 'sprite', (map, png) =>
+ @_generate_placeholder 'URL_AND_IMAGE_POSITION', map.string, png.string
+
+ ###*
+ * sprite-width(map)
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {Integer}
+ * width in pixels
+ ###
+ stylus_instance.define 'sprite-width', (map, png) =>
+ @_generate_placeholder 'WIDTH', map.string, png.string
+
+ ###*
+ * sprite-height(map)
+ * @param {String} map
+ * string returned by sprite-map()
+ * @param {String} png
+ * image filename without .png extension
+ * @return {Integer}
+ * height in pixels
+ ###
+ stylus_instance.define 'sprite-height', (map, png) =>
+ @_generate_placeholder 'HEIGHT', map.string, png.string
+
+ # event emitted by stylus.render()
+ stylus_instance.on 'end', (css, callback) =>
+ async.series @series, (err) =>
+ return callback err, css if err
+
+ # replace placeholders in css
+ css = css.replace /["']SPRITE_(.+?)_PLACEHOLDER\((.+?), (.*?)\)["']/g, (match, key, sprite_key, png) =>
+ sprite = @sprites[sprite_key]
+ image = sprite.images[png]
+ switch key
+ when 'POSITION'
+ return image.coords()
+ when 'URL'
+ return sprite.digest_url()
+ when 'URL_AND_IMAGE_POSITION'
+ return "url(#{sprite.digest_url()}) #{image.coords()}"
+ when 'WIDTH'
+ return image.px image.width
+ when 'HEIGHT'
+ return image.px image.height
+
+ # save final sprites to disk
+ series = []
+ for own sprite_key, sprite of @sprites
+ ((sprite) -> series.push (next) -> sprite.render next)(sprite)
+ async.series series, (err) =>
+ # complete stylus rendering
+ callback null, css
+
+ # notify done callback if one was provided
+ @options.done() if typeof @options.done is 'function'
+
+ return
+ return
+ return
+ return
+
+ _sprite_key_from_map: (map) ->
+ if (matches = map.match(/sprite:(.+?);/)) isnt null then matches[1] else undefined
+
+ _generate_placeholder: (key, map, png) ->
+ sprite_key = @_sprite_key_from_map map
+ sprite = @sprites[sprite_key] ?= new Sprite map
+ if png?
+ @series.push (callback) =>
+ sprite.add png, callback
+ "SPRITE_#{key}_PLACEHOLDER(#{sprite_key}, #{png ?= ''})"
+
+ image_path: (png) ->
+ @options.image_path + png + '.png'
+
+ sprite_url: (png) ->
+ @options.sprite_url + png + '.png'
+
+ sprite_path: (png) ->
+ @options.sprite_path + png + '.png'
+
+ relative_path: (file) ->
+ file.replace process.cwd() + '/', ''
+
+class Sprite
+ constructor: (map) ->
+ @options =
+ repeat: 'no-repeat'
+ for token in map.split(';').slice(0, -1)
+ token = token.split(':')
+ @options[token[0].trim()] = token[1].trim().toLowerCase()
+ @images = {}
+ @x = 0
+ @y = 0
+ @width = 0
+ @height = 0
+ @png = undefined
+ @_digest = undefined
+ return
+
+ digest: ->
+ return @_digest if typeof @_digest isnt 'undefined'
+ blob = ''
+ blob += image.toString() + '|' for own key, image of @images
+ @_digest = require('crypto').createHash('md5').update(blob).digest('hex').substr(-10)
+
+ digest_file: ->
+ undefined unless @digest()
+ instance.sprite_path @options.sprite + '-' + @digest()
+
+ digest_url: ->
+ undefined unless @digest()
+ instance.sprite_url @options.sprite + '-' + @digest()
+
+ add: (file, callback) ->
+ @_digest = undefined
+ # if existing image within sprite
+ unless typeof @images[file] is 'undefined'
+ @images[file] # cached
+ callback null
+ else # new image not in sprite
+ # calculate
+ image = @images[file] = new Image file, @x, @y, (err) =>
+ return callback err if err
+ # TODO: allow repeat to dictate how cursor is incremented here; or do it all-at-once during render
+ @width = Math.max @width, image.width
+ @y = @height += image.height
+ callback null
+ return
+
+ render: (callback) ->
+ # save sprite image
+ sprite = @
+
+ # create new blank sprite canvas
+ sprite.png = gd.createTrueColor sprite.width, sprite.height
+ transparency = sprite.png.colorAllocateAlpha 0, 0, 0, 127
+ sprite.png.fill 0, 0, transparency
+ sprite.png.colorTransparent transparency
+ sprite.png.alphaBlending 0
+ sprite.png.saveAlpha 1
+
+ # compile sprite in memory
+ series = []
+ for own key of sprite.images
+ ((image) -> series.push (next) ->
+ image.open ->
+ #console.log "rendering #{image.file} over #{sprite.options.sprite} at #{image.coords()} with #{sprite.options.repeat}..."
+ # TODO: support smart rendering for more compact image placement
+ switch sprite.options.repeat
+ when 'no-repeat'
+ image.png.copy sprite.png, image.x, image.y, 0, 0, image.width, image.height
+ when 'repeat-x'
+ #TODO: account for spacing here
+ for x in [0..sprite.width] by image.width
+ image.png.copy sprite.png, x, image.y, 0, 0, image.width, image.height
+ when 'repeat-y'
+ #TODO: account for spacing here
+ for y in [0..sprite.height] by image.height
+ image.png.copy sprite.png, image.x, y, 0, 0, image.width, image.height
+ next()
+ return
+ return)(sprite.images[key])
+ async.series series, (err) ->
+ callback err if err
+
+ # delete old sprites off disk
+ pattern = sprite.digest_file().replace /-[\w\d+]+\.png$/, '-*.png'
+ files = require('glob').sync pattern
+ for file in files
+ fs.unlinkSync file
+
+ # override sprite png on disk
+ sprite.png.savePng sprite.digest_file(), 0, ->
+ console.log "Wrote #{instance.relative_path sprite.digest_file()}."
+ # TODO: add pngcrush here
+ callback null, sprite.digest_file()
+ return
+ return
+ return
+
+class Image
+ constructor: (@file, @x, @y, callback) ->
+ @png = undefined
+ @height = undefined
+ @width = undefined
+ @absfile = instance.image_path @file
+ @open (err) =>
+ return callback err if err
+ @height = @png.height
+ @width = @png.width
+ callback null
+ return
+
+ toString: ->
+ "Image#file=#{@file},x=#{@x},y=#{@y},width=#{@width},height=#{@height}"
+
+ open: (callback) ->
+ gd.openPng @absfile, (err, png) =>
+ return callback err if err
+ @png = png
+ callback null
+
+ px: (i) ->
+ if i is 0 then 0 else i + 'px'
+
+ coords: ->
+ @px(@x * -1) + ' ' + @px(@y * -1)
+
+module.exports = (stylus_instance, options) ->
+ instance = new Lemonade options
+ instance.infect stylus_instance if stylus_instance?
+ instance
BIN test/fixtures/private/images/flame_a_0001.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/flame_a_0002.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/flame_a_0003.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/flame_a_0004.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/flame_a_0005.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/flame_a_0006.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-128x128.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-16x16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-20x20.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-24x24.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-256x256.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-32x32.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/private/images/time-48x48.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 test/fixtures/private/stylesheets/application.styl
@@ -0,0 +1,60 @@
+$ui = sprite-map('ui', 'spacing:10;')
+$icons = sprite-map('icons')
+$x = sprite-map('x', 'repeat:repeat-x;')
+$y = sprite-map('y', 'repeat:repeat-y;')
+
+// Default Functions
+
+image-width(img)
+ return image-size(img)[0]
+
+image-height(img)
+ return image-size(img)[1]
+
+// Basic Testing
+
+$animated_flame = sprite-map('flame')
+#flame
+ background: url(sprite-url($animated_flame)) no-repeat
+ height: sprite-height($animated_flame, 'flame_a_0001')
+ width: sprite-width($animated_flame, 'flame_a_0001')
+.flame-frame-1
+ background-position: sprite-position($animated_flame, 'flame_a_0001') !important
+.flame-frame-2
+ background-position: sprite-position($animated_flame, 'flame_a_0002') !important
+.flame-frame-3
+ background-position: sprite-position($animated_flame, 'flame_a_0003') !important
+.flame-frame-4
+ background-position: sprite-position($animated_flame, 'flame_a_0004') !important
+.flame-frame-5
+ background-position: sprite-position($animated_flame, 'flame_a_0005') !important
+.flame-frame-6
+ background-position: sprite-position($animated_flame, 'flame_a_0006') !important
+
+.example1
+ $img = 'time-256x256'
+ background: url(sprite-url($icons)) no-repeat
+ background-position: sprite-position($icons, $img)
+ height: sprite-height($icons, $img)
+ width: sprite-width($icons, $img)
+.example2
+ $img = 'time-128x128'
+ background: sprite($ui, $img) no-repeat
+ height: sprite-height($ui, $img)
+ width: sprite-width($ui, $img)
+.example3
+ $img = 'time-48x48'
+ background: sprite($x, $img) repeat-x
+ height: sprite-height($x, $img)
+ width: 100%
+.example4
+ $img = 'time-32x32'
+ background: url(sprite-url($y, $img)) repeat-y sprite-position($y, 'time-32x32')
+ width: sprite-width($y, $img)
+ height: 100px
+.example5
+ $img = '../images/money-128x128.png'
+ $_img = '../../public/images/money-128x128.png'
+ background: url($img) no-repeat
+ width: image-width($_img)
+ height: image-height($_img)
BIN test/fixtures/public/images/flame-4e9c94d3fa.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/public/images/icons-ca35fa0eb8.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/public/images/money-128x128.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/public/images/ui-f3b4c8fb16.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/public/images/x-b7c3547257.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN test/fixtures/public/images/y-551357a982.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 test/fixtures/public/index.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<head>
+ <meta charset="utf-8"/>
+ <link href="stylesheets/application.css" rel="stylesheet" type="texamplet/css" />
+</head>
+<body onload="animate_flame()">
+ <div id="flame"></div>
+ <script>
+ function animate_flame() {
+ var frame = 0
+ , flame = document.getElementById('flame');
+ setInterval(function() {
+ frame = (frame + 1) % 6
+ flame.className = 'flame-frame-' + (frame + 1);
+ }, 100);
+ }
+ </script>
+ <div class="example1"></div>
+ <div class="example2"></div>
+ <div class="example3"></div>
+ <div class="example4"></div>
+ <div class="example5"></div>
+</body>
49 test/fixtures/public/stylesheets/application.css
@@ -0,0 +1,49 @@
+#flame {
+ background: url(../images/flame-4e9c94d3fa.png) no-repeat;
+ height: 512px;
+ width: 512px;
+}
+.flame-frame-1 {
+ background-position: 0 0 !important;
+}
+.flame-frame-2 {
+ background-position: 0 -512px !important;
+}
+.flame-frame-3 {
+ background-position: 0 -1024px !important;
+}
+.flame-frame-4 {
+ background-position: 0 -1536px !important;
+}
+.flame-frame-5 {
+ background-position: 0 -2048px !important;
+}
+.flame-frame-6 {
+ background-position: 0 -2560px !important;
+}
+.example1 {
+ background: url(../images/icons-ca35fa0eb8.png) no-repeat;
+ background-position: 0 0;
+ height: 256px;
+ width: 256px;
+}
+.example2 {
+ background: url(../images/ui-f3b4c8fb16.png) 0 0 no-repeat;
+ height: 128px;
+ width: 128px;
+}
+.example3 {
+ background: url(../images/x-b7c3547257.png) 0 0 repeat-x;
+ height: 48px;
+ width: 100%;
+}
+.example4 {
+ background: url(../images/y-551357a982.png) repeat-y 0 0;
+ width: 32px;
+ height: 100px;
+}
+.example5 {
+ background: url("../images/money-128x128.png") no-repeat;
+ width: 128px;
+ height: 128px;
+}
18 test/integration/server.js
@@ -0,0 +1,18 @@
+var stylus = require('stylus')
+ , fs = require('fs')
+ , styl_input_filename = '../fixtures/private/stylesheets/application.styl'
+ , css_output_filename = '../fixtures/public/stylesheets/application.css'
+ , styl_input = fs.readFileSync(styl_input_filename).toString('utf-8');
+
+stylus(styl_input)
+ .set('filename', styl_input_filename)
+ .plugin(__dirname + '/../../lib/stylus-lemonade', {
+ image_path: __dirname + '/../fixtures/private/images/',
+ sprite_path: __dirname + '/../fixtures/public/images/',
+ sprite_url: '../images/'
+ })
+ .render(function(err, css_output) {
+ if (err) throw err;
+ fs.writeFileSync(css_output_filename, css_output);
+ console.log('Wrote ' + css_output_filename + '.');
+ });
3 test/integration/start
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+node $DEBUG server.js && \
+google-chrome $PWD/../fixtures/public/index.html
2 test/unit/test
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+mocha $DEBUG --timeout 60000 --compilers coffee:coffee-script test.coffee
59 test/unit/test.coffee
@@ -0,0 +1,59 @@
+assert = require 'assert'
+
+describe 'Lemonade', ->
+ lemonade = undefined
+
+ setup = (done) ->
+ fs = require('fs')
+ styl_input_filename = __dirname + '/../fixtures/private/stylesheets/application.styl'
+ css_output_filename = __dirname + '/../fixtures/public/stylesheets/application.css'
+ styl_input = fs.readFileSync(styl_input_filename).toString('utf-8')
+
+ stylus = require 'stylus'
+ lemonade = require __dirname + '/../../lib/stylus-lemonade'
+
+ stylus_instance = stylus(styl_input).set('filename', styl_input_filename)
+ lemonade = lemonade(null, {
+ image_path: __dirname + '/../fixtures/private/images/',
+ sprite_path: __dirname + '/../fixtures/public/images/',
+ sprite_url: '../images/'
+ debug: true
+ done: done
+ })
+ lemonade.infect stylus_instance
+ stylus_instance.render (err, css_output) ->
+
+ beforeEach setup
+
+ it 'exists', ->
+ assert lemonade
+
+ describe 'Sprite', ->
+ it 'exists', ->
+ assert lemonade.sprites
+
+ it 'has unique digest per sprite', ->
+ digests = {}
+ all_unique = true
+ for own sprite_key, sprite of lemonade.sprites
+ key = sprite.digest()
+ if digests[key]?
+ all_unique = false
+ else
+ digests[key] = true
+ assert all_unique
+
+ it 'deletes old sprites on render', ->
+ # create fake old sprite image
+ fs = require 'fs'
+ fake_file = lemonade.sprite_path 'icons-xxxxxxxxxx'
+ fs.writeFileSync fake_file, '' # touch
+ # verify fake old sprite image created successfully
+ assert fs.existsSync fake_file
+ setup -> # re-render
+ # verify fake old sprite image was deleted
+ assert not fs.existsSync fake_file
+
+ describe 'Image', ->
+ it 'exists', ->
+ assert lemonade.sprites.icons.images

0 comments on commit 73f663b

Please sign in to comment.