├── .jshintignore ├── index.js ├── .gitignore ├── test ├── data │ ├── gradient.gif │ ├── gradient.jpg │ ├── gradient.png │ ├── gradient.webp │ ├── gradient.special.png │ ├── gradient.svg │ └── large.css ├── module-test.js ├── binary-test.js └── embed-test.js ├── .travis.yml ├── .jshintrc ├── LICENSE ├── package.json ├── History.md ├── bin └── enhancecss ├── Readme.md └── lib └── enhance.js /.jshintignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/enhance'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /test/data/gradient.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.gif -------------------------------------------------------------------------------- /test/data/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.jpg -------------------------------------------------------------------------------- /test/data/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.png -------------------------------------------------------------------------------- /test/data/gradient.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.webp -------------------------------------------------------------------------------- /test/data/gradient.special.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubpawlowicz/enhance-css/HEAD/test/data/gradient.special.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | - '0.11' 6 | matrix: 7 | allow_failures: 8 | - node_js: '0.11' 9 | install: 10 | - npm update npm -g 11 | - npm install 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "curly": false, 4 | "eqeqeq": false, 5 | "eqnull": true, 6 | "immed": true, 7 | "indent": 2, 8 | "latedef": true, 9 | "noarg": true, 10 | "node" : true, 11 | "plusplus": false, 12 | "quotmark": "single", 13 | "strict": false, 14 | "undef": true, 15 | "unused": true 16 | } 17 | -------------------------------------------------------------------------------- /test/data/gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 JakubPawlowicz.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enhance-css", 3 | "version": "1.1.0", 4 | "author": "Jakub Pawlowicz (http://twitter.com/jakubpawlowicz)", 5 | "description": "A well-tested CSS enhancer (Base64, assets hosts, cache boosters, etc)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "css", 9 | "enhance", 10 | "base64", 11 | "assets", 12 | "asset hosts" 13 | ], 14 | "homepage": "https://github.com/jakubpawlowicz/enhance-css", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jakubpawlowicz/enhance-css.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/jakubpawlowicz/enhance-css/issues" 21 | }, 22 | "bin": { 23 | "enhancecss": "./bin/enhancecss" 24 | }, 25 | "main": "index.js", 26 | "files": [ 27 | "bin", 28 | "lib", 29 | "History.md", 30 | "index.js", 31 | "LICENSE" 32 | ], 33 | "scripts": { 34 | "check": "jshint ./bin/enhancecss .", 35 | "prepublish": "npm run check", 36 | "test": "vows" 37 | }, 38 | "dependencies": { 39 | "commander": "2.3.x" 40 | }, 41 | "devDependencies": { 42 | "jshint": "2.5.x", 43 | "vows": "0.7.x" 44 | }, 45 | "engines": { 46 | "node": ">=0.8.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/module-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var EnhanceCSS = require('../lib/enhance.js'); 4 | 5 | vows.describe('module').addBatch({ 6 | 'imported as a function': { 7 | topic: function() { 8 | var instance = new EnhanceCSS(); 9 | return instance.process.bind(instance); 10 | }, 11 | 'should not throw an error': function(process) { 12 | assert.doesNotThrow(function() { 13 | process('a{color:red}'); 14 | }); 15 | } 16 | }, 17 | 'initialization without new (back-compat)': { 18 | topic: function() { 19 | return EnhanceCSS(); 20 | }, 21 | 'should be an EnhanceCSS instance': function(instance) { 22 | assert.isObject(instance); 23 | assert.equal(instance instanceof EnhanceCSS, true); 24 | assert.isFunction(instance.process); 25 | }, 26 | 'should process CSS correctly': function(instance) { 27 | assert.equal(instance.process('a{color:red}').embedded.plain, 'a{color:red}'); 28 | } 29 | }, 30 | 'extended via prototype': { 31 | topic: function() { 32 | EnhanceCSS.prototype.foo = function(data, callback) { 33 | callback(null, this.process(data)); 34 | }; 35 | new EnhanceCSS().foo('a{color:red}', this.callback); 36 | }, 37 | 'should output correct CSS': function(error, processed) { 38 | assert.equal(processed.embedded.plain, 'a{color:red}'); 39 | }, 40 | teardown: function() { 41 | delete EnhanceCSS.prototype.foo; 42 | } 43 | }, 44 | 'initialization without options': { 45 | topic: function() { 46 | return new EnhanceCSS(); 47 | }, 48 | 'should process CSS correctly': function(instance) { 49 | assert.equal(instance.process('a{color:red}').embedded.plain, 'a{color:red}'); 50 | } 51 | } 52 | }).export(module); 53 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | [1.1.0 / 2014-03-16](https://github.com/jakubpawlowicz/enhance-css/compare/v1.0.0...v1.1.0) 2 | ================== 3 | 4 | * Fixed issue [#19](https://github.com/jakubpawlowicz/enhance-css/issues/19) - adds option forcing embed on all assets. 5 | * Fixed issue [#21](https://github.com/jakubpawlowicz/enhance-css/issues/21) - adds warnings to binary and library. 6 | 7 | [1.0.0 / 2014-02-23](https://github.com/jakubpawlowicz/enhance-css/compare/v0.6.0...v1.0.0) 8 | ================== 9 | 10 | * Adds a slightly different CLI options (because of #10). 11 | * Adds JSHint and makes sure code is valid. Patch by [@XhmikosR](https://github.com/XhmikosR). 12 | * Drops node 0.6 support. 13 | * Fixes #10 - use commander for CLI options parsing. 14 | * Fixes #16 - use prototypal inheritance. 15 | * Fixes #17 - makes the options argument to `new EnhanceCSS()` optional. 16 | 17 | 0.6.0 / 2012-11-30 18 | ================== 19 | 20 | * Added `stamp` option (defaults to true) which controls adding timestamps. Patch by [@borbit](https://github.com/borbit). 21 | * Added `--nostamp` option to binary. 22 | 23 | 0.5.2 / 2012-09-05 24 | ================== 25 | 26 | * Added relative protocol to asset hosts if protocol part is not provided. Patch by [@borbit](https://github.com/borbit). 27 | 28 | 0.5.1 / 2012-08-14 29 | ================== 30 | 31 | * Fixed parsing relative URLs (kudos to [@borbit](https://github.com/borbit) for the patch!) 32 | 33 | 0.5.0 / 2012-08-05 34 | ================== 35 | 36 | * Added Windows support with tests. 37 | 38 | 0.4.1 / 2012-08-02 39 | ================== 40 | 41 | * Fixed vows dev dependency. 42 | * Added `fs.existsSync` fallback to get rid of node.js's v0.8 warnings. 43 | 44 | 0.4.0 / 2012-07-09 45 | ================== 46 | 47 | * Requires node.js 0.6+. 48 | * Replaced gzip with node.js's native zlib. 49 | * Fixed asynchronous mode for binaries (creating gzip data). 50 | * Added testing for `noembed` and `pregzip`. 51 | 52 | 0.3.3 / 2012-07-04 53 | ================== 54 | 55 | * Fix for script failing for missing embedded files when using crypted stamps. 56 | 57 | 0.3.2 / 2012-07-03 58 | ================== 59 | 60 | * Leaves missing files as is. 61 | 62 | 0.3.1 / 2012-07-03 63 | ================== 64 | 65 | * Fixed assembling MD5 hash file name. 66 | 67 | 0.3.0 / 2012-07-03 68 | ================== 69 | 70 | * Added node.js 0.4.x requirement. 71 | * Added `cryptedStamp` option for renaming image files with MD5 hash attached (hard cache boosters). 72 | 73 | 0.2.2 / 2011-09-25 74 | ================== 75 | 76 | * Fixed dependencies - missing 'gzip'. Thanks to [@fairwinds](https://github.com/fairwinds) for reporting it. 77 | 78 | 0.2.1 / 2011-04-07 79 | ================== 80 | 81 | * Fixed bug in assembling compressed output (for large files only). 82 | 83 | 0.2.0 / 2011-04-03 84 | ================== 85 | 86 | * Added `--pregzip` option for automatic gzipping of enhanced files (not available when output is set to STDOUT). 87 | * Added binary file tests. 88 | 89 | 0.1.0 / 2011-03-20 90 | ================== 91 | 92 | * First version of enhance-css library. 93 | * Implemented GIF, JPG, PNG, and SVG images embedding (performed if the `?embed` parameter is present). 94 | * Implemented cache booster (via timestamp). 95 | * Implemented randomized asset hosts picker. 96 | -------------------------------------------------------------------------------- /bin/enhancecss: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* jshint latedef: false */ 4 | 5 | var commands = require('commander'); 6 | var EnhanceCSS = require('../index'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | 10 | var packageConfig = fs.readFileSync(path.join(path.dirname(fs.realpathSync(process.argv[1])), '../package.json')); 11 | var buildVersion = JSON.parse(packageConfig).version; 12 | 13 | // Specify commander options to parse command line params correctly 14 | commands 15 | .version(buildVersion, '-v, --version') 16 | .usage('[options] [source-file]') 17 | .option('-r, --root [root-path]', 'Set a root path to which resolve absolute @import rules') 18 | .option('-o, --output [output-file]', 'Use [output-file] as output instead of STDOUT') 19 | .option('--crypted-stamp', 'Rename image files with MD5 hash attached (hard cache boosters)') 20 | .option('--no-stamp', 'Disable adding stamp to URLs') 21 | .option('--no-embed-version', 'Output both embedded and non embedded version') 22 | .option('--force-embed', 'Forces embed on all supported assets') 23 | .option('--asset-hosts [host-pattern]', 'Use one or more asset hosts, e.g assets[0,1,2].example.com') 24 | .option('--pregzip', 'Automatically gzip the enhanced files (not available when no output file given)') 25 | .parse(process.argv); 26 | 27 | var fromStdin = !process.env.__DIRECT__ && !process.stdin.isTTY; 28 | 29 | // If no sensible data passed in just print help and exit 30 | if (!fromStdin && commands.args.length === 0) { 31 | commands.outputHelp(); 32 | return 0; 33 | } 34 | 35 | var options = { 36 | source: commands.args[0], 37 | target: commands.output, 38 | rootPath: commands.root || process.cwd(), 39 | assetHosts: !!commands.assetHosts, 40 | pregzip: !!commands.pregzip, 41 | noEmbedVersion: !commands.embedVersion, 42 | cryptedStamp: !!commands.cryptedStamp, 43 | stamp: !!commands.stamp, 44 | forceEmbed: !!commands.forceEmbed 45 | }; 46 | 47 | if (options.source) { 48 | fs.readFile(options.source, 'utf8', function(error, text) { 49 | if (error) 50 | throw error; 51 | enhance(text, output); 52 | }); 53 | } else { 54 | var stdin = process.openStdin(); 55 | stdin.setEncoding('utf-8'); 56 | var text = ''; 57 | stdin.on('data', function(chunk) { text += chunk; }); 58 | stdin.on('end', function() { enhance(text, output); }); 59 | } 60 | 61 | function enhance(source, callback) { 62 | return new EnhanceCSS(options).process(source, function(error, data) { 63 | if (error) 64 | throw error; 65 | 66 | callback(data); 67 | }); 68 | } 69 | 70 | function write(target, content) { 71 | if (typeof target == 'string') { 72 | fs.writeFileSync(target, content.plain); 73 | 74 | if (options.pregzip) 75 | fs.writeFileSync(target + '.gz', content.compressed); 76 | } else { 77 | target.write(content); 78 | } 79 | } 80 | 81 | function reportWarnings(list) { 82 | list.forEach(function(warning) { 83 | console.warn('WARNING: ' + warning); 84 | }); 85 | } 86 | 87 | function output(enhanced) { 88 | if (options.target) { 89 | write(options.target, enhanced.embedded); 90 | if (options.noEmbedVersion) 91 | write(options.target.replace(/\.(\w+)$/, '-noembed.$1'), enhanced.notEmbedded); 92 | } else { 93 | write(process.stdout, enhanced.embedded); 94 | } 95 | 96 | reportWarnings(enhanced.warnings); 97 | } 98 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://badge.fury.io/js/enhance-css.svg)](https://badge.fury.io/js/enhance-css) 2 | [![Build Status](https://secure.travis-ci.org/jakubpawlowicz/enhance-css.svg)](https://travis-ci.org/jakubpawlowicz/enhance-css) 3 | [![Dependency Status](https://david-dm.org/jakubpawlowicz/enhance-css.svg)](https://david-dm.org/jakubpawlowicz/enhance-css) 4 | [![devDependency Status](https://david-dm.org/jakubpawlowicz/enhance-css/dev-status.svg)](https://david-dm.org/jakubpawlowicz/enhance-css#info=devDependencies) 5 | 6 | ## What is enhance-css? 7 | 8 | Enhance-css is a [node.js](http://nodejs.org/) tool which can tweak your CSS files to: 9 | 10 | * improve caching - by rewriting URLs and renaming files to include either timestamps or MD5 hashes; 11 | * parellelize requests - by rewriting URLs with one or more asset hosts; 12 | * reduce number of requests - by embedding images as [Base64](http://en.wikipedia.org/wiki/Base64) data. 13 | 14 | There is also an option to create non-embedded version suited well 15 | for older browsers (IE 7 and below). 16 | 17 | 18 | ## Usage 19 | 20 | ### What are the requirements? 21 | 22 | ``` 23 | node.js 0.8.0+ (fully tested on OS X 10.6+, CentOS, and Windows 7) 24 | ``` 25 | 26 | ### How to install enhance-css? 27 | 28 | ``` 29 | npm install enhance-css 30 | ``` 31 | 32 | ### How to use enhance-css CLI? 33 | 34 | ``` 35 | enhancecss [options] [source-file] 36 | 37 | -h, --help output usage information 38 | -v, --version output the version number 39 | -r, --root [root-path] Set a root path to which resolve absolute @import rules 40 | -o, --output [output-file] Use [output-file] as output instead of STDOUT 41 | --crypted-stamp Rename image files with MD5 hash attached (hard cache boosters) 42 | --no-stamp Disable adding stamp to URLs 43 | --no-embed-version Output both embedded and non embedded version 44 | --force-embed Forces embed on all supported assets 45 | --asset-hosts [host-pattern] Use one or more asset hosts, e.g assets[0,1,2].example.com 46 | --pregzip Automatically gzip the enhanced files (not available when no output file given) 47 | ``` 48 | 49 | #### Examples: 50 | 51 | Most likely you are going to pass multiple CSS files into it 52 | and specify root directory and output file, e.g. 53 | 54 | ```bash 55 | cat path/to/first.css path/to/second.css path/to/third.css | enhancecss -o bundled.css --root ./public/ 56 | ``` 57 | 58 | The `--root` parameter is required to properly locate images referenced in the css files. 59 | 60 | To **embed images** in Base64 just add the *embed* argument to the image url, e.g. 61 | 62 | ```css 63 | a { background: url(/images/arrow.png?embed) 0 0 no-repeat; } 64 | ``` 65 | 66 | ### Non-embedded version 67 | 68 | In case you also need to support older browser, just add `--noembedversion` parameter, e.g. 69 | 70 | ```bash 71 | cat path/to/first.css path/to/second.css path/to/third.css | enhancecss -o bundled.css --root ./public/ --noembedversion 72 | ``` 73 | 74 | which will result in two output files: *bundled.css* and *bundled-noembed.css*. 75 | 76 | ### Asset hosts 77 | 78 | To use one or more asset hosts, just specify `--assetshosts` parameter, e.g. 79 | 80 | ```bash 81 | cat path/to/first.css path/to/second.css path/to/third.css | enhancecss -o bundled.css --root ./public/ --assethosts assets[0,1].example.com 82 | ``` 83 | 84 | which will result in all non-embedded image URLs bound to either assets0.example.com or assets1.example.com. 85 | 86 | ### What are the enhance-css' dev commands? 87 | 88 | First clone the source, then run: 89 | 90 | * `npm run check` to check JS sources with [JSHint](https://github.com/jshint/jshint/) 91 | * `npm test` for the test suite 92 | 93 | 94 | ## License 95 | 96 | Enhance-css is released under the [MIT License](/LICENSE). 97 | -------------------------------------------------------------------------------- /test/binary-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var fs = require('fs'); 4 | var exec = require('child_process').exec; 5 | var zlib = require('zlib'); 6 | 7 | var isWindows = process.platform == 'win32'; 8 | 9 | var source = 'a{background:url(/test/data/gradient.png?embed)}'; 10 | 11 | var checkFiles = function(fileName, options) { 12 | var pathToFile = function(noEmbed, pregzip) { 13 | return '/tmp/' + fileName + (noEmbed ? '-noembed' : '') + '.css' + (pregzip ? '.gz' : ''); 14 | }; 15 | 16 | assert.equal(fs.existsSync(pathToFile()), true); 17 | assert.equal(fs.existsSync(pathToFile(true)), !!options.noEmbed); 18 | assert.equal(fs.existsSync(pathToFile(false, true)), !!options.pregzip); 19 | assert.equal(fs.existsSync(pathToFile(true, true)), !!(options.pregzip && options.noEmbed)); 20 | 21 | // verify content 22 | assert.include(fs.readFileSync(pathToFile()).toString('utf8'), 'a{background:url(data:image/png;base64'); 23 | if (options.noEmbed) { 24 | assert.include(fs.readFileSync(pathToFile(true)).toString('utf8'), 'a{background:url(/test/data/gradient'); 25 | 26 | if (options.stamp === false) { 27 | assert.match(fs.readFileSync(pathToFile(true)).toString('utf8'), /gradient\.\w+\)/); 28 | } 29 | } 30 | 31 | if (options.pregzip) { 32 | zlib.gunzip(fs.readFileSync(pathToFile(false, true)), function(error, result) { 33 | assert.include(result.toString('utf8'), 'a{background:url(data:image/png;base64'); 34 | }); 35 | 36 | if (options.noEmbed) { 37 | zlib.gunzip(fs.readFileSync(pathToFile(true, true)), function(error, result) { 38 | assert.include(result.toString('utf8'), 'a{background:url(/test/data/gradient'); 39 | }); 40 | } 41 | } 42 | }; 43 | 44 | var cleanup = function(no, callback) { 45 | var swallowErrors = function() {}; 46 | 47 | fs.unlink('/tmp/test' + no + '.css', swallowErrors); 48 | fs.unlink('/tmp/test' + no + '-noembed.css', swallowErrors); 49 | fs.unlink('/tmp/test' + no + '.css.gz', swallowErrors); 50 | fs.unlink('/tmp/test' + no + '-noembed.css.gz', swallowErrors); 51 | 52 | if (callback) 53 | callback(); 54 | }; 55 | 56 | var binaryContext = function(options, context) { 57 | if (isWindows) 58 | return {}; 59 | 60 | context.topic = function() { 61 | exec('__DIRECT__=1 ./bin/enhancecss ' + options, this.callback); 62 | }; 63 | return context; 64 | }; 65 | 66 | var pipelinedContext = function(options, context) { 67 | if (isWindows) 68 | return {}; 69 | 70 | var cssSource = source; 71 | if ('source' in context) { 72 | cssSource = context.source; 73 | delete context.source; 74 | } 75 | 76 | context.topic = function() { 77 | exec('echo "' + cssSource + '" | ./bin/enhancecss ' + options, this.callback); 78 | }; 79 | return context; 80 | }; 81 | 82 | vows.describe('enhance css binary').addBatch({ 83 | 'no option': binaryContext('', { 84 | 'should give usage info': function(error, stdout) { 85 | assert.notEqual(-1, stdout.indexOf('Usage:')); 86 | } 87 | }), 88 | 'help option': binaryContext('-h', { 89 | 'should give usage info': function(error, stdout) { 90 | assert.notEqual(-1, stdout.indexOf('Usage:')); 91 | } 92 | }), 93 | 'version option': binaryContext('-v', { 94 | 'should give usage info': function(error, stdout) { 95 | var version = JSON.parse(fs.readFileSync('./package.json')).version; 96 | assert.equal(stdout, version + '\n'); 97 | } 98 | }), 99 | 'simple embed': pipelinedContext('-o /tmp/test.css', { 100 | 'should give empty output': function(error, stdout) { 101 | assert.isEmpty(stdout); 102 | }, 103 | 'should create valid files': function() { 104 | checkFiles('test', { 105 | noEmbed: false, 106 | pregzip: false 107 | }); 108 | }, 109 | teardown: cleanup(1) 110 | }), 111 | 'simple embed with no stamps': pipelinedContext('--no-embed-version --no-stamp -o /tmp/test1.css', { 112 | 'should give empty output': function(error, stdout) { 113 | assert.isEmpty(stdout); 114 | }, 115 | 'should create valid files': function() { 116 | checkFiles('test1', { 117 | stamp: false, 118 | noEmbed: true, 119 | pregzip: false 120 | }); 121 | }, 122 | teardown: cleanup(1) 123 | }), 124 | 'embed with --no-embed-version option': pipelinedContext('--no-embed-version -o /tmp/test2.css', { 125 | 'should give empty output': function(error, stdout) { 126 | assert.isEmpty(stdout); 127 | }, 128 | 'should create valid files': function() { 129 | checkFiles('test2', { 130 | noEmbed: true, 131 | pregzip: false 132 | }); 133 | }, 134 | teardown: cleanup(2) 135 | }), 136 | 'embed with noembed and gzip': pipelinedContext('--no-embed-version --pregzip -o /tmp/test3.css', { 137 | 'should give empty output': function(error, stdout) { 138 | assert.isEmpty(stdout); 139 | }, 140 | 'should create valid files': function() { 141 | checkFiles('test3', { 142 | noEmbed: true, 143 | pregzip: true 144 | }); 145 | }, 146 | teardown: cleanup(3) 147 | }), 148 | 'noembed and crypted stamp options': pipelinedContext('--crypted-stamp --no-embed-version -o /tmp/test4.css', { 149 | 'should give empty output': function(error, stdout) { 150 | assert.isEmpty(stdout); 151 | }, 152 | 'should create valid files': function() { 153 | checkFiles('test4', { noEmbed: true }); 154 | }, 155 | 'should create crypted file': function() { 156 | var data = fs.readFileSync(process.cwd() + '/test/data/gradient.png'); 157 | var stamp = require('crypto').createHash('md5'); 158 | stamp.update(data.toString('utf8')); 159 | var cryptedStamp = stamp.digest('hex'); 160 | 161 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient-' + cryptedStamp + '.png'), true); 162 | }, 163 | teardown: cleanup(4, function() { 164 | exec('rm -rf ' + process.cwd() + '/test/data/gradient-*.png'); 165 | }) 166 | }), 167 | 'forced embed': pipelinedContext('--force-embed -o /tmp/test5.css', { 168 | 'should give empty output': function(error, stdout) { 169 | assert.isEmpty(stdout); 170 | }, 171 | 'should create valid files': function() { 172 | checkFiles('test5', { noEmbed: false }); 173 | } 174 | }), 175 | 'warnings': pipelinedContext('-o /tmp/test6', { 176 | 'source': 'a{background:url(/test/data/gradient.webp?embed)}', 177 | 'should give empty output': function(error, stdout) { 178 | assert.isEmpty(stdout); 179 | }, 180 | 'should output warnings in stderr': function(error, stdout, stderr) { 181 | assert.equal(stderr, 'WARNING: File \'/test/data/gradient.webp\' skipped because of unknown content type.\n'); 182 | }, 183 | teardown: cleanup(6) 184 | }) 185 | }).export(module); 186 | -------------------------------------------------------------------------------- /lib/enhance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enhance-css - https://github.com/jakubpawlowicz/enhance-css 3 | * Released under the terms of MIT license 4 | * 5 | * Copyright (C) 2014 JakubPawlowicz.com 6 | */ 7 | 8 | /* jshint latedef: false */ 9 | 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | var zlib = require('zlib'); 13 | var crypto = require('crypto'); 14 | var querystring = require('querystring'); 15 | 16 | var isWindows = process.platform == 'win32'; 17 | 18 | var EnhanceCSS = module.exports = function EnhanceCSS(options) { 19 | options = options || {}; 20 | 21 | if (!(this instanceof EnhanceCSS)) 22 | return new EnhanceCSS(options); 23 | 24 | options.stamp = 'stamp' in options ? 25 | options.stamp : 26 | true; 27 | 28 | this.options = options; 29 | this.urlPattern = /url\(([^\)]+)\)/g; 30 | this.hostsCycle = null; 31 | }; 32 | 33 | EnhanceCSS.prototype.process = function(css, callback) { 34 | var self = this; 35 | var options = this.options; 36 | var missing = {}; 37 | var embedUrls = {}; 38 | var allUrls = []; 39 | var data = { 40 | original: css, 41 | warnings: [] 42 | }; 43 | 44 | // Only to find duplicates 45 | (css.match(this.urlPattern) || []).forEach(function(url) { 46 | var pathInfo = self.parseImageUrl(url.substring(4, url.length - 1)); 47 | 48 | if (pathInfo.query.embed !== undefined) { 49 | if (embedUrls[pathInfo.relative]) 50 | embedUrls[pathInfo.relative]++; 51 | else 52 | embedUrls[pathInfo.relative] = 1; 53 | } 54 | 55 | allUrls.push(pathInfo); 56 | }); 57 | 58 | // Get embedded version 59 | data.embedded = {}; 60 | data.embedded.plain = css.replace(this.urlPattern, function(match, url) { 61 | var pathInfo = self.parseImageUrl(url); 62 | 63 | if (pathInfo.relative.indexOf('data:image') === 0) 64 | return match; 65 | 66 | if (pathInfo.remote) { 67 | addWarning(data.warnings, pathInfo, 'skipped because is not local'); 68 | return match; 69 | } 70 | 71 | // Break early if file does not exist 72 | if (!pathInfo.exists) { 73 | addWarning(data.warnings, pathInfo, 'does not exist'); 74 | missing[pathInfo.relative] = 1; 75 | return match; 76 | } 77 | 78 | // Break unless ?embed param or there's more than one such image 79 | var moreThanOnce = embedUrls[pathInfo.relative] > 1; 80 | if (!options.forceEmbed && (pathInfo.query.embed === undefined || moreThanOnce)) { 81 | if (moreThanOnce) 82 | addWarning(data.warnings, pathInfo, 'set for embedding more than once'); 83 | if (options.stamp) 84 | addFileStamp(pathInfo, options); 85 | return ['url(', (self.nextAssetHost() || ''), pathInfo.relative, ')'].join(''); 86 | } 87 | 88 | var type = path.extname(pathInfo.relative).substring(1); 89 | if (type == 'jpg') 90 | type = 'jpeg'; 91 | if (type == 'svg') 92 | type = 'svg+xml'; 93 | 94 | // Break unless unsupported type 95 | if (!/(jpeg|gif|png|svg\+xml)/.test(type)) { 96 | addWarning(data.warnings, pathInfo, 'skipped because of unknown content type'); 97 | return match; 98 | } 99 | 100 | var base64 = fs.readFileSync(pathInfo.absolute).toString('base64'); 101 | 102 | return 'url(data:image/' + type + ';base64,' + base64 + ')'; 103 | }); 104 | 105 | // Get not embedded version (aka <= IE7) 106 | if (options.noEmbedVersion) { 107 | data.notEmbedded = {}; 108 | data.notEmbedded.plain = css.replace(this.urlPattern, function(match, url) { 109 | var pathInfo = self.parseImageUrl(url); 110 | 111 | // Break early if file does not exist 112 | if (!pathInfo.exists) 113 | return match; 114 | 115 | if (options.stamp) 116 | addFileStamp(pathInfo, options); 117 | 118 | return ['url(', (self.nextAssetHost() || ''), pathInfo.relative, ')'].join(''); 119 | }); 120 | } 121 | 122 | if (options.cryptedStamp) { 123 | allUrls.forEach(function(url) { 124 | addFileStamp(url, options); 125 | }); 126 | } 127 | 128 | // Update missing & duplicates lists 129 | data.missing = Object.keys(missing); 130 | data.duplicates = []; 131 | for (var key in embedUrls) { 132 | if (hasOwnProperty.call(embedUrls, key)) { 133 | if (embedUrls[key] > 1) 134 | data.duplicates.push(key); 135 | } 136 | } 137 | 138 | // Create gzipped version too if requested 139 | if (options.pregzip) { 140 | var count = options.noEmbedVersion ? 2 : 1; 141 | var compress = function(type) { 142 | zlib.gzip(data[type].plain, function(error, result) { 143 | data[type].compressed = result; 144 | if (--count === 0) 145 | callback(null, data); 146 | }); 147 | }; 148 | 149 | compress('embedded'); 150 | if (options.noEmbedVersion) 151 | compress('notEmbedded'); 152 | 153 | return; 154 | } 155 | 156 | if (callback) 157 | callback(null, data); 158 | else 159 | return data; 160 | }; 161 | 162 | EnhanceCSS.prototype.parseImageUrl = function(url) { 163 | var remote = /^(http:\/\/|https:\/\/|\/\/)/.test(url); 164 | var tokens = url.replace(/['"]/g, '').split('?'); 165 | var query = tokens[1] ? querystring.parse(tokens[1]) : {}; 166 | var imagePath = remote ? 167 | tokens[0] : 168 | path.normalize(tokens[0]); 169 | var absolutePath = remote ? 170 | imagePath : 171 | path.join(this.options.rootPath, imagePath); 172 | 173 | if (isWindows && imagePath.indexOf('data:image') < 0) 174 | imagePath = imagePath.replace(/\\/g, '/'); 175 | 176 | return { 177 | remote: remote, 178 | relative: imagePath, 179 | absolute: absolutePath, 180 | query: query, 181 | exists: remote ? false : fs.existsSync(absolutePath) 182 | }; 183 | }; 184 | 185 | EnhanceCSS.prototype.nextAssetHost = function() { 186 | var hosts = this.options.assetHosts; 187 | if (!hosts) 188 | return null; 189 | if (hosts.indexOf('[') == -1) 190 | return fixAssetHost(hosts); 191 | 192 | if (!this.hostsCycle) { 193 | this.hostsCycle = { 194 | next: function() { 195 | if (!this.cycleList) { 196 | var cycleList = []; 197 | var start = hosts.indexOf('['); 198 | var end = hosts.indexOf(']'); 199 | var pattern = hosts.substring(start + 1, end); 200 | 201 | pattern.split(',').forEach(function(version) { 202 | cycleList.push(hosts.replace(/\[([^\]])+\]/, version)); 203 | }); 204 | 205 | this.cycleList = cycleList; 206 | this.index = 0; 207 | } 208 | 209 | if (this.index == this.cycleList.length) 210 | this.index = 0; 211 | return this.cycleList[this.index++]; 212 | } 213 | }; 214 | } 215 | 216 | return fixAssetHost(this.hostsCycle.next()); 217 | }; 218 | 219 | function fixAssetHost(host) { 220 | if (/^http:\/\//.test(host) || /^https:\/\//.test(host) || /^\/\//.test(host)) 221 | return host; 222 | 223 | return '//' + host; 224 | } 225 | 226 | function addFileStamp(pathInfo, options) { 227 | if (!fs.existsSync(pathInfo.absolute)) 228 | return; 229 | 230 | if (options.cryptedStamp && (pathInfo.query.embed === undefined || options.noEmbedVersion)) { 231 | var source = fs.readFileSync(pathInfo.absolute); 232 | var encrypted = crypto.createHash('md5'); 233 | var toStampedPath = function(path) { 234 | var extensionDotIndex = path.lastIndexOf('.'); 235 | return path.substring(0, extensionDotIndex) + '-' + stamp + '.' + path.substring(extensionDotIndex + 1); 236 | }; 237 | 238 | encrypted.update(source.toString('utf8')); 239 | var stamp = encrypted.digest('hex'); 240 | var targetPath = toStampedPath(pathInfo.absolute); 241 | 242 | if (!fs.existsSync(targetPath)) 243 | fs.writeFileSync(targetPath, source); 244 | 245 | pathInfo.relative = toStampedPath(pathInfo.relative); 246 | } else { 247 | pathInfo.relative += '?' + Date.parse(fs.statSync(pathInfo.absolute).mtime) / 1000; 248 | } 249 | } 250 | 251 | function addWarning(warnings, pathInfo, reason) { 252 | var message = 'File \'' + pathInfo.relative + '\' ' + reason + '.'; 253 | if (warnings.indexOf(message) === -1) 254 | warnings.push(message); 255 | } 256 | -------------------------------------------------------------------------------- /test/data/large.css: -------------------------------------------------------------------------------- 1 | /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } 2 | /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } 3 | /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } /*reset*/ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } /* remember to define focus styles! */ :focus { outline: 0; } body { line-height: 1; color: black; background: white; } ol, ul { list-style: none; } /* tables still need 'cellspacing="0"' in the markup */ table { border-collapse: separate; border-spacing: 0; } caption, th, td { text-align: left; font-weight: normal; } blockquote:before, blockquote:after, q:before, q:after { content: ""; } blockquote, q { quotes: "" ""; } .clear { clear: both; display: inline-block; } .clear:after, .container:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .clear { height: 1%; } .clear { display: block; } -------------------------------------------------------------------------------- /test/embed-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | var fs = require('fs'); 4 | var zlib = require('zlib'); 5 | var path = require('path'); 6 | var exec = require('child_process').exec; 7 | var crypto = require('crypto'); 8 | var EnhanceCSS = require('../lib/enhance.js'); 9 | 10 | var runOn = function(css, options) { 11 | options = options || {}; 12 | options.rootPath = options.rootPath || process.cwd(); 13 | 14 | return function() { 15 | return new EnhanceCSS(options).process(css, this.callback); 16 | }; 17 | }; 18 | 19 | var base64 = function(imageName) { 20 | return fs.readFileSync(process.cwd() + '/test/data/' + imageName).toString('base64'); 21 | }; 22 | 23 | var mtime = function(imageName) { 24 | return Date.parse(fs.statSync(process.cwd() + '/test/data/' + imageName).mtime) / 1000; 25 | }; 26 | 27 | var cryptedStamp = function(imageName) { 28 | var data = fs.readFileSync(process.cwd() + '/test/data/' + imageName); 29 | var stamp = crypto.createHash('md5'); 30 | stamp.update(data.toString('utf8')); 31 | return stamp.digest('hex'); 32 | }; 33 | 34 | vows.describe('embedding images').addBatch({ 35 | 'plain content': { 36 | topic: runOn('div{width:100px;height:50px}'), 37 | 'should be left intact': function(data) { 38 | assert.equal(data.embedded.plain, data.original); 39 | }, 40 | 'should yield no warnings': function(data) { 41 | assert.deepEqual(data.warnings, []); 42 | } 43 | }, 44 | 'no embed': { 45 | topic: runOn('a{background:url(/test/data/gradient.jpg);}'), 46 | 'should add a timestamp': function(data) { 47 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}'); 48 | } 49 | }, 50 | 'unsupported': { 51 | topic: runOn('a{background:url(/test/data/gradient.webp?embed);}', { stamp: false }), 52 | 'should be left intact': function(data) { 53 | assert.equal(data.embedded.plain, data.original); 54 | }, 55 | 'should yield a warning': function(data) { 56 | assert.deepEqual(data.warnings, ['File \'/test/data/gradient.webp\' skipped because of unknown content type.']); 57 | } 58 | }, 59 | 'urls with special characters #1': { 60 | topic: runOn('a{background:url("/test/data/gradient.jpg");}'), 61 | 'should be processed': function(data) { 62 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}'); 63 | } 64 | }, 65 | 'urls with special characters #2': { 66 | topic: runOn('a{background:url("/test/data/gradient.jpg");}'), 67 | 'should be processed': function(data) { 68 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}'); 69 | } 70 | }, 71 | 'already embedded': { 72 | topic: runOn('a{background:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAAKAAoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD0D/h7BrH/AEG7P/vmH/Gj/h7BrH/Qbs/++Yf8a/kX/tbVf+gnqH/gbc//AByj+1tV/wCgnqH/AIG3P/xyv9DFmeH0/wCEnL+n/LqP9z+75/gux/qgsJlWn/CRl/T/AJcR/wCnfkf/2Q==)}'), 73 | 'should not be changed': function(data) { 74 | assert.equal(data.embedded.plain, data.original); 75 | }, 76 | 'should yield no warnings': function(data) { 77 | assert.deepEqual(data.warnings, []); 78 | } 79 | }, 80 | 'same urls with mixed characters': { 81 | topic: runOn('a{background:url("/test/data/gradient.jpg?embed");} div{background:url(/test/data/gradient.jpg?embed);}'), 82 | 'should not be embedded': function(data) { 83 | assert.equal(data.embedded.plain, 84 | 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');} ' + 85 | 'div{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ');}'); 86 | } 87 | }, 88 | 'url with relative parts': { 89 | topic: runOn('a{background:url(/test/data/../data/gradient.png)}'), 90 | 'should be normalized': function(data) { 91 | assert.equal(data.embedded.plain, 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}'); 92 | } 93 | }, 94 | 'remote url': { 95 | 'via http': { 96 | topic: runOn('a{background:url(http://pro.goalsmashers.com/test.png)}'), 97 | 'should not be transformed': function(data) { 98 | assert.equal(data.embedded.plain, data.original); 99 | }, 100 | 'should yield a warning': function(data) { 101 | assert.deepEqual(data.warnings, ['File \'http://pro.goalsmashers.com/test.png\' skipped because is not local.']); 102 | } 103 | }, 104 | 'via https': { 105 | topic: runOn('a{background:url(https://pro.goalsmashers.com/test.png)}'), 106 | 'should not be transformed': function(data) { 107 | assert.equal(data.embedded.plain, data.original); 108 | }, 109 | 'should yield a warning': function(data) { 110 | assert.deepEqual(data.warnings, ['File \'https://pro.goalsmashers.com/test.png\' skipped because is not local.']); 111 | } 112 | }, 113 | 'same protocol': { 114 | topic: runOn('a{background:url(//pro.goalsmashers.com/test.png)}'), 115 | 'should not be transformed': function(data) { 116 | assert.equal(data.embedded.plain, data.original); 117 | }, 118 | 'should yield a warning': function(data) { 119 | assert.deepEqual(data.warnings, ['File \'//pro.goalsmashers.com/test.png\' skipped because is not local.']); 120 | } 121 | } 122 | }, 123 | 'one file to be embedded': { 124 | topic: function() { 125 | return function(type) { 126 | return 'a{background:url(/test/data/gradient.' + type + '?embed)}'; 127 | }; 128 | }, 129 | 'should give Base64 embedded jpg': function(css) { 130 | assert.equal(runOn(css('jpg'))().embedded.plain, 'a{background:url(data:image/jpeg;base64,' + base64('gradient.jpg') + ')}'); 131 | }, 132 | 'should give Base64 embedded png': function(css) { 133 | assert.equal(runOn(css('png'))().embedded.plain, 'a{background:url(data:image/png;base64,' + base64('gradient.png') + ')}'); 134 | }, 135 | 'should give Base64 embedded gif': function(css) { 136 | assert.equal(runOn(css('gif'))().embedded.plain, 'a{background:url(data:image/gif;base64,' + base64('gradient.gif') + ')}'); 137 | }, 138 | 'should give Base64 embedded svg': function(css) { 139 | assert.equal(runOn(css('svg'))().embedded.plain, 'a{background:url(data:image/svg+xml;base64,' + base64('gradient.svg') + ')}'); 140 | } 141 | }, 142 | 'same file marked with ?embed twice': { 143 | topic: runOn('a{background:url(/test/data/gradient.jpg?embed)} div{background:url(/test/data/gradient.jpg?embed)}'), 144 | 'should not embed to Base64': function(data) { 145 | assert.equal(data.embedded.plain, 146 | 'a{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')} div{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}' 147 | ); 148 | }, 149 | 'should yield a warning': function(data) { 150 | assert.deepEqual(data.warnings, ['File \'/test/data/gradient.jpg\' set for embedding more than once.']); 151 | } 152 | }, 153 | 'more than one file and only one marked with ?embed': { 154 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.png?embed)} p{border-image:url(/test/data/gradient.png)}'), 155 | 'should embed one file to Base64': function(data) { 156 | assert.equal(data.embedded.plain, [ 157 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}', 158 | 'div{background:url(data:image/png;base64,' + base64('gradient.png') + ')}', 159 | 'p{border-image:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}' 160 | ].join(' ')); 161 | } 162 | }, 163 | 'not embedded files': { 164 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.jpg)}'), 165 | 'should get mtime timestamp': function(data) { 166 | assert.equal(data.embedded.plain, 167 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')} div{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}'); 168 | } 169 | }, 170 | 'not found files': { 171 | topic: runOn('a{background:url(/test/data/gradient2.png)}'), 172 | 'should be left intact': function(data) { 173 | assert.equal(data.embedded.plain, data.original); 174 | }, 175 | 'should yield a warning': function(data) { 176 | assert.deepEqual(data.warnings, ['File \'/test/data/gradient2.png\' does not exist.']); 177 | } 178 | }, 179 | 'forced embedding': { 180 | 'for same assets': { 181 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.png)}', { forceEmbed: true }), 182 | 'should embed all resources': function(data) { 183 | assert.equal(data.embedded.plain, [ 184 | 'a{background:url(data:image/png;base64,' + base64('gradient.png') + ')}', 185 | 'div{background:url(data:image/png;base64,' + base64('gradient.png') + ')}', 186 | ].join(' ')); 187 | } 188 | }, 189 | 'for different assets': { 190 | topic: runOn('a{background:url(/test/data/gradient.png)} div{background:url(/test/data/gradient.jpg)}', { forceEmbed: true }), 191 | 'should embed all resources': function(data) { 192 | assert.equal(data.embedded.plain, [ 193 | 'a{background:url(data:image/png;base64,' + base64('gradient.png') + ')}', 194 | 'div{background:url(data:image/jpeg;base64,' + base64('gradient.jpg') + ')}', 195 | ].join(' ')); 196 | } 197 | } 198 | }, 199 | 'adding assets hosts': { 200 | topic: 'a{background:url(/test/data/gradient.png)} p{background:url(/test/data/gradient.jpg)} div{background:url(/test/data/gradient.gif)}', 201 | 'single': function(css) { 202 | assert.equal(runOn(css, { assetHosts: 'assets.example.com' })().embedded.plain, [ 203 | 'a{background:url(//assets.example.com/test/data/gradient.png?' + mtime('gradient.png') + ')}', 204 | 'p{background:url(//assets.example.com/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}', 205 | 'div{background:url(//assets.example.com/test/data/gradient.gif?' + mtime('gradient.gif') + ')}' 206 | ].join(' ')); 207 | }, 208 | 'multiple': function(css) { 209 | assert.equal(runOn(css, { assetHosts: 'assets[0,1,2].example.com' })().embedded.plain, [ 210 | 'a{background:url(//assets0.example.com/test/data/gradient.png?' + mtime('gradient.png') + ')}', 211 | 'p{background:url(//assets1.example.com/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}', 212 | 'div{background:url(//assets2.example.com/test/data/gradient.gif?' + mtime('gradient.gif') + ')}' 213 | ].join(' ')); 214 | } 215 | } 216 | }) 217 | .addBatch({ 218 | 'getting non-embedded version (IE7)': { 219 | topic: 'a{background:url(/test/data/gradient.png)} p{background:url(/test/data/gradient.jpg)}', 220 | 'not by default': function(css) { 221 | assert.isUndefined(runOn(css)().notEmbedded); 222 | }, 223 | 'if requested': function(css) { 224 | assert.equal(runOn(css, { noEmbedVersion: true })().notEmbedded.plain, 225 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')} p{background:url(/test/data/gradient.jpg?' + mtime('gradient.jpg') + ')}'); 226 | } 227 | }, 228 | 'getting non-embedded version (IE7) with embed': { 229 | topic: 'a{background:url(/test/data/gradient.png?embed)}', 230 | 'if requested': function(css) { 231 | assert.equal(runOn(css, { noEmbedVersion: true })().notEmbedded.plain, 232 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}'); 233 | } 234 | }, 235 | 'getting non-embedded version (IE7) with duplicates and embed': { 236 | topic: 'a{background:url(/test/data/gradient.png?embed)} p{background:url(/test/data/gradient.png?embed)}', 237 | 'if requested': function(css) { 238 | assert.equal(runOn(css, { noEmbedVersion: true })().notEmbedded.plain, 239 | 'a{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')} p{background:url(/test/data/gradient.png?' + mtime('gradient.png') + ')}'); 240 | } 241 | } 242 | }).addBatch({ 243 | 'should not add crypted stamp instead of timestamp': { 244 | 'on CSS without images': { 245 | topic: runOn('a{background:#fff}', { cryptedStamp: true }), 246 | 'should act as identity transformation': function(css) { 247 | assert.equal(css.embedded.plain, css.original); 248 | } 249 | }, 250 | 'on CSS with embedded images': { 251 | topic: runOn('a{background:url(/test/data/gradient.jpg?embed)}', { cryptedStamp: true }), 252 | 'should not create new file': function() { 253 | var stamp = cryptedStamp('gradient.jpg'); 254 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient-' + stamp + '.jpg'), false); 255 | } 256 | } 257 | } 258 | }).addBatch({ 259 | 'should add crypted stamp instead of timestamp on CSS with normal images': { 260 | topic: runOn('a{background:url(/test/data/gradient.png)}', { cryptedStamp: true }), 261 | 'should create new file': function() { 262 | var stamp = cryptedStamp('gradient.png'); 263 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient-' + stamp + '.png'), true); 264 | }, 265 | 'should include stamped file in embed source': function(css) { 266 | var stamp = cryptedStamp('gradient.png'); 267 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.embedded.plain); 268 | }, 269 | teardown: function() { 270 | exec('rm -rf test/data/gradient-*'); 271 | } 272 | } 273 | }).addBatch({ 274 | 'should add crypted stamp instead of timestamp on non-embedded source': { 275 | topic: runOn('a{background:url(/test/data/gradient.png)}', { 276 | cryptedStamp: true, 277 | noEmbedVersion: true 278 | }), 279 | 'once file exists': { 280 | topic: function(css) { 281 | var self = this; 282 | var stamp = cryptedStamp('gradient.png'); 283 | 284 | fs.exists(process.cwd() + '/test/data/gradient-' + stamp + '.png', function() { 285 | self.callback(css, stamp); 286 | }); 287 | }, 288 | 'should include stamped file in embed source': function(css, stamp) { 289 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.embedded.plain); 290 | }, 291 | 'should include stamped file in non-embedded source': function(css, stamp) { 292 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.notEmbedded.plain); 293 | } 294 | }, 295 | teardown: function() { 296 | exec('rm -rf test/data/gradient-*'); 297 | } 298 | } 299 | }).addBatch({ 300 | 'should add crypted stamp instead of timestamp on non-embedded source for embedded image': { 301 | topic: runOn('a{background:url(/test/data/gradient.png?embed)}', { 302 | cryptedStamp: true, 303 | noEmbedVersion: true 304 | }), 305 | 'once file exists': { 306 | topic: function(css) { 307 | var self = this; 308 | var stamp = cryptedStamp('gradient.png'); 309 | 310 | fs.exists(process.cwd() + '/test/data/gradient-' + stamp + '.png', function() { 311 | self.callback(css, stamp); 312 | }); 313 | }, 314 | 'should not include stamped file in embed source': function(css, stamp) { 315 | assert.notEqual('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.embedded.plain); 316 | }, 317 | 'should include stamped file in non-embedded source': function(css, stamp) { 318 | assert.equal('a{background:url(/test/data/gradient-' + stamp + '.png)}', css.notEmbedded.plain); 319 | } 320 | }, 321 | teardown: function() { 322 | exec('rm -rf test/data/gradient-*'); 323 | } 324 | } 325 | }).addBatch({ 326 | 'should correctly process files with dots': { 327 | topic: runOn('a{background:url(/test/data/gradient.special.png)}', { cryptedStamp: true }), 328 | 'should create new file': function() { 329 | var stamp = cryptedStamp('gradient.png'); 330 | assert.equal(fs.existsSync(process.cwd() + '/test/data/gradient.special-' + stamp + '.png'), true); 331 | }, 332 | 'should include stamped file in embed source': function(css) { 333 | var stamp = cryptedStamp('gradient.png'); 334 | assert.equal('a{background:url(/test/data/gradient.special-' + stamp + '.png)}', css.embedded.plain); 335 | }, 336 | teardown: function() { 337 | exec('rm -rf test/data/gradient.special-*'); 338 | } 339 | } 340 | }).addBatch({ 341 | 'should correctly process missing files with embed': { 342 | topic: runOn('a{background:url(/test/data/gradient2.png?embed)}', { cryptedStamp: true }), 343 | 'should keep path as is': function(css) { 344 | assert.equal('a{background:url(/test/data/gradient2.png?embed)}', css.embedded.plain); 345 | } 346 | }, 347 | 'should correctly process missing files for crypted stamps': { 348 | topic: runOn('a{background:url(/test/data/gradient2.png)}', { cryptedStamp: true }), 349 | 'should keep path as is': function(css) { 350 | assert.equal('a{background:url(/test/data/gradient2.png)}', css.embedded.plain); 351 | } 352 | } 353 | }).addBatch({ 354 | 'compressed content': { 355 | topic: runOn('a{background:#fff}'), 356 | 'not by default': function(data) { 357 | assert.isUndefined(data.embedded.compressed); 358 | } 359 | }, 360 | 'compressed embedded content': { 361 | topic: runOn('a{background:#fff}', { pregzip: true }), 362 | 'should be buffer': function(data) { 363 | assert.ok(Buffer.isBuffer(data.embedded.compressed)); 364 | }, 365 | 'should be different from uncompressed': function(data) { 366 | assert.notEqual(data.embedded.compressed.toString(), data.embedded.plain); 367 | }, 368 | 'should be different from original': function(data) { 369 | assert.notEqual(data.embedded.compressed.toString(), data.original); 370 | }, 371 | 'uncompressing': { 372 | topic: function(data) { 373 | zlib.unzip(data.embedded.compressed, this.callback); 374 | }, 375 | 'should be equal to embedded': function(error, uncompressed) { 376 | assert.equal('a{background:#fff}', uncompressed); 377 | } 378 | } 379 | }, 380 | 'compressed non-embedded content': { 381 | topic: runOn('a{background:#fff}', { 382 | pregzip: true, 383 | noEmbedVersion: true 384 | }), 385 | 'should be buffer': function(data) { 386 | assert.ok(Buffer.isBuffer(data.notEmbedded.compressed)); 387 | }, 388 | 'should be different from uncompressed': function(data) { 389 | assert.notEqual(data.notEmbedded.compressed.toString(), data.notEmbedded.plain); 390 | }, 391 | 'should be different from original': function(data) { 392 | assert.notEqual(data.notEmbedded.compressed.toString(), data.original); 393 | }, 394 | 'uncompressing': { 395 | topic: function(data) { 396 | zlib.unzip(data.notEmbedded.compressed, this.callback); 397 | }, 398 | 'should be equal to embedded': function(error, uncompressed) { 399 | assert.equal('a{background:#fff}', uncompressed); 400 | } 401 | } 402 | }, 403 | 'long content': { 404 | topic: runOn(fs.readFileSync('./test/data/large.css', 'utf-8'), { pregzip: true }), 405 | 'uncompressing': { 406 | topic: function(data) { 407 | zlib.unzip(data.embedded.compressed, this.callback); 408 | }, 409 | 'should be equal to embedded': function(error, uncompressed) { 410 | assert.equal(fs.readFileSync('./test/data/large.css', 'utf-8'), uncompressed); 411 | } 412 | } 413 | } 414 | }).addBatch({ 415 | 'list of missing files': { 416 | topic: runOn('a{background:url(/test/data/gradient2.png)} p{background:url(/test/data/gradient2.jpg)}')().missing, 417 | 'should have both files': function(missing) { 418 | assert.equal(missing.length, 2); 419 | }, 420 | 'should have files in right order': function(missing) { 421 | assert.equal(missing[0], '/test/data/gradient2.png'); 422 | assert.equal(missing[1], '/test/data/gradient2.jpg'); 423 | } 424 | }, 425 | 'list of not embedded files (duplicates)': { 426 | topic: runOn('a{background:url(/test/data/gradient.png?embed)} p{background:url(/test/data/gradient.png?embed)}')().duplicates, 427 | 'should have one file': function(duplicates) { 428 | assert.equal(duplicates.length, 1); 429 | }, 430 | 'should have gradient.png': function(duplicates) { 431 | assert.equal(duplicates[0], '/test/data/gradient.png'); 432 | } 433 | } 434 | }).addBatch({ 435 | 'parse absolute url': { 436 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('/test/data/gradient.png'), 437 | 'should get right relative path': function(parsed) { 438 | assert.equal(parsed.relative, '/test/data/gradient.png'); 439 | }, 440 | 'should get right absolute path': function(parsed) { 441 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png')); 442 | }, 443 | 'should exists': function(parsed) { 444 | assert.isTrue(parsed.exists); 445 | }, 446 | 'should not have query options': function(parsed) { 447 | assert.isEmpty(parsed.query); 448 | } 449 | }, 450 | 'parse absolute url with query string': { 451 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('/test/data/gradient.png?embed&x=y'), 452 | 'should get right relative path': function(parsed) { 453 | assert.equal(parsed.relative, '/test/data/gradient.png'); 454 | }, 455 | 'should get right absolute path': function(parsed) { 456 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png')); 457 | }, 458 | 'should exists': function(parsed) { 459 | assert.isTrue(parsed.exists); 460 | }, 461 | 'should have query options': function(parsed) { 462 | assert.isNotNull(parsed.query.embed); 463 | assert.equal('y', parsed.query.x); 464 | } 465 | }, 466 | 'parse non-canonical absolute urls': { 467 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('/test/data/../data/gradient.png'), 468 | 'should get right relative path': function(parsed) { 469 | assert.equal(parsed.relative, '/test/data/gradient.png'); 470 | }, 471 | 'should get right absolute path': function(parsed) { 472 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png')); 473 | }, 474 | 'should exists': function(parsed) { 475 | assert.isTrue(parsed.exists); 476 | }, 477 | 'should not have query options': function(parsed) { 478 | assert.isEmpty(parsed.query); 479 | } 480 | }, 481 | 'parse absolute urls with special characters': { 482 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('"/test/data/gradient.png"'), 483 | 'should get right relative path': function(parsed) { 484 | assert.equal(parsed.relative, '/test/data/gradient.png'); 485 | }, 486 | 'should get right absolute path': function(parsed) { 487 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png')); 488 | }, 489 | 'should exists': function(parsed) { 490 | assert.isTrue(parsed.exists); 491 | }, 492 | 'should not have query options': function(parsed) { 493 | assert.isEmpty(parsed.query); 494 | } 495 | }, 496 | 'parse relative url': { 497 | topic: new EnhanceCSS({ rootPath: process.cwd() }).parseImageUrl('test/data/gradient.png'), 498 | 'should get right relative path': function(parsed) { 499 | assert.equal(parsed.relative, 'test/data/gradient.png'); 500 | }, 501 | 'should get right absolute path': function(parsed) { 502 | assert.equal(parsed.absolute, path.join(process.cwd(), 'test', 'data', 'gradient.png')); 503 | }, 504 | 'should exists': function(parsed) { 505 | assert.isTrue(parsed.exists); 506 | } 507 | } 508 | }).addBatch({ 509 | 'get empty asset host': { 510 | topic: new EnhanceCSS().nextAssetHost(), 511 | 'from empty configuration': function(host) { 512 | assert.equal(host, null); 513 | } 514 | }, 515 | 'get single asset host fixed with': { 516 | topic: new EnhanceCSS({ assetHosts: 'assets.example.com' }).nextAssetHost(), 517 | 'relative protocol': function(host) { 518 | assert.equal(host, '//assets.example.com'); 519 | } 520 | }, 521 | 'get single asset host not fixed if': [ 522 | { 523 | topic: new EnhanceCSS({ assetHosts: '//assets.example.com' }).nextAssetHost(), 524 | 'relative protocol passed': function(host) { 525 | assert.equal(host, '//assets.example.com'); 526 | } 527 | }, 528 | { 529 | topic: new EnhanceCSS({ assetHosts: 'http://assets.example.com' }).nextAssetHost(), 530 | '"http" protocol passed': function(host) { 531 | assert.equal(host, 'http://assets.example.com'); 532 | } 533 | }, 534 | { 535 | topic: new EnhanceCSS({ assetHosts: 'https://assets.example.com' }).nextAssetHost(), 536 | '"https" protocol passed': function(host) { 537 | assert.equal(host, 'https://assets.example.com'); 538 | } 539 | } 540 | ], 541 | 'get multiple asset hosts fixed with': { 542 | topic: function() { 543 | return new EnhanceCSS({ assetHosts: 'assets[0,1].example.com' }); 544 | }, 545 | 'relative protocol, for first in list': function(enhance) { 546 | assert.equal(enhance.nextAssetHost(), '//assets0.example.com'); 547 | }, 548 | 'relative protocol, for second in list': function(enhance) { 549 | assert.equal(enhance.nextAssetHost(), '//assets1.example.com'); 550 | } 551 | }, 552 | 'get multiple asset hosts not fixed if': [ 553 | { 554 | topic: function() { 555 | return new EnhanceCSS({ assetHosts: '//assets[0,1].example.com' }); 556 | }, 557 | 'relative protocol passed, for first in list': function(enhance) { 558 | assert.equal(enhance.nextAssetHost(), '//assets0.example.com'); 559 | }, 560 | 'relative protocol passed, for second in list': function(enhance) { 561 | assert.equal(enhance.nextAssetHost(), '//assets1.example.com'); 562 | } 563 | }, 564 | { 565 | topic: function() { 566 | return new EnhanceCSS({ assetHosts: 'http://assets[0,1].example.com' }); 567 | }, 568 | '"http" protocol passed, for first in list': function(enhance) { 569 | assert.equal(enhance.nextAssetHost(), 'http://assets0.example.com'); 570 | }, 571 | '"http" protocol passed, for second in list': function(enhance) { 572 | assert.equal(enhance.nextAssetHost(), 'http://assets1.example.com'); 573 | } 574 | }, 575 | { 576 | topic: function() { 577 | return new EnhanceCSS({ assetHosts: 'https://assets[0,1].example.com' }); 578 | }, 579 | '"https" protocol passed, for first in list': function(enhance) { 580 | assert.equal(enhance.nextAssetHost(), 'https://assets0.example.com'); 581 | }, 582 | '"https" protocol passed, for second in list': function(enhance) { 583 | assert.equal(enhance.nextAssetHost(), 'https://assets1.example.com'); 584 | } 585 | } 586 | ], 587 | 'get one asset host': { 588 | topic: new EnhanceCSS({ assetHosts: '//assets.example.com' }).nextAssetHost(), 589 | 'as first host from list': function(host) { 590 | assert.equal(host, '//assets.example.com'); 591 | }, 592 | 'as second host from list': function(host) { 593 | assert.equal(host, '//assets.example.com'); 594 | } 595 | }, 596 | 'get one asset host from multiple configuration - ': { 597 | topic: function() { 598 | return new EnhanceCSS({ assetHosts: '//assets[0,1,2].example.com' }); 599 | }, 600 | 'first': function(enhanceCSS) { 601 | assert.equal(enhanceCSS.nextAssetHost(), '//assets0.example.com'); 602 | }, 603 | 'second': function(enhanceCSS) { 604 | assert.equal(enhanceCSS.nextAssetHost(), '//assets1.example.com'); 605 | }, 606 | 'third': function(enhanceCSS) { 607 | assert.equal(enhanceCSS.nextAssetHost(), '//assets2.example.com'); 608 | }, 609 | 'fourth': function(enhanceCSS) { 610 | assert.equal(enhanceCSS.nextAssetHost(), '//assets0.example.com'); 611 | } 612 | }, 613 | 'get one asset host from list of different subdomains': { 614 | topic: function() { 615 | return new EnhanceCSS({ assetHosts: '//[alpha,beta,gamma].example.com' }); 616 | }, 617 | 'first': function(enhanceCSS) { 618 | assert.equal(enhanceCSS.nextAssetHost(), '//alpha.example.com'); 619 | }, 620 | 'second': function(enhanceCSS) { 621 | assert.equal(enhanceCSS.nextAssetHost(), '//beta.example.com'); 622 | }, 623 | 'third': function(enhanceCSS) { 624 | assert.equal(enhanceCSS.nextAssetHost(), '//gamma.example.com'); 625 | }, 626 | 'fourth': function(enhanceCSS) { 627 | assert.equal(enhanceCSS.nextAssetHost(), '//alpha.example.com'); 628 | } 629 | } 630 | }).addBatch({ 631 | 'not embedded files should not get mtime timestamp if "stamp" option equals false': { 632 | topic: runOn('div{background:url(/test/data/gradient.jpg)}', { 633 | stamp: false, 634 | noEmbedVersion: true 635 | }), 636 | 'in the "embedded" version': function(data) { 637 | assert.equal(data.embedded.plain, data.original); 638 | }, 639 | 'in the "not embedded" version': function(data) { 640 | assert.equal(data.notEmbedded.plain, data.original); 641 | } 642 | } 643 | }).export(module); 644 | --------------------------------------------------------------------------------